Repository: freeCodeCamp/solana-curriculum Branch: main Commit: f220f058c9d0 Files: 208 Total size: 1.0 MB Directory structure: gitextract_on91dfbr/ ├── .devcontainer.json ├── .editorconfig ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── BUG.md │ └── HELP.md ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .prettierignore ├── .prettierrc ├── .vscode/ │ └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── bash/ │ ├── .bashrc │ └── sourcerer.sh ├── build-a-client-side-app/ │ ├── .gitignore │ ├── mess.json │ └── mess.ts ├── build-a-smart-contract/ │ ├── package.json │ ├── program/ │ │ ├── Cargo.toml │ │ ├── src/ │ │ │ └── lib.rs │ │ └── tests/ │ │ └── process_instruction.rs │ └── wallet.json ├── build-a-university-certification-nft/ │ ├── client/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app.css │ │ │ ├── app.tsx │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── index.d.ts │ ├── index.js │ ├── metadatas.json │ ├── package.json │ ├── server.js │ └── utils.js ├── build-an-anchor-leaderboard/ │ └── rock-destroyer/ │ ├── .gitignore │ ├── .prettierignore │ ├── Anchor.toml │ ├── Cargo.toml │ ├── migrations/ │ │ └── deploy.ts │ ├── package.json │ ├── programs/ │ │ └── rock-destroyer/ │ │ ├── Cargo.toml │ │ ├── Xargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── tests/ │ │ └── rock-destroyer.ts │ └── tsconfig.json ├── build-and-deploy-your-freeform-app/ │ └── .gitkeep ├── config/ │ ├── projects.json │ └── state.json ├── curriculum/ │ └── locales/ │ └── english/ │ ├── build-a-client-side-app.md │ ├── build-a-smart-contract.md │ ├── build-a-university-certification-nft.md │ ├── build-an-anchor-leaderboard.md │ ├── build-and-deploy-your-freeform-app.md │ ├── learn-anchor-by-building-tic-tac-toe-part-1.md │ ├── learn-anchor-by-building-tic-tac-toe-part-2.md │ ├── learn-how-to-build-a-client-side-app-part-1.md │ ├── learn-how-to-build-a-client-side-app-part-2.md │ ├── learn-how-to-build-for-mainnet.md │ ├── learn-how-to-deploy-to-devnet.md │ ├── learn-how-to-interact-with-on-chain-programs.md │ ├── learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract.md │ ├── learn-solanas-token-program-by-minting-a-fungible-token.md │ └── learn-the-metaplex-sdk-by-minting-an-nft.md ├── freecodecamp.conf.json ├── learn-anchor-by-building-tic-tac-toe-part-1/ │ └── .gitkeep ├── learn-anchor-by-building-tic-tac-toe-part-2/ │ ├── .prettierignore │ └── tic-tac-toe/ │ ├── Anchor.toml │ ├── Cargo.toml │ ├── migrations/ │ │ └── deploy.ts │ ├── package.json │ ├── programs/ │ │ └── tic-tac-toe/ │ │ ├── Cargo.toml │ │ ├── Xargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── tests/ │ │ └── tic-tac-toe.ts │ └── tsconfig.json ├── learn-how-to-build-a-client-side-app-part-1/ │ ├── .gitkeep │ └── tic-tac-toe/ │ ├── .gitignore │ ├── Anchor.toml │ ├── Cargo.toml │ ├── app/ │ │ ├── .gitignore │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ ├── utils.js │ │ └── wallet.js │ ├── migrations/ │ │ └── deploy.ts │ ├── package.json │ ├── programs/ │ │ └── tic-tac-toe/ │ │ ├── Cargo.toml │ │ ├── Xargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── tests/ │ │ └── tic-tac-toe.ts │ └── tsconfig.json ├── learn-how-to-build-a-client-side-app-part-2/ │ ├── app/ │ │ ├── .gitignore │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ ├── package.json │ │ ├── utils.js │ │ ├── wallet.js │ │ └── web3.js │ └── tic_tac_toe.ts ├── learn-how-to-build-for-mainnet/ │ ├── _answer/ │ │ ├── Anchor.toml │ │ ├── Cargo.toml │ │ ├── app/ │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app.css │ │ │ │ ├── app.tsx │ │ │ │ ├── index.css │ │ │ │ ├── main.tsx │ │ │ │ ├── utils.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ └── vite.config.ts │ │ ├── migrations/ │ │ │ └── deploy.ts │ │ ├── package.json │ │ ├── programs/ │ │ │ └── todo/ │ │ │ ├── Cargo.toml │ │ │ ├── Xargo.toml │ │ │ └── src/ │ │ │ └── lib.rs │ │ ├── tests/ │ │ │ └── todo.ts │ │ └── tsconfig.json │ └── todo/ │ ├── .gitignore │ ├── .prettierignore │ ├── Anchor.toml │ ├── Cargo.toml │ ├── app/ │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app.css │ │ │ ├── app.tsx │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── utils.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── migrations/ │ │ └── deploy.ts │ ├── package.json │ ├── programs/ │ │ └── todo/ │ │ ├── Cargo.toml │ │ ├── Xargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── tests/ │ │ └── todo.ts │ └── tsconfig.json ├── learn-how-to-deploy-to-devnet/ │ └── todo/ │ ├── Anchor.toml │ ├── Cargo.toml │ ├── app/ │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app.css │ │ │ ├── app.tsx │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── utils.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── migrations/ │ │ └── deploy.ts │ ├── package.json │ ├── programs/ │ │ └── todo/ │ │ ├── Cargo.toml │ │ ├── Xargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── tests/ │ │ └── todo.ts │ └── tsconfig.json ├── learn-how-to-interact-with-on-chain-programs/ │ ├── cluster-devnet.env │ ├── cluster-mainnet-beta.env │ ├── cluster-testnet.env │ ├── package.json │ └── src/ │ ├── _answer/ │ │ └── client/ │ │ ├── hello-world.js │ │ └── main.js │ └── program-rust/ │ ├── .gitignore │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/ │ ├── .gitignore │ ├── package.json │ ├── src/ │ │ ├── client/ │ │ │ ├── hello_world.ts │ │ │ ├── main.ts │ │ │ └── utils.ts │ │ └── program-rust/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ └── tsconfig.json ├── learn-solanas-token-program-by-minting-a-fungible-token/ │ ├── .gitkeep │ ├── package.json │ └── utils.js ├── learn-the-metaplex-sdk-by-minting-an-nft/ │ ├── package.json │ ├── server.js │ ├── spl-program/ │ │ ├── create-mint-account.js │ │ ├── create-token-account.js │ │ ├── get-token-account.js │ │ ├── get-token-info.js │ │ ├── mint.js │ │ ├── package.json │ │ ├── transfer.js │ │ └── utils.js │ └── utils.js ├── package.json ├── renovate.json └── tooling/ ├── camper-info.js ├── helpers.js ├── rejig.js └── seed.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer.json ================================================ { "customizations": { "vscode": { "extensions": [ "dbaeumer.vscode-eslint", "freeCodeCamp.freecodecamp-courses@1.7.5", "freeCodeCamp.freecodecamp-dark-vscode-theme" ] } }, "forwardPorts": [8080], "workspaceFolder": "/workspace/solana-curriculum", "dockerFile": "./Dockerfile", "context": "." } ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [package.json] indent_style = space indent_size = 2 [*.md] trim_trailing_whitespace = false ================================================ FILE: .github/ISSUE_TEMPLATE/BUG.md ================================================ --- name: Bug Found about: Report a bug with the platform/content title: '[BUG]: ' --- ### Issue/Experience ### Output of running `node tooling/camper-info.js` from the workspace root ================================================ FILE: .github/ISSUE_TEMPLATE/HELP.md ================================================ --- name: Help Needed about: Get help with a project or lesson title: '[HELP]: ' --- ### Project ### Lesson Number ### Question ### Code and Screenshots ================================================ FILE: .gitignore ================================================ target node_modules Cargo.lock !.gitkeep __test # Should be included in repo, but does not get updated .logs test-ledger .anchor ================================================ FILE: .gitpod.Dockerfile ================================================ FROM gitpod/workspace-full:2024-05-22-07-25-51 ARG REPO_NAME=solana-curriculum ARG HOMEDIR=/workspace/$REPO_NAME WORKDIR ${HOMEDIR} RUN bash -c 'VERSION="20" \ && source $HOME/.nvm/nvm.sh && nvm install $VERSION \ && nvm use $VERSION && nvm alias default $VERSION' RUN echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix RUN sudo apt-get update && sudo apt-get upgrade -y # Solana RUN sh -c "$(curl -sSfL https://release.solana.com/v1.17.18/install)" # RUN wget https://github.com/solana-labs/solana/releases/download/v1.16.9/solana-release-x86_64-unknown-linux-gnu.tar.bz2 # RUN tar jxf solana-release-x86_64-unknown-linux-gnu.tar.bz2 # RUN cd solana-release/ # ENV PATH="$PWD/bin:${PATH}" RUN npm i -g @coral-xyz/anchor-cli@0.30.0 ================================================ FILE: .gitpod.yml ================================================ image: file: .gitpod.Dockerfile # Commands to start on workspace startup tasks: - init: npm ci ports: - port: 8080 onOpen: open-preview # TODO: See about publishing to Open VSX for smoother process vscode: extensions: - https://github.com/freeCodeCamp/courses-vscode-extension/releases/download/v1.7.5/freecodecamp-courses-1.7.5.vsix - https://github.com/freeCodeCamp/freecodecamp-dark-vscode-theme/releases/download/v1.0.0/freecodecamp-dark-vscode-theme-1.0.0.vsix ================================================ FILE: .prettierignore ================================================ **/.cache **/package-lock.json **/pkg ================================================ FILE: .prettierrc ================================================ { "endOfLine": "lf", "semi": true, "singleQuote": true, "jsxSingleQuote": true, "tabWidth": 2, "trailingComma": "none", "arrowParens": "avoid" } ================================================ FILE: .vscode/settings.json ================================================ { "files.exclude": { ".devcontainer.json": false, ".editorconfig": false, ".git": false, ".github": false, ".gitignore": false, ".gitpod.Dockerfile": false, ".gitpod.yml": false, ".logs": false, ".prettierignore": false, ".prettierrc": false, ".vscode": false, "bash": false, "client": false, "config": false, "curriculum": false, "tooling": false, "Dockerfile": false, "freecodecamp.conf.json": false, "node_modules": false, "package.json": false, "package-lock.json": false, "LICENSE": false, "README.md": false, "renovate.json": false, "build-x-using-y": false, "learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract": false, "learn-how-to-interact-with-on-chain-programs": false, "build-a-smart-contract": false, "learn-solanas-token-program-by-minting-a-fungible-token": false, "learn-the-metaplex-sdk-by-minting-an-nft": false, "build-a-university-certification-nft": false, "learn-anchor-by-building-tic-tac-toe-part-1": false, "learn-anchor-by-building-tic-tac-toe-part-2": false, "build-an-anchor-leaderboard": false, "learn-how-to-build-a-client-side-app-part-1": false, "learn-how-to-build-a-client-side-app-part-2": false, "build-a-client-side-app": false, "learn-how-to-build-for-mainnet": false, "learn-how-to-deploy-to-devnet": false, "build-and-deploy-your-freeform-app": false }, "terminal.integrated.profiles.linux": { "bash": { "path": "bash", "icon": "terminal-bash", "args": ["--init-file", "./bash/sourcerer.sh"] } }, "terminal.integrated.defaultProfile.linux": "bash", "workbench.colorTheme": "freeCodeCamp Dark Theme" } ================================================ FILE: Dockerfile ================================================ FROM ubuntu:20.04 ARG USERNAME=camper ARG REPO_NAME=solana-curriculum ARG HOMEDIR=/workspace/$REPO_NAME ENV TZ="America/New_York" ENV HOME=/workspace RUN apt-get update && apt-get install -y sudo # Unminimize Ubuntu to restore man pages RUN yes | unminimize # Set up timezone RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # Set up user, disable pw, and add to sudo group RUN adduser --disabled-password \ --gecos '' ${USERNAME} RUN adduser ${USERNAME} sudo RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> \ /etc/sudoers # Install packages for projects RUN sudo apt-get install -y curl git bash-completion man-db htop nano # Install Node LTS RUN curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - RUN sudo apt-get install -y nodejs # Rust RUN sudo apt-get install -y build-essential RUN curl https://sh.rustup.rs -sSf | sh -s -- -y ENV PATH="/workspace/.cargo/bin:${PATH}" # Solana RUN sh -c "$(curl -sSfL https://release.solana.com/v1.17.18/install)" # RUN wget https://github.com/solana-labs/solana/releases/download/v1.16.9/solana-release-x86_64-unknown-linux-gnu.tar.bz2 # RUN tar jxf solana-release-x86_64-unknown-linux-gnu.tar.bz2 # RUN cd solana-release/ # ENV PATH="$PWD/bin:${PATH}" # /usr/lib/node_modules is owned by root, so this creates a folder ${USERNAME} # can use for npm install --global WORKDIR ${HOMEDIR} RUN mkdir ~/.npm-global RUN npm config set prefix '~/.npm-global' RUN npm install -g yarn @coral-xyz/anchor-cli@0.30.0 # Configure course-specific environment COPY . . WORKDIR ${HOMEDIR} RUN cd ${HOMEDIR} && npm install # wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.16_amd64.deb # sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2.16_amd64.deb ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2022-2023, freeCodeCamp All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # freeCodeCamp: Solana Curriculum Get started here: https://web3.freecodecamp.org/solana ## Projects | Project | Description | | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Learn How to Set Up Solana by Building a Hello World Smart Contract | In this course, you will learn how to set up Solana by building a simple hello world contract. | | Learn How to Interact with On-Chain Programs | In this course, you will learn how to write the client code to interact with your previously deployed hello world smart contract. | | Build a Smart Contract | In this integrated project, you will use what you previously learnt to build a smart contract, and interact with it. | | Learn Solana's Token Program by Minting a Fungible Token | In this course, you will learn how to use Solana's token program by minting a fungible token. | | Learn the Metaplex SDK by Minting an NFT | In this course, you will learn how to use the Metaplex JS SDK to mint an NFT. | | Build a University Certification NFT | In this integrated project, you will use what you previously learnt to build out the logic for an NFT-issuing system for university certifications. | | Learn Anchor by Building Tic-Tac-Toe: Part 1 | In this course, you will learn how to use Anchor, a framework for building smart contracts on Solana, to build an on-chain Tic-Tac-Toe game. | | Learn Anchor by Building Tic-Tac-Toe: Part 2 | In this course, you will learn how to test the previously built Tic-Tac-Toe game. | | Build an Anchor Leaderboard | In this integrated project, you will use what you previously learnt to build the program logic for a game leaderboard | | Learn How to Build a Client-Side App: Part 1 | In this course, you will learn how to build a multiplayer, client-side app that interacts with your previously deployed Tic-Tac-Toe game | | Learn How to Build a Client-Side App: Part 2 | In this course, you will learn how to use the Phantom wallet browser extension to connect to your local validator, connect your wallet to a dApp, and sign transactions. | | Build a Client Side App | In this integrated project, you will use what you previously learnt to build an app your friends can use to message one another. | | More Coming Soon... | Keep an 👁️ out | ================================================ FILE: bash/.bashrc ================================================ # ~/.bashrc: executed by bash(1) for non-login shells. # see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) # for examples # If not running interactively, don't do anything case $- in *i*) ;; *) return;; esac # don't put duplicate lines or lines starting with space in the history. # See bash(1) for more options HISTCONTROL=ignoreboth # append to the history file, don't overwrite it shopt -s histappend # for setting history length see HISTSIZE and HISTFILESIZE in bash(1) HISTSIZE=1000 HISTFILESIZE=2000 # check the window size after each command and, if necessary, # update the values of LINES and COLUMNS. shopt -s checkwinsize # If set, the pattern "**" used in a pathname expansion context will # match all files and zero or more directories and subdirectories. #shopt -s globstar # make less more friendly for non-text input files, see lesspipe(1) [ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" # set variable identifying the chroot you work in (used in the prompt below) if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then debian_chroot=$(cat /etc/debian_chroot) fi # set a fancy prompt (non-color, unless we know we "want" color) case "$TERM" in xterm-color|*-256color) color_prompt=yes;; esac # uncomment for a colored prompt, if the terminal has the capability; turned # off by default to not distract the user: the focus in a terminal window # should be on the output of commands, not on the prompt #force_color_prompt=yes if [ -n "$force_color_prompt" ]; then if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then # We have color support; assume it's compliant with Ecma-48 # (ISO/IEC-6429). (Lack of such support is extremely rare, and such # a case would tend to support setf rather than setaf.) color_prompt=yes else color_prompt= fi fi if [ "$color_prompt" = yes ]; then PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' else PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' fi unset color_prompt force_color_prompt # If this is an xterm set the title to user@host:dir case "$TERM" in xterm*|rxvt*) PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1" ;; *) ;; esac # enable color support of ls and also add handy aliases if [ -x /usr/bin/dircolors ]; then test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" alias ls='ls --color=auto' #alias dir='dir --color=auto' #alias vdir='vdir --color=auto' alias grep='grep --color=auto' alias fgrep='fgrep --color=auto' alias egrep='egrep --color=auto' fi # colored GCC warnings and errors #export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01' # some more ls aliases alias ll='ls -alF' alias la='ls -A' alias l='ls -CF' # Add an "alert" alias for long running commands. Use like so: # sleep 10; alert alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"' # Alias definitions. # You may want to put all your additions into a separate file like # ~/.bash_aliases, instead of adding them here directly. # See /usr/share/doc/bash-doc/examples in the bash-doc package. if [ -f ~/.bash_aliases ]; then . ~/.bash_aliases fi # enable programmable completion features (you don't need to enable # this, if it's already enabled in /etc/bash.bashrc and /etc/profile # sources /etc/bash.bashrc). if ! shopt -oq posix; then if [ -f /usr/share/bash-completion/bash_completion ]; then . /usr/share/bash-completion/bash_completion elif [ -f /etc/bash_completion ]; then . /etc/bash_completion fi fi PS1='\[\]\u\[\] \[\]\w\[\]$(__git_ps1 " (%s)") $ ' for i in $(ls -A $HOME/.bashrc.d/); do source $HOME/.bashrc.d/$i; done . "$HOME/.cargo/env" export PATH="$HOME/.npm-global/bin:$PATH" # freeCodeCamp - Needed for most tests to work WD=/workspace/solana-curriculum PROMPT_COMMAND='>| $WD/.logs/.terminal-out.log && cat $WD/.logs/.temp.log >| $WD/.logs/.terminal-out.log && truncate -s 0 $WD/.logs/.temp.log; echo $PWD >> $WD/.logs/.cwd.log; history -a $WD/.logs/.bash_history.log; echo $PWD\$ $(history | tail -n 1) >> $WD/.logs/.history_cwd.log;' exec > >(tee -ia $WD/.logs/.temp.log) 2>&1 ================================================ FILE: bash/sourcerer.sh ================================================ #!/bin/bash source ./bash/.bashrc echo "BashRC Sourced" ================================================ FILE: build-a-client-side-app/.gitignore ================================================ mess ================================================ FILE: build-a-client-side-app/mess.json ================================================ { "version": "0.1.0", "name": "mess", "instructions": [ { "name": "init", "accounts": [ { "name": "chat", "isMut": true, "isSigner": false, "docs": [ "Global chat account to hold 20 messages" ] }, { "name": "payer", "isMut": true, "isSigner": true }, { "name": "systemProgram", "isMut": false, "isSigner": false } ], "args": [] }, { "name": "send", "accounts": [ { "name": "chat", "isMut": true, "isSigner": false }, { "name": "sender", "isMut": false, "isSigner": true } ], "args": [ { "name": "text", "type": "string" } ] } ], "accounts": [ { "name": "Chat", "type": { "kind": "struct", "fields": [ { "name": "messages", "type": { "vec": { "defined": "Message" } } } ] } } ], "types": [ { "name": "Message", "type": { "kind": "struct", "fields": [ { "name": "sender", "type": "publicKey" }, { "name": "text", "type": "string" } ] } } ] } ================================================ FILE: build-a-client-side-app/mess.ts ================================================ export type Mess = { "version": "0.1.0", "name": "mess", "instructions": [ { "name": "init", "accounts": [ { "name": "chat", "isMut": true, "isSigner": false, "docs": [ "Global chat account to hold 20 messages" ] }, { "name": "payer", "isMut": true, "isSigner": true }, { "name": "systemProgram", "isMut": false, "isSigner": false } ], "args": [] }, { "name": "send", "accounts": [ { "name": "chat", "isMut": true, "isSigner": false }, { "name": "sender", "isMut": false, "isSigner": true } ], "args": [ { "name": "text", "type": "string" } ] } ], "accounts": [ { "name": "chat", "type": { "kind": "struct", "fields": [ { "name": "messages", "type": { "vec": { "defined": "Message" } } } ] } } ], "types": [ { "name": "Message", "type": { "kind": "struct", "fields": [ { "name": "sender", "type": "publicKey" }, { "name": "text", "type": "string" } ] } } ] }; export const IDL: Mess = { "version": "0.1.0", "name": "mess", "instructions": [ { "name": "init", "accounts": [ { "name": "chat", "isMut": true, "isSigner": false, "docs": [ "Global chat account to hold 20 messages" ] }, { "name": "payer", "isMut": true, "isSigner": true }, { "name": "systemProgram", "isMut": false, "isSigner": false } ], "args": [] }, { "name": "send", "accounts": [ { "name": "chat", "isMut": true, "isSigner": false }, { "name": "sender", "isMut": false, "isSigner": true } ], "args": [ { "name": "text", "type": "string" } ] } ], "accounts": [ { "name": "chat", "type": { "kind": "struct", "fields": [ { "name": "messages", "type": { "vec": { "defined": "Message" } } } ] } } ], "types": [ { "name": "Message", "type": { "kind": "struct", "fields": [ { "name": "sender", "type": "publicKey" }, { "name": "text", "type": "string" } ] } } ] }; ================================================ FILE: build-a-smart-contract/package.json ================================================ { "name": "build-a-smart-contract", "type": "module", "dependencies": { "@solana/web3.js": "1.87.7", "borsh": "0.7.0" }, "scripts": { "build": "cargo build-sbf --manifest-path program/Cargo.toml --sbf-out-dir=dist/program", "deploy": "solana program deploy --keypair wallet.json dist/program/message.so" } } ================================================ FILE: build-a-smart-contract/program/Cargo.toml ================================================ [package] name = "message" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] borsh = "0.10.3" borsh-derive = "0.10.3" solana-program = "1.17.3" [lib] name = "message" crate-type = ["cdylib", "lib"] ================================================ FILE: build-a-smart-contract/program/src/lib.rs ================================================ ================================================ FILE: build-a-smart-contract/program/tests/process_instruction.rs ================================================ extern crate message; use message::process_instruction; use borsh::BorshDeserialize; use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; #[derive(BorshDeserialize, Debug)] pub struct MessageStructForTest { pub message: String } #[test] fn owner_not_program_id() { let program_id = Pubkey::new_unique(); let owner = Pubkey::new_unique(); let mut data = vec![0; 284]; let mut lam = 2; let account_info = AccountInfo::new( &program_id, false, true, &mut lam, &mut data, &owner, false, 2, ); let accounts = vec![account_info]; let instruction_data = vec![]; let result = process_instruction(&program_id, &accounts, &instruction_data); assert_eq!(result, Err(ProgramError::IncorrectProgramId)); } #[test] fn instruction_is_deserialized() { let program_id = Pubkey::new_unique(); let mut data = vec![0; 284]; let mut lam = 2; let account_info = AccountInfo::new( &program_id, false, true, &mut lam, &mut data, &program_id, false, 2, ); let accounts = vec![account_info]; let instruction_data = "Hello World!".as_bytes(); let _result = process_instruction(&program_id, &accounts, &instruction_data); let data = MessageStructForTest::try_from_slice(String::from_utf8(accounts[0].data.borrow().to_vec()).unwrap().as_bytes()).unwrap(); let fmt = format!("{: <280}", "Hello World!"); assert_eq!(fmt, data.message); } #[test] fn instruction_not_string() { let program_id = Pubkey::new_unique(); let mut data = vec![0; 284]; let mut lam = 2; let account_info = AccountInfo::new( &program_id, false, true, &mut lam, &mut data, &program_id, false, 2, ); let accounts = vec![account_info]; let instruction_data = vec![0, 159, 146, 150]; let result = process_instruction(&program_id, &accounts, &instruction_data); assert_eq!(result, Err(ProgramError::InvalidInstructionData)); } #[test] fn instruction_too_long() { let program_id = Pubkey::new_unique(); let mut data = vec![0; 284]; let mut lam = 2; let account_info = AccountInfo::new( &program_id, false, true, &mut lam, &mut data, &program_id, false, 2, ); let accounts = vec![account_info]; let instruction_data = vec![0; 281]; let result = process_instruction(&program_id, &accounts, &instruction_data); assert_eq!(result, Err(ProgramError::InvalidInstructionData)); } #[test] fn no_accounts() { let program_id = Pubkey::new_unique(); let accounts = vec![]; let instruction_data = vec![]; let result = process_instruction(&program_id, &accounts, &instruction_data); assert_eq!(result, Err(ProgramError::NotEnoughAccountKeys)); } #[test] fn instruction_data_padded() { let program_id = Pubkey::new_unique(); let mut data = vec![0; 284]; let mut lam = 2; let account_info = AccountInfo::new( &program_id, false, true, &mut lam, &mut data, &program_id, false, 2, ); let accounts = vec![account_info]; let instruction_data = "Hello World!".as_bytes(); let _result = process_instruction(&program_id, &accounts, &instruction_data); let account_data = accounts[0].data.borrow(); assert_eq!(account_data.len(), 284); } #[test] fn success() { let program_id = Pubkey::new_unique(); let mut data = vec![0; 284]; let mut lam = 2; let account_info = AccountInfo::new( &program_id, false, true, &mut lam, &mut data, &program_id, false, 2, ); let accounts = vec![account_info]; let instruction_data = "Hello World!".as_bytes(); let result = process_instruction(&program_id, &accounts, &instruction_data); assert_eq!(result, Ok(())); } ================================================ FILE: build-a-smart-contract/wallet.json ================================================ [143,176,178,47,104,172,153,69,159,235,167,100,198,176,237,12,40,244,4,227,138,109,255,63,148,130,55,85,138,167,29,47,116,163,179,15,205,180,80,61,111,245,231,19,180,55,178,17,209,19,9,73,227,57,40,209,71,188,174,32,62,95,107,144] ================================================ FILE: build-a-university-certification-nft/client/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: build-a-university-certification-nft/client/index.html ================================================ Solana University Certification Dashboard
================================================ FILE: build-a-university-certification-nft/client/package.json ================================================ { "name": "client", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "npm i && vite --clearScreen=false", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { "@metaplex-foundation/js": "0.18.3", "@solana/spl-token": "0.3.7", "@solana/web3.js": "1.87.7", "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@types/react": "18.2.31", "@types/react-dom": "18.2.14", "@vitejs/plugin-react": "3.1.0", "assert": "2.1.0", "crypto-browserify": "3.12.0", "rollup-plugin-node-polyfills": "0.2.1", "typescript": "4.9.3", "util": "0.12.5", "vite": "4.5.6" } } ================================================ FILE: build-a-university-certification-nft/client/src/app.css ================================================ #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } form { display: flex; flex-direction: column; align-items: end; } label { margin: 0.5rem 0; } .controls { margin-top: 1.5rem; } ================================================ FILE: build-a-university-certification-nft/client/src/app.tsx ================================================ import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { Keypair, PublicKey, Signer } from '@solana/web3.js'; import { createMintAccount as camperCreateMintAccount, getMintAccounts as camperGetMintAccounts, createTokenAccount as camperCreateTokenAccount, mintToken as camperMintToken, uploadFile as camperUploadFile, getNFTs as camperGetNFTs } from '../../index.js'; import './app.css'; import { toMetaplexFile } from '@metaplex-foundation/js'; // 1) Create a certificate program - create a new mint // 2) Register new student - create a token account for the student // 3) Issue a certificate - mint NFT to the student's token account // 4) View a certificate - view the certificate's metadata export function App() { const [output, setOutput] = useState('OUTPUT'); const [payer, setPayer] = useState(); const [mintAddress, setMintAddress] = useState(); const [ownerAddress, setOwnerAddress] = useState(); const [uri, setUri] = useState(); const [invalidInputs, setInvalidInputs] = useState([]); const createMintAccount: CreateMintAccountF = async () => { if (!payer) { setInvalidInputs(['payer']); return; } setInvalidInputs([]); try { const mint = await camperCreateMintAccount({ payer }); setOutput(JSON.stringify(mint, null, 2)); } catch (e) { console.warn(e); setOutput( 'Error creating mint account:\n\n' + JSON.stringify(e, null, 2) ); } }; const getMintAccounts: GetMintAccountsF = async () => { if (!payer) { setInvalidInputs(['payer']); return; } setInvalidInputs([]); try { const mintAccounts = await camperGetMintAccounts({ payer }); setOutput(JSON.stringify(mintAccounts, null, 2)); } catch (e) { console.warn(e); setOutput( 'Error getting mint accounts:\n\n' + JSON.stringify(e, null, 2) ); } }; const createTokenAccount: CreateTokenAccountF = async () => { if (!payer || !mintAddress || !ownerAddress) { setInvalidInputs(['payer', 'mintAddress', 'ownerAddress']); return; } setInvalidInputs([]); try { const tokenAccount = await camperCreateTokenAccount({ payer, mintAddress, ownerAddress }); console.log(tokenAccount); setOutput(tokenAccount.address.toBase58()); } catch (e) { console.warn(e); setOutput( 'Error creating token account:\n\n' + JSON.stringify(e, null, 2) ); } }; const mintToken: MintTokenF = async () => { if (!payer || !mintAddress || !ownerAddress || !uri) { setInvalidInputs(['payer', 'mintAddress', 'ownerAddress', 'uri']); return; } setInvalidInputs([]); const year = new Date().getFullYear(); try { const nft = await camperMintToken({ payer, mintAddress, ownerAddress, year, uri }); console.log(nft); setOutput(JSON.stringify(nft, null, 2)); } catch (e) { console.warn(e); setOutput('Error minting token:\n\n' + JSON.stringify(e, null, 2)); } }; const getNFTs: GetNFTsF = async () => { if (!ownerAddress) { setInvalidInputs(['ownerAddress']); return; } setInvalidInputs([]); try { const nfts = await camperGetNFTs({ ownerAddress }); console.log(nfts); setOutput(JSON.stringify(nfts, null, 2)); } catch (e) { console.warn(e); setOutput('Error getting NFTs:\n\n' + JSON.stringify(e, null, 2)); } }; const imageInput = useRef(null); const previewImg = useRef(null); const uriInput = useRef(null); function setAuthority(e: ChangeEvent) { if (e.target) { try { const keypair = Keypair.fromSecretKey( new Uint8Array(JSON.parse(e.target.value)) ); setPayer(keypair); } catch (e) { console.warn(e); setOutput('Invalid payer secret key\n\n' + JSON.stringify(e, null, 2)); } } } function setMint(e: ChangeEvent) { if (e.target) { try { const mint = new PublicKey(e.target.value); setMintAddress(mint); } catch (e) { console.warn(e); setOutput('Invalid mint public key\n\n' + JSON.stringify(e, null, 2)); } } } function setOwner(e: ChangeEvent) { if (e.target) { try { const owner = new PublicKey(e.target.value); setOwnerAddress(owner); } catch (e) { console.warn(e); setOutput( 'Invalid student public key\n\n' + JSON.stringify(e, null, 2) ); } } } function setUriInput(e: ChangeEvent) { if (e.target) { setUri(e.target.value); } } useEffect(() => { if (uri) { if (uriInput.current && uri !== uriInput.current.value) { uriInput.current.value = uri; } } }, [uri]); async function uploadFile() { if (imageInput.current) { if (imageInput.current.files) { const file = imageInput.current.files[0]; try { const arrayBuffer = await file.arrayBuffer(); const metaplexFile = toMetaplexFile(arrayBuffer, file.name); if (!payer) { setInvalidInputs(['payer']); return; } setInvalidInputs([]); const uri = await camperUploadFile({ metaplexFile, payer }); setUri(uri); setOutput(uri); } catch (e) { console.warn(e); setOutput('Invalid file\n\n' + JSON.stringify(e, null, 2)); } } } } return (

Solana University Certification Dashboard

nft preview
{invalidInputs.length > 0 && }
); } type OutputT = { output: string; }; type CreateMintAccountF = () => Promise; type GetMintAccountsF = () => Promise; type CreateTokenAccountF = () => Promise; type MintTokenF = () => Promise; type GetNFTsF = () => Promise; type CreateCertificateProgramT = { createMintAccount: CreateMintAccountF; }; /** * Create a new Mint for a certificate program */ function CreateCertificateProgram({ createMintAccount }: CreateCertificateProgramT) { return (

Create Certificate Program

); } /** * Get all mitns. Useful to get the mint address for a certificate program */ function GetCertificatePrograms({ getMintAccounts }: { getMintAccounts: GetMintAccountsF; }) { return (

Get Certificate Programs

); } /** * Create a new token account associated with the public key of the student */ function RegisterStudent({ createTokenAccount }: { createTokenAccount: CreateTokenAccountF; }) { return (

Register Student

); } /** * Mint a new NFT to the student's token account */ function GrantCertificate({ mintToken }: { mintToken: MintTokenF }) { return (

Grant Certificate

); } function ViewStudentCertificate({ getNFTs }: { getNFTs: GetNFTsF }) { return (

View Student Certificate

); } type DisplayPngT = { buffer: Buffer; }; function DisplayPng({ buffer }: DisplayPngT) { return ; } function Output({ output }: OutputT) { return (
        {output}
      
); } function ValidationError({ invalidInputs }: { invalidInputs: string[] }) { return (

Form requires: {invalidInputs.join(', ')}

); } ================================================ FILE: build-a-university-certification-nft/client/src/index.css ================================================ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #000000; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; --solana-purple: #9945ff; --solana-green: #14f195; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 1.8em; line-height: 1.1; background: -webkit-linear-gradient( 120deg, var(--solana-green), var(--solana-purple) ); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } .controls { display: flex; flex-direction: row; gap: 1em; padding: 1em; border-radius: 8px; background-color: rgba(40, 40, 40, 0.5); backdrop-filter: blur(8px); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: var(--solana-purple); cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ================================================ FILE: build-a-university-certification-nft/client/src/main.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import { App } from './app'; import './index.css'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ); ================================================ FILE: build-a-university-certification-nft/client/src/vite-env.d.ts ================================================ /// ================================================ FILE: build-a-university-certification-nft/client/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": false, "skipLibCheck": true, "esModuleInterop": false, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "ESNext", "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: build-a-university-certification-nft/client/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: build-a-university-certification-nft/client/vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'; import nodePolyfills from 'rollup-plugin-node-polyfills'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], resolve: { alias: { stream: 'node_modules/rollup-plugin-node-polyfills/polyfills/stream.js', events: 'node_modules/rollup-plugin-node-polyfills/polyfills/events.js', assert: 'assert', crypto: 'node_modules/crypto-browserify/index.js', util: 'util', 'near-api-js': 'near-api-js/dist/near-api-js.js' } }, define: { 'process.env': process.env ?? {} }, build: { target: 'esnext', rollupOptions: { plugins: [nodePolyfills({ crypto: true })] } }, optimizeDeps: { esbuildOptions: { plugins: [NodeGlobalsPolyfillPlugin({ buffer: true })] } } }); ================================================ FILE: build-a-university-certification-nft/index.d.ts ================================================ import { MetaplexFile, CreateNftOutput, FindNftsByOwnerOutput } from '@metaplex-foundation/js'; import { Account, createMint } from '@solana/spl-token'; import { AccountInfo, ParsedAccountData, PublicKey, Signer } from '@solana/web3.js'; declare function uploadFile({ metaplexFile, payer }: { metaplexFile: MetaplexFile; payer: Signer; }): Promise; declare function createMintAccount({ payer }: { payer: Signer; }): ReturnType; declare function getMintAccounts({ payer }: { payer: Signer; }): Promise[]>; declare function createTokenAccount({ payer, mintAddress, ownerAddress }: { payer: Signer; mintAddress: PublicKey; ownerAddress: PublicKey; }): Promise; declare function mintToken({ payer, mintAddress, ownerAddress, year, uri }: { payer: Signer; mintAddress: PublicKey; ownerAddress: PublicKey; year: number; uri: string; }): Promise; declare function getNFTs({ ownerAddress }: { ownerAddress: PublicKey; }): Promise; ================================================ FILE: build-a-university-certification-nft/index.js ================================================ ================================================ FILE: build-a-university-certification-nft/metadatas.json ================================================ {} ================================================ FILE: build-a-university-certification-nft/package.json ================================================ { "name": "build-a-university-certification-nft", "scripts": { "start:server": "node server.js", "start:client": "cd client && npm run dev -- --force" }, "type": "module", "dependencies": { "cors": "2.8.5", "express": "4.20.0" } } ================================================ FILE: build-a-university-certification-nft/server.js ================================================ import { readFileSync, writeFileSync } from 'fs'; import cors from 'cors'; import express from 'express'; import { setDefaultResultOrder } from 'dns'; setDefaultResultOrder('ipv4first'); const app = express(); app.use(cors()); app.use((req, res, next) => { console.log(req.method, req.url); next(); }); app.use(express.json()); app.get('/status/ping', (_req, res) => { console.log('Got ping'); return res.status(200).send('pong'); }); app.get('/meta/:id', (req, res) => { console.log('GET', req.params.id); const metadatas = getMetadatas(); const metadata = metadatas[req.params.id]; if (!metadata) { return res.status(404).end(); } return res.send(Buffer.from(metadata)); }); app.put('/meta/:id', (req, res) => { console.log('POST', req.params.id); const metadatas = getMetadatas(); metadatas[req.params.id] = req.body; writeMetadatas(metadatas); res.status(200).end(); }); const PORT = process.env.PORT || 3002; app.listen(PORT, () => { console.log('Server started at', `http://127.0.0.1:${PORT}`); }); function getMetadatas() { const metadatas = readFileSync('metadatas.json', 'utf8'); return JSON.parse(metadatas); } function writeMetadatas(metadatas) { writeFileSync('metadatas.json', JSON.stringify(metadatas)); } ================================================ FILE: build-a-university-certification-nft/utils.js ================================================ export function localStorage(options) { return { install(metaplex) { metaplex.storage().setDriver(new LocalStorageDriver(options)); } }; } class LocalStorageDriver { constructor(options) { if (!options.baseUrl) { throw new Error('Missing baseUrl option'); } this.baseUrl = options.baseUrl; this.costPerByte = 2; } async getUploadPrice(bytes) { const { amount } = await import('@metaplex-foundation/js'); return amount(this.costPerByte * bytes, { symbol: 'SOL', decimals: 9 }); } async upload(file) { const uri = `${this.baseUrl}meta/${file.uniqueName}`; await fetch(uri, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(file.buffer) }); return uri; } async download(uri) { const { toMetaplexFile } = await import('@metaplex-foundation/js'); const res = await fetch(uri); if (!res) { throw new Error(`URI not found: ${uri}`); } const buffer = await res.arrayBuffer(); const metaplexFile = toMetaplexFile(buffer, uri); return metaplexFile; } } ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/.gitignore ================================================ .anchor .DS_Store target **/*.rs.bk node_modules test-ledger ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/.prettierignore ================================================ .anchor .DS_Store target node_modules dist build test-ledger ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/Anchor.toml ================================================ [features] seeds = false skip-lint = false [programs.localnet] rock_destroyer = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/Cargo.toml ================================================ [workspace] members = [ "programs/*" ] [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/migrations/deploy.ts ================================================ // Migrations are an early feature. Currently, they're nothing more than this // single deploy script that's invoked from the CLI, injecting a provider // configured from the workspace's Anchor.toml. const anchor = require("@coral-xyz/anchor"); module.exports = async function (provider) { // Configure client to use the provider. anchor.setProvider(provider); // Add your deploy script here. }; ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/package.json ================================================ { "scripts": { "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" }, "dependencies": { "@coral-xyz/anchor": "^0.27.0" }, "devDependencies": { "chai": "^4.3.4", "mocha": "^9.0.3", "ts-mocha": "^10.0.0", "@types/bn.js": "^5.1.0", "@types/chai": "^4.3.0", "@types/mocha": "^9.0.0", "typescript": "^4.3.5", "prettier": "^2.6.2" } } ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/programs/rock-destroyer/Cargo.toml ================================================ [package] name = "rock-destroyer" version = "0.1.0" description = "Created with Anchor" edition = "2021" [lib] crate-type = ["cdylib", "lib"] name = "rock_destroyer" [features] no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] [dependencies] anchor-lang = "0.27.0" ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/programs/rock-destroyer/Xargo.toml ================================================ [target.bpfel-unknown-unknown.dependencies.std] features = [] ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/programs/rock-destroyer/src/lib.rs ================================================ use anchor_lang::prelude::*; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] pub mod rock_destroyer { use super::*; pub fn initialize(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct Initialize {} ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/tests/rock-destroyer.ts ================================================ import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { RockDestroyer } from "../target/types/rock_destroyer"; describe("rock-destroyer", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.RockDestroyer as Program; it("Is initialized!", async () => { // Add your test here. const tx = await program.methods.initialize().rpc(); console.log("Your transaction signature", tx); }); }); ================================================ FILE: build-an-anchor-leaderboard/rock-destroyer/tsconfig.json ================================================ { "compilerOptions": { "types": ["mocha", "chai"], "typeRoots": ["./node_modules/@types"], "lib": ["es2015"], "module": "commonjs", "target": "es6", "esModuleInterop": true } } ================================================ FILE: build-and-deploy-your-freeform-app/.gitkeep ================================================ ================================================ FILE: config/projects.json ================================================ [ { "id": 0, "title": "Learn How to Set Up Solana by Building a Hello World Smart Contract", "dashedName": "learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract", "isIntegrated": false, "description": "In this course, you will learn how to set up Solana by building a simple hello world contract.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 58 }, { "id": 1, "title": "Learn How to Interact with On-Chain Programs", "dashedName": "learn-how-to-interact-with-on-chain-programs", "isIntegrated": false, "description": "In this course, you will learn how to write the client code to interact with your previously deployed hello world smart contract.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 65 }, { "id": 2, "title": "Build a Smart Contract", "dashedName": "build-a-smart-contract", "isIntegrated": true, "description": "In this integrated project, you will use what you previously learnt to build a smart contract, and interact with it.", "isPublic": true, "currentLesson": 1, "numberOfLessons": 1 }, { "id": 3, "title": "Learn Solana's Token Program by Minting a Fungible Token", "dashedName": "learn-solanas-token-program-by-minting-a-fungible-token", "isIntegrated": false, "description": "In this course, you will learn how to use Solana's token program by minting a fungible token.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 52 }, { "id": 4, "title": "Learn the Metaplex SDK by Minting an NFT", "dashedName": "learn-the-metaplex-sdk-by-minting-an-nft", "isIntegrated": false, "description": "In this course, you will learn how to use the Metaplex JS SDK to mint an NFT.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 51 }, { "id": 5, "title": "Build a University Certification NFT", "dashedName": "build-a-university-certification-nft", "isIntegrated": true, "description": "In this integrated project, you will use what you previously learnt to build out the logic for an NFT-issuing system for university certifications.", "isPublic": true, "currentLesson": 1, "numberOfLessons": 1 }, { "id": 6, "title": "Learn Anchor by Building Tic-Tac-Toe: Part 1", "dashedName": "learn-anchor-by-building-tic-tac-toe-part-1", "isIntegrated": false, "description": "In this course, you will learn how to use Anchor, a framework for building smart contracts on Solana, to build an on-chain Tic-Tac-Toe game.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 68 }, { "id": 7, "title": "Learn Anchor by Building Tic-Tac-Toe: Part 2", "dashedName": "learn-anchor-by-building-tic-tac-toe-part-2", "isIntegrated": false, "description": "In this course, you will learn how to test the previously built Tic-Tac-Toe game.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 67 }, { "id": 8, "title": "Build an Anchor Leaderboard", "dashedName": "build-an-anchor-leaderboard", "isIntegrated": true, "description": "In this integrated project, you will use what you previously learnt to build the program logic for a game leaderboard.", "isPublic": true, "currentLesson": 1, "numberOfLessons": 1 }, { "id": 9, "title": "Learn How to Build a Client-Side App: Part 1", "dashedName": "learn-how-to-build-a-client-side-app-part-1", "isIntegrated": false, "description": "In this project, you will learn how to build a multi-player client-side app that interacts with your previously deployed Tic-Tac-Toe game.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 62 }, { "id": 10, "title": "Learn How to Build a Client-Side App: Part 2", "dashedName": "learn-how-to-build-a-client-side-app-part-2", "isIntegrated": false, "description": "In this project, you will learn how to use the Phantom browser extension to connect to your local validator, connect your wallet to your dApp, and sign transactions.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 35 }, { "id": 11, "title": "Build a Client-Side App", "dashedName": "build-a-client-side-app", "isIntegrated": true, "description": "In this integrated project, you will use what you previously learnt to build an app your friends can use to message one another.", "isPublic": true, "currentLesson": 1, "numberOfLessons": 1 }, { "id": 12, "title": "Learn How to Build for Mainnet", "dashedName": "learn-how-to-build-for-mainnet", "isIntegrated": false, "description": "In this project, you will learn how to build a dApp from start to finish, preparing for deployment to mainnet-beta.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 1 }, { "id": 13, "title": "Learn How to Deploy to Devnet", "dashedName": "learn-how-to-deploy-to-devnet", "isIntegrated": false, "description": "In this project, you will learn how to put an app on the public devnet.", "isPublic": true, "currentLesson": 1, "runTestsOnWatch": true, "numberOfLessons": 1 }, { "id": 14, "title": "Build and Deploy Your Freeform App", "dashedName": "build-and-deploy-your-freeform-app", "isIntegrated": true, "description": "The final project of this course. Use what you have learnt to build and deploy your own app to the public devnet or mainnet-beta, and share your work with the world!", "isPublic": true, "currentLesson": 1, "numberOfLessons": 1 } ] ================================================ FILE: config/state.json ================================================ { "currentProject": null, "locale": "english" } ================================================ FILE: curriculum/locales/english/build-a-client-side-app.md ================================================ # Solana - Build a Client Side App ## 1 ### --description-- You are developing a client-side app to interact with the `mess.so` program. The `mess.so` program consists of a `chat` account that stores a list of <= 20 messages. You will be working entirely within the `build-a-client-side-app/` directory. ### User Stories 1. You should generate two new keypairs stored in `messer-1.json` and `messer-2.json`. 2. You should deploy the `mess.so` program to a local Solana validator. 3. You should airdrop SOL into each of the two keypairs. 4. You should initialize the `chat` account, by calling the `init` instruction. 1. The payer should be one of the two keypairs. 5. You should send at least 20 messages using your client app. 1. At least 5 messages should be sent from each of the two keypairs. #### Types - The `mess` program IDL is stored in `mess.ts`. ### Notes - The `mess.so` program id is `TODO`. - The `chat` account has a seed of `"global"`. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The mess program should be deployed. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getProgramAccounts", "params": [ "BPFLoader2111111111111111111111111111111111", { "encoding": "base64", "dataSlice": { "length": 0, "offset": 0 } } ] }'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.exists(jsonOut.result.find(r => r.pubkey === __programId)); } catch (e) { assert.fail( e, `Try running \`solana-test-validator --bpf-program ${__programId} mess.so --reset\`` ); } ``` There should be a keypair named `messer-1.json`. ```js const { access, constants } = await import('fs/promises'); await access(join(project.dashedName, 'messer-1.json'), constants.F_OK); try { const { Keypair } = await import('@solana/web3.js'); const keypair = Keypair.fromSecretKey(Uint8Array.from(__messer1_json)); } catch (e) { assert.fail(e, 'Try running `solana-keygen new --outfile messer-1.json`.'); } ``` There should be a keypair named `messer-2.json`. ```js const { access, constants } = await import('fs/promises'); await access(join(project.dashedName, 'messer-2.json'), constants.F_OK); try { const { Keypair } = await import('@solana/web3.js'); const keypair = Keypair.fromSecretKey(Uint8Array.from(__messer2_json)); } catch (e) { assert.fail(e, 'Try running `solana-keygen new --outfile messer-2.json`.'); } ``` The `messer-1.json` keypair should have a balance greater than 0 SOL. ```js const { stdout } = await __helpers.getCommandOutput( `solana balance ${project.dashedName}/messer-1.json` ); const balance = stdout.trim()?.match(/\d+/)?.[0]; assert.isAbove( parseInt(balance), 0, 'Try running `solana airdrop 1 ./messer-1.json`.' ); ``` The `messer-2.json` keypair should have a balance greater than 0 SOL. ```js const { stdout } = await __helpers.getCommandOutput( `solana balance ${project.dashedName}/messer-2.json` ); const balance = stdout.trim()?.match(/\d+/)?.[0]; assert.isAbove( parseInt(balance), 0, 'Try running `solana airdrop 1 ./messer-2.json`.' ); ``` The `chat` account should be initialized. ```js const accountInfo = await __connection.getAccountInfo(__chatPublicKey); assert.exists(accountInfo); ``` The `messer-1.json` keypair should have sent at least 5 messages. ```js const pubkey = __messer1_keypair.publicKey; const chatData = await program.account.chat.fetch(pubkey); const messages = chatData.messages.filter(m => m.sender.equals(pubkey)); assert.isAtLeast(messages.length, 5); ``` The `messer-2.json` keypair should have sent at least 5 messages. ```js const pubkey = __messer2_keypair.publicKey; const chatData = await program.account.chat.fetch(pubkey); const messages = chatData.messages.filter(m => m.sender.equals(pubkey)); assert.isAtLeast(messages.length, 5); ``` At least 20 messages should have been sent. ```js const chatData = await program.account.chat.fetch(__chatPublicKey); assert.isAtLeast(chatData.messages.length, 20); ``` ### --before-all-- ```js const { AnchorProvider, setProvider, Program } = await import( '@coral-xyz/anchor' ); const { PublicKey, Connection, Keypair } = await import('@solana/web3.js'); setProvider(AnchorProvider.env()); const IDL = JSON.parse(await __helpers.getFile('mess.json')); const PROGRAM_ID = new PublicKey( '8D2EQasXmadK7bWhRPrkryhAGYtERQzzGMJVGiisUqqh' ); const program = new Program(IDL, PROGRAM_ID); const connection = new Connection('http://localhost:8899', 'confirmed'); const [chatPublicKey, _] = PublicKey.findProgramAddressSync( [Buffer.from('global')], new PublicKey('8D2EQasXmadK7bWhRPrkryhAGYtERQzzGMJVGiisUqqh') ); try { const messer1_keypair = JSON.parse( await __helpers.getFile(join(project.dashedName, 'messer-1.json')) ); const messer2_keypair = JSON.parse( await __helpers.getFile(join(project.dashedName, 'messer-2.json')) ); global.__messer1_json = messer1_keypair; global.__messer2_json = messer2_keypair; const keypair1 = Keypair.fromSecretKey(Uint8Array.from(__messer1_keypair)); const keypair2 = Keypair.fromSecretKey(Uint8Array.from(__messer2_keypair)); global.__messer1_keypair = keypair1; global.__messer2_keypair = keypair2; } catch (e) { logover.warn( 'You need to create two keypairs. Try running `solana-keygen new --outfile messer-1.json` and `solana-keygen new --outfile messer-2.json`.', e ); } global.__chatPublicKey = chatPublicKey; global.__connection = connection; global.__programId = PROGRAM_ID; global.__program = program; ``` ### --after-all-- ```js delete global.__messer1_keypair; delete global.__messer2_keypair; delete global.__chatPublicKey; delete global.__messer1_json; delete global.__messer2_json; delete global.__connection; delete global.__programId; delete global.__program; ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/build-a-smart-contract.md ================================================ # Solana - Build a Smart Contract ## 1 ### --description-- You need to create a smart contract in Rust, deploy the contract to your localnet, and write a Nodejs script to interact with the contract. **User Stories** - You should generate a new keypair - The keypair should be stored in `wallet.json` - The keypair should be used to deploy the program account - The keypair should **not** use a BIP39 passphrase - You should write a smart contract in Rust - The program should be written in `program/src/lib.rs` - The program should exort a `process_instruction` function with the `solana_program::entrypoint` parameter signature - The program should return the `NotEnoughAccountKeys` variant of `ProgramError` if the number of accounts is less than 1 - The program should return the `IncorrectProgramId` variant of `ProgramError` if the account owner does not match the program id - The program should own a data account for storing a text message of 280 characters - The program data account should hold data in the form of: ```rust struct MessageAccount { message: String, } ``` - The program should deserialize the `InstructionData` into a `String`, and store the string in the program data account - If the `InstructionData` is not deserializable into a `String`, the program should return the `InvalidInstructionData` variant of `ProgramError` - If the `String` length is greater than 280 characters, the program should return the `InvalidInstructionData` variant of `ProgramError` - The `InstructionData` should be padded with space characters to 280 characters - The program should be built using `cargo-build-sbf` - The resulting `.so` and `.json` files should be stored in the `dist/program/` directory - You should write a script interacting with the smart contract - The script entrypoint should be `client/main.js` - The script should expect a string as a command line argument - If no argument is provided, the script should throw an error with the message `"No message provided"` - The string should be sent as the instruction data when calling the smart contract - The script should use the account stored in `wallet.json` to pay for transactions - The script should use the `dist/program/` keypair file to get the program id - The script should create a program data account, if one does not already exist - The program data account public key should be created using `"fcc-seed"` as the seed - You should have a local Solana cluster running at port `8899` - The program should be deployed to the local cluster - The program data account should be created on the local cluster **NOTES:** - All referenced paths are relative to `build-a-smart-contract/` ### --tests-- You should have a `wallet.json` file in the root of your project. ```js const walletExists = __helpers.fileExists(join(__loc, 'wallet.json')); assert.isTrue(walletExists, 'wallet.json should exist'); ``` You should have a local Solana cluster running at port `8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` You should have a `dist/program/` directory. ```js const distExists = __helpers.fileExists(join(__loc, 'dist')); assert.isTrue(distExists, 'dist/ should exist'); const programExists = __helpers.fileExists(join(__loc, 'dist', 'program')); assert.isTrue(programExists, 'dist/program/ should exist'); ``` You should have a `.so` file in the `dist/program/` directory. ```js const dir = await __helpers.getDirectory(join(__loc, 'dist', 'program')); let program; for (const file of dir) { if (file.endsWith('.so')) { program = file; } } assert.exists(program, 'dist/program/ should have a .so file'); ``` You should have a `.json` file in the `dist/program/` directory. ```js const dir = await __helpers.getDirectory(join(__loc, 'dist', 'program')); let keypair; for (const file of dir) { if (file.endsWith('.json')) { keypair = file; } } assert.exists(keypair, 'dist/program/ should have a .json file'); ``` You should deploy the `.so` file as an executable program to the local net. ```js const programKeypair = await __helpers.getProgramKeypair(); assert.exists(programKeypair, 'dist/program/ does not have a .json file'); const programId = programKeypair.publicKey; const connection = __helpers.establishConnection(); assert.exists(connection, 'unable to establish connection to localnet'); const programAccountInfo = await connection.getAccountInfo(programId); assert.exists(programAccountInfo, 'Program not deployed to the local net'); assert.equal( programAccountInfo.executable, true, 'Program is not deployed as an executable' ); ``` The owner of the program account should be the associated account of the `wallet.json` keypair. ```js const camperKeypair = await __helpers.getCamperKeypair(); assert.exists(camperKeypair, 'wallet.json does not exist'); const connection = __helpers.establishConnection(); assert.exists(connection, 'unable to establish connection to localnet'); const camperAccount = await connection.getAccountInfo(camperKeypair.publicKey); assert.exists(camperAccount, 'wallet.json does not have an associated account'); const programKeypair = await __helpers.getProgramKeypair(); assert.exists(programKeypair, 'dist/program/ does not have a .json file'); const programId = programKeypair.publicKey; const programAccountInfo = await connection.getAccountInfo(programId); assert.exists(programAccountInfo, 'Program not deployed to the local net'); assert.equal( programAccountInfo.executable, true, 'Program is not deployed as an executable' ); const { stdout, stderr } = await __helpers.getCommandOutput( `solana program show ${programId}` ); assert.include(stdout, 'Authority:', "Program owner not found, run 'solana program show ' and make sure there's an 'Authority' ID"); const authority = stdout.match(/Authority: \S+/gm) assert.equal( authority[0].split(' ')[1], camperKeypair.publicKey.toBase58(), 'Program account owner does not match the wallet.json account owner' ); ``` The program should return the `IncorrectProgramId` variant of `ProgramError` if the account owner does not match the program id. ```js // Should pass `owner_not_program_id` test const { stdout, stderr } = await __helpers.getCommandOutput( `cargo test owner_not_program_id`, `${__loc}/program` ); assert.include(stdout, 'test owner_not_program_id ... ok'); ``` The program should return the `NotEnoughAccountKeys` variant of `ProgramError` if the number of account keys is less than 1. ```js // Should pass `no_accounts` test const { stdout, stderr } = await __helpers.getCommandOutput( `cargo test no_accounts`, `${__loc}/program` ); assert.include(stdout, 'test no_accounts ... ok'); ``` The program should own a data account for storing a text message of 280 characters. ```js const programKeypair = await __helpers.getProgramKeypair(); assert.exists(programKeypair, 'dist/program/ does not have a .json file'); const programId = programKeypair.publicKey; const connection = __helpers.establishConnection(); assert.exists(connection, 'unable to establish connection to localnet'); const dataAccountPublicKey = await __helpers.getDataAccountPublicKey(); assert.exists(dataAccountPublicKey, 'Unable to get data account public key'); const dataAccountInfo = await connection.getAccountInfo(dataAccountPublicKey); assert.exists(dataAccountInfo, 'Data account does not exist'); assert.equal( dataAccountInfo.owner.toBase58(), programId.toBase58(), 'Data account owner does not match program id' ); ``` The data account should be created using the `wallet.json` public key, `"fcc-seed"` as the seed, and the program id as the owner. ```js const expectedDataAccountPublicKey = await __helpers.getDataAccountPublicKey(); assert.exists( expectedDataAccountPublicKey, 'Unable to get data account public key' ); ``` The program should deserialize the `InstructionData` into a `String`, and store the string in the program data account. ```js // Should pass `instruction_is_deserialized` test const { stdout, stderr } = await __helpers.getCommandOutput( `cargo test instruction_is_deserialized`, `${__loc}/program` ); assert.include(stdout, 'test instruction_is_deserialized ... ok'); ``` If the `InstructionData` is not deserializable into a `String`, the program should return the `InvalidInstructionData` variant of `ProgramError`. ```js // Should pass `instruction_not_string` test const { stdout, stderr } = await __helpers.getCommandOutput( `cargo test instruction_not_string`, `${__loc}/program` ); assert.include(stdout, 'test instruction_not_string ... ok'); ``` If the `String` length is greater than 280 characters, the program should return the `InvalidInstructionData` variant of `ProgramError`. ```js // Should pass `instruction_too_long` test const { stdout, stderr } = await __helpers.getCommandOutput( `cargo test instruction_too_long`, `${__loc}/program` ); assert.include(stdout, 'test instruction_too_long ... ok'); ``` The `InstructionData` should be padded with space characters to 280 characters. ```js // Should pass `instruction_data_padded` test const { stdout, stderr } = await __helpers.getCommandOutput( `cargo test instruction_data_padded`, `${__loc}/program` ); assert.include(stdout, 'test instruction_data_padded ... ok'); ``` You should write a `client/main.js` script interacting with the smart contract. ```js const clientExists = await __helpers.fileExists(join(__loc, 'client')); assert.isTrue(clientExists, 'client/ does not exist'); const mainExists = await __helpers.fileExists(join(__loc, 'client', 'main.js')); assert.isTrue(mainExists, 'client/main.js does not exist'); ``` Calling `node client/main.js` should throw an error with the message `"No message provided"`. ```js const { stdout, stderr } = await __helpers.getCommandOutput( `node client/main.js`, __loc ); assert.include(stderr, 'No message provided'); ``` Calling `node client/main.js "Test string"` should change the message stored in the program data account. ```js const { stdout, stderr } = await __helpers.getCommandOutput( `node client/main.js "Test string"`, __loc ); const connection = __helpers.establishConnection(); assert.exists(connection, 'unable to establish connection to localnet'); const dataAccountPublicKey = await __helpers.getDataAccountPublicKey(); assert.exists(dataAccountPublicKey, 'Unable to get data account public key'); const message = await __helpers.getMessage(connection, dataAccountPublicKey); assert.include(message, 'Test string'); assert.equal(message, 'Test string'.padEnd(280, ' ')); ``` ### --before-all-- ```js global.__loc = 'build-a-smart-contract'; ``` ### --after-all-- ```js delete global.__loc; ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/build-a-university-certification-nft.md ================================================ # Solana - Build a University Certification NFT ## 1 ### --description-- You have been contacted by Solana University to build an NFT that will be used to certify students who have completed their course. You will be building an NFT that will be minted by the university and will be used to certify students who have completed their course. **User Stories** 1. You should generate a new keypair and store it in a file called `solana-university-wallet.json` 2. You should use the `solana-university-wallet.json` keypair as the payer for all transactions 3. You should generate two more keypairs stored in `student-1.json` and `student-2.json` 4. You should deploy the Metaplex Token Metadata program to your local Solana cluster 5. You should create a connection to your local cluster which should be used for all transactions 6. You should use the provided `localStorage` function in `utils.js` for the Metaplex storage driver 7. You should export a function named `uploadFile` from `index.js` with the signature defined in `index.d.ts` 1. `uploadFile` should upload the provided `metaplexFile` parameter to the storage driver 2. `uploadFile` should upload metadata consiting of the image URL returned from the storage driver, and the `fileName` property of the `metaplexFile` 3. `uploadFile` should use the provided `payer` parameter as the fee payer for the metadata upload transaction 8. You should export a function named `createMintAccount` from `index.js` with the signature defined in `index.d.ts` 1. `createMintAccount` should create and initialise a new NFT mint, using the provided `payer` parameter as the fee payer, mint authority, and freeze authority 9. You should export a function named `getMintAccounts` from `index.js` with the signature defined in `index.d.ts` 1. `getMintAccounts` should return all mint accounts owned by the provided `payer` parameter 10. You should export a function named `createTokenAccount` from `index.js` with the signatrure defined in `index.d.ts` 11. `createTokenAccount` should get or create an associated token account for the provided `ownerAddress` parameter 12. `createTokenAccount` should use the provided `payer` parameter to pay for the transaction fee 13. `createTokenAccount` should use the provided `mintAddress` parameter as the mint associated with the token account 14. You should export a function named `mintToken` from `index.js` with the signature defined in `index.d.ts` 1. `mintToken` should mint an NFT to the associated token account of the provided account (`ownerAddress`), using the existing mint account (`mintAddress`) 2. `mintToken` should use the provided `uri` parameter to point to the JSON metadata 3. `mintToken` should use the provided `year` parameter to give the NFT a `name` of `SOL-{year}` 4. `mintToken` should mint an NFT with `0` royalties when resold 5. `mintToken` should mint an NFT with a `symbol` of `SOLU` 6. `mintToken` should mint an NFT that is set to immutable 7. `mintToken` should mint an NFT owned by the associated token account of the provided account (`ownerAddress`) 8. `mintToken` should mint an NFT with an update authority set to the provided `payer` parameter 9. `mintToken` should mint an NFT with an mint authority set to the provided `payer` parameter 15. You should export a function named `getNFTs` from `index.js` with the signature defined in `index.d.ts` 1. `getNFTs` should return all NFTs owned by the provided `ownerAddress` parameter 16. You should use the Solana Univeristy Dashboard (`client/` _see below_) to create a new mint account 1. The `payer` should be the `solana-university-wallet.json` keypair 17. You should use the Solana University Dashboard to create two token accounts associated with the new mint account, and owned by `student-1.json` and `student-2.json` respectively 18. You should use the Solana University Dashboard to upload a metaplex file to the storage driver 1. You can use any image file for this, but one is provided: `solanaLogoMark.png` 19. You should use the Solana University Dashboard to mint one token to each new token account **Types** The expected signatures for your functions are visible in the `index.d.ts` file. This file should **not** be modified. **Commands** | Command | Description | | ---------------------- | ------------------------------------- | | `npm run start:server` | Start the local storage driver | | `npm run start:client` | Start the Solana University dashboard | **Notes** - You should work entirely within the `build-a-university-certification-nft` directory. - You can use provided Solana University dashboard (`client/`) to test and play around with your code. - Useful links to API documentation: - [Solana JS SDK](https://solana-labs.github.io/solana-web3.js/) - [Metaplex JS SDK](https://github.com/metaplex-foundation/js) ### --tests-- You should deploy the Metaplex Token Metadata program to the local Solana cluster. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getProgramAccounts", "params": [ "BPFLoaderUpgradeab1e11111111111111111111111", { "encoding": "base64", "dataSlice": { "length": 0, "offset": 0 } } ] }'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.exists( jsonOut.result.find( r => r.pubkey === 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s' ) ); } catch (e) { assert.fail( e, 'Try running `solana-test-validator --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s ./mlp_token.so --reset`' ); } ``` The `~/.config/solana/cli/config.yml` file should have the URL set to `localhost`. ```js const { stdout } = await __helpers.getCommandOutput('solana config get'); const toMatch = 'RPC URL: http://localhost:8899'; assert.include(stdout, toMatch); ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The local storage driver should be running at `http://localhost:3002/`. ```js try { const res = await fetch('http://localhost:3002/status/ping'); // Response should be 200 with text "pong" if (res.status === 200) { const text = await res.text(); if (text !== 'pong') { throw new Error(`Expected response text "pong", got ${text}`); } } else { throw new Error(`Expected status code 200, got ${res.status}`); } } catch (e) { assert.fail(e); } ``` You should create a new keypair named `student-1.json`. ```js const walletPath = join(__projectDir, 'student-1.json'); const walletJsonExists = __helpers.fileExists(walletPath); assert.isTrue(walletJsonExists, 'The `student-1.json` file should exist'); const walletJson = JSON.parse(await __helpers.getFile(walletPath)); assert.isArray( walletJson, 'The `student-1.json` file should be an array of numbers.\nRun `solana-keygen new --outfile student-1.json` to create a new keypair.' ); ``` You should create a new keypair named `student-2.json`. ```js const walletPath = join(__projectDir, 'student-2.json'); const walletJsonExists = __helpers.fileExists(walletPath); assert.isTrue(walletJsonExists, 'The `student-2.json` file should exist'); const walletJson = JSON.parse(await __helpers.getFile(walletPath)); assert.isArray( walletJson, 'The `student-2.json` file should be an array of numbers.\nRun `solana-keygen new --outfile student-2.json` to create a new keypair.' ); ``` You should create a new keypair named `solana-university-wallet.json`. ```js const walletPath = join(__projectDir, 'solana-university-wallet.json'); const walletJsonExists = __helpers.fileExists(walletPath); assert.isTrue( walletJsonExists, 'The `solana-university-wallet.json` file should exist' ); const walletJson = JSON.parse(await __helpers.getFile(walletPath)); assert.isArray( walletJson, 'The `solana-university-wallet.json` file should be an array of numbers.\nRun `solana-keygen new --outfile solana-university-wallet.json` to create a new keypair.' ); ``` The `index.js` file should export a `uploadFile` function. ```js const { uploadFile } = await __helpers.importSansCache( join(__projectDir, 'index.js') ); assert.isFunction(uploadFile); ``` The `uploadFile` function should match the `index.d.ts` signature definition. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id.name === 'uploadFile'; }); assert.exists( functionDeclaration, 'A function named `uploadFile` should exist' ); const exports = babelisedCode.getType('ExportNamedDeclaration'); const functionIsExported = exports.some(e => { return ( e.declaration?.id?.name === 'uploadFile' || e.specifiers?.find(s => s.exported.name === 'uploadFile') ); }); assert.isTrue( functionIsExported, 'The `uploadFile` function should be exported' ); ``` The `index.js` file should export a `createMintAccount` function. ```js const { createMintAccount } = await __helpers.importSansCache( './' + join(__projectDir, 'index.js') ); assert.isFunction(createMintAccount); ``` The `createMintAccount` function should match the `index.d.ts` signature definition. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id.name === 'createMintAccount'; }); assert.exists( functionDeclaration, 'A function named `createMintAccount` should exist' ); const exports = babelisedCode.getType('ExportNamedDeclaration'); const functionIsExported = exports.some(e => { return ( e.declaration?.id?.name === 'createMintAccount' || e.specifiers?.find(s => s.exported.name === 'createMintAccount') ); }); assert.isTrue( functionIsExported, 'The `createMintAccount` function should be exported' ); ``` The `createMintAccount` function should create a new mint account for an NFT. ```js // `payer` should be payer // `payer` should be mint authority and freeze authority // The mint should have 0 decimal places try { const { createMintAccount } = await __helpers.importSansCache( './' + join(__projectDir, 'index.js') ); const { Keypair, Connection } = await import('@solana/web3.js'); const { TOKEN_PROGRAM_ID } = await import('@solana/spl-token'); const connection = new Connection('http://127.0.0.1:8899', 'finalized'); const payer = Keypair.generate(); async function airdrop() { const airdropSignature = await connection.requestAirdrop( payer.publicKey, 1000000000 ); // Confirm transaction await connection.confirmTransaction(airdropSignature); } await airdrop(); const mint = await createMintAccount({ payer }); const mintAccounts = await connection.getParsedProgramAccounts( TOKEN_PROGRAM_ID, { filters: [ { dataSize: 82 }, { memcmp: { offset: 4, bytes: payer.publicKey.toBase58() } } ] } ); const mintAccount = mintAccounts[0]; assert.exists(mintAccount, 'The mint account should exist'); assert.equal( mintAccount.account.data.parsed.info.mintAuthority, payer.publicKey.toBase58(), 'The mint authority should be the payer' ); assert.equal( mintAccount.account.data.parsed.info.freezeAuthority, payer.publicKey.toBase58(), 'The freeze authority should be the payer' ); assert.equal( mintAccount.account.data.parsed.info.decimals, 0, 'The mint should have 0 decimal places' ); } catch (e) { assert.fail(e); } ``` The `createMintAccount` function should return the `PublicKey` of the mint account. ```js try { const { createMintAccount } = await __helpers.importSansCache( './' + join(__projectDir, 'index.js') ); const { Keypair, Connection } = await import('@solana/web3.js'); const { TOKEN_PROGRAM_ID } = await import('@solana/spl-token'); const connection = new Connection('http://127.0.0.1:8899', 'finalized'); const payer = Keypair.generate(); async function airdrop() { const airdropSignature = await connection.requestAirdrop( payer.publicKey, 1000000000 ); // Confirm transaction await connection.confirmTransaction(airdropSignature); } await airdrop(); const mint = await createMintAccount({ payer }); const mintAccounts = await connection.getParsedProgramAccounts( TOKEN_PROGRAM_ID, { filters: [ { dataSize: 82 }, { memcmp: { offset: 4, bytes: payer.publicKey.toBase58() } } ] } ); const mintAccount = mintAccounts[0]; assert.equal( mintAccount.pubkey.toBase58(), mint.toBase58(), 'The mint account should be returned' ); } catch (e) { assert.fail(e); } ``` The `index.js` file should export a `getMintAccounts` function. ```js const { getMintAccounts } = await __helpers.importSansCache( './' + join(__projectDir, 'index.js') ); assert.isFunction(getMintAccounts); ``` The `getMintAccounts` function should match the `index.d.ts` signature definition. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id.name === 'getMintAccounts'; }); assert.exists( functionDeclaration, 'A function named `getMintAccounts` should exist' ); const exports = babelisedCode.getType('ExportNamedDeclaration'); const functionIsExported = exports.some(e => { return ( e.declaration?.id?.name === 'getMintAccounts' || e.specifiers?.find(s => s.exported.name === 'getMintAccounts') ); }); assert.isTrue( functionIsExported, 'The `getMintAccounts` function should be exported' ); ``` The `getMintAccounts` function should return all mint accounts owned by the `payer` argument. ```js try { const { getMintAccounts, createMintAccount } = await __helpers.importSansCache('./' + join(__projectDir, 'index.js')); const { Keypair, Connection } = await import('@solana/web3.js'); const { createMint } = await import('@solana/spl-token'); const connection = new Connection('http://127.0.0.1:8899', 'finalized'); const payer = Keypair.generate(); async function airdrop() { const airdropSignature = await connection.requestAirdrop( payer.publicKey, 1000000000 ); // Confirm transaction await connection.confirmTransaction(airdropSignature); } await airdrop(); const mintAuthority = payer.publicKey; const freezeAuthority = payer.publicKey; const mint = await createMint( connection, payer, mintAuthority, freezeAuthority, 0 ); const mintAccounts = await getMintAccounts({ payer }); assert.isArray(mintAccounts, '`getMintAccounts` should return an array'); const mintAccount = mintAccounts[0]; assert.exists( mintAccount, 'This test creates a mint account. At least one account should exist' ); assert.equal( mintAccount.pubkey.toBase58(), mint.toBase58(), 'The mint account should match the payer' ); } catch (e) { assert.fail(e); } ``` The `index.js` file should export a `createTokenAccount` function. ```js const { createTokenAccount } = await __helpers.importSansCache( './' + join(__projectDir, 'index.js') ); assert.isFunction(createTokenAccount); ``` The `createTokenAccount` function should match the `index.d.ts` signature definition. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id.name === 'createTokenAccount'; }); assert.exists( functionDeclaration, 'A function named `createTokenAccount` should exist' ); const exports = babelisedCode.getType('ExportNamedDeclaration'); const functionIsExported = exports.some(e => { return ( e.declaration?.id?.name === 'createTokenAccount' || e.specifiers?.find(s => s.exported.name === 'createTokenAccount') ); }); assert.isTrue( functionIsExported, 'The `createTokenAccount` function should be exported' ); ``` The `index.js` file should export a `mintToken` function. ```js const { mintToken } = await __helpers.importSansCache( './' + join(__projectDir, 'index.js') ); assert.isFunction(mintToken); ``` The `mintToken` function should match the `index.d.ts` signature definition. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id.name === 'mintToken'; }); assert.exists(functionDeclaration, 'A function named `mintToken` should exist'); const exports = babelisedCode.getType('ExportNamedDeclaration'); const functionIsExported = exports.some(e => { return ( e.declaration?.id?.name === 'mintToken' || e.specifiers?.find(s => s.exported.name === 'mintToken') ); }); assert.isTrue( functionIsExported, 'The `mintToken` function should be exported' ); ``` The `index.js` file should export a `getNFTs` function. ```js const { getNFTs } = await __helpers.importSansCache( './' + join(__projectDir, 'index.js') ); assert.isFunction(getNFTs); ``` The `getNFTs` function should match the `index.d.ts` signature definition. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id.name === 'getNFTs'; }); assert.exists(functionDeclaration, 'A function named `getNFTs` should exist'); const exports = babelisedCode.getType('ExportNamedDeclaration'); const functionIsExported = exports.some(e => { return ( e.declaration?.id?.name === 'getNFTs' || e.specifiers?.find(s => s.exported.name === 'getNFTs') ); }); assert.isTrue(functionIsExported, 'The `getNFTs` function should be exported'); ``` The `getNFTs` function should return all NFTs owned by the `ownerAddress` argument. ```js try { const { Connection, Keypair } = await import('@solana/web3.js'); const { Metaplex } = await import('@metaplex-foundation/js'); const { getNFTs } = await __helpers.importSansCache( './' + join(__projectDir, 'index.js') ); // Create two NFTs owned by `ownerAddress` const connection = new Connection('http://127.0.0.1:8899', 'finalized'); const payer = Keypair.generate(); const owner = Keypair.generate(); const ownerAddress = owner.publicKey; async function airdrop(acc) { const airdropSignature = await connection.requestAirdrop( acc.publicKey, 1000000000 ); // Confirm transaction await connection.confirmTransaction(airdropSignature); } await airdrop(payer); await airdrop(owner); const metaplex = Metaplex.make(connection); const createResponse = await metaplex.nfts().create( { tokenOwner: ownerAddress, uri: 'http://localhost:1213', name: `Test`, sellerFeeBasisPoints: 0, maxSupply: 1, symbol: 'fCCTest', isMutable: false, updateAuthority: payer, mintAuthority: payer }, { payer } ); // Call `getNFTs` const nfts = await getNFTs({ ownerAddress, payer }); assert.isArray(nfts, '`getNFTs` should return an array'); assert.equal( nfts.length, 1, 'The `getNFTs` function should return all NFTs owned by the `ownerAddress` argument' ); const createResponse2 = await metaplex.nfts().create( { tokenOwner: ownerAddress, uri: 'http://localhost:1213', name: `Test2`, sellerFeeBasisPoints: 0, maxSupply: 1, symbol: 'fCCTest', isMutable: false, updateAuthority: payer, mintAuthority: payer }, { payer } ); const nfts2 = await getNFTs({ ownerAddress, payer }); assert.isArray(nfts2, '`getNFTs` should return an array'); assert.equal( nfts2.length, 2, 'The `getNFTs` function should return all NFTs owned by the `ownerAddress` argument' ); } catch (e) { assert.fail(e); } ``` ### --before-all-- ```js const __projectDir = 'build-a-university-certification-nft'; const codeString = await __helpers.getFile( './' + join(__projectDir, 'index.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.__projectDir = __projectDir; global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.__projectDir; delete global.babelisedCode; ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/build-an-anchor-leaderboard.md ================================================ # Solana - Build an Anchor Leaderboard ## 1 ### --description-- You are developing an on-chain game called _Rock Destroyer_. You will be writing the program logic for the game leaderboard using the Anchor framework, as well as writing tests to ensure the program logic is correct. You will be working entirely within the `build-an-anchor-leaderboard/rock-destroyer` directory. The `rock-destroyer` directory is an Anchor boilerplate project with a front-end already set up. ### User Stories #### Setup 1. You should generate a new keypair and store it in a file called `game-owner.json`. 2. You should add the correct program id to the `programs.localnet.rock_destroyer` key in the `Anchor.toml` file. #### Program 1. You should add the correct program id to the `declare_id!` call in the `lib.rs` file. **`initialize_leaderboard`** 1. The `rock_destroyer` program should expose an `initialize_leaderboard` instruction handler. 2. The `initialize_leaderboard` instruction handler should take a context generic over an `InitializeLeaderboard` accounts struct. 3. The `initialize_leaderboard` instruction handler should initialize the `leaderboard` account with the `players` field set to an empty vector. **`InitializeLeaderboard`** 1. The `leaderboard` account should be initialized, if it does not already exist. - This should be payed for by the `game_owner` account - The correct amount of space for 5 players should be allocated - The PDA should be seeded with `"leaderboard"` and the `game_owner` public key 2. The `game_owner` account should be a signer. - The following constraints should be enforced: - The account should be mutable - The account public key should match the `game-owner.json` file public key - The account owner should be the system program **`new_game`** 1. The `rock_destroyer` program should expose a `new_game` instruction handler. 2. The `new_game` instruction handler should take a context generic over a `NewGame` accounts struct. 3. The `new_game` instruction handler should take a `String` argument. 4. The `new_game` instruction handler should transfer 1 SOL from the `user` account to the `game_owner` account.[^1] 5. The `new_game` instruction handler should add a new `Player` to the leaderboard with: - `username` set to the `String` argument - `pubkey` set to the `user` account public key - `score` set to `0` - `has_payed` set to `true` 6. If the leaderboard is full, the player with the lowest score should be replaced. **`NewGame`** 1. The `user` account should be a signer. - The following constraints should be enforced: - The account should be mutable 2. The `game_owner` account should be an unchecked account. - The following constraints should be enforced: - The account should be mutable - The account public key should match the `game-owner.json` file public key - The account owner should be the system program 3. The `leaderboard` account should be mutable. **`add_player_to_leaderboard`** 1. The `rock_destroyer` program should expose an `add_player_to_leaderboard` instruction handler. 2. The `add_player_to_leaderboard` instruction handler should take a context generic over an `AddPlayerToLeaderboard` accounts struct. 3. The `add_player_to_leaderboard` instruction handler should take a `u64` argument. 4. The player matching the user account public key should be updated with: - `score` set to the `u64` argument - `has_payed` set to `false` 5. If no player matching the user account public key exists and has payed, an Anchor error variant of `PlayerNotFound` should be returned. **`AddPlayerToLeaderboard`** 1. The `leaderboard` account should be mutable. 2. The `user` account should be a signer. - The following constraints should be enforced: - The account should be mutable #### Tests 1. There should be an `it` block named `"initializes leaderboard"`. - Call the `initialize_leaderboard` instruction - Assert the `leaderboard` account equals `{ players: [] }` 2. There should be an `it` block named `"creates a new game"`. - Call the `new_game` instruction with a `username` argument of `"camperbot"` - Assert the `leaderboard` account has at least one player - Assert the player has the correct `username` - Assert the player has the correct `pubkey` - Assert the player has a `hasPayed` value of `true` - Assert the player has a `score` value of `0` - Assert the balance of the `user` account has decreased by at least 1 SOL (_remember transaction fees_) 3. There should be an `it` block named `"adds a player to the leaderboard"`. - Call the `add_player_to_leaderboard` instruction with an argument of `100` - Assert a player has a `score` value of `100` - Assert a player has a `hasPayed` value of `false` 4. There should be an `it` block named `" - Assert the `PlayerNotFound` error variant is returned when the `user` account has not payed #### Types
InitializeLeaderboard ```rust leaderboard: Account<'info, Leaderboard>, game_owner: Signer<'info>, system_program: Program<'info, System>, ```
NewGame ```rust user: Signer<'info>, game_owner: AccountInfo<'info>, leaderboard: Account<'info, Leaderboard>, system_program: Program<'info, System>, ```
AddPlayerToLeaderboard ```rust leaderboard: Account<'info, Leaderboard>, user: Signer<'info>, ```
Leaderboard ```rust players: Vec ```
Player ```rust username: String, // max length 32 pubkey: Pubkey, score: u64, has_payed: bool, ```
### Notes - You should not add any external dependencies to the `package.json` file for the tests - You have access to `chai` - Many tests rely on previous user stories being correctly implemented [^1]: Hint: You can use the `transfer` function from the `system_instruction` module in the `solana_program` crate. ### --tests-- You should generate a new keypair and store it in a file called `game-owner.json`. ```js try { const fileExists = await __fsp.access( __path.join(__projectDir, './game-owner.json'), __fsp.constants.F_OK ); } catch (e) { assert.fail(e); } ``` You should add the correct program id to the `programs.localnet.rock_destroyer` key in the `Anchor.toml` file. ```js const anchorToml = await __fsp.readFile( __path.join(__projectDir, 'Anchor.toml'), 'utf-8' ); const actualProgramId = anchorToml.match(/rock_destroyer = "(.*)"/)?.[1]; const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', __projectDir ); const expectedProgramId = stdout.match(/rock_destroyer: (.*)/)?.[1]; assert.equal(actualProgramId, expectedProgramId); ``` You should add the correct program id to the `declare_id!` call in the `lib.rs` file. ```js const librs = await __fsp.readFile( __path.join(__projectDir, 'programs/rock-destroyer/src/lib.rs'), 'utf-8' ); const actualProgramId = librs.match(/declare_id\!\((.*)\)/)?.[1]; const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', __projectDir ); const expectedProgramId = stdout.match(/rock_destroyer: (.*)/)?.[1]; assert.equal(actualProgramId.replaceAll(/['"`]/g), expectedProgramId); ``` The `rock_destroyer` program should expose an `initialize_leaderboard` instruction handler. ```js const testDir = await __createTestDir(4); await __buildTestDir(4); const { RockDestroyer } = await __helpers.importSansCache( __path.join(testDir, 'target/types/rock_destroyer') ); const ixs = RockDestroyer.instructions; const initializeLeaderboardIx = ixs.find( ix => ix.name === 'initializeLeaderboard' ); assert.exists( initializeLeaderboardIx, 'The `RockDestroyer` object in `target/types/rock_destroyer` should have an `instructions[].name` property equal to `initializeLeaderboard`' ); ``` The `initialize_leaderboard` instruction handler should take a context generic over an `InitializeLeaderboard` accounts struct. ```js const testDir = await __createTestDir(5); await __buildTestDir(5); const { RockDestroyer } = await __helpers.importSansCache( __path.join(testDir, 'target/types/rock_destroyer') ); const ixs = RockDestroyer.instructions; const initializeLeaderboardIx = ixs.find( ix => ix.name === 'initializeLeaderboard' ); const accounts = initializeLeaderboardIx.accounts; assert.deepInclude(accounts, { name: 'leaderboard', isMut: true, isSigner: false }); assert.deepInclude(accounts, { name: 'gameOwner', isMut: true, isSigner: true }); assert.deepInclude(accounts, { name: 'systemProgram', isMut: false, isSigner: false }); ``` The `rock_destroyer` program should expose a `new_game` instruction handler. ```js const testDir = await __createTestDir(4); await __buildTestDir(4); const { RockDestroyer } = await __helpers.importSansCache( __path.join(testDir, 'target/types/rock_destroyer') ); const ixs = RockDestroyer.instructions; const newGameIx = ixs.find(ix => ix.name === 'newGame'); assert.exists( newGameIx, 'The `RockDestroyer` object in `target/types/rock_destroyer` should have an `instructions[].name` property equal to `newGame`' ); ``` The `new_game` instruction handler should take a context generic over a `NewGame` accounts struct. ```js const testDir = await __createTestDir(6); await __buildTestDir(6); const { RockDestroyer } = await __helpers.importSansCache( __path.join(testDir, 'target/types/rock_destroyer') ); const ixs = RockDestroyer.instructions; const newGameIx = ixs.find(ix => ix.name === 'newGame'); const accounts = newGameIx.accounts; assert.deepInclude(accounts, { name: 'user', isMut: true, isSigner: true }); assert.deepInclude(accounts, { name: 'leaderboard', isMut: true, isSigner: false }); assert.deepInclude(accounts, { name: 'gameOwner', isMut: true, isSigner: false }); assert.deepInclude(accounts, { name: 'systemProgram', isMut: false, isSigner: false }); ``` The `rock_destroyer` program should expose an `add_player_to_leaderboard` instruction handler. ```js const testDir = await __createTestDir(4); await __buildTestDir(4); const { RockDestroyer } = await __helpers.importSansCache( __path.join(testDir, 'target/types/rock_destroyer') ); const ixs = RockDestroyer.instructions; const initializeLeaderboardIx = ixs.find( ix => ix.name === 'addPlayerToLeaderboard' ); assert.exists( initializeLeaderboardIx, 'The `RockDestroyer` object in `target/types/rock_destroyer` should have an `instructions[].name` property equal to `addPlayerToLeaderboard`' ); ``` The `add_player_to_leaderboard` instruction handler should take a context generic over an `AddPlayerToLeaderboard` accounts struct. ```js const testDir = await __createTestDir(7); await __buildTestDir(7); const { RockDestroyer } = await __helpers.importSansCache( __path.join(testDir, 'target/types/rock_destroyer') ); const ixs = RockDestroyer.instructions; const ix = ixs.find(ix => ix.name === 'addPlayerToLeaderboard'); const accounts = ix.accounts; assert.deepInclude(accounts, { name: 'leaderboard', isMut: true, isSigner: false }); assert.deepInclude(accounts, { name: 'user', isMut: true, isSigner: true }); ``` There should be an `it` block named `"initializes leaderboard"`. ```js const callExpressions = babelisedCode .getType('CallExpression') .filter(c => { return; c.callee?.name === 'it'; }) .map(c => c.arguments?.[1]?.value); assert.include(callExpressions, 'initializes leaderboard'); ``` There should be an `it` block named `"creates a new game"`. ```js const callExpressions = babelisedCode .getType('CallExpression') .filter(c => { return; c.callee?.name === 'it'; }) .map(c => c.arguments?.[1]?.value); assert.include(callExpressions, 'creates a new game'); ``` There should be an `it` block named `"adds a player to the leaderboard"`. ```js const callExpressions = babelisedCode .getType('CallExpression') .filter(c => { return; c.callee?.name === 'it'; }) .map(c => c.arguments?.[1]?.value); assert.include(callExpressions, 'adds a player to the leaderboard'); ``` There should be an `it` block named `"throws an error when the user has not payed"`. ```js const callExpressions = babelisedCode .getType('CallExpression') .filter(c => { return; c.callee?.name === 'it'; }) .map(c => c.arguments?.[1]?.value); assert.include(callExpressions, 'throws an error when the user has not payed'); ``` ### --before-all-- ```js const __fsp = await import('fs/promises'); const __path = await import('path'); const __projectDir = 'build-an-anchor-leaderboard/_answer/rock-destroyer'; const __testDir = 'build-an-anchor-leaderboard/__test/rock-destroyer'; const codeString = await __helpers.getFile( './' + join(__projectDir, 'tests/index.ts') ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); async function __createTestDir(num) { const testDir = `${__testDir}-${num}`; // Remove old test dir logover.debug('Removing old test dir'); await __fsp.rm(testDir, { recursive: true, force: true }); // Create new test dir logover.debug('Creating new test dir'); await __fsp.cp(__projectDir, testDir, { recursive: true }); return testDir; } async function __buildTestDir(num) { const { stdout, stderr } = await __helpers.getCommandOutput( 'anchor build', `${__testDir}-${num}` ); if (stderr) { throw new Error(stderr); } return stdout; } global.__projectDir = __projectDir; global.__testDir = __testDir; global.__fsp = __fsp; global.__path = __path; global.__buildTestDir = __buildTestDir; global.__createTestDir = __createTestDir; global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.__projectDir; delete global.__testDir; delete global.__fsp; delete global.__path; delete global.babelisedCode; delete global.__buildTestDir; delete global.__createTestDir; ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/build-and-deploy-your-freeform-app.md ================================================ # Solana - Build and Deploy Your Freeform App ## 1 ### --description-- Congratulations on making it to the final project for this course! 🚀 🧨 🌟 You have learnt a lot about the Solana ecosystem, and how to build on it. Now, it is time for you to test your skills and build an app of your own. Once you are done, feel free to share a link to it on Twitter, and `@freeCodeCamp`. You can use whatever tools you want to build your app, and you are encouraged to explore the library ecosystem to find the best tools for the job. - You can use to develop most of your dApp in your browser. ### Concepts Covered - Accounts - Program (executable) accounts - State (data) accounts - Serialization and Deserialization - Rent - Tokens - Fungible tokens - Non-fungible tokens - Creating and Minting - Metadata - Transactions - Instructions - Signatures ### Tools Covered - Solana CLI - `@solana/web3.js` - `@solana/spl-token` - Anchor CLI - `@coral-xyz/anchor` - `@metaplex-foundation/js` ### Project Ideas - A Twitter clone - A blogging platform - An NFT marketplace - A decentralized exchange - A CLI game ### --tests-- Once you are done, enter `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-anchor-by-building-tic-tac-toe-part-1.md ================================================ # Solana - Learn Anchor by Building Tic-Tac-Toe: Part 1 ## 1 ### --description-- Previously, you built and deployed a program (smart contract) to the Solana blockchain using the native `solana_program` crate. In this project, you will use the Anchor framework to build and deploy a program to the Solana blockchain. Anchor offers quick and convenient tools and modules for building and deploying programs. Open a new terminal, and install the Anchor Version Manager (AVM) to get started. ```bash cargo install --git https://github.com/coral-xyz/anchor avm --locked --force ``` ### --tests-- You should have `avm` installed. ```js const { stdout } = await __helpers.getCommandOutput('avm --version'); assert.include(stdout, 'avm'); ``` ## 2 ### --description-- Anchor Version Manager is a tool for using multiple versions of the Anchor CLI. Use `avm` to install the Anchor CLI. ```bash avm install 0.28.0 ``` ### --tests-- You should have version `0.28.0` of the Anchor CLI installed. ```js const { stdout } = await __helpers.getCommandOutput('avm list'); assert.include(stdout, 'installed, current)'); ``` ## 3 ### --description-- Instruct `avm` to use the latest version of the Anchor CLI: ```bash avm use 0.28.0 ``` ### --tests-- You should be using version `0.28.0` of the Anchor CLI. ```js const { stdout } = await __helpers.getCommandOutput('avm list'); assert.include(stdout, 'installed, current)'); ``` ## 4 ### --description-- Verify you are using the latest version of the Anchor CLI: ```bash anchor --version ``` ### --tests-- You should see version `0.28` printed to the console. ```js const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, '0.28'); ``` ## 5 ### --description-- You will be building a Tic-Tac-Toe game on the Solana blockchain. Within `learn-anchor-by-building-tic-tac-toe-part-1/`, create a new project named `tic-tac-toe`: ```bash anchor init --no-git tic-tac-toe ``` **Note:** The `--no-git` flag is used to prevent the project from being initialized as a git repository. ### --tests-- You should be in the `learn-anchor-by-building-tic-tac-toe-part-1` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/?$`); assert.match(cwd, dirRegex); ``` You should run `anchor init --no-git tic-tac-toe` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal(lastCommand.trim(), 'anchor init --no-git tic-tac-toe'); ``` You should have a `tic-tac-toe` directory. ```js const exists = __helpers.fileExists(`${project.dashedName}/tic-tac-toe`); assert.isTrue(exists); ``` ## 6 ### --description-- Anchor has created a `tic-tac-toe` directory with the following structure: ```bash tic-tac-toe/ ├── app/ ├── migrations/ │ └── deploy.ts ├── programs/ │ └── tic-tac-toe/ │ ├── src/ │ │ └── lib.rs │ ├── Cargo.toml │ └── Xargo.toml ├── tests/ │ └── tic-tac-toe.ts ├── Anchor.toml ├── Cargo.toml ├── package.json ├── tsconfig.json └── yarn.lock ``` In your terminal, change into the `tic-tac-toe` directory. ### --tests-- You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` ## 7 ### --description-- The `app` directory is a placeholder for a web app that would interact with the program. The `migrations/deploy.ts` script is run on `anchor migrate`. The `programs` directory contains all the programs (smart contracts) that will be deployed to the Solana blockchain. The `tests` directory contains the client-side tests for your programs. You will be mostly working in `programs/tic_tac_toe/src/lib.rs`. Get the program id (public key) of the `tic-tac-toe` program: ```bash anchor keys list ``` ### --tests-- The public key of your `tic-tac-toe` program should be printed to the console. ```js const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/tic-tac-toe` ); const publicKey = stdout.match(/[^\s]{44}/)[0]; const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, publicKey); ``` You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` ## 8 ### --description-- The Anchor CLI provides an `anchor test` command that: 1. Builds all programs 2. Starts a local Solana cluster 3. Deploys the programs to the cluster 4. Calls the `test` script in the `scripts` table in `Anchor.toml` 5. Cleans up the local Solana cluster Run the `anchor test` command: ```bash anchor test ``` ### --tests-- The `anchor test` command should error ❌. ```js const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, 'Error: failed to send transaction'); ``` You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` ## 9 ### --description-- You should see the following error: ```bash Error: failed to send transaction: Transaction simulation failed: This program may not be used for executing instructions ``` For your program, you will need to manually start a local Solana validator. Do so, in a new terminal. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 10 ### --description-- With the local validator running, pass the `--skip-local-validator` flag to tell Anchor to not start its own local validator when running tests. ### --tests-- The `anchor test --skip-local-validator` command should error. ```js const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, 'Error: AnchorError occurred.'); ``` You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 11 ### --description-- You should see the following error: ```bash Error: AnchorError occurred. Error Code: DeclaredProgramIdMismatch. Error Number: 4100. Error Message: The declared program id does not match the actual program id. ``` Double-check the program id in the `Anchor.toml` file. Run `anchor keys list` again to see if it matches the `programs.localnet.tic_tac_toe` value. ### --tests-- You should run `anchor keys list` and see the program id printed to the console. ```js const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/tic-tac-toe` ); const publicKey = stdout.match(/[^\s]{44}/)?.[0]; const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, publicKey); ``` You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` ## 12 ### --description-- Copy the program id, and replace the default values with it in two locations: 1. The string value within the `declare_id` macro in `programs/tic-tac-toe/src/lib.rs` 2. The `tic_tac_toe` key within `Anchor.toml` ### --tests-- The `lib.rs` file should contain the program id within the `declare_id!()` call. ```js const librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/tic-tac-toe` ); const expectedProgramId = stdout.match(/[^\s]{44}/)?.[0]; const actualProgramId = librs.match(/declare_id!\("([^\)]+)"\)/)?.[1]; assert.equal(actualProgramId, expectedProgramId); ``` The `Anchor.toml` file should contain the program id as the value for the `tic_tac_toe` key. ```js const toml = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/Anchor.toml` ); const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/tic-tac-toe` ); const expectedProgramId = stdout.match(/[^\s]{44}/)?.[0]; const actualProgramId = toml.match(/tic_tac_toe = "([^\"]+)"/)?.[1]; assert.equal(actualProgramId, expectedProgramId); ``` ## 13 ### --description-- Run the test command again: ```bash anchor test --skip-local-validator ``` ### --tests-- The `anchor test --skip-local-validator` command should succeed. ```js const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, '1 passing'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 14 ### --description-- Shifting focus to the `lib.rs` file, you will see a few similarities to the native Solana program development workflow. Instead of an entrypoint function, the `program` attribute defines the module containing all instruction handlers defining all entries into a Solana program. The `initialize` function is an instruction handler. It is a function that takes a `Context` as an argument. The context contains the program id, and the accounts passed into the function. Anchor expects all accounts to be fully declared as inputs to the handler. Rename the `initialize` function to `setup_game`. ### --tests-- The `setup_game` function should exist in the `lib.rs` file. ```js assert.match(__librs, /fn setup_game/); ``` The `initialize` function should not exist in the `lib.rs` file. ```js assert.notMatch(__librs, /fn initialize/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 15 ### --description-- The `Initialize` struct is annotated to derive `Accounts`. This means that the `Initialize` struct will be used to deserialize the accounts passed into the `setup_game` function. If a client does not pass in the correct accounts, the deserialization will fail. This is one of the ways that Anchor ensures that the client is passing in the correct accounts. Rename the `Initialize` struct to `SetupGame`. ### --tests-- The `SetupGame` struct should exist in the `lib.rs` file. ```js assert.match(__librs, /struct SetupGame/); ``` The `setup_game` function should take a `Context` as an argument. ```js assert.match(__librs, /pub fn setup_game/); assert.match(__librs, /ctx: Context/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 16 ### --description-- Setting up the game will require creating an account to store the game state. This account will be owned by the system program, and will be created with a program derived address (PDA). You could manually calculate the rent required, validate a passed in account can pay, create the account, and send the create transaction. However, Anchor provides a convenient attribute macro to automate this process: ```rust #[derive(Accounts)] pub struct AccountsInContext<'info> { #[account(init)] pub derived_account: Account<'info, AccountStruct> } ``` The `#[account()]` attribute with an `init` parameter will create the account when required. The `AccountStruct` is a struct that will be used to deserialize the account data. The `AccountStruct` must implement the `AnchorSerialize` and `AnchorDeserialize` traits. Within `SetupGame`, add a public field `game` with a type of `Account<'info, Game>`. Annotate the field such that it is initialized when required. ### --tests-- `SetupGame` should contain a field `game`. ```js assert.match(__librs, /pub game:/); ``` `game` should be typed `Account<'info, Game>`. ```js assert.match(__librs, /pub game: Account<'info\s*,\s*Game>/); ``` `game` should be annotated with `#[account(init)]`. ```js assert.match(__librs, /#\[\s*account\s*\(\s*init\s*\)\s*\]\s*pub game:/); ``` `SetupGame` should be punctuated with a lifetime `'info`. ```js assert.match(__librs, /pub struct SetupGame\s*<'info>/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 17 ### --description-- When an account is initialized, another account must pay for the rent and transaction fees. Declare another account in `SetupGame` called `player_one`. Give `player_one` a type of `Signer<'info>`. **Note:** The `Signer` trait is a special trait that indicates the account is a signer. This is required for lamports to be transferred **from** the account. ### --tests-- `SetupGame` should contain a field `player_one`. ```js assert.match(__librs, /pub player_one:/); ``` `player_one` should be typed `Signer<'info>`. ```js assert.match(__librs, /pub player_one: Signer\s*<'info>/); ``` `player_one` should be annotated with `#[account()]`. ```js assert.match(__librs, /#\[\s*account\s*\(\s*\)\s*\]\s*pub player_one:/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 18 ### --description-- Now, mark the `player_one` account as the payer for the `game` account: ```rust #[derive(Accounts)] pub struct AccountsInContext<'info> { #[account( init, payer = payer_account )] pub derived_account: Account<'info, AccountStruct>, #[account()] pub payer_account: Signer<'info> } ``` ### --tests-- `game` should be annotated with `payer = player_one`. ```js const librs = ( await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ) )?.replaceAll(/[ \t]{2,}/g, ' '); assert.match( librs, /#\[\s*account\s*\(\s*init\s*,\s*payer\s*=\s*player_one\s*\)\s*\]\s*pub game:/ ); ``` ## 19 ### --description-- In order for any data in an account to be changed, the account must be mutable: ```rust #[account(mut)] pub mutable_account: Signer<'info> ``` Mark the `player_one` account as mutable. ### --tests-- `player_one` should be annotated with `#[account(mut)]`. ```js const librs = ( await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ) )?.replaceAll(/[ \t]{2,}/g, ' '); assert.match(librs, /#\[\s*account\s*\(\s*mut\s*\)\s*\]\s*pub player_one:/); ``` ## 20 ### --description-- The `#[account(init)]` attribute will create the account when required. However, the account must be rent exempt, and have enough space to store any data expected: ```rust #[account(init, space = )] pub derived_account: Account<'info, AccountStruct> ``` Add a `space` parameter to the `game` account with a value of `10`. This means the account will be initialised with 10 bytes of space. ### --tests-- `game` should be annotated with `space = 10`. ```js const librs = ( await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ) )?.replaceAll(/[ \t]{2,}/g, ' '); assert.match( librs, /#\[\s*account\s*\([^\)]*?space\s*=\s*10[^\)]*?\)]\s*pub game:/ ); ``` ## 21 ### --description-- Creating an account with `10` bytes allocated is great and all, but you do not actually know how much space you need until you have defined the `Game` struct representing the account data. Define a public struct `Game`, and annotate it with `#[account]` to indicate it is a Solana account. ### --tests-- `pub struct Game` should exist in the `lib.rs` file. ```js assert.match(__librs, /pub struct Game/); ``` `Game` should be annotated with `#[account]`. ```js assert.match(__librs, /#\[\s*account\s*\]\s*pub struct Game/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 22 ### --description-- A game of tic-tac-toe consists of two players. Keep track of the players, by adding a `players` field in `Game` with a type of `[Pubkey; 2]`. ### --tests-- `Game` should contain a field `players`. ```js const game = __librs.match(/pub struct Game\s*{([^}]*)}/s)?.[1]; assert.match(game, /players:/); ``` `players` should be typed `[Pubkey; 2]`. ```js assert.match(__librs, /players: \[\s*Pubkey\s*;\s*2\s*\]/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 23 ### --description-- Keep track of the current turn number, by adding a `turn` field in `Game` with a type of `u8`. ### --tests-- `Game` should contain a field `turn`. ```js const game = __librs.match(/pub struct Game\s*{([^}]*)}/s)?.[1]; assert.match(game, /turn:/); ``` `turn` should be typed `u8`. ```js assert.match(__librs, /turn: u8/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 24 ### --description-- Keep track of the board state (the value of each tile), by adding a `board` field in `Game` with a type of `[[Option; 3]; 3]`. ### --tests-- `Game` should contain a field `board`. ```js const game = __librs.match(/pub struct Game\s*{([^}]*)}/s)?.[1]; assert.match(game, /board:/); ``` `board` should be typed `[[Option; 3]; 3]`. ```js assert.match( __librs, /board: \[\[\s*Option\s*<\s*Sign\s*>\s*;\s*3\s*\]\s*;\s*3\s*\]/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 25 ### --description-- Keep track of the current game condition, by adding a `state` field in `Game` with a type of `GameState`. ### --tests-- `Game` should contain a field `state`. ```js const game = __librs.match(/pub struct Game\s*{([^}]*)}/s)?.[1]; assert.match(game, /state:/); ``` `state` should be typed `GameState`. ```js assert.match(__librs, /state: GameState/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 26 ### --description-- Define a public enum `Sign` with variants `X` and `O`. ### --tests-- `pub enum Sign` should exist in the `lib.rs` file. ```js assert.match(__librs, /pub enum Sign/); ``` `Sign` should have a variant `X`. ```js const sign = __librs.match(/pub enum Sign\s*{([^}]*)}/s)?.[1]; assert.match(sign, /X/); ``` `Sign` should have a variant `O`. ```js const sign = __librs.match(/pub enum Sign\s*{([^}]*)}/s)?.[1]; assert.match(sign, /O/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 27 ### --description-- Define a public enum `GameState` with variants `Active`, `Tie`, and `Won`. The `Won` variant should contain a named field `winner` with a type of `Pubkey`. ### --tests-- `pub enum GameState` should exist in the `lib.rs` file. ```js assert.match(__librs, /pub enum GameState/); ``` `GameState` should have a variant `Active`. ```js const gameState = __librs.match(/pub enum GameState\s*{([^}]*)}/s)?.[1]; assert.match(gameState, /Active/); ``` `GameState` should have a variant `Tie`. ```js const gameState = __librs.match(/pub enum GameState\s*{([^}]*)}/s)?.[1]; assert.match(gameState, /Tie/); ``` `GameState` should have a variant `Won { winner: Pubkey }`. ```js const gameState = __librs.match( /pub enum GameState ({[\s\S]*?({[\s\S]*?}[\s\S]*?}|}))/s )?.[1]; assert.match(gameState, /Won\s*{\s*winner:\s*Pubkey\s*,?\s*}/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 28 ### --description-- In order for Anchor to serialize and deserialize the `Game` account data, `GameState` and `Sign` must implement the `AnchorSerialize` and `AnchorDeserialize` traits. Derive the `AnchorSerialize` and `AnchorDeserialize` traits for `GameState` and `Sign`. ### --tests-- `GameState` should be annotated with `#[derive(AnchorSerialize, AnchorDeserialize)]`. ```js assert.match( __librs, /#\[\s*derive\s*\(\s*AnchorSerialize\s*,\s*AnchorDeserialize\s*\)\s*\]\s*pub enum GameState/ ); ``` `Sign` should be annotated with `#[derive(AnchorSerialize, AnchorDeserialize)]`. ```js assert.match( __librs, /#\[\s*derive\s*\(\s*AnchorSerialize\s*,\s*AnchorDeserialize\s*\)\s*\]\s*pub enum Sign/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 29 ### --description-- On top of `AnchorSerialize` and `AnchorDeserialize`, both `GameState` and `Sign` must also implement the `Clone` trait. Derive the `Clone` trait for `GameState` and `Sign`. ### --tests-- `GameState` should be annotated with `#[derive(Clone)]`. ```js assert.match( __librs, /#\[\s*derive\s*\([^\]]*?Clone[^\]]*?\)\s*\]\s*pub enum GameState/ ); ``` `Sign` should be annotated with `#[derive(Clone)]`. ```js assert.match( __librs, /#\[\s*derive\s*\([^\]]*?Clone[^\]]*?\)\s*\]\s*pub enum Sign/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 30 ### --description-- Finally, because `Sign` is within a slice, it must also implement the `Copy` trait. Derive the `Copy` trait for `Sign`. ### --tests-- `Sign` should be annotated with `#[derive(Copy)]`. ```js const librs = ( await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ) )?.replaceAll(/[ \t]{2,}/g, ' '); assert.match( librs, /#\[\s*derive\s*\([^\]]*?Copy[^\]]*?\)\s*\]\s*pub enum Sign/ ); ``` ## 31 ### --description-- In order for an account to be created, the `System` program must be used. The `System` program is a built-in program that is available to all Solana programs, but must be annotated as needed in the context. Add a public `system_program` field to the `SetupGame` struct, and type it as `Program<'info, System>`. ### --tests-- `SetupGame` should contain a field `system_program`. ```js const setupGame = __librs.match( /pub struct SetupGame\s*<'info\s*>\s*{([^}]*?)}/s )?.[1]; assert.match(setupGame, /system_program:/); ``` `system_program` should be typed `Program<'info, System>`. ```js const setupGame = __librs.match( /pub struct SetupGame\s*<'info\s*>\s*{([^}]*)}/s )?.[1]; assert.match(setupGame, /system_program: Program\s*<\s*'info\s*,\s*System\s*>/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 32 ### --description-- Now that `Game` is fully defined, its size can be determined. Doing so involves summing the size of each field, and adding 8 bytes for the account discriminator. For `Game`: | Field | Unit Size | Quantity | Total Size | | ------- | --------- | -------- | ---------- | | players | 32 | 2 | 64 | | turn | 1 | 1 | 1 | | board | 1 + 1 | 3 \* 3 | 18 | | state | 1 + 32 | 1 | 33 | Anchor provides a table of sizes for each Rust type: `https://www.anchor-lang.com/docs/space` Replace the `10` bytes allocated for the `Game` account with the correct size. ### --tests-- The `game` field in `SetupGame` should be annotated with `#[account(space = 8 + (32*2) + (1) + ((1+1)*(3*3)) + (1+32))]`. ```js const librs = ( await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ) )?.replaceAll(/[ \t]{2,}/g, ' '); const setupGame = librs.match( /pub struct SetupGame\s*<'info\s*>\s*{([^}]*)}/s )?.[1]; const mat = setupGame?.match( /#\[\s*account\s*\([^\]]*?space\s*=\s*([^\]]+?)\s*\)\s*\]\s*pub game:/ )?.[1]; assert.exists( mat, `game field should be annotated with #[account(space = )]` ); const math = eval(mat); assert.equal( math, 8 + 32 * 2 + 1 + (1 + 1) * (3 * 3) + (1 + 32), `space should sum up to correct size` ); ``` ## 33 ### --description-- **ATTENTION**: Your `lib.rs` file should have been seeded with all the game code. The game code is not relevant to Anchor, but you are still encouraged to read through it to understand how the game works. Just a few things to fix. First, derive `PartialEq` for `GameState` and `Sign`. ### --tests-- `GameState` should be annotated with `#[derive(PartialEq)]`. ```js assert.match( __librs, /#\[\s*derive\s*\([^\]]*?PartialEq[^\]]*?\)\s*\]\s*pub enum GameState/ ); ``` `Sign` should be annotated with `#[derive(PartialEq)]`. ```js assert.match( __librs, /#\[\s*derive\s*\([^\]]*?PartialEq[^\]]*?\)\s*\]\s*pub enum Sign/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --force-- #### --"learn-anchor-by-building-tic-tac-toe-part-1/tic-tac-toe/programs/tic-tac-toe/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("BUfb6FXLkiSpMnJnMR4Q5uGZYZkaNGytjhLwiiJQsE8F"); #[program] pub mod tic_tac_toe { use super::*; pub fn setup_game(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct SetupGame<'info> { #[account(init, payer = player_one, space = 8 + Game::MAXIMUM_SIZE)] pub game: Account<'info, Game>, #[account(mut)] pub player_one: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct Game { players: [Pubkey; 2], // (32 * 2) turn: u8, // 1 board: [[Option; 3]; 3], // 9 * (1 + 1) = 18 state: GameState, // 32 + 1 } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub enum GameState { Active, Tie, Won { winner: Pubkey }, } #[derive( AnchorSerialize, AnchorDeserialize, Copy, Clone, )] pub enum Sign { X, O, } /// A tile on the game board. pub struct Tile { row: u8, column: u8, } impl Game { pub const MAXIMUM_SIZE: usize = (32 * 2) + 1 + ((1 + 1) * 9) + (1 + 32); pub fn start(&mut self, players: [Pubkey; 2]) -> Result<()> { // TODO: Ensure the game is not already started. self.players = players; self.turn = 1; Ok(()) } pub fn is_active(&self) -> bool { self.state == GameState::Active } fn current_player_index(&self) -> usize { ((self.turn - 1) % 2) as usize } pub fn current_player(&self) -> Pubkey { self.players[self.current_player_index()] } pub fn play(&mut self, tile: &Tile) -> Result<()> { // TODO: Ensure the game is active. match tile { tile @ Tile { row: 0..=2, column: 0..=2, } => match self.board[tile.row as usize][tile.column as usize] { Some(_) => { // TODO: Return an error that the tile is already set. return Err(); }, None => { self.board[tile.row as usize][tile.column as usize] = Some(Sign::from_usize(self.current_player_index()).unwrap()); } }, _ => { // TODO: Return an error that the tile is out of bounds. return Err(); }, } self.update_state(); if GameState::Active == self.state { self.turn += 1; } Ok(()) } fn is_winning_trio(&self, trio: [(usize, usize); 3]) -> bool { let [first, second, third] = trio; self.board[first.0][first.1].is_some() && self.board[first.0][first.1] == self.board[second.0][second.1] && self.board[first.0][first.1] == self.board[third.0][third.1] } fn update_state(&mut self) { for i in 0..=2 { // three of the same in one row if self.is_winning_trio([(i, 0), (i, 1), (i, 2)]) { self.state = GameState::Won { winner: self.current_player(), }; return; } // three of the same in one column if self.is_winning_trio([(0, i), (1, i), (2, i)]) { self.state = GameState::Won { winner: self.current_player(), }; return; } } // three of the same in one diagonal if self.is_winning_trio([(0, 0), (1, 1), (2, 2)]) || self.is_winning_trio([(0, 2), (1, 1), (2, 0)]) { self.state = GameState::Won { winner: self.current_player(), }; return; } // reaching this code means the game has not been won, // so if there are unfilled tiles left, it's still active for row in 0..=2 { for column in 0..=2 { if self.board[row][column].is_none() { return; } } } // game has not been won // game has no more free tiles // -> game ends in a tie self.state = GameState::Tie; } } ``` ## 34 ### --description-- Second, in order to be able to match the correct `Sign` variant with the current player, `num_traits::FromPrimitive` should be derived for `Sign`. To derive `FromPrimitive`, you need to add `num-traits` and `num-derive` to the dependencies in `programs/tic-tac-toe/Cargo.toml`. **Note:** `num-derive` enables the use of `#[derive(FromPrimitive)]` on a struct or enum. ### --tests-- You should add `num-traits` to the dependencies in `programs/tic-tac-toe/Cargo.toml`. ```js assert.match(__cargo_toml, /num-traits/); ``` You should add `num-derive` to the dependencies in `programs/tic-tac-toe/Cargo.toml`. ```js assert.match(__cargo_toml, /num-derive/); ``` ### --before-all-- ```js const __cargo_toml = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/Cargo.toml` ); global.__cargo_toml = __cargo_toml?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__cargo_toml; ``` ## 35 ### --description-- Now, derive `num_derive::FromPrimitive` for `Sign`. ### --tests-- `Sign` should derive `num_derive::FromPrimitive`. ```js assert.match( __librs, /#\[\s*derive\s*\([^\]]*?num_derive\s*::\s*FromPrimitive[^\]]*?\)\s*\]\s*pub enum Sign/ ); ``` `num_traits::FromPrimitive` should be brought into the module scope. ```js assert.match(__librs, /use num_traits::FromPrimitive;/); ``` `num_derive` should be brought into the module scope. ```js assert.match(__librs, /use num_derive/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 36 ### --description-- The third fix is to add errors to the `play` method. Define a public enum `TicTacToeError` with the variants `TileAlreadySet` and `TileOutOfBounds`. ### --tests-- A `TicTacToeError` enum should be defined. ```js assert.match(__librs, /pub enum TicTacToeError/); ``` `TicTacToeError` should have the variant `TileAlreadySet`. ```js const ticTacToeError = __librs.match( /pub enum TicTacToeError\s*{([^}]*)}/s )?.[1]; assert.match(ticTacToeError, /TileAlreadySet/); ``` `TicTacToeError` should have the variant `TileOutOfBounds`. ```js const ticTacToeError = __librs.match( /pub enum TicTacToeError\s*{([^}]*)}/s )?.[1]; assert.match(ticTacToeError, /TileOutOfBounds/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 37 ### --description-- In the appropriate location, return the `TileAlreadySet` error. ### --tests-- The first `return Err()` should return the `TileAlreadySet` error. ```js const librs = await __helpers .getFile(`${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs`) ?.replaceAll(/[ \t]{2,}/g, ' '); const firstReturnErr = librs.match(/return Err\s*\(([^\)]*?)\),/)?.[1]; assert.match(firstReturnErr, /TicTacToeError\s*::\s*TileAlreadySet/); ``` ## 38 ### --description-- Anchor does not understand the type `TicTacToeError` yet. To convert it into an error Anchor understands, use the `error_code` attribute macro above the `TicTacToeError` enum. ```rust #[error_code] pub enum MyCustomError { ... } ``` ### --tests-- Your `TicTacToeError` enum should have the `error_code` attribute macro. ```js const librs = await __helpers .getFile(`${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs`) ?.replaceAll(/[ \t]{2,}/g, ' '); assert.match(librs, /#\[\s*error_code\s*\]\s*pub enum TicTacToeError/); ``` ## 39 ### --description-- Convert the `TileAlreadySet` error into an `anchor_lang::error::Error` by calling the derived `into` method on it. ### --tests-- You should have `return Err(TicTacToeError::TileAlreadySet.into());`. ```js const librs = await __helpers .getFile(`${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs`) ?.replaceAll(/[ \t]{2,}/g, ' '); const firstReturnErr = librs.match(/return Err\s*\(([^\)]*?)\),/)?.[1]; assert.match( firstReturnErr, /TicTacToeError\s*::\s*TileAlreadySet\s*.\s*into\(\)/ ); ``` ## 40 ### --description-- In the appropriate location, return the `TileOutOfBounds` error. ### --tests-- The second `return Err()` should return the `TileOutOfBounds` error. ```js const secondReturnErr = [ ...__librs.matchAll(/return Err\s*\(([^\)]*?)\),/g) ]?.[1]?.[1]; assert.match(secondReturnErr, /TicTacToeError\s*::\s*TileOutOfBounds/); ``` The `TileOutOfBounds` error should be converted into an `anchor_lang::error::Error`. ```js const secondReturnErr = [ ...__librs.matchAll(/return Err\s*\(([^\)]*?)\),/g) ]?.[1]?.[1]; assert.match( secondReturnErr, /TicTacToeError\s*::\s*TileOutOfBounds\s*.\s*into\(\)/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 41 ### --description-- Another way to return an error is to use the `require!` macro: ```rust require!(condition, MyCustomError::MyCustomErrorVariant); ``` In the appropriate location, use the `require!` macro to return early if the game state is not `Active`. Add the following variant and return with `TicTacToeError::GameAlreadyOver`. ### --tests-- The `require!` macro should be used to return early if the game state is not `Active`. ```js // `require!(` comes after `-> Result<()> {`, and before `match tile {` const playFunction = __librs.match( /pub fn play\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^}]*)}/s )?.[2]; assert.match(playFunction, /require!\(/); ``` The `require!` condition should use the provided `is_active` method. ```js const playFunction = __librs.match( /pub fn play\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^}]*)}/s )?.[2]; const requireCondition = playFunction?.match( /require!\s*\(([^\)]*?)\)\s*;\s*match\s*tile\s*{/ )?.[1]; assert.match(requireCondition, /self\s*.\s*is_active\s*\(\s*\)/); ``` The `require!` macro should return the `GameAlreadyOver` error. ```js const playFunction = __librs.match( /pub fn play\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^}]*)}/s )?.[2]; const requireCondition = playFunction?.match( /require!\s*\(([^\)]*?)\)\s*;\s*match\s*tile\s*{/ )?.[1]; assert.match(requireCondition, /TicTacToeError\s*::\s*GameAlreadyOver/); ``` The `TicTacToeError` enum should have the variant `GameAlreadyOver`. ```js const ticTacToeError = __librs.match( /pub enum TicTacToeError\s*{([^}]*)}/s )?.[1]; assert.match(ticTacToeError, /GameAlreadyOver/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 42 ### --description-- The final _TODO_ in the game logic is to return early if the `start` method is called when the `turn` is greater than `0`. Within the `start` method, use the `require_eq!` macro to return early if the `turn` is not equal to `0`. Add the following variant and return with `TicTacToeError::GameAlreadyStarted`. ### --tests-- The `require_eq!` macro should be used to return early if the `turn` is not equal to `0`. ```js const startFn = __librs.match( /pub fn start\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^}]*)}/s )?.[2]; const requireEq = startFn?.match(/require_eq!\s*\(([^\)]*?)\)\s*;/)?.[1]; assert.exists(requireEq, '`require_eq!` should be called'); assert.match( requireEq, /self\s*.\s*turn\s*,\s*0/, '`require_eq!` should be called with `self.turn` and `0`' ); ``` The `require_eq!` macro should return the `GameAlreadyStarted` error. ```js const startFn = __librs.match( /pub fn start\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^}]*)}/s )?.[2]; const requireEq = startFn?.match(/require_eq!\s*\(([^\)]*?)\)\s*;/)?.[1]; assert.match( requireEq, /TicTacToeError\s*::\s*GameAlreadyStarted/, 'the third argument to `require_eq!` should be `TicTacToeError::GameAlreadyStarted`' ); ``` The `TicTacToeError` enum should have the variant `GameAlreadyStarted`. ```js const ticTacToeError = __librs.match( /pub enum TicTacToeError\s*{([^}]*)}/s )?.[1]; assert.match(ticTacToeError, /GameAlreadyStarted/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 43 ### --description-- Focussing your attention back to the program, the Context provides all the accounts as defined in the generic passed to `Context`. These accounts can be accessed by name: ```rust #[derive(Accounts)] pub struct AccountsStruct<'info> { pub account_1: AccountInfo<'info>, pub account_2: AccountInfo<'info>, pub account_3: ProgramAccount<'info, TicTacToe>, } pub fn instruction_handler(ctx: Context) -> Result<()> { let account_1 = &ctx.accounts.account_1; let account_2 = &ctx.accounts.account_2; let account_3 = &ctx.accounts.account_3; } ``` Within the `setup_game` instruction handler, declare a variable `player_one` and assign the corresponding account reference to it. ### --tests-- The `player_one` variable should be declared. ```js const setupGame = __librs.match( /pub fn setup_game\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(setupGame, /let\s*player_one/); ``` The `player_one` variable should be assigned `&ctx.accounts.player_one`. ```js const setupGame = __librs.match( /pub fn setup_game\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; const playerOne = setupGame?.match(/let\s*player_one\s*=\s*([^\;]*?)\;/)?.[1]; assert.match(playerOne, /&\s*ctx\.accounts\.player_one/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 44 ### --description-- Setting up the game requires three steps: 1. The public address of the first player 2. The public address of the second player 3. To call the `start` method on the `game` account Withing `setup_game` declare a variable `player_one_pubkey` and assign the return of the `key` method provided by the `player_one` account to it. ### --tests-- The `player_one_pubkey` variable should be declared. ```js const setupGame = __librs.match( /pub fn setup_game\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(setupGame, /let\s*player_one_pubkey/); ``` The `player_one_pubkey` variable should be assigned `player_one.key()`. ```js const setupGame = __librs.match( /pub fn setup_game\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; const playerOnePubkey = setupGame?.match( /let\s*player_one_pubkey\s*=\s*([^\;]*?)\;/ )?.[1]; assert.match(playerOnePubkey, /player_one\s*.\s*key\s*\(\s*\)/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 45 ### --description-- Instruction handlers can be called with arguments, and the values accessed through parameters: ```rust pub fn instruction_handler(ctx: Context, arg1: u8, arg2: u8) -> Result<()> {} ``` In order to get the second player's public key, add a `player_two_pubkey` parameter to the `setup_game` instruction handler. Type it with `Pubkey`. ### --tests-- The `setup_game` instruction handler should have a `player_two_pubkey` parameter. ```js const setupGame = __librs.match( /pub fn setup_game\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(setupGame, /player_two_pubkey\s*:/); ``` The `player_two_pubkey` parameter should be of type `Pubkey`. ```js const setupGame = __librs.match( /pub fn setup_game\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(setupGame, /player_two_pubkey\s*:\s*Pubkey/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 46 ### --description-- Within the `setup_game` instruction handler, declare a variable `game`, and assign a mutable reference to the `game` account to it. ### --tests-- The `game` variable should be declared. ```js const setupGame = __librs.match( /pub fn setup_game\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(setupGame, /let\s*game/); ``` The `game` variable should be assigned `&mut ctx.accounts.game`. ```js const setupGame = __librs.match( /pub fn setup_game\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; const game = setupGame?.match(/let\s+game\s*=\s*([^\;]*?)\;/)?.[1]; assert.match(game, /&\s*mut\s+ctx\.accounts\.game/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 47 ### --description-- Within the `setup_game` instruction handler, replace the `Ok(())` witha call to the `start` method on the `game` account, passing in the `player_one_pubkey` and `player_two_pubkey` variables in the expected format. ### --tests-- `setup_game` should have `game.start([player_one_pubkey, player_two_pubkey])`. ```js const librs = await __helpers .getFile(`${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs`) ?.replaceAll(/[ \t]{2,}/g, ' '); const setupGame = librs.match( /pub fn setup_game\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)}/s )?.[2]; assert.match( setupGame, /game\s*.\s*start\s*\(\s*\[\s*player_one_pubkey\s*,\s*player_two_pubkey\s*\]\s*\)/ ); ``` ## 48 ### --description-- Currently, the first player has to provide a separate keypair for the `game` account. Then, the first player would also need to share this with the second player in order for them to play. Instead, you can make use of a PDA to generate a deterministic address for the `game` account: ```rust #[derive(Accounts)] pub struct InitialisePDAAccount<'info> { #[account( init, payer = payer, seeds = [b"", payer.key().as_ref()], bump ) ] pub pda_account: Account<'info, PDAAccount>, } ``` Within `lib.rs`, initialize the `game` account using two seeds: the first the byte string `"game"`, and the second the payer's public key. ### --tests-- `SetupGame` should annotate the `game` field with `#[account(seeds = [b"game", player_one.key().as_ref()])]`. ```js const librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); const setupGameStruct = librs.match(/struct\s*SetupGame\s*{([^\}]*)}/s)?.[1]; const accountAttribute = setupGameStruct?.match( /#\[account\s*\(([^\]]*?)\]\s*pub\s*game/ )?.[1]; assert.match( accountAttribute, /seeds\s*=\s*\[\s*b"game"\s*,\s*player_one\s*.\s*key\s*\(\s*\)\s*.\s*as_ref\s*\(\s*\)\s*\]/ ); ``` ## 49 ### --description-- The seeds are used to hash the address of the `game` account. Being a PDA, the address is deterministic, meaning that the same seeds will always produce the same address. Also, the produced public key must **not** be on the ed25519 curve. To ensure this, an extra seed is added. This is called the bump seed. Explicitly tell Anchor to generate the bump seed, by annotating the `game` field with `#[account(bump)]`. ### --tests-- `SetupGame` should annotate the `game` field with `#[account(bump)]`. ```js const librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); const setupGameStruct = librs.match(/struct\s*SetupGame\s*{([^\}]*)}/s)?.[1]; const accountAttribute = setupGameStruct?.match( /#\[account\s*\(([^\]]*?)\]\s*pub\s*game/ )?.[1]; assert.match(accountAttribute, /bump/); ``` ## 50 ### --description-- Run the tests to see if the `setup_game` instruction handler is working correctly. ### --tests-- The test for `setup_game` should pass for `anchor test --skip-local-validator`. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '1 passing'); ``` You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --force-- #### --"learn-anchor-by-building-tic-tac-toe-part-1/tic-tac-toe/tests/tic-tac-toe.ts"-- ```typescript import { AnchorError, Program, AnchorProvider, setProvider, workspace } from '@coral-xyz/anchor'; import { TicTacToe } from '../target/types/tic_tac_toe'; import { expect } from 'chai'; import { Keypair, PublicKey } from '@solana/web3.js'; describe('tic-tac-toe', () => { // Configure the client to use the local cluster. setProvider(AnchorProvider.env()); const program = workspace.TicTacToe as Program; const programProvider = program.provider as AnchorProvider; it('initializes a game', async () => { const playerOne = Keypair.generate(); const playerTwo = Keypair.generate(); const [gamePublicKey, _] = PublicKey.findProgramAddressSync( [Buffer.from('game'), playerOne.publicKey.toBuffer()], program.programId ); // Airdrop to playerOne const sg = await programProvider.connection.requestAirdrop( playerOne.publicKey, 1_000_000_000 ); await programProvider.connection.confirmTransaction(sg); await program.methods .setupGame(playerTwo.publicKey) .accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }) .signers([playerOne]) .rpc(); const gameData = await program.account.game.fetch(gamePublicKey); expect(gameData.turn).to.equal(1); expect(gameData.players).to.eql([playerOne.publicKey, playerTwo.publicKey]); expect(gameData.state).to.eql({ active: {} }); expect(gameData.board).to.eql([ [null, null, null], [null, null, null], [null, null, null] ]); }); }); ``` ## 51 ### --description-- Using a constant string, and the first player's public key as seeds only allows one game to be played. Dynamic seeds can be added to the `game` account to allow for multiple games to be played with the same player accounts, using the `instruction` attribute: ```rust pub fn initialize(ctx: Context, arg_1: String, arg_2: u8) -> Result<()> {} #[derive(Accounts)] #[instruction(arg_1: String, arg_2: u8)] pub struct Init<'info> { #[account( init, payer = payer, seeds = [arg_1.as_bytes(), payer.key().as_ref(), arg_2], bump )] pub pda: Account<'info, Game>, pub payer: Signer<'info>, pub system_program: Program<'info, System>, } ``` The `instruction` attribute provides access to the instruction's arugments. You have to list them in the same order as in the instruction but you can omit all arguments after the last one you need. Within `lib.rs`, add a third parameter `game_id` of type `String` to the `setup_game` instruction handler. ### --tests-- The `setup_game` instruction handler should have a `game_id` parameter. ```js const setupGame = __librs.match( /pub fn setup_game\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/s )?.[1]; assert.match(setupGame, /game_id\s*:/); ``` The `game_id` parameter should be of type `String`. ```js const setupGame = __librs.match( /pub fn setup_game\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/s )?.[1]; assert.match(setupGame, /game_id\s*:\s*String/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 52 ### --description-- Within `lib.rs`, annotate the `SetupGame` struct with the `instruction` attribute, passing in the required parameters to access the `game_id` parameter. ### --tests-- The `SetupGame` struct should be annotated with `#[instruction(player_two_pubkey: Pubkey, game_id: String)]`. ```js const librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); assert.match( librs, /(?<=#\[\s*instruction\s*\(\s*player_two_pubkey\s*:\s*Pubkey\s*,\s*game_id\s*:\s*String\s*\)\s*])\s*pub\s+struct\s+SetupGame/ ); ``` ## 53 ### --description-- Within `lib.rs`, add the `game_id` parameter as a seed to the `seeds` value of the `game` account. ### --tests-- The `game` field should be annotated with `#[account(seeds = [b"game", player_one.key().as_ref(), game_id.as_bytes()])]`. ```js const librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); const setupGameStruct = librs.match(/struct\s*SetupGame\s*{([^\}]*)}/s)?.[1]; const accountAttribute = setupGameStruct?.match( /#\[account\s*\(([^\]]*?)\]\s*pub\s*game/ )?.[1]; assert.match( accountAttribute, /seeds\s*=\s*\[\s*b"game"\s*,\s*player_one\s*.\s*key\s*\(\s*\)\s*.\s*as_ref\s*\(\s*\)\s*,\s*game_id\s*.\s*as_bytes\s*\(\s*\)\s*\]/ ); ``` ## 54 ### --description-- To prevent Rust from complaining about the `game_id` parameter not being used, prefix it with an underscore. ### --tests-- The `game_id` parameter should be prefixed with an underscore in the `setup_game` instruction handler. ```js const setupGame = __librs.match( /pub fn setup_game\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(setupGame, /_game_id\s*:/); ``` The `game_id` parameter should be prefixed with an underscore in the `instruction` attribute. ```js const setupGame = __librs.match( /pub fn setup_game\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; const instructionAttribute = setupGame?.match( /#\[instruction\s*\(([^\]]*?)\)\s*\]/ )?.[1]; assert.match(instructionAttribute, /_game_id\s*:\s*String/); ``` The `game_id` parameter should be prefixed with an underscore in the `game` account's `seeds` value. ```js const setupGameStruct = __librs.match(/struct\s*SetupGame\s*{([^\}]*)}/s)?.[1]; const accountAttribute = setupGameStruct?.match( /#\[account\s*\(([^\]]*?)\]\s*pub\s*game/ )?.[1]; assert.match( accountAttribute, /seeds\s*=\s*\[\s*b"game"\s*,\s*player_one\s*.\s*key\s*\(\s*\)\s*.\s*as_ref\s*\(\s*\)\s*,\s*_game_id\s*.\s*as_bytes\s*\(\s*\)\s*\]/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 55 ### --description-- Run the tests to see if the `setup_game` instruction handler is working correctly. ### --tests-- The test for `setup_game` should pass when `anchor test --skip-local-validator`. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '1 passing'); ``` You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --force-- #### --"learn-anchor-by-building-tic-tac-toe-part-1/tic-tac-toe/tests/tic-tac-toe.ts"-- ```typescript import { AnchorError, Program, AnchorProvider, setProvider, workspace } from '@coral-xyz/anchor'; import { TicTacToe } from '../target/types/tic_tac_toe'; import { expect } from 'chai'; import { Keypair, PublicKey } from '@solana/web3.js'; describe('tic-tac-toe', () => { // Configure the client to use the local cluster. setProvider(AnchorProvider.env()); const program = workspace.TicTacToe as Program; const programProvider = program.provider as AnchorProvider; it('initializes a game', async () => { const playerOne = Keypair.generate(); const playerTwo = Keypair.generate(); const gameId = 'game-1'; const [gamePublicKey, _] = PublicKey.findProgramAddressSync( [ Buffer.from('game'), playerOne.publicKey.toBuffer(), Buffer.from(gameId) ], program.programId ); // Airdrop to playerOne const sg = await programProvider.connection.requestAirdrop( playerOne.publicKey, 1_000_000_000 ); await programProvider.connection.confirmTransaction(sg); await program.methods .setupGame(playerTwo.publicKey, gameId) .accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }) .signers([playerOne]) .rpc(); const gameData = await program.account.game.fetch(gamePublicKey); expect(gameData.turn).to.equal(1); expect(gameData.players).to.eql([playerOne.publicKey, playerTwo.publicKey]); expect(gameData.state).to.eql({ active: {} }); expect(gameData.board).to.eql([ [null, null, null], [null, null, null], [null, null, null] ]); }); }); ``` ## 56 ### --description-- Within `lib.rs`, define another instruction handler called `play`. It should take a `ctx` parameter of type `Context`, and return a `Result<()>`. ### --tests-- The `play` instruction handler should be defined. ```js assert.match(__librs, /pub fn play\s*\([^\)]*?\)/); ``` The `play` instruction handler should take a `ctx` parameter of type `Context`. ```js const playFn = __librs.match(/pub fn play\s*\(([^\)]*?)\)/)?.[1]; assert.match(playFn, /ctx\s*:\s*Context\s*<\s*Play\s*>/); ``` The `play` instruction handler should return a `Result<()>`. ```js const playReturn = __librs.match(/pub fn play\s*\([^\)]*?\)([^\{]*){/)?.[1]; assert.match(playReturn, /->\s*Result\s*<\s*\(\s*\)\s*>\s*/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 57 ### --description-- Within `lib.rs`, define a new public struct `Play` that implements the `Accounts` trait. ### --tests-- The `Play` struct should be defined. ```js assert.match(__librs, /pub struct Play/); ``` The `Play` struct should implement the `Accounts` trait. ```js assert.match( __librs, /(?<=#\[derive\s*\(\s*Accounts\s*\)\s*\])\s*pub struct Play/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 58 ### --description-- The `play` instruction handler will need access to the `game` account. Within `Play`, define a field called `game` of type `Account<'info, Game>`. ### --tests-- The `game` field should be defined. ```js const playStruct = __librs.match(/pub struct Play[^\{]*?{([^\}]*)}/)?.[1]; assert.match(playStruct, /game\s*:/); ``` The `game` field should be of type `Account<'info, Game>`. ```js const playStruct = __librs.match(/pub struct Play[^\{]*?{([^\}]*?)}/)?.[1]; assert.match(playStruct, /game\s*:\s*Account\s*<\s*'info\s*,\s*Game\s*>/); ``` The `Play` struct should take a generic lifetime parameter `'info`. ```js assert.match(__librs, /pub struct Play\s*<'info>/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 59 ### --description-- The `play` instruction handler will need access to the player who called it. Within `Play`, define a field called `player` of type `Signer<'info>`. ### --tests-- The `player` field should be defined. ```js const playStruct = __librs.match(/pub struct Play[^\{]*?{([^\}]*)}/)?.[1]; assert.match(playStruct, /player\s*:/); ``` The `player` field should be of type `Signer<'info>`. ```js const playStruct = __librs.match(/pub struct Play[^\{]*?{([^\}]*?)}/)?.[1]; assert.match(playStruct, /player\s*:\s*Signer\s*<\s*'info\s*>/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 60 ### --description-- Within the `play` instruction handler, declare a variable `game`, and assign a mutable reference to the `game` account to it. ### --tests-- The `game` variable should be declared. ```js const playFn = __librs.match( /pub fn play\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(playFn, /let game/); ``` The `game` variable should be assigned `&mut ctx.accounts.game`. ```js const playFn = __librs.match( /pub fn play\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(playFn, /let game\s*=\s*&\s*mut\s*ctx\s*.\s*accounts\s*.\s*game/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 61 ### --description-- Along with the `require!` macro, Anchor provides a `require_keys_eq!` macro. This macro takes two public keys, and ensures they are equal: ```rust require_keys_eq!( ctx.accounts.account_1.key(), ctx.accounts.account_2.key(), OptionalCustomError::MyError ); ``` **Note:** This is specifically provided, because the `require_eq!` macro should not be used to compare public keys. Within the `play` instruction handler, use the `require_keys_eq!` macro to ensure the expected current player is the same as the player who called the instruction. ### --tests-- `play` should have `require_keys_eq!(game.current_player(), ctx.accounts.player.key());`. ```js const librs = await __helpers .getFile(`${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs`) ?.replaceAll(/[ \t]{2,}/g, ' '); const playFn = librs.match( /pub fn play\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match( playFn, /require_keys_eq\s*!\s*\(\s*game\s*.\s*current_player\s*\(\s*\)\s*,\s*ctx\s*.\s*accounts\s*.\s*player\s*.\s*key\s*\(\s*\)\s*\)/ ); ``` ## 62 ### --description-- Add a third argument of `TicTacToeError::NotPlayersTurn` to the `require_keys_eq!` macro. Also, define the `NotPlayersTurn` error variant in the `TicTacToeError` enum. ### --tests-- `play` should have `require_keys_eq!(game.current_player(), ctx.accounts.player.key(), TicTacToeError::NotPlayersTurn);`. ```js const playFn = __librs.match( /pub fn play\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match( playFn, /require_keys_eq\s*!\s*\(\s*game\s*.\s*current_player\s*\(\s*\)\s*,\s*ctx\s*.\s*accounts\s*.\s*player\s*.\s*key\s*\(\s*\)\s*,\s*TicTacToeError\s*::\s*NotPlayersTurn\s*\)/ ); ``` `TicTacToeError` should have a `NotPlayersTurn` variant. ```js const ticTacToError = __librs.match(/enum\s*TicTacToeError\s*{([^\}]*)}/)?.[1]; assert.match(ticTacToError, /NotPlayersTurn/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); global.__librs = __librs?.replaceAll(/[ \t]{2,}/g, ' '); ``` ### --after-all-- ```js delete global.__librs; ``` ## 63 ### --description-- Within the `play` instruction handler, call the `play` method on the `game` account. Pass in a reference to a variable `tile`. ### --tests-- `play` should have `game.play(&tile);`. ```js const librs = await __helpers .getFile(`${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs`) ?.replaceAll(/[ \t]{2,}/g, ' '); const playFn = librs.match( /pub fn play\s*\([^\)]*?\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(playFn, /game\s*.\s*play\s*\(\s*&\s*tile\s*\)/); ``` ## 64 ### --description-- Adjust the `play` instruction handler signature to take a `tile` parameter of type `Tile`. ### --tests-- `play` should take a `tile` parameter of type `Tile`. ```js const playFnParams = __librs.match( /pub fn play\s*\(([^\)]*?)\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{([^\}]*)\}/ )?.[1]; assert.match(playFnParams, /tile\s*:\s*Tile/); ``` ## 65 ### --description-- Run the tests. ### --tests-- The tests for the `play` instruction handler should error ❌. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '3 failing'); ``` You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --force-- #### --"learn-anchor-by-building-tic-tac-toe-part-1/tic-tac-toe/tests/tic-tac-toe.ts"-- ```typescript import { AnchorError, Program, AnchorProvider, setProvider, workspace } from '@coral-xyz/anchor'; import { TicTacToe } from '../target/types/tic_tac_toe'; import { expect } from 'chai'; import { Keypair, PublicKey } from '@solana/web3.js'; describe('tic-tac-toe', () => { // Configure the client to use the local cluster. setProvider(AnchorProvider.env()); const program = workspace.TicTacToe as Program; const programProvider = program.provider as AnchorProvider; it('initializes a game', async () => { const playerOne = Keypair.generate(); const playerTwo = Keypair.generate(); const gameId = 'game-1'; const [gamePublicKey, _] = PublicKey.findProgramAddressSync( [ Buffer.from('game'), playerOne.publicKey.toBuffer(), Buffer.from(gameId) ], program.programId ); // Airdrop to playerOne const sg = await programProvider.connection.requestAirdrop( playerOne.publicKey, 1_000_000_000 ); await programProvider.connection.confirmTransaction(sg); await program.methods .setupGame(playerTwo.publicKey, gameId) .accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }) .signers([playerOne]) .rpc(); const gameData = await program.account.game.fetch(gamePublicKey); expect(gameData.turn).to.equal(1); expect(gameData.players).to.eql([playerOne.publicKey, playerTwo.publicKey]); expect(gameData.state).to.eql({ active: {} }); expect(gameData.board).to.eql([ [null, null, null], [null, null, null], [null, null, null] ]); }); it('has player one win', async () => { const playerOne = Keypair.generate(); const playerTwo = Keypair.generate(); const gameId = 'game-2'; const [gamePublicKey, _bump] = PublicKey.findProgramAddressSync( [ Buffer.from('game'), playerOne.publicKey.toBuffer(), Buffer.from(gameId) ], program.programId ); // Airdrop to playerOne const sg = await programProvider.connection.requestAirdrop( playerOne.publicKey, 1_000_000_000 ); await programProvider.connection.confirmTransaction(sg); await program.methods .setupGame(playerTwo.publicKey, gameId) .accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }) .signers([playerOne]) .rpc(); let gameData = await program.account.game.fetch(gamePublicKey); expect(gameData.turn).to.equal(1); await play( program, gamePublicKey, playerOne, { row: 0, column: 0 }, 2, { active: {} }, [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ] ); await play( program, gamePublicKey, playerTwo, { row: 1, column: 0 }, 3, { active: {} }, [ [{ x: {} }, null, null], [{ o: {} }, null, null], [null, null, null] ] ); await play( program, gamePublicKey, playerOne, { row: 0, column: 1 }, 4, { active: {} }, [ [{ x: {} }, { x: {} }, null], [{ o: {} }, null, null], [null, null, null] ] ); await play( program, gamePublicKey, playerTwo, { row: 1, column: 1 }, 5, { active: {} }, [ [{ x: {} }, { x: {} }, null], [{ o: {} }, { o: {} }, null], [null, null, null] ] ); await play( program, gamePublicKey, playerOne, { row: 0, column: 2 }, 5, { won: { winner: playerOne.publicKey } }, [ [{ x: {} }, { x: {} }, { x: {} }], [{ o: {} }, { o: {} }, null], [null, null, null] ] ); }); it('handles ties', async () => { const playerOne = Keypair.generate(); const playerTwo = Keypair.generate(); const gameId = 'game-3'; const [gamePublicKey, _bump] = PublicKey.findProgramAddressSync( [ Buffer.from('game'), playerOne.publicKey.toBuffer(), Buffer.from(gameId) ], program.programId ); // Airdrop to playerOne const sg = await programProvider.connection.requestAirdrop( playerOne.publicKey, 1_000_000_000 ); await programProvider.connection.confirmTransaction(sg); await program.methods .setupGame(playerTwo.publicKey, gameId) .accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }) .signers([playerOne]) .rpc(); let gameState = await program.account.game.fetch(gamePublicKey); expect(gameState.turn).to.equal(1); await play( program, gamePublicKey, playerOne, { row: 0, column: 0 }, 2, { active: {} }, [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ] ); await play( program, gamePublicKey, playerTwo, { row: 1, column: 1 }, 3, { active: {} }, [ [{ x: {} }, null, null], [null, { o: {} }, null], [null, null, null] ] ); await play( program, gamePublicKey, playerOne, { row: 2, column: 0 }, 4, { active: {} }, [ [{ x: {} }, null, null], [null, { o: {} }, null], [{ x: {} }, null, null] ] ); await play( program, gamePublicKey, playerTwo, { row: 1, column: 0 }, 5, { active: {} }, [ [{ x: {} }, null, null], [{ o: {} }, { o: {} }, null], [{ x: {} }, null, null] ] ); await play( program, gamePublicKey, playerOne, { row: 1, column: 2 }, 6, { active: {} }, [ [{ x: {} }, null, null], [{ o: {} }, { o: {} }, { x: {} }], [{ x: {} }, null, null] ] ); await play( program, gamePublicKey, playerTwo, { row: 0, column: 1 }, 7, { active: {} }, [ [{ x: {} }, { o: {} }, null], [{ o: {} }, { o: {} }, { x: {} }], [{ x: {} }, null, null] ] ); await play( program, gamePublicKey, playerOne, { row: 2, column: 1 }, 8, { active: {} }, [ [{ x: {} }, { o: {} }, null], [{ o: {} }, { o: {} }, { x: {} }], [{ x: {} }, { x: {} }, null] ] ); await play( program, gamePublicKey, playerTwo, { row: 2, column: 2 }, 9, { active: {} }, [ [{ x: {} }, { o: {} }, null], [{ o: {} }, { o: {} }, { x: {} }], [{ x: {} }, { x: {} }, { o: {} }] ] ); await play( program, gamePublicKey, playerOne, { row: 0, column: 2 }, 9, { tie: {} }, [ [{ x: {} }, { o: {} }, { x: {} }], [{ o: {} }, { o: {} }, { x: {} }], [{ x: {} }, { x: {} }, { o: {} }] ] ); }); it('handles invalid plays', async () => { const playerOne = Keypair.generate(); const playerTwo = Keypair.generate(); const gameId = 'game-4'; const [gamePublicKey, _bump] = PublicKey.findProgramAddressSync( [ Buffer.from('game'), playerOne.publicKey.toBuffer(), Buffer.from(gameId) ], program.programId ); // Airdrop to playerOne const sg = await programProvider.connection.requestAirdrop( playerOne.publicKey, 1_000_000_000 ); await programProvider.connection.confirmTransaction(sg); await program.methods .setupGame(playerTwo.publicKey, gameId) .accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }) .signers([playerOne]) .rpc(); let gameData = await program.account.game.fetch(gamePublicKey); expect(gameData.turn).to.equal(1); await play( program, gamePublicKey, playerOne, { row: 0, column: 0 }, 2, { active: {} }, [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ] ); try { await play( program, gamePublicKey, playerOne, // same player in subsequent turns // change sth about the tx because // duplicate tx that come in too fast // after each other may get dropped { row: 1, column: 0 }, 2, { active: {} }, [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ] ); chai.assert(false, "should've failed but didn't "); } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; expect(err.error.errorCode.code).to.equal('NotPlayersTurn'); expect(err.error.errorCode.number).to.equal(6003); expect(err.program.equals(program.programId)).is.true; expect(err.error.comparedValues).to.deep.equal([ playerTwo.publicKey, playerOne.publicKey ]); } try { await play( program, gamePublicKey, playerTwo, { row: 5, column: 1 }, // out of bounds row 3, { active: {} }, [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ] ); chai.assert(false, "should've failed but didn't "); } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; expect(err.error.errorCode.number).to.equal(6000); expect(err.error.errorCode.code).to.equal('TileOutOfBounds'); } try { await play( program, gamePublicKey, playerTwo, { row: 0, column: 0 }, 3, { active: {} }, [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ] ); chai.assert(false, "should've failed but didn't "); } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; expect(err.error.errorCode.number).to.equal(6001); expect(err.error.errorCode.code).to.equal('TileAlreadySet'); } await play( program, gamePublicKey, playerTwo, { row: 1, column: 0 }, 3, { active: {} }, [ [{ x: {} }, null, null], [{ o: {} }, null, null], [null, null, null] ] ); await play( program, gamePublicKey, playerOne, { row: 0, column: 1 }, 4, { active: {} }, [ [{ x: {} }, { x: {} }, null], [{ o: {} }, null, null], [null, null, null] ] ); await play( program, gamePublicKey, playerTwo, { row: 1, column: 1 }, 5, { active: {} }, [ [{ x: {} }, { x: {} }, null], [{ o: {} }, { o: {} }, null], [null, null, null] ] ); await play( program, gamePublicKey, playerOne, { row: 0, column: 2 }, 5, { won: { winner: playerOne.publicKey } }, [ [{ x: {} }, { x: {} }, { x: {} }], [{ o: {} }, { o: {} }, null], [null, null, null] ] ); try { await play( program, gamePublicKey, playerOne, { row: 0, column: 2 }, 6, { won: { winner: playerOne.publicKey } }, [ [{ x: {} }, { x: {} }, null], [{ o: {} }, { o: {} }, null], [null, null, null] ] ); chai.assert(false, "should've failed but didn't "); } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; expect(err.error.errorCode.number).to.equal(6002); expect(err.error.errorCode.code).to.equal('GameAlreadyOver'); } }); }); async function play( program: Program, game: PublicKey, player: Keypair, tile: { row: number; column: number }, expectedTurn: number, expectedGameState: | { active: {} } | { won: { winner: PublicKey } } | { tie: {} }, expectedBoard: Array> ) { await program.methods .play(tile) .accounts({ player: player.publicKey, game }) .signers([player]) .rpc(); const gameData = await program.account.game.fetch(game); expect(gameData.turn).to.equal(expectedTurn); expect(gameData.state).to.eql(expectedGameState); expect(gameData.board).to.eql(expectedBoard); } ``` ## 66 ### --description-- The tests failed, because a mutable reference to the `game` account is required, but the account is not marked as `mut`. Mark the `game` account as `mut`. ### --tests-- The `Play` struct should have `game` annotated with `#[account(mut)]`. ```js const librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); const playStruct = librs.match(/pub\s+struct\s+Play[^\{]*?{([^\}]*)}/)?.[1]; assert.match( playStruct, /#[\s\n]*account[\s\n]*\([\s\n]*mut[\s\n]*\)\s*pub\s+game/ ); ``` ## 67 ### --description-- Run the tests to ensure everything is working as expected. ### --tests-- All tests should pass for the `anchor test --skip-local-validator` command ✅. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '4 passing'); ``` You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 68 ### --description-- **Summary** - Derive `Accounts` for context structs - Annotate custom accounts with `#[account]` - Annotate custom errors with `#[error_code]` - Use the `instruction` attribute to access the instruction data - Anchor provides various account constraints: - `init` - Initialises an account, setting the owner field of the created account to the currently executing program - `mut` - Checks the given account is mutable, and persists any state changes - The `Account` struct verifies program ownership - The `Signer` struct verifies the account in the transaction also signed the transaction - The `Program` struct validates the account provided is the given program 🎆 Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-anchor-by-building-tic-tac-toe-part-2.md ================================================ # Solana - Learn How to Test an Anchor Program: Part 2 ## 1 ### --description-- In the previous project, you used Anchor to create a program with instructions to play a game of Tic-Tac-Toe. This same program has been carried over as the boilerplate for this project. Anchor automatically generated some test boilerplate for the `tic-tac-toe` program in the `tests/` directory. You will be mostly working in this directory. Within a new terminal, change into the `tic-tac-toe` directory. ### --tests-- You should be in the `tic-tac-toe` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/?$`); assert.match(cwd, dirRegex); ``` ## 2 ### --description-- The boilerplate includes a `program` variable that uses the generated `TicTacToe` IDL to create a program instance. The program can send transactions, fetch deserialized accounts, decode instruction data, subscribe to account changes, and listen to events. Within `tic-tac-toe/tests/tic-tac-toe.ts`, immediately below the `program` variable declaration, declare a `programProvider` variable and assign the following to it: ```typescript program.provider as AnchorProvider; ``` ### --tests-- The `programProvider` variable should be declared. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'programProvider'; }); assert.exists( variableDeclaration, 'A variable named `programProvider` should exist' ); ``` The `programProvider` variable should be assigned `program.provider as AnchorProvider`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'programProvider'; }); assert.exists( variableDeclaration, 'A variable named `programProvider` should exist' ); const tAsExpression = variableDeclaration.declarations?.[0]?.init; const { object, property } = tAsExpression.expression; assert.equal( object.name, 'program', 'The `programProvider` variable should be assigned `program.provider`' ); assert.equal( property.name, 'provider', 'The `programProvider` variable should be assigned `program.provider`' ); const tAnnotation = tAsExpression.typeAnnotation; assert.equal( tAnnotation.typeName.name, 'AnchorProvider', 'The `programProvider` variable should be assigned `program.provider as AnchorProvider`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --force-- #### --"learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/tests/tic-tac-toe.ts"-- ```typescript import { AnchorProvider, workspace, setProvider, Program } from '@coral-xyz/anchor'; import { TicTacToe } from '../target/types/tic_tac_toe'; describe('TicTacToe', () => { // Configure the client to use the local cluster. setProvider(AnchorProvider.env()); const program = workspace.TicTacToe as Program; it('Is initialized!', async () => { // Add your test here. const tx = await program.methods.initialize().rpc(); console.log('Your transaction signature', tx); }); }); ``` ## 3 ### --description-- To get autocomplete for the program, build your program with: ```bash anchor build ``` Anchor creates an IDL from your program, and stores it in the `target/types/tic_tac_toe.ts` file. ### --tests-- The `target/types/tic_tac_toe.ts` file should exist. ```js const isFile = __helpers.fileExists( `${project.dashedName}/tic-tac-toe/target/types/tic_tac_toe.ts` ); assert.isTrue(isFile); ``` ## 4 ### --description-- Within the `it` callback, change the `initialize` call to `setupGame`. ### --tests-- `tests/tic-tac-toe.ts` should have `const tx = await program.methods.setupGame().rpc();`. ```js const memberExpression = babelisedCode.getType('MemberExpression').find(m => { return ( m.object?.object?.name === 'program' && m.object?.property?.name === 'methods' ); }); assert.exists(memberExpression, '`program.methods.` should exist'); const { property } = memberExpression; assert.equal( property.name, 'setupGame', '`program.methods.setupGame` should exist' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 5 ### --description-- Now, you need to create the accounts to pass to the `setupGame` instruction handler. At the top of the `it` callback, generate two new keypairs, and assign them to two new variables: `playerOne`, and `playerTwo`. ### --tests-- The `playerOne` variable should be declared. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclarationNames = blockStatement?.body?.map(v => { return v?.declarations?.[0]?.id?.name; }); assert.include( variableDeclarationNames, 'playerOne', 'A variable named `playerOne` should exist' ); ``` The `playerOne` variable should be assigned `Keypair.generate()`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'playerOne'; }); assert.exists(variableDeclaration, 'A variable named `playerOne` should exist'); const memberExpression_playerOne = variableDeclaration.declarations?.[0]?.init?.callee; const { property, object } = memberExpression_playerOne; assert.equal( object.name, 'Keypair', 'The `playerOne` variable should be assigned `Keypair.generate()`' ); assert.equal( property.name, 'generate', 'The `playerOne` variable should be assigned `Keypair.generate()`' ); ``` The `playerTwo` variable should be declared. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclarationNames = blockStatement?.body?.map(v => { return v?.declarations?.[0]?.id?.name; }); assert.include( variableDeclarationNames, 'playerTwo', 'A variable named `playerTwo` should exist' ); ``` The `playerTwo` variable should be assigned `Keypair.generate()`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'playerTwo'; }); assert.exists(variableDeclaration, 'A variable named `playerTwo` should exist'); const memberExpression_playerTwo = variableDeclaration.declarations?.[0]?.init?.callee; const { property, object } = memberExpression_playerTwo; assert.equal( object.name, 'Keypair', 'The `playerTwo` variable should be assigned `Keypair.generate()`' ); assert.equal( property.name, 'generate', 'The `playerTwo` variable should be assigned `Keypair.generate()`' ); ``` The `Keypair` class should be imported from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source?.value === '@solana/web3.js'; }); assert.exists( importDeclaration, 'An import from `@solana/web3.js` should exist' ); const specifierNames = importDeclaration.specifiers?.map(s => { return s?.local?.name; }); assert.include( specifierNames, 'Keypair', 'The `Keypair` class should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 6 ### --description-- Within the `it` callback, create a new `gameId` variable, and assign it a value of `"game-1"`. ### --tests-- The `gameId` variable should be declared. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclarationNames = blockStatement?.body?.map(v => { return v?.declarations?.[0]?.id?.name; }); assert.include( variableDeclarationNames, 'gameId', 'A variable named `gameId` should exist' ); ``` The `gameId` variable should be assigned `"game-1"`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'gameId'; }); assert.exists(variableDeclaration, 'A variable named `gameId` should exist'); const { value } = variableDeclaration.declarations?.[0]?.init; assert.equal( value, 'game-1', 'The `gameId` variable should be assigned `"game-1"`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 7 ### --description-- Now, PDA's will make more sense - they can be repeatably, programmatically generated: ```typescript const [pda, bump] = PublicKey.findProgramAddressSync( [Buffer.from(seed)], program.programId ); ``` Destructure a variable `gamePublicKey` from `PublicKey.findProgramAddressSync`, using `"game"`, the payer's public key, and `gameId` as seeds. ### --tests-- `tests/tic-tac-toe.ts` should have `const [gamePublicKey, _] = PublicKey.findProgramAddressSync([Buffer.from('game'), playerOne.publicKey.toBuffer(), Buffer.from(gameId)], program.programId);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `const[gamePublicKey,_]=PublicKey.findProgramAddressSync([Buffer.from('game'),playerOne.publicKey.toBuffer(),Buffer.from(gameId)],program.programId)`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 8 ### --description-- Pass the public key of `playerTwo` as an argument to the `setupGame` instruction call. ### --tests-- `tests/tic-tac-toe.ts` should have `const tx = await program.methods.setupGame(playerTwo.publicKey).rpc();`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `const tx=await program.methods.setupGame(playerTwo.publicKey).rpc()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 9 ### --description-- Pass `gameId` as the second argument to the `setupGame` instruction call. ### --tests-- `tests/tic-tac-toe.ts` should have `const tx = await program.methods.setupGame(playerTwo.publicKey, gameId).rpc();`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `const tx=await program.methods.setupGame(playerTwo.publicKey,gameId).rpc()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 10 ### --description-- In a new terminal, start a clean local cluster: ```bash solana-test-validator --reset ``` ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 11 ### --description-- Run the tests, using the local validator you just started: ```bash anchor test --skip-local-validator ``` ### --tests-- `anchor test` should error with `Error: Invalid arguments: game not provided`. ```js const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, 'Error: Invalid arguments: game not provided'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 12 ### --description-- You called `setupGame` without passing in any accounts. So, Anchor tried to create the `game` account using the transaction payer - your local Solana wallet. Chain a `.accounts` call to the `setupGame` call, and pass in: ```typescript { game: gamePublicKey, playerOne: playerOne.publicKey, } ``` ### --tests-- `tests/tic-tac-toe.ts` should have `const tx = await program.methods.setupGame(playerTwo.publicKey, gameId).accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }).rpc();`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `const tx=await program.methods.setupGame(playerTwo.publicKey,gameId).accounts({game:gamePublicKey,playerOne:playerOne.publicKey}).rpc()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 13 ### --description-- Run the tests again. ### --tests-- You `anchor test --skip-local-validator` test should error with `Error: Signature verification failed`. ```js const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, 'Error: Signature verification failed'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 14 ### --description-- Seeing as the `game` and `playerOne` accounts are mutated (e.g. funds taken for fees, data changed), these accounts need to sign the transaction. However, one of the main benefits with PDAs is the owner is the program. So, a PDA does not need to sign transactions mutating it within the owner program. Chain a `.signers` call to the `setupGame` call, and pass in an array of the `playerOne` keypair. ### --tests-- `tests/tic-tac-toe.ts` should have `const tx = await program.methods.setupGame(playerTwo.publicKey, gameId).accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }).signers([playerOne]).rpc();`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `const tx=await program.methods.setupGame(playerTwo.publicKey,gameId).accounts({game:gamePublicKey,playerOne:playerOne.publicKey}).signers([playerOne]).rpc()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 15 ### --description-- Run the tests again. ### --tests-- The `anchor test --skip-local-validator` test should error with `Error: failed to send transaction`. ```js const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, 'Error: failed to send transaction'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 16 ### --description-- This time, the transaction failed, because `playerOne` is used as the payer, but has not even been added to the blockchain, let alone have any funds 😱 Within the `it` callback, before the transaction is sent, declare a variable `sg` and assign the transaction signature for requesting an airdrop of 1 SOL to `playerOne`: ```typescript await programProvider.connection.requestAirdrop(, ); ``` ### --tests-- `tests/tic-tac-toe.ts` should have `const sg = await programProvider.connection.requestAirdrop(playerOne.publicKey, 1_000_000_000);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `const sg=await programProvider.connection.requestAirdrop(playerOne.publicKey,1_000_000_000)`, `const sg=await programProvider.connection.requestAirdrop(playerOne.publicKey,1000000000)` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 17 ### --description-- Before trying to spend any funds, you need to ensure the transaction has been confirmed. Below the `requestAirdrop` call, add: ```typescript await programProvider.connection.confirmTransaction(); ``` ### --tests-- `tests/tic-tac-toe.ts` should have `await programProvider.connection.confirmTransaction(sg);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `await programProvider.connection.confirmTransaction(sg)`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 18 ### --description-- Finally, run the tests again. ### --tests-- The `anchor test --skip-local-validator` tests should pass ✅. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '1 passing'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 19 ### --description-- To clarify what is happening behind the scenes when Anchor calls your program, split your instruction and transaction up into two lines: ```typescript const ix = program.methods.().accounts().signers(); await ix.rpc(); ``` ### --tests-- `tests/tic-tac-toe.ts` should have `const ix = program.methods.setupGame(playerTwo.publicKey, gameId).accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }).signers([playerOne]);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'ix'; }); assert.exists(variableDeclaration, 'A variable named `ix` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const ix=program.methods.setupGame(playerTwo.publicKey,gameId).accounts({game:gamePublicKey,playerOne:playerOne.publicKey}).signers([playerOne])`; assert.deepInclude(actualCodeString, expectedCodeString); ``` `tests/tic-tac-toe.ts` should have `await ix.rpc();`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `await ix.rpc()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 20 ### --description-- Run the tests again to ensure everything still works. ### --tests-- The `anchor test --skip-local-validator` test should pass ✅. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '1 passing'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 21 ### --description-- After the transaction is sent, you can fetch an account's data to see if it was correctly set up: ```typescript const accountData = await program.account..fetch(); ``` Declare a variable `gameData` and assign it the `game` account's data. ### --tests-- `tests/tic-tac-toe.ts` should have `const gameData = await program.account.game.fetch(gamePublicKey);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); assert.exists(variableDeclaration, 'A variable named `gameData` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const gameData=await program.account.game.fetch(gamePublicKey)`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 22 ### --description-- Assert the `game` account has a `turn` property equal to `1`. _Hint:_ The boilerplate created by Anchor comes with `chai.js`. ### --tests-- `tests/tic-tac-toe.ts` should throw if `gameData.turn !== 1`. ```js // Get all code in the `it` callback // Remove everything defined before `const gameData`, and remove the `gameData` declaration // Eval with fixture `gameData`, `playerOne`, and `playerTwo` const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const gameData = { turn: 0 }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { await eval(`(async () => { const gameData= { turn: 1 }; ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 23 ### --description-- Assert the `game` account has a `players` property equal to an array of the public keys of `playerOne` and `playerTwo`. ### --tests-- `tests/tic-tac-toe.ts` should throw if `gameData.players[0] !== playerOne.publicKey`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const playerOne = { publicKey: 'playerOne' }; const playerTwo = { publicKey: 'playerTwo' }; const gameData = { turn: 1, players: ['notPlayerOne', playerTwo.publicKey] }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { const playerOne = { publicKey: 'playerOne' }; const playerTwo = { publicKey: 'playerTwo' }; const gameData = { turn: 1, players: [playerOne.publicKey, playerTwo.publicKey] }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` `tests/tic-tac-toe.ts` should throw if `gameData.players[1] !== playerTwo.publicKey`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const playerOne = { publicKey: 'playerOne' }; const playerTwo = { publicKey: 'playerTwo' }; const gameData = { turn: 1, players: [playerOne.publicKey, 'notPlayerOne'] }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { const playerOne = { publicKey: 'playerOne' }; const playerTwo = { publicKey: 'playerTwo' }; const gameData = { turn: 1, players: [playerOne.publicKey, playerTwo.publicKey] }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 24 ### --description-- Assert the `game` account has a `state` property equal to `active: {}`. ### --tests-- `tests/tic-tac-toe.ts` should throw if `gameData.state.active !== {}`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const playerOne = { publicKey: 'playerOne' }; const playerTwo = { publicKey: 'playerTwo' }; const gameData = { turn: 1, players: [playerOne.publicKey, playerTwo.publicKey], state: { active: '' } }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', "The `it` callback should throw when `gameData.state.active == ''" ); logover.debug(e); } try { const playerOne = { publicKey: 'playerOne' }; const playerTwo = { publicKey: 'playerTwo' }; const gameData = { turn: 1, players: [playerOne.publicKey, playerTwo.publicKey], state: { active: {} } }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail( e, 'The `it` callback should not throw when `gameData.state.active == {}' ); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 25 ### --description-- Assert the `game` account has a `board` property equal to a 3x3 array of `null` values. ### --tests-- `tests/tic-tac-toe.ts` should throw if `gameData.board !== [[null,null,null],[null,null,null],[null,null,null]]`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it'; }); const blockStatement = callExpression?.arguments?.[0]?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const playerOne = { publicKey: 'playerOne' }; const playerTwo = { publicKey: 'playerTwo' }; const gameData = { turn: 1, players: [playerOne.publicKey, playerTwo.publicKey], state: { active: {} }, board: [ [false, null, null], [null, null, null], [null, null, null] ] }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', 'The `it` callback should throw when `gameData.state.board == [[false,null,null],[null,null,null],[null,null,null]]' ); logover.debug(e); } try { const playerOne = { publicKey: 'playerOne' }; const playerTwo = { publicKey: 'playerTwo' }; const gameData = { turn: 1, players: [playerOne.publicKey, playerTwo.publicKey], state: { active: {} }, board: [ [null, null, null], [null, null, null], [null, null, null] ] }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail( e, 'The `it` callback should not throw when `gameData.state.board == [[null,null,null],[null,null,null],[null,null,null]]' ); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 26 ### --description-- Run the tests again to ensure everything still works. ### --tests-- The `anchor test --skip-local-validator` test should pass ✅. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '1 passing'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 27 ### --description-- Within the `tic-tac-toe.ts` file, within the `describe` callback, add a new `it` function call with a title of `"has player one win"`, and an empty, asynchronous callback function. ### --tests-- `tests/tic-tac-toe.ts` should have `it('has player one win', async () => {});`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'describe'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `it('has player one win',async()=>{})`, `it("has player one win",async()=>{})` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 28 ### --description-- Within the second `it` callback, generate two keypairs, and assign them to new variables `playerOne` and `playerTwo`. ### --tests-- `tests/tic-tac-toe.ts` should have `const playerOne = Keypair.generate();`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'playerOne'; }); assert.exists(variableDeclaration, 'A variable named `playerOne` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const playerOne=Keypair.generate()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` `tests/tic-tac-toe.ts` should have `const playerTwo = Keypair.generate();`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'playerTwo'; }); assert.exists(variableDeclaration, 'A variable named `playerTwo` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const playerTwo=Keypair.generate()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 29 ### --description-- Declare a variable `gameId`, and assign `"game-2"` to it. ### --tests-- `tests/tic-tac-toe.ts` should have `const gameId = "game-2";`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'gameId'; }); assert.exists(variableDeclaration, 'A variable named `gameId` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const gameId="game-2"`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 30 ### --description-- Destructure a `gamePublicKey` variable from deriving the program address. ### --tests-- `tests/tic-tac-toe.ts` should have `const [gamePublicKey] = PublicKey.findProgramAddressSync([Buffer.from("game"),playerOne.publicKey.toBuffer(),Buffer.from(gameId)], program.programId);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.elements?.[0]?.name === 'gamePublicKey'; }); assert.exists( variableDeclaration, 'A variable named `gamePublicKey` should be destructured' ); const awaitExpression = variableDeclaration?.declarations?.[0]?.init; const actualCodeString = babelisedCode.generateCode(awaitExpression, { compact: true }); const expectedCodeStrings = [ `PublicKey.findProgramAddressSync([Buffer.from("game"),playerOne.publicKey.toBuffer(),Buffer.from(gameId)],program.programId)`, `PublicKey.findProgramAddressSync([Buffer.from('game'),playerOne.publicKey.toBuffer(),Buffer.from(gameId)],program.programId)` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 31 ### --description-- Request an airdrop for the first player, and await confirmation. ### --tests-- `tests/tic-tac-toe.ts` should have `const sg = await programProvider.connection.requestAirdrop(playerOne.publicKey, 1_000_000_000)`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'sg'; }); assert.exists(variableDeclaration, 'A variable named `sg` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeStrings = [ `const sg=await programProvider.connection.requestAirdrop(playerOne.publicKey,1_000_000_000)`, `const sg=await programProvider.connection.requestAirdrop(playerOne.publicKey,1000000000)` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` `tests/tic-tac-toe.ts` should have `await programProvider.connection.confirmTransaction(sg);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `await programProvider.connection.confirmTransaction(sg)`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 32 ### --description-- Call your program's `setupGame` method, passing in the required arguments, and adding the necessary accounts and signers. ### --tests-- `tests/tic-tac-toe.ts` should have `await program.methods.setupGame(playerTwo.publicKey, gameId).accounts({ game: gamePublicKey, playerOne: playerOne.publicKey }).signers([playerOne]).rpc();`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `await program.methods.setupGame(playerTwo.publicKey,gameId).accounts({game:gamePublicKey,playerOne:playerOne.publicKey}).signers([playerOne]).rpc()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 33 ### --description-- Fetch the `game` account's data, and assign it to a variable `gameData`. ### --tests-- `tests/tic-tac-toe.ts` should have `const gameData = await program.account.game.fetch(gamePublicKey);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); assert.exists(variableDeclaration, 'A variable named `gameData` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const gameData=await program.account.game.fetch(gamePublicKey)`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 34 ### --description-- Assert the `game` account has a `turn` property equal to `1`. ### --tests-- `tests/tic-tac-toe.ts` should throw if `gameData.turn !== 1`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const gameData = { turn: 0 }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { const gameData = { turn: 1 }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 35 ### --description-- Now, to play a move, call the `play` method of your program. Pass in `{ row: 0, column: 0 }` as the tile to play. ### --tests-- `tests/tic-tac-toe.ts` should have `await program.methods.play({ row: 0, column: 0 });`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await program.methods.play({row:0,column:0})`, `await program.methods.play({column:0,row:0})` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 36 ### --description-- Off of the `play` method call, chain a call to `accounts`, passing in the `game` and correct `player` public keys. ### --tests-- `tests/tic-tac-toe.ts` should have `await program.methods.play({ row: 0, column: 0 }).accounts({ game: gamePublicKey, player: playerOne.publicKey });`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await program.methods.play({row:0,column:0}).accounts({game:gamePublicKey,player:playerOne.publicKey})`, `await program.methods.play({column:0,row:0}).accounts({game:gamePublicKey,player:playerOne.publicKey})`, `await program.methods.play({row:0,column:0}).accounts({player:playerOne.publicKey,game:gamePublicKey})`, `await program.methods.play({column:0,row:0}).accounts({player:playerOne.publicKey,game:gamePublicKey})` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 37 ### --description-- Off of the `accounts` method call, chain a call to `signers`, passing in the correct keypair. ### --tests-- `tests/tic-tac-toe.ts` should have `await program.methods.play({ row: 0, column: 0 }).accounts({ game: gamePublicKey, player: playerOne.publicKey }).signers([playerOne]);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await program.methods.play({row:0,column:0}).accounts({game:gamePublicKey,player:playerOne.publicKey}).signers([playerOne])`, `await program.methods.play({column:0,row:0}).accounts({game:gamePublicKey,player:playerOne.publicKey}).signers([playerOne])`, `await program.methods.play({row:0,column:0}).accounts({player:playerOne.publicKey,game:gamePublicKey}).signers([playerOne])`, `await program.methods.play({column:0,row:0}).accounts({player:playerOne.publicKey,game:gamePublicKey}).signers([playerOne])` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 38 ### --description-- Off of the `signers` method call, chain the `rpc` method call in order to send the transaction to the network. ### --tests-- `tests/tic-tac-toe.ts` should have `await program.methods.play({ row: 0, column: 0 }).accounts({ game: gamePublicKey, player: playerOne.publicKey }).signers([playerOne]).rpc();`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const awaitExpression = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(awaitExpression, { compact: true }); const expectedCodeStrings = [ `await program.methods.play({row:0,column:0}).accounts({game:gamePublicKey,player:playerOne.publicKey}).signers([playerOne]).rpc()`, `await program.methods.play({column:0,row:0}).accounts({game:gamePublicKey,player:playerOne.publicKey}).signers([playerOne]).rpc()`, `await program.methods.play({row:0,column:0}).accounts({player:playerOne.publicKey,game:gamePublicKey}).signers([playerOne]).rpc()`, `await program.methods.play({column:0,row:0}).accounts({player:playerOne.publicKey,game:gamePublicKey}).signers([playerOne]).rpc()` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 39 ### --description-- After the `play` method call, fetch the `game` account's data, and assign it to a variable `gameData2`. ### --tests-- `tests/tic-tac-toe.ts` should have `const gameData2 = await program.account.game.fetch(gamePublicKey);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'gameData2'; }); assert.exists(variableDeclaration, 'A variable named `gameData2` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const gameData2=await program.account.game.fetch(gamePublicKey)`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 40 ### --description-- Assert the `game` account has a `turn` property equal to `2`. ### --tests-- `tests/tic-tac-toe.ts` should throw if `gameData2.turn !== 2`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData2'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const gameData2 = { turn: 1 }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { const gameData2 = { turn: 2 }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 41 ### --description-- Assert the `game` account has a `board` property equal to `[[{x:{}}, null, null], [null, null, null], [null, null, null]]`. **Note:** The Rust code uses an enum to denote `X` and `O` tiles. This is deserialized to a JavaScript object with a single key, `x` or `o`, depending on the tile. This is why the `x` tile is represented as `{x:{}}` in the assertion. ### --tests-- `tests/tic-tac-toe.ts` should throw if `gameData2.board !== [[{x:{}}, null, null], [null, null, null], [null, null, null]]`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData2'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const gameData2 = { turn: 2, board: [ [null, null, null], [null, null, null], [null, null, null] ] }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { const gameData2 = { turn: 2, board: [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ] }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 42 ### --description-- Assert the `game` account has a `state` property equal to `{ active: {} }`. ### --tests-- `tests/tic-tac-toe.ts` should throw if `gameData2.state !== { active: {} }`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData2'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const gameData2 = { turn: 2, board: [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ], state: { won: { winner: playerOne.publicKey } } }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { const gameData2 = { turn: 2, board: [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ], state: { active: {} } }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 43 ### --description-- Run the tests to see if everything is working as expected. ### --tests-- The `anchor test --skip-local-validator` tests should pass ✅. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '2 passing'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 44 ### --description-- You will be making many of these `play` calls. So, abstract the logic into a function. Outwith the `describe` call, define an async function `play` with the following signature: ```ts async function play( program: Program, game: PublicKey, player: Keypair, tile: { row: number; column: number }, expectedTurn: number, expectedGameState: | { active: {} } | { won: { winner: PublicKey } } | { tie: {} }, expectedBoard: Array> ): Promise; ``` ### --tests-- `tests/tic-tac-toe.ts` should have an async function `play` with the correct signature. ```js const functionDeclaration = babelisedCode .getType('FunctionDeclaration') .find(f => { return f?.id?.name === 'play'; }); assert.exists(functionDeclaration, 'A function named `play` should exist'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 45 ### --description-- Cut the `play` call from the `it` block, and paste it into the `play` function, as well as the `fetch` call and subsequent assertions. Change the arguments to use the `play` function parameters, and rename `gameData2` to `gameData`. ### --tests-- `tests/tic-tac-toe.ts` should have a `play` function that calls `await program.methods.play(tile).accounts({player: player.publicKey, game}).signers([player]).rpc();` ```js const functionDeclaration = babelisedCode .getType('FunctionDeclaration') .find(f => { return f?.id?.name === 'play'; }); const blockStatement = functionDeclaration?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await program.methods.play(tile).accounts({player:player.publicKey,game}).signers([player]).rpc()`, `await program.methods.play(tile).accounts({game,player:player.publicKey}).signers([player]).rpc()` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` `tests/tic-tac-toe.ts` should have a `play` function that calls `const gameData = await program.account.game.fetch(game);` ```js const functionDeclaration = babelisedCode .getType('FunctionDeclaration') .find(f => { return f?.id?.name === 'play'; }); const blockStatement = functionDeclaration?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); assert.exists(variableDeclaration, 'A variable named `gameData` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const gameData=await program.account.game.fetch(game)`; assert.deepInclude(actualCodeString, expectedCodeString); ``` `tests/tic-tac-toe.ts` should have a `play` function that asserts `gameData.turn === expectedTurn`. ```js const functionDeclaration = babelisedCode .getType('FunctionDeclaration') .find(f => { return f?.id?.name === 'play'; }); const blockStatement = functionDeclaration?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const expectedTurn = 1; const expectedGameState = { active: {} }; const expectedBoard = [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ]; const gameData = { turn: 0, board: expectedBoard, state: expectedGameState }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { const expectedTurn = 1; const expectedGameState = { active: {} }; const expectedBoard = [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ]; const gameData = { turn: expectedTurn, board: expectedBoard, state: expectedGameState }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` `tests/tic-tac-toe.ts` should have a `play` function that asserts `gameData.state === expectedGameState`. ```js const functionDeclaration = babelisedCode .getType('FunctionDeclaration') .find(f => { return f?.id?.name === 'play'; }); const blockStatement = functionDeclaration?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const expectedTurn = 1; const expectedGameState = { won: { winner: playerOne.publicKey } }; const expectedBoard = [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ]; const gameData = { turn: expectedTurn, board: expectedBoard, state: { active: {} } }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { const expectedTurn = 1; const expectedGameState = { active: {} }; const expectedBoard = [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ]; const gameData = { turn: expectedTurn, board: expectedBoard, state: expectedGameState }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` `tests/tic-tac-toe.ts` should have a `play` function that asserts `gameData.board === expectedBoard`. ```js const functionDeclaration = babelisedCode .getType('FunctionDeclaration') .find(f => { return f?.id?.name === 'play'; }); const blockStatement = functionDeclaration?.body; const ind = blockStatement?.body?.findIndex(v => { return v?.declarations?.[0]?.id?.name === 'gameData'; }); blockStatement?.body?.splice(0, ind + 1); const assertionCodeString = babelisedCode.generateCode(blockStatement); // Bring chai and `chai.expect` into scope for eval const chai = await import('chai'); const { expect } = chai; try { const expectedTurn = 1; const expectedGameState = { active: {} }; const expectedBoard = [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ]; const gameData = { turn: expectedTurn, board: [ [null, null, null], [null, null, null], [null, null, null] ], state: expectedGameState }; await eval(`(async () => { ${assertionCodeString} })()`); assert(false, 'fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'The `it` callback should throw'); logover.debug(e); } try { const expectedTurn = 1; const expectedGameState = { active: {} }; const expectedBoard = [ [{ x: {} }, null, null], [null, null, null], [null, null, null] ]; const gameData = { turn: expectedTurn, board: expectedBoard, state: expectedGameState }; await eval(`(async () => { ${assertionCodeString} })()`); } catch (e) { assert.fail(e, 'The `it` callback should not throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 46 ### --description-- Back within the second `it` callback, use the `play` function to play that same move for `playerOne`. ### --tests-- `tests/tic-tac-toe.ts` should call `await play(program, gamePublicKey, playerOne, { row: 0, column: 0 }, 2, { active: {} }, [[{x:{}}, null, null], [null, null, null], [null, null, null]]);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await play(program,gamePublicKey,playerOne,{row:0,column:0},2,{active:{}},[[{x:{}},null,null],[null,null,null],[null,null,null]])`, `await play(program,gamePublicKey,playerOne,{column:0,row:0},2,{active:{}},[[{x:{}},null,null],[null,null,null],[null,null,null]])` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 47 ### --description-- Run the tests to confirm everything is still working. ### --tests-- The `anchor test --skip-local-validator` tests should pass ✅. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '2 passing'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 48 ### --description-- Call the play function two more times. Once for player two at `{row: 1, column: 0}`, and once for player one at `{row: 0, column: 1}`. ### --tests-- `tests/tic-tac-toe.ts` should call `await play(program, gamePublicKey, playerTwo, {row:1,column:0}, 3, {active:{}}, [[{x:{}},null,null],[{o:{}},null,null],[null,null,null]]);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await play(program,gamePublicKey,playerTwo,{row:1,column:0},3,{active:{}},[[{x:{}},null,null],[{o:{}},null,null],[null,null,null]])`, `await play(program,gamePublicKey,playerTwo,{column:0,row:1},3,{active:{}},[[{x:{}},null,null],[{o:{}},null,null],[null,null,null]])` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` `tests/tic-tac-toe.ts` should call `await play(program, gamePublicKey, playerOne, {row:0,column:1}, 4, {active:{}}, [[{x:{}},{x:{}},null],[{o:{}},null,null],[null,null,null]]);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await play(program,gamePublicKey,playerOne,{row:0,column:1},4,{active:{}},[[{x:{}},{x:{}},null],[{o:{}},null,null],[null,null,null]])`, `await play(program,gamePublicKey,playerOne,{column:1,row:0},4,{active:{}},[[{x:{}},{x:{}},null],[{o:{}},null,null],[null,null,null]])` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 49 ### --description-- Run the tests to confirm everything is still working. ### --tests-- The `anchor test --skip-local-validator` tests should pass ✅. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '2 passing'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 50 ### --description-- Call the play function two more times. Once for player two at `{row: 1, column: 1}`, and once for player one at `{row: 0, column: 2}`. ### --tests-- `tests/tic-tac-toe.ts` should call `await play(program, gamePublicKey, playerTwo, {row:1,column:1}, 5, {active:{}}, [[{x:{}},{x:{}},null],[{o:{}},{o:{}},null],[null,null,null]]);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await play(program,gamePublicKey,playerTwo,{row:1,column:1},5,{active:{}},[[{x:{}},{x:{}},null],[{o:{}},{o:{}},null],[null,null,null]])`, `await play(program,gamePublicKey,playerTwo,{column:1,row:1},5,{active:{}},[[{x:{}},{x:{}},null],[{o:{}},{o:{}},null],[null,null,null]])` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` `tests/tic-tac-toe.ts` should call `await play(program, gamePublicKey, playerOne, {row:0,column:2}, 5, {won:{winner:playerOne.publicKey}}, [[{x:{}},{x:{}},{x:{}}],[{o:{}},{o:{}},null],[null,null,null]]);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'has player one win' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await play(program,gamePublicKey,playerOne,{row:0,column:2},5,{won:{winner:playerOne.publicKey}},[[{x:{}},{x:{}},{x:{}}],[{o:{}},{o:{}},null],[null,null,null]])`, `await play(program,gamePublicKey,playerOne,{column:2,row:0},5,{won:{winner:playerOne.publicKey}},[[{x:{}},{x:{}},{x:{}}],[{o:{}},{o:{}},null],[null,null,null]])` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 51 ### --description-- Run the tests to confirm everything is still working. ### --tests-- The `anchor test --skip-local-validator` tests should pass ✅. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '2 passing'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 52 ### --description-- Within the `describe` callback, create a new `it` call with a title of `"handles ties"`, and an asynchronous callback. ### --tests-- `tests/tic-tac-toe.ts` should have a `describe` callback that calls `it` with a title of `"handles ties"`. ```js const callExpressions = babelisedCode.getType('CallExpression').filter(c => { return c.callee?.name === 'it'; }); assert.include( callExpressions.map(c => c.arguments?.[0]?.value), 'handles ties' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 53 ### --description-- Within the `"handles ties"` callback, set up a new game similarly to the previous tests, but with a `gameId` of `"game-3"`. ### --tests-- A `playerOne` variable should be declared and assigned `Keypair.generate()`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles ties'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'playerOne'; }); assert.exists(variableDeclaration, 'A variable named `playerOne` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const playerOne=Keypair.generate()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` A `playerTwo` variable should be declared and assigned `Keypair.generate()`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles ties'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'playerTwo'; }); assert.exists(variableDeclaration, 'A variable named `playerTwo` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const playerTwo=Keypair.generate()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` A `gameId` variable should be declared and assigned `"game-3"`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles ties'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'gameId'; }); assert.exists(variableDeclaration, 'A variable named `gameId` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const gameId="game-3"`; assert.deepInclude(actualCodeString, expectedCodeString); ``` A `gamePublicKey` variable should be destructed from a `PublicKey.findProgramAddressSync` call. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles ties'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.elements?.[0]?.name === 'gamePublicKey'; }); assert.exists( variableDeclaration, 'A variable named `gamePublicKey` should be destructured' ); const awaitExpression = variableDeclaration?.declarations?.[0]?.init; const actualCodeString = babelisedCode.generateCode(awaitExpression, { compact: true }); const expectedCodeStrings = [ `PublicKey.findProgramAddressSync([Buffer.from("game"),playerOne.publicKey.toBuffer(),Buffer.from(gameId)],program.programId)`, `PublicKey.findProgramAddressSync([Buffer.from('game'),playerOne.publicKey.toBuffer(),Buffer.from(gameId)],program.programId)` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` An airdrop should be requested and confirmed for `playerOne`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles ties'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const babelisedCode2 = new __helpers.Babeliser(actualCodeString, { plugins: ['typescript'] }); const callExpression2 = babelisedCode2.getType('CallExpression').find(c => { return c.callee?.object?.object?.name === 'programProvider'; }); assert.exists(callExpression2, 'Airdrop should be requested for playerOne'); ``` The `setupGame` method should be called on the program. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles ties'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `await program.methods.setupGame(playerTwo.publicKey,gameId).accounts({game:gamePublicKey,playerOne:playerOne.publicKey}).signers([playerOne]).rpc()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 54 ### --description-- Within the `"handles ties"` callback, use the `play` function to play the game until the players tie each other. ### --tests-- The `"handles ties"` callback should all `play` until a tie. ```js // Get all tile plays in `it` callback // Compare to possible tie board states const callExpression = babelisedCode.getType('CallExpression').find(c => { return c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles ties'; }); const blockStatement = callExpression?.arguments?.[1]?.body; const plays = blockStatement?.body?.filter(v => { return v.expression?.argument?.callee?.name === 'play'; }); const assertionCodeString = babelisedCode.generateCode({ type: 'BlockStatement', body: plays, directives: [] }); let program, gamePublicKey, playerOne = 1, playerTwo = 2; const board = [ [null, null, null], [null, null, null], [null, null, null] ]; const play = (p, g, pl, tile) => { board[tile.row][tile.column] = pl; }; await eval(`(async () => {${assertionCodeString}})()`); function checkTie(board) { for (const row of board) { for (const column of row) { if (column === null) { return false; } } } for (let i = 0; i < board.length; i++) { if (board[i][0] === board[i][1] && board[i][1] === board[i][2]) { return false; } if (board[0][i] === board[1][i] && board[1][i] === board[2][i]) { return false; } if (board[0][0] === board[1][1] && board[1][1] === board[2][2]) { return false; } if (board[0][2] === board[1][1] && board[1][1] === board[2][0]) { return false; } } return true; } assert.isTrue(checkTie(board)); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 55 ### --description-- Run the tests to confirm everything is still working. ### --tests-- The `anchor test --skip-local-validator` tests should pass ✅. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.include(terminalOutput, '3 passing'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 56 ### --description-- Within the `describe` callback, create a new `it` call with a title of `"handles invalid plays"`, and an asynchroneous callback. ### --tests-- `tests/tic-tac-toe.ts` should have a `describe` callback that calls `it` with a title of `"handles invalid plays"`. ```js const callExpressions = babelisedCode.getType('CallExpression').filter(c => { return c.callee?.name === 'it'; }); assert.include( callExpressions.map(c => c.arguments?.[0]?.value), 'handles invalid plays' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 57 ### --description-- Within the `"handles invalid plays"` callback, set up a new game similarly to the previous tests, but with a `gameId` of `"game-4"`. ### --tests-- A `playerOne` variable should be declared and assigned `Keypair.generate()`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'playerOne'; }); assert.exists(variableDeclaration, 'A variable named `playerOne` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const playerOne=Keypair.generate()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` A `playerTwo` variable should be declared and assigned `Keypair.generate()`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'playerTwo'; }); assert.exists(variableDeclaration, 'A variable named `playerTwo` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeString = `const playerTwo=Keypair.generate()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` A `gameId` variable should be declared and assigned `"game-4"`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.name === 'gameId'; }); assert.exists(variableDeclaration, 'A variable named `gameId` should exist'); const actualCodeString = babelisedCode.generateCode(variableDeclaration, { compact: true }); const expectedCodeStrings = [`const gameId="game-4"`, `const gameId='game-4'`]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` A `gamePublicKey` variable should be destructed from a `PublicKey.findProgramAddressSync` call. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const variableDeclaration = blockStatement?.body?.find(v => { return v?.declarations?.[0]?.id?.elements?.[0]?.name === 'gamePublicKey'; }); assert.exists( variableDeclaration, 'A variable named `gamePublicKey` should be destructured' ); const awaitExpression = variableDeclaration?.declarations?.[0]?.init; const actualCodeString = babelisedCode.generateCode(awaitExpression, { compact: true }); const expectedCodeStrings = [ `PublicKey.findProgramAddressSync([Buffer.from("game"),playerOne.publicKey.toBuffer(),Buffer.from(gameId)],program.programId)`, `PublicKey.findProgramAddressSync([Buffer.from('game'),playerOne.publicKey.toBuffer(),Buffer.from(gameId)],program.programId)` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` An airdrop should be requested and confirmed for `playerOne`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const babelisedCode2 = new __helpers.Babeliser(actualCodeString, { plugins: ['typescript'] }); const callExpression2 = babelisedCode2.getType('CallExpression').find(c => { return c.callee?.object?.object?.name === 'programProvider'; }); assert.exists(callExpression2, 'Airdrop should be requested for playerOne'); ``` The `setupGame` method should be called on the program. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeString = `await program.methods.setupGame(playerTwo.publicKey,gameId).accounts({game:gamePublicKey,playerOne:playerOne.publicKey}).signers([playerOne]).rpc()`; assert.deepInclude(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 58 ### --description-- Within the `"handles invalid plays"` callback, use the `play` function to have player one place an `X` in the top left corner. ### --tests-- `tests/tic-tac-toe.ts` should have `await play(program, gamePublicKey, playerOne, {row:0,column:0}, 2, {active:{}}, [[{x:{}},null,null],[null,null,null],[null,null,null]]);`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const actualCodeString = babelisedCode.generateCode(blockStatement, { compact: true }); const expectedCodeStrings = [ `await play(program,gamePublicKey,playerOne,{row:0,column:0},2,{active:{}},[[{x:{}},null,null],[null,null,null],[null,null,null]])`, `await play(program,gamePublicKey,playerOne,{column:0,row:0},2,{active:{}},[[{x:{}},null,null],[null,null,null],[null,null,null]])` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 59 ### --description-- To test whether the `play` instruction handle correctly throws an error when a player tries to play out of turn, wrap a `play` call in a `try...catch` block. The `try` should throw an error if the `play` call does not throw an error. ### --tests-- `tests/tic-tac-toe.ts` should have `await play(program, gamePublicKey, playerOne, {row:1,column:0}, 2, {active:{}}, [[{x:{}},null,null],[null,null,null],[null,null,null]]);` in the `try` block. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatement = blockStatement?.body?.find(v => { return v?.block?.type === 'TryStatement'; }); assert.exists(tryStatement, 'A try statement should exist'); const actualCodeString = babelisedCode.generateCode(tryStatement, { compact: true }); const expectedCodeString = `try{await play(program,gamePublicKey,playerOne,{row:1,column:0},2,{active:{}},[[{x:{}},null,null],[null,null,null],[null,null,null]])}catch(e){}`; assert.deepInclude(actualCodeString, expectedCodeString); ``` `tests/tic-tac-toe.ts` should have a `try` block that throws if `play` does not throw. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatement = blockStatement?.body?.find(v => { return v?.block?.type === 'TryStatement'; }); const tryBlock = tryStatement?.block; const actualCodeString = babelisedCode.generateCode(tryBlock, { compact: true }); const chai = await import('chai'); const { expect } = chai; const play = () => {}; let program, gamePublicKey, playerOne, playerTwo; try { await eval(`(async () => { ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', 'Try block should throw if `play` does not throw' ); } ``` ## 60 ### --description-- Within the `catch` block, assert the caught error is an instance of `AnchorError`. ### --tests-- `tests/tic-tac-toe.ts` should have a `catch` block that asserts `e instanceof AnchorError`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatement = blockStatement?.body?.find(v => { return v?.block?.type === 'TryStatement'; }); const catchBlock = tryStatement?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'NotPlayersTurn', number: 6003 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw with an `AnchorError`'); } try { await eval(`(async () => { let ${errorParameterName} = ${'Not an AnchorError'}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', 'Catch block should throw without an `AnchorError`' ); } ``` `AnchorError` should be imported from `@coral-xyz/anchor`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source?.value === '@coral-xyz/anchor'; }); assert.exists( importDeclaration, 'An import from `@coral-xyz/anchor` should exist' ); const specifierNames = importDeclaration.specifiers?.map(s => { return s?.local?.name; }); assert.include( specifierNames, 'AnchorError', 'The `Keypair` class should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 61 ### --description-- Anchor deserializes any Rust enum annotated with `error_code` into a JavaScript object consisting of a numeric code, and a string name matching the pascalcase enum variant. Within the `catch` block, assert the caught error has an `error.errorCode.code` property equal to `"NotPlayersTurn"`, and an `error.errorCode.number` property equal to `6003`. ### --tests-- `tests/tic-tac-toe.ts` should have a `catch` block that asserts `e.error.errorCode.code === "NotPlayersTurn"`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatement = blockStatement?.body?.find(v => { return v?.block?.type === 'TryStatement'; }); const catchBlock = tryStatement?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'NotPlayersTurn', number: 6003 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw'); } try { __error.error.errorCode.code = 'fcc'; await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'Catch block should throw'); } ``` `tests/tic-tac-toe.ts` should have a `catch` block that asserts `e.error.errorCode.number === 6003`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatement = blockStatement?.body?.find(v => { return v?.block?.type === 'TryStatement'; }); const catchBlock = tryStatement?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'NotPlayersTurn', number: 6003 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw'); } try { __error.error.errorCode.number = 6000; await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'Catch block should throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 62 ### --description-- If you have multiple programs, or a function involves multiple program calls, the Anchor error also provides a `program` property, which is the program that threw the error. Within the `catch` block, assert the program that threw the error is equal to `program.programId`. ### --tests-- `tests/tic-tac-toe.ts` should have a `catch` block that asserts `e.program === program.programId`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatement = blockStatement?.body?.find(v => { return v?.block?.type === 'TryStatement'; }); const catchBlock = tryStatement?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'NotPlayersTurn', number: 6003 }, comparedValues: [] }; this.program = { programId: 1 }; } } let program = { programId: 1 }, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw'); } try { program.programId = 2; await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'Catch block should throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 63 ### --description-- Within the `"handles invalid plays"` callback, add another `try...catch` block testing for the case where a player tries to play in a tile that is out of bounds. ### --tests-- The `"handles invalid plays"` callback should have a second `try...catch` block. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement2 = tryStatements?.[1]; assert.exists(tryStatement2); ``` The `try` block should call `play` with an invalid tile. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement2 = tryStatements?.[1]; assert.exists(tryStatement2, "A second `try...catch` block should exist"); const tryBlock = tryStatement2?.block; const actualCodeString = babelisedCode.generateCode(tryBlock, { compact: true }); const chai = await import('chai'); const { expect } = chai; const play = (p,g,pl,t) => { const {row, column} = t; assert.exists(row, "`row` should exist); assert.exists(column, "`column` should exist"); const isRowOut = row >= 3 || row < 0; const isColumnOut = column >= 3 || column < 0; assert(isRowOut || isColumnOut, "`row` and/or `column` should be out of bounds"); }; let program, gamePublicKey, playerOne, playerTwo; await eval(`(async () => { ${actualCodeString} })()`); ``` The `try` block should throw if the `play` call does not throw. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement2 = tryStatements?.[1]; assert.exists(tryStatement2, 'A second `try...catch` block should exist'); const tryBlock = tryStatement2?.block; const actualCodeString = babelisedCode.generateCode(tryBlock, { compact: true }); const chai = await import('chai'); const { expect } = chai; const play = () => {}; let program, gamePublicKey, playerOne, playerTwo; try { await eval(`(async () => { ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', 'The `try` block should throw, if `play` does not' ); } ``` The `catch` block should assert the error is an instance of `AnchorError`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement2 = tryStatements?.[1]; assert.exists(tryStatement2, 'A second `try...catch` block should exist'); const catchBlock = tryStatement2?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement2?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'TileOutOfBounds', number: 6000 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw with an `AnchorError`'); } try { await eval(`(async () => { let ${errorParameterName} = ${'Not an AnchorError'}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', 'Catch block should throw without an `AnchorError`' ); } ``` The `catch` block should assert the error has an `error.errorCode.number` property equal to `6000`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement2 = tryStatements?.[1]; assert.exists(tryStatement2, 'A second `try...catch` block should exist'); const catchBlock = tryStatement2?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement2?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'TileOutOfBounds', number: 6000 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw'); } try { __error.error.errorCode.number = 6003; await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'Catch block should throw'); } ``` The `catch` block should assert the error has an `error.errorCode.code` property equal to `"TileOutOfBounds"`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement2 = tryStatements?.[1]; assert.exists(tryStatement2, 'A second `try...catch` block should exist'); const catchBlock = tryStatement2?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement2?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'TileOutOfBounds', number: 6000 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw'); } try { __error.error.errorCode.code = 'fcc'; await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'Catch block should throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 64 ### --description-- Within the `"handles invalid plays"` callback, add another `try...catch` block testing for the case where a player tries to play in a tile that is already occupied. ### --tests-- The `"handles invalid plays"` callback should have a third `try...catch` block. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement3 = tryStatements?.[2]; assert.exists(tryStatement3); ``` The `try` block should call `play` with a tile position that is already occupied. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement3 = tryStatements?.[2]; assert.exists(tryStatement3); assert.exists(tryStatement3, "A third `try...catch` block should exist"); const tryBlock = tryStatement3?.block; const actualCodeString = babelisedCode.generateCode(tryBlock, { compact: true }); const chai = await import('chai'); const { expect } = chai; const play = (p,g,pl,t) => { const {row, column} = t; assert.exists(row, "`row` should exist); assert.exists(column, "`column` should exist"); const isRowOut = row === 0 || row === 1; const isColumnOut = column === 0; assert(isRowOut && isColumnOut, "`row` and `column` should already be occupied"); }; let program, gamePublicKey, playerOne, playerTwo; await eval(`(async () => { ${actualCodeString} })()`); ``` The `try` block should throw if the `play` call does not throw. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement3 = tryStatements?.[2]; assert.exists(tryStatement3); assert.exists(tryStatement3, 'A third `try...catch` block should exist'); const tryBlock = tryStatement3?.block; const actualCodeString = babelisedCode.generateCode(tryBlock, { compact: true }); const chai = await import('chai'); const { expect } = chai; const play = () => {}; let program, gamePublicKey, playerOne, playerTwo; try { await eval(`(async () => { ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', 'The `try` block should throw, if `play` does not' ); } ``` The `catch` block should assert the error is an instance of `AnchorError`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement3 = tryStatements?.[2]; const catchBlock = tryStatement3?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement3?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'TileAlreadySet', number: 6001 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw with an `AnchorError`'); } try { await eval(`(async () => { let ${errorParameterName} = ${'Not an AnchorError'}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', 'Catch block should throw without an `AnchorError`' ); } ``` The `catch` block should assert the error has an `error.errorCode.number` property equal to `6001`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement3 = tryStatements?.[2]; assert.exists(tryStatement3, 'A third `try...catch` block should exist'); const catchBlock = tryStatement3?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement3?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'TileAlreadySet', number: 6001 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw'); } try { __error.error.errorCode.number = 6003; await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'Catch block should throw'); } ``` The `catch` block should assert the error has an `error.errorCode.code` property equal to `"TileAlreadySet"`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement3 = tryStatements?.[2]; assert.exists(tryStatement3, 'A third `try...catch` block should exist'); const catchBlock = tryStatement3?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement3?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'TileAlreadySet', number: 6001 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw'); } try { __error.error.errorCode.code = 'fcc'; await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'Catch block should throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 65 ### --description-- Within the `"handles invalid plays"` callback, call the `play` function as many times as necessary to have a player win the game. ### --tests-- All `play` calls outwith `try...catch` blocks should result in a win. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const plays = blockStatement?.body?.filter(v => { return v.expression?.argument?.callee?.name === 'play'; }); const assertionCodeString = babelisedCode.generateCode({ type: 'BlockStatement', body: plays, directives: [] }); let program, gamePublicKey, playerOne = 1, playerTwo = 4; const board = [ [null, null, null], [null, null, null], [null, null, null] ]; const play = (p, g, pl, tile) => { board[tile.row][tile.column] = pl; }; await eval(`(async () => {${assertionCodeString}})()`); function checkWin(board) { // All rows board.forEach(r => { const rowSum = r.reduce((acc, curr) => curr && acc + curr, 0); if (rowSum === playerOne * 3 || rowSum === playerTwo * 3) { return true; } }); // All columns for (let i = 0; i < board.length; i++) { const columnSum = board[0][i] + board[1][i] + board[2][i]; if (columnSum === playerOne * 3 || columnSum === playerTwo * 3) { return true; } } for (let i = 0; i < board.length; i++) { if (board[i][0] === board[i][1] && board[i][1] === board[i][2]) { return true; } if (board[0][i] === board[1][i] && board[1][i] === board[2][i]) { return true; } if (board[0][0] === board[1][1] && board[1][1] === board[2][2]) { return true; } if (board[0][2] === board[1][1] && board[1][1] === board[2][0]) { return true; } } return false; } assert.isTrue(checkWin(board), `Found board of: ${JSON.stringify(board)}`); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 66 ### --description-- Within the `"handles invalid plays"` callback, add another `try...catch` block testing for the case where a player tries to play after the game is over. ### --tests-- The `"handles invalid plays"` callback should have a fourth `try...catch` block. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement4 = tryStatements?.[3]; assert.exists(tryStatement4); ``` The `try` block should call `play` after the game is over. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement4 = tryStatements?.[3]; assert.exists(tryStatement4); assert.exists(tryStatement4, "A fourth `try...catch` block should exist"); const tryBlock = tryStatement4?.block; const actualCodeString = babelisedCode.generateCode(tryBlock, { compact: true }); const chai = await import('chai'); const { expect } = chai; const play = (p,g,pl,t) => { const {row, column} = t; assert.exists(row, "`row` should exist); assert.exists(column, "`column` should exist"); }; let program, gamePublicKey, playerOne, playerTwo; await eval(`(async () => { ${actualCodeString} })()`); ``` The `try` block should throw if the `play` call does not throw. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement4 = tryStatements?.[3]; assert.exists(tryStatement4); assert.exists(tryStatement4, 'A fourth `try...catch` block should exist'); const tryBlock = tryStatement4?.block; const actualCodeString = babelisedCode.generateCode(tryBlock, { compact: true }); const chai = await import('chai'); const { expect } = chai; const play = () => {}; let program, gamePublicKey, playerOne, playerTwo; try { await eval(`(async () => { ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', 'The `try` block should throw, if `play` does not' ); } ``` The `catch` block should assert the error is an instance of `AnchorError`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement4 = tryStatements?.[3]; const catchBlock = tryStatement4?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement4?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'TileAlreadySet', number: 6001 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw with an `AnchorError`'); } try { await eval(`(async () => { let ${errorParameterName} = ${'Not an AnchorError'}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual( e.message, 'fcc', 'Catch block should throw without an `AnchorError`' ); } ``` The `catch` block should assert the error has an `error.errorCode.number` property equal to `6002`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement4 = tryStatements?.[3]; assert.exists(tryStatement4, 'A fourth `try...catch` block should exist'); const catchBlock = tryStatement4?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement4?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'GameAlreadyOver', number: 6002 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw'); } try { __error.error.errorCode.number = 6003; await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'Catch block should throw'); } ``` The `catch` block should assert the error has an `error.errorCode.code` property equal to `"GameAlreadyOver"`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.name === 'it' && c.arguments?.[0]?.value === 'handles invalid plays' ); }); const blockStatement = callExpression?.arguments?.[1]?.body; const tryStatements = blockStatement?.body?.filter(v => { return v?.block?.type === 'TryStatement'; }); const tryStatement4 = tryStatements?.[3]; assert.exists(tryStatement4, 'A fourth `try...catch` block should exist'); const catchBlock = tryStatement4?.handler?.body; const actualCodeString = babelisedCode.generateCode(catchBlock, { compact: true }); const errorParameterName = tryStatement4?.handler?.param?.name; const chai = await import('chai'); const { expect } = chai; class AnchorError { constructor() { this.error = { errorCode: { code: 'GameAlreadyOver', number: 6002 }, comparedValues: [] }; } } let program, playerOne, playerTwo; let __error = new AnchorError(); try { await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); } catch (e) { assert.fail(e, 'Catch block should not throw'); } try { __error.error.errorCode.code = 'fcc'; await eval(`(async () => { let ${errorParameterName} = ${__error}; ${actualCodeString} })()`); assert.fail('fcc'); } catch (e) { assert.notEqual(e.message, 'fcc', 'Catch block should throw'); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/tests/tic-tac-toe.ts` ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 67 ### --description-- **Summary** - The Anchor `workspace` contains all the programs in your project - Each program has a `methods` property containing all the program's instructions - The `accounts` method is used to pass in all the required public keys for the chained instruction - The `signers` method is used to pass in all the keypairs for the chained instruction - The `rpc` method sends the transaction to the Solana cluster - Each program has an `account` property containing all the program's accounts - An account's data can be fetched with: `program.account..fetch()` - Program Derived Addresses are used to programmatically generate a public key for an account - The `PublicKey.findProgramAddressSync` method can be used to derive a PDA 🎆 Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-how-to-build-a-client-side-app-part-1.md ================================================ # Solana - Learn How to Build a Client-Side App: Part 1 ## 1 ### --description-- Previously, you built, tested, and deployed a Tic-Tac-Toe program. In this project, you will learn how to write a client-side application that interacts with your program API. Open a new terminal, and cd into the `learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/` directory. ### --tests-- You should be in the `learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/?$`); assert.match(cwd, dirRegex); ``` ## 2 ### --description-- Add the correct program public key to the `programs/tic-tac-toe/src/lib.rs` and `Anchor.toml` files. ### --tests-- The `lib.rs` file should contain the program id within the `declare_id!()` call. ```js const librs = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/programs/tic-tac-toe/src/lib.rs` ); const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/tic-tac-toe` ); const expectedProgramId = stdout.match(/[^\s]{44}/)?.[0]; const actualProgramId = librs.match(/declare_id!\("([^\)]+)"\)/)?.[1]; assert.equal(actualProgramId, expectedProgramId); ``` The `Anchor.toml` file should contain the program id as the value for the `tic_tac_toe` key. ```js const toml = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/Anchor.toml` ); const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/tic-tac-toe` ); const expectedProgramId = stdout.match(/[^\s]{44}/)?.[0]; const actualProgramId = toml.match(/tic_tac_toe = "([^\"]+)"/)?.[1]; assert.equal(actualProgramId, expectedProgramId); ``` ## 3 ### --description-- Build the program. ### --tests-- The program should successfully build. ```js const { access, constants } = await import('fs/promises'); try { await access( `${project.dashedName}/tic-tac-toe/target/deploy/tic_tac_toe.so`, constants.F_OK ); } catch (e) { assert.fail( `Try running \`anchor build\` in the \`tic-tac-toe\` directory:\n\n${JSON.stringify( e, null, 2 )}` ); } ``` ## 4 ### --description-- You have been started out with boilerplate client-side code in the `app/` directory. In the terminal, change into the `app/` directory. ### --tests-- Your current working directory should be `learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/app/` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/tic-tac-toe/app/?$`); assert.match(cwd, dirRegex); ``` ## 5 ### --description-- Within `app/`, create a `web3.js` file for all of your Solana and Anchor code. ### --tests-- You should have a `learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/app/web3.js` file. ```js const { access, constants } = await import('fs/promises'); await access(join(project.dashedName, 'tic-tac-toe/app/web3.js')); ``` ## 6 ### --description-- Within `web3.js`, export a named variable `PROGRAM_ID`, and set it to the public key of your program. ### --tests-- You should have `export const PROGRAM_ID = new PublicKey(...)`. ```js const codeString = await __helpers.getFile( `${project.dashedName}/tic-tac-toe/app/web3.js` ); const babelisedCode = new __helpers.Babeliser(codeString); const actualCodeString = babelisedCode.generateCode(babelisedCode.parsedCode, { compact: true }); const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/tic-tac-toe` ); const expectedProgramId = stdout.match(/[^\s]{44}/)?.[0]; const expectedCodeStrings = [ `export const PROGRAM_ID=new PublicKey('${expectedProgramId}');`, `export const PROGRAM_ID=new PublicKey("${expectedProgramId}");` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ## 7 ### --description-- Within `app/` use `yarn` to install `@solana/web3.js@1.78`. ### --tests-- You should install version `1.78` of the `@solana/web3.js` package. ```js const packageJson = JSON.parse( await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/package.json') ) ); assert.property( packageJson.dependencies, '@solana/web3.js', 'The `package.json` file should have a `@solana/web3.js` dependency.' ); assert.equal( packageJson.dependencies['@solana/web3.js'], '1.78', 'Try running `yarn add @solana/web3.js@1.78` in the terminal.' ); ``` ## 8 ### --description-- Within `web3.js`, import the `PublicKey` class. ### --tests-- You should have `import { PublicKey } from "@solana/web3.js"`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists(importDeclaration, 'You should import from `@solana/web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'PublicKey', '`PublicKey` should be imported from `@solana/web3.js`' ); ``` ## 9 ### --description-- Generally, a browser-based client app follows the following workflow to interact with Solana programs: 1. Client connects to user wallet 2. Client builds a transaction 3. User signs the transaction 4. Client sends transaction to network Within `web3.js`, export a named function `connectWallet`. ### --tests-- You should have `export function connectWallet() {}`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const exportDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => { return e.declaration?.id?.name === 'connectWallet'; }); assert.exists(exportDeclaration, 'You should export `connectWallet`'); ``` ## 10 ### --description-- For your Tic-Tac-Toe program, the first transaction to build is the `setup_game` instruction. Within, `web3.js`, export a named, async function `startGame`. ### --tests-- You should have `export async function startGame() {}`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const exportDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => { return e.declaration?.id?.name === 'startGame'; }); assert.exists(exportDeclaration, 'You should export `startGame`'); assert.isTrue(exportDeclaration.declaration.async); ``` ## 11 ### --description-- Another transaction to build is the `play` instruction. Within `web3.js`, export a named, async function `handlePlay` that expects an `id` argument. ### --tests-- You should have `export async function handlePlay(id) {}`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const exportDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => { return e.declaration?.id?.name === 'handlePlay'; }); assert.exists(exportDeclaration, 'You should export `handlePlay`'); ``` ## 12 ### --description-- In order to send transactions to the network, the client has to connect to the network. Within `web3.js`, declare a variable named `connection`, and set it to connect with the default port of a local validator. ### --tests-- You should have `const connection = new Connection("http://localhost:8899")`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'connection'); assert.exists( connectionVariableDeclaration, 'You should declare a variable named `connection`' ); const newExpression = connectionVariableDeclaration.declarations[0].init; assert.equal( newExpression.callee.name, 'Connection', 'You should initialise `connection` with a new `Connection`' ); assert.equal( newExpression.arguments[0].value, 'http://localhost:8899', "You should create a new connection with `new Connection('http://localhost:8899')`" ); ``` You should import `Connection` from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists(importDeclaration, 'You should import from `@solana/web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'Connection', '`Connection` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 13 ### --description-- Each program game state account requires a game id. This id is used to find the program address on the ed25519 curve. Within `web3.js`, export a named function `deriveGamePublicKey` that expects two arguments: `playerOnePublicKey` and `gameId`. ### --tests-- You should have `export function deriveGamePublicKey(playerOnePublicKey, gameId) {}`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const exportDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => { return e.declaration?.id?.name === 'deriveGamePublicKey'; }); assert.exists(exportDeclaration, 'You should export `deriveGamePublicKey`'); ``` ## 14 ### --description-- Two players are required. Within `tic-tac-toe/`, create two keypairs: `player-one.json` and `player-two.json` ### --tests-- You should have a `learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/player-one.json` file. ```js const { access, constants } = await import('fs/promises'); await access( join(project.dashedName, 'tic-tac-toe/player-one.json'), constants.F_OK ); ``` The `player-one.json` file should contain a valid keypair. ```js const keyjson = JSON.parse( await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/player-one.json') ) ); try { const { Keypair } = await import('@solana/web3.js'); const keypair = Keypair.fromSecretKey(new Uint8Array(keyjson)); } catch (e) { assert.fail( `Try running \`solana-keygen new --outfile player-one.json\` in the \`tic-tac-toe\` directory:\n\n${JSON.stringify( e, null, 2 )}` ); } ``` You should have a `learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/player-two.json` file. ```js const { access, constants } = await import('fs/promises'); await access( join(project.dashedName, 'tic-tac-toe/player-two.json'), constants.F_OK ); ``` The `player-two.json` file should contain a valid keypair. ```js const keyjson = JSON.parse( await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/player-two.json') ) ); try { const { Keypair } = await import('@solana/web3.js'); const keypair = Keypair.fromSecretKey(new Uint8Array(keyjson)); } catch (e) { assert.fail( `Try running \`solana-keygen new --outfile player-two.json\` in the \`tic-tac-toe\` directory:\n\n${JSON.stringify( e, null, 2 )}` ); } ``` ## 15 ### --description-- Within `app/`, run `yarn dev` in your terminal to serve your app. Open your browser to the localhost shown in the output. Pay attention to the required inputs for the game. ### --tests-- You should have your app served at `http://localhost:5173`. ```js const response = await fetch('http://localhost:5173'); assert.equal(response.status, 200, 'The server should be running.'); ``` ## 16 ### --description-- Typically, connecting to a wallet involves using browser API to connect to a wallet browser extension. _You will do this in the next project._ For this project, you will directly input the keypair array. For convienience, the UI includes the keypairs you created in the previous step for easy copy-pasting, which is then stored in the browser's session storage. Within the `connectWallet` function, create a keypair from the session storage: ```js const keypairStr = sessionStorage.getItem('keypair'); const keypairArr = JSON.parse(keypairStr); const uint8Arr = new Uint8Array(keypairArr); const keypair = Keypair.fromSecretKey(uint8Arr); ``` ### --tests-- You should have the above code within the `connectWallet` function. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'connectWallet'); assert.exists( functionDeclaration, 'You should declare a function named `connectWallet`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeStrings = [ `const keypairStr=sessionStorage.getItem('keypair');const keypairArr=JSON.parse(keypairStr);const uint8Arr=new Uint8Array(keypairArr);const keypair=Keypair.fromSecretKey(uint8Arr);`, `const keypairStr=sessionStorage.getItem("keypair");const keypairArr=JSON.parse(keypairStr);const uint8Arr=new Uint8Array(keypairArr);const keypair=Keypair.fromSecretKey(uint8Arr);` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` You should import `Keypair` from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists(importDeclaration, 'You should import from `@solana/web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'Keypair', '`Keypair` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 17 ### --description-- Typically, wallet vendors provide interfaces and plugins to interact with their wallet. For this project, a `Wallet` class is provided to simulate the wallet interface. Within the `connectWallet` function, create a new `Wallet` instance assigned to a `wallet` variable. ### --tests-- You should have `const wallet = new Wallet(keypair);`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'connectWallet'); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedString = `const wallet=new Wallet(keypair);`; assert.include(actualCodeString, expectedString); ``` You should import `Wallet` from `./wallet.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists(importDeclaration, 'You should import from `./wallet.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'Wallet', '`Wallet` should be imported from `./wallet.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 18 ### --description-- Anchor expects a `Provider` instance to be passed to the `Program` constructor. The `Provider` instance is used to sign transactions based on the provided wallet. Within the `connectWallet` function, declare a `provider` variable set to: ```js const provider = new AnchorProvider(connection, wallet, {}); ``` **Note:** The empty `{}` options property prevents the default `AnchorProvider` options from being used which only work on Nodejs clients. ### --tests-- You should have the above code within the `connectWallet` function. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'connectWallet'); assert.exists( functionDeclaration, 'You should declare a function named `connectWallet`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const provider=new AnchorProvider(connection,wallet,{});`; assert.include(actualCodeString, expectedCodeString); ``` You should import `AnchorProvider` from `@coral-xyz/anchor`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@coral-xyz/anchor'; }); assert.exists(importDeclaration, 'You should import from `@coral-xyz/anchor`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'AnchorProvider', '`AnchorProvider` should be imported from `@coral-xyz/anchor`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 19 ### --description-- Install the `@coral-xyz/anchor@0.28` package. ### --tests-- You should install version `0.28` of the `@coral-xyz/anchor` package. ```js const packageJson = JSON.parse( await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/package.json') ) ); assert.property( packageJson.dependencies, '@coral-xyz/anchor', 'The `package.json` file should have a `@coral-xyz/anchor` dependency.' ); assert.equal( packageJson.dependencies['@coral-xyz/anchor'], '0.28', 'Try running `yarn add @coral-xyz/anchor@0.28` in the terminal.' ); ``` ## 20 ### --description-- To tell Anchor which provider to use for all transactions, use the `setProvider` function from `@coral-xyz/anchor` and pass it the `provider` variable. ### --tests-- You should have `setProvider(provider)` within the `connectWallet` function. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'connectWallet'); assert.exists( functionDeclaration, 'You should declare a function named `connectWallet`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `setProvider(provider);`; assert.include(actualCodeString, expectedCodeString); ``` You should import `setProvider` from `@coral-xyz/anchor`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@coral-xyz/anchor'; }); assert.exists(importDeclaration, 'You should import from `@coral-xyz/anchor`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'setProvider', '`setProvider` should be imported from `@coral-xyz/anchor`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 21 ### --description-- Within the `connectWallet` function, declare a `program` variable set to: ```js new Program(IDL, PROGRAM_ID, provider); ``` ### --tests-- You should have the above code within the `connectWallet` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'connectWallet'); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedString = `const program=new Program(IDL,PROGRAM_ID,provider);`; assert.include(actualCodeString, expectedString); ``` ## 22 ### --description-- Import the `IDL` generated by Anchor. ### --tests-- You should have `import { IDL } from "../target/types/tic_tac_toe";`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '../target/types/tic_tac_toe'; }); assert.exists(importDeclaration, 'You should import from `../target/types`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'IDL', '`IDL` should be imported'); ``` ## 23 ### --description-- To make the program available globally, attach it to the `window`. ### --tests-- You should have `window.program = program` in `connectWallet`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const actualCodeString = babelisedCode.generateCode(babelisedCode.parsedCode, { compact: true }); const expectedCodeString = `window.program=program;`; assert.include(actualCodeString, expectedCodeString); ``` ## 24 ### --description-- Now, within the `app/index.js` file, call the `connectWallet` function in the `connectWalletBtnEl` event listener callback at the indicated comment. ### --tests-- You should add `connectWallet()` below the `// TODO: Connect to wallet` comment. ```js const callExpression = babelisedCode .getType('CallExpression') .find(c => c.callee.object.name === 'connectWalletBtnEl'); const tryStatementBlock = callExpression.arguments[1]?.body?.body?.find( s => s.type === 'TryStatement' )?.block; const actualCodeString = babelisedCode.generateCode(tryStatementBlock, { compact: true }); const expectedCodeString = `connectWallet()`; assert.include(actualCodeString, expectedCodeString); ``` You should import `connectWallet` from `./web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './web3.js'; }); assert.exists(importDeclaration, 'You should import from `./web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'connectWallet', '`connectWallet` should be imported' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/index.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 25 ### --description-- Within the `startGame` function in `web3.js`, declare a `gameId` variable set to the `"gameId"` session storage item. ### --tests-- You should have `const gameId = sessionStorage.getItem("gameId");` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeStrings = [ `const gameId=sessionStorage.getItem("gameId")`, `const gameId=sessionStorage.getItem('gameId')` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ## 26 ### --description-- Within the `startGame` function, declare a `playerOnePublicKey` variable set to the `"playerOnePublicKey"` session storage item. ### --tests-- You should have `const playerOnePublicKey = sessionStorage.getItem("playerOnePublicKey");` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeStrings = [ `const playerOnePublicKey=sessionStorage.getItem("playerOnePublicKey")`, `const playerOnePublicKey=sessionStorage.getItem('playerOnePublicKey')` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ## 27 ### --description-- Within the `startGame` function, declare a `playerTwoPublicKey` variable set to the `"playerTwoPublicKey"` session storage item. ### --tests-- You should have `const playerTwoPublicKey = sessionStorage.getItem("playerTwoPublicKey");` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeStrings = [ `const playerTwoPublicKey=sessionStorage.getItem("playerTwoPublicKey")`, `const playerTwoPublicKey=sessionStorage.getItem('playerTwoPublicKey')` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ## 28 ### --description-- Within the `startGame` function, declare a `gamePublicKey` variable set to the result of calling the `deriveGamePublicKey` function with the `playerOnePublicKey`, `gameId`, and `PROGRAM_ID` variables. ### --tests-- You should have `const gamePublicKey = deriveGamePublicKey(playerOnePublicKey, gameId, PROGRAM_ID);` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeStrings = [ `const gamePublicKey=deriveGamePublicKey(playerOnePublicKey,gameId,PROGRAM_ID)`, `const gamePublicKey=deriveGamePublicKey(playerOnePublicKey,gameId,PROGRAM_ID)` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ## 29 ### --description-- Within the `startGame` function, set the `"gamePublicKey"` session storage item to the `gamePublicKey` variable value. ### --tests-- You should have `sessionStorage.setItem("gamePublicKey", gamePublicKey.toString());` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeStrings = [ `sessionStorage.setItem("gamePublicKey",gamePublicKey.toString())`, `sessionStorage.setItem('gamePublicKey',gamePublicKey.toString())` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ## 30 ### --description-- Within the `startGame` function, declare a `keypairStr` variable set to the `"keypair"` session storage item. ### --tests-- You should have `const keypairStr = sessionStorage.getItem("keypair");` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeStrings = [ `const keypairStr=sessionStorage.getItem("keypair")`, `const keypairStr=sessionStorage.getItem('keypair')` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ## 31 ### --description-- Within the `startGame` function, declare a `keypairArr` variable set to the result of calling `JSON.parse` with the `keypairStr` variable. ### --tests-- You should have `const keypairArr = JSON.parse(keypairStr);` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeStrings = [ `const keypairArr=JSON.parse(keypairStr)`, `const keypairArr=JSON.parse(keypairStr)` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ## 32 ### --description-- Within the `startGame` function, declare a `uint8Arr` variable set to a new `Uint8Array` with the `keypairArr` variable. ### --tests-- You should have `const uint8Arr = new Uint8Array(keypairArr);` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const uint8Arr=new Uint8Array(keypairArr)`; assert.include(actualCodeString, expectedCodeString); ``` ## 33 ### --description-- Within the `startGame` function, declare a `keypair` variable set to the result of calling `Keypair.fromSecretKey` with the `uint8Arr` variable. ### --tests-- You should have `const keypair = Keypair.fromSecretKey(uint8Arr);` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const keypair=Keypair.fromSecretKey(uint8Arr)`; assert.include(actualCodeString, expectedCodeString); ``` ## 34 ### --description-- Within the `startGame` function, call the `setupGame` instruction attaching the necessary accounts and signers. ### --tests-- You should have `await program.methods.setupGame(playerTwoPublicKey,gameId).accounts({player:keypair.publicKey,game:gamePublicKey}).signers([keypair]).rpc()` within the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `await program.methods.setupGame(playerTwoPublicKey,gameId).accounts({player:keypair.publicKey,game:gamePublicKey}).signers([keypair]).rpc()`; assert.include(actualCodeString, expectedCodeString); ``` ## 35 ### --description-- Deriving the public key requires converting the inputs to buffers. Also, the `@coral-xyz/anchor` internals make use of buffers for the transactions. The browser does not have a Buffer class. Instead, you will need to install the `buffer` package, and attach it to the `window`. Install the `buffer` package in the `app/` directory. ### --tests-- You should install the `buffer` package. ```js const packageJson = JSON.parse( await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/package.json') ) ); assert.property( packageJson.dependencies, 'buffer', 'The `package.json` file should have a `buffer` dependency.' ); ``` ## 36 ### --description-- Within the `web3.js` file, import the `Buffer` class named export from the `buffer` package. ### --tests-- You should have `import { Buffer } from "buffer";` within the `web3.js` file. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === 'buffer'; }); assert.exists(importDeclaration, 'You should import from `buffer`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'Buffer', '`Buffer` should be imported'); ``` ## 37 ### --description-- Within the `web3.js` file, attach the `Buffer` class to the `window` using the same name. ### --tests-- You should have `window.Buffer = Buffer;` within the `web3.js` file. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); assert.match(codeString, /window\.Buffer\s*=\s*Buffer/); ``` ## 38 ### --description-- Within the `deriveGamePublicKey` function, use the `PublicKey.findProgramAddressSync` function to derive the game public key from the `playerOnePublicKey` and `gameId` parameters. Return the public key. _Remember all the seeds defined for the `tic-tac-toe` program_ ### --tests-- You should have `return PublicKey.findProgramAddressSync([Buffer.from("game"),playerOnePublicKey.toBuffer(),Buffer.from(gameId)], PROGRAM_ID)[0];` within the `deriveGamePublicKey` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'deriveGamePublicKey'); assert.exists( functionDeclaration, 'You should declare a function named `deriveGamePublicKey`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `return PublicKey.findProgramAddressSync([Buffer.from("game"),playerOnePublicKey.toBuffer(),Buffer.from(gameId)],PROGRAM_ID)[0]`; assert.include(actualCodeString, expectedCodeString); ``` ## 39 ### --description-- Within `web3.js`, declare a an async `getGameAccount` function. ### --tests-- You should have `async function getGameAccount() {}` within the `web3.js` file. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); assert.match( codeString, /async\s+function\s+getGameAccount\s*\(\s*\)\s*\{\s*\}/ ); ``` ## 40 ### --description-- Within the `getGameAccount` function, declare a `gamePublicKey` variable set to the `"gamePublicKey"` session storage item passed to the `PublicKey` constructor. ### --tests-- You should have `const gamePublicKey = new PublicKey(sessionStorage.getItem("gamePublicKey"));` within the `getGameAccount` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'getGameAccount'); assert.exists( functionDeclaration, 'You should declare a function named `getGameAccount`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const gamePublicKey=new PublicKey(sessionStorage.getItem("gamePublicKey"))`; assert.include(actualCodeString, expectedCodeString); ``` ## 41 ### --description-- Within the `getGameAccount` function, declare a `gameData` variable set to the result of awaiting a fetch to the `program` variable's `game` account. ### --tests-- You should have `const gameData = await program.account.game.fetch(gamePublicKey);` within the `getGameAccount` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'getGameAccount'); assert.exists( functionDeclaration, 'You should declare a function named `getGameAccount`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const gameData=await program.account.game.fetch(gamePublicKey)`; assert.include(actualCodeString, expectedCodeString); ``` ## 42 ### --description-- Within the `getGameAccount` function, return the `gameData` variable. ### --tests-- You should have `return gameData;` within the `getGameAccount` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'getGameAccount'); assert.exists( functionDeclaration, 'You should declare a function named `getGameAccount`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `return gameData`; assert.include(actualCodeString, expectedCodeString); ``` ## 43 ### --description-- Seeing as the game is played by multiple players, the client needs to have the latest game state. Within the `web3.js` file, declare and export an async `updateBoard` function. ### --tests-- You should have `export async function updateBoard() {}` within the `web3.js` file. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); assert.match( codeString, /export\s+async\s+function\s+updateBoard\s*\(\s*\)\s*\{\s*\}/ ); ``` ## 44 ### --description-- Within the `updateBoard` function, declare a `gameData` variable set to the result of calling the `getGameAccount` function. ### --tests-- You should have `const gameData = await getGameAccount();` within the `updateBoard` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'updateBoard'); assert.exists( functionDeclaration, 'You should declare a function named `updateBoard`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const gameData=await getGameAccount()`; assert.include(actualCodeString, expectedCodeString); ``` ## 45 ### --description-- Within the `updateBoard` function, declare a `board` variable set to the `gameData.board` property. ### --tests-- You should have `const board = gameData.board;` within the `updateBoard` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'updateBoard'); assert.exists( functionDeclaration, 'You should declare a function named `updateBoard`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const board=gameData.board`; assert.include(actualCodeString, expectedCodeString); ``` ## 46 ### --description-- A utility function has been provided that takes the `board` array, and sets the HTML elements to the correct values. Within the `web3.js` file, import the `setTiles` function from `./utils.js`, and call it within the `updateBoard` function with the `board` variable. ### --tests-- You should have `setTiles(board);` within the `updateBoard` function. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'updateBoard'); assert.exists( functionDeclaration, 'You should declare a function named `updateBoard`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `setTiles(board)`; assert.include(actualCodeString, expectedCodeString); ``` You should import `setTiles` from `./utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'You should import from `./utils.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'setTiles', '`setTiles` should be imported'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 47 ### --description-- At the end of the `startGame` function, call the `updateBoard` function to ensure after the play, the board is updated. ### --tests-- You should have `await updateBoard();` at the end of the `startGame` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'startGame'); assert.exists( functionDeclaration, 'You should declare a function named `startGame`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `await updateBoard()`; assert.include(actualCodeString, expectedCodeString); ``` ## 48 ### --description-- Within the `index.js` file, in the `startGameBtnEl` event listener callback, call the `startGame` function at the indicated comment. ### --tests-- You should have `await startGame();` below `// TODO: Create a new game`. ```js const callExpression = babelisedCode .getType('CallExpression') .find(c => c.callee?.object?.name === 'startGameBtnEl'); const tryStatementBlock = callExpression.arguments[1]?.body?.body?.find( s => s.type === 'TryStatement' )?.block; const actualCodeString = babelisedCode.generateCode(tryStatementBlock, { compact: true }); const expectedCodeString = `await startGame()`; assert.include(actualCodeString, expectedCodeString); ``` You should import `startGame` from `./web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './web3.js'; }); assert.exists(importDeclaration, 'You should import from `./web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'startGame', '`startGame` should be imported'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/index.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 49 ### --description-- Within the `index.js` file, in the `joinGameBtnEl` event listener callback, call the `updateBoard` function at the indicated comment. ### --tests-- You should have `await updateBoard();` below `// TODO: Join an existing game`. ```js const callExpression = babelisedCode .getType('CallExpression') .find(c => c.callee.object?.name === 'joinGameBtnEl'); const tryStatementBlock = callExpression.arguments[1]?.body?.body?.find( s => s.type === 'TryStatement' )?.block; const actualCodeString = babelisedCode.generateCode(tryStatementBlock, { compact: true }); const expectedCodeString = `updateBoard()`; assert.include(actualCodeString, expectedCodeString); ``` You should import `updateBoard` from `./web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './web3.js'; }); assert.exists(importDeclaration, 'You should import from `./web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'updateBoard', '`updateBoard` should be imported' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/index.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 50 ### --description-- Within the `index.js` file, in the `DOMContentLoaded` event listener callback, call the `updateBoard` function at the indicated comment only if the `"gamePublicKey"` session storage item and `program` exist. ### --tests-- You should have `if (program && sessionStorage.getItem("gamePublicKey")) {await updateBoard();}` below `// TODO: If program and gamePublicKey exist, update board`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/index.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const callExpression = babelisedCode .getType('CallExpression') .find( c => c.callee.object?.name === 'document' && c.callee.property?.name === 'addEventListener' ); const newBabelisedCode = new __helpers.Babeliser( babelisedCode.generateCode(callExpression) ); const tryStatement = newBabelisedCode.getType('TryStatement')?.[0]; const tryStatementBlock = tryStatement.block; const actualCodeString = babelisedCode.generateCode(tryStatementBlock, { compact: true }); const expectedCodeStrings = [ `if(program&&sessionStorage.getItem("gamePublicKey")){await updateBoard()`, `if(sessionStorage.getItem("gamePublicKey")&&program){await updateBoard()` ]; const promises = expectedCodeStrings.map((expectedCodeString, index) => { return new Promise((resolve, reject) => { try { assert.include(actualCodeString, expectedCodeString); resolve(index + 1); } catch (e) { reject(e); } }); }); await Promise.any(promises); ``` ## 51 ### --description-- Within `web3.js` initialize `window.program` to `null`. ### --tests-- You should have `window.program = null;` within the `web3.js` file. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); assert.match(codeString, /window\.program\s*=\s*null/); ``` ## 52 ### --description-- Within the `handlePlay` function in `web3.js`, use the utility function `idToTile` to convert the `id` parameter to a `tile` variable. ### --tests-- You should have `const tile = idToTile(id);` within the `handlePlay` function. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'handlePlay'); assert.exists( functionDeclaration, 'You should declare a function named `handlePlay`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const tile=idToTile(id)`; assert.include(actualCodeString, expectedCodeString); ``` You should import `idToTile` from `./utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'You should import from `./utils.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'idToTile', '`idToTile` should be imported'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 53 ### --description-- Within the `handlePlay` function, declare a `keypair` variable set to an instance of the correct `Keypair` value. ### --tests-- You should have `const keypair = Keypair.fromSecretKey(new Uint8Array(JSON.parse(sessionStorage.getItem("keypair"))));` within the `handlePlay` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'handlePlay'); assert.exists( functionDeclaration, 'You should declare a function named `handlePlay`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const keypair=Keypair.fromSecretKey(new Uint8Array(JSON.parse(sessionStorage.getItem("keypair"))))`; assert.include(actualCodeString, expectedCodeString); ``` ## 54 ### --description-- Within the `handlePlay` function, declare a `gamePublicKey` variable set to the `"gamePublicKey"` session storage item passed to the `PublicKey` constructor. ### --tests-- You should have `const gamePublicKey = new PublicKey(sessionStorage.getItem("gamePublicKey"));` within the `handlePlay` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'handlePlay'); assert.exists( functionDeclaration, 'You should declare a function named `handlePlay`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `const gamePublicKey=new PublicKey(sessionStorage.getItem("gamePublicKey"))`; assert.include(actualCodeString, expectedCodeString); ``` ## 55 ### --description-- Within the `handlePlay` function, call the `play` instruction attaching the necessary accounts and signers. ### --tests-- You should have `await program.methods.play(tile).accounts({ player: keypair.publicKey, game: gamePublicKey }).signers([keypair]).rpc();` within the `handlePlay` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'handlePlay'); assert.exists( functionDeclaration, 'You should declare a function named `handlePlay`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `await program.methods.play(tile).accounts({player:keypair.publicKey,game:gamePublicKey}).signers([keypair]).rpc()`; assert.include(actualCodeString, expectedCodeString); ``` ## 56 ### --description-- Within the `handlePlay` function, call the `updateBoard` function. ### --tests-- You should have `await updateBoard();` within the `handlePlay` function. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'handlePlay'); assert.exists( functionDeclaration, 'You should declare a function named `handlePlay`' ); const actualCodeString = babelisedCode.generateCode(functionDeclaration, { compact: true }); const expectedCodeString = `await updateBoard()`; assert.include(actualCodeString, expectedCodeString); ``` ## 57 ### --description-- Within the `index.js` file, in the `tdEl` event listener callback, call the `handlePlay` function at the indicated comment. Remember to pass in the `id` of the `tdEl` element. ### --tests-- You should have `await handlePlay(event.target.id);` below `// TODO: Play tile`. ```js const callExpression = babelisedCode .getType('CallExpression') .find(c => c.callee.object?.name === 'tdEl'); const tryStatementBlock = callExpression.arguments[1]?.body?.body?.find( s => s.type === 'TryStatement' )?.block; const actualCodeString = babelisedCode.generateCode(tryStatementBlock, { compact: true }); const expectedCodeString = `handlePlay(event.target.id)`; assert.include(actualCodeString, expectedCodeString); ``` You should import `handlePlay` from `./web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './web3.js'; }); assert.exists(importDeclaration, 'You should import from `./web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'handlePlay', '`handlePlay` should be imported' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'tic-tac-toe/app/index.js') ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 58 ### --description-- Start the Solana local validator, and deploy the tic-tac-toe program. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The tic-tac-toe program should be deployed. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getProgramAccounts", "params": [ "BPFLoader2111111111111111111111111111111111", { "encoding": "base64", "dataSlice": { "length": 0, "offset": 0 } } ] }'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); const { stdout: keys } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/tic-tac-toe` ); const expectedProgramId = keys.match(/[^\s]{44}/)?.[0]; try { const jsonOut = JSON.parse(stdout); assert.exists(jsonOut.result.find(r => r.pubkey === expectedProgramId)); } catch (e) { assert.fail( e, `Try running \`solana-test-validator --bpf-program ${expectedProgramId} ./target/deploy/tic_tac_toe.so --reset\`` ); } ``` ## 59 ### --description-- Start the Solana local validator, and initialize your two player accounts with funds. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The `player-one.json` account should have at least 1 SOL. ```js const { stdout } = await __helpers.getCommandOutput( `solana balance ${project.dashedName}/tic-tac-toe/player-one.json` ); const balance = stdout.trim()?.match(/\d+/)?.[0]; assert.isAtLeast( parseInt(balance), 1, 'Try running `solana airdrop 1 ./player-one.json` within `tic-tac-toe/`' ); ``` The `player-two.json` account should have at least 1 SOL. ```js const { stdout } = await __helpers.getCommandOutput( `solana balance ${project.dashedName}/tic-tac-toe/player-two.json` ); const balance = stdout.trim()?.match(/\d+/)?.[0]; assert.isAtLeast( parseInt(balance), 1, 'Try running `solana airdrop 1 ./player-one.json` within `tic-tac-toe/`' ); ``` ## 60 ### --description-- Then, opening your browser to the port your app is served on, you should now be able to play a game. **NOTE:** Open one tab for each player, add the necessary keypairs, choose a game id, start a game with player one, join the game with player two, then start playing. ### --tests-- The `player-one.json` account should make at least one transaction. ```js const { Connection, PublicKey } = await import('@solana/web3.js'); const connection = new Connection('http://localhost:8899', 'confirmed'); const { stdout } = await __helpers.getCommandOutput( `solana address -k ${project.dashedName}/tic-tac-toe/player-one.json` ); const pubkey = new PublicKey(stdout.trim()); const transactions = await connection.getConfirmedSignaturesForAddress2(pubkey); // 2 is used because the first transaction is likely an airdrop assert.isAtLeast(transactions, 2, 'Try playing a game with player one'); ``` The `player-two.json` account should make at least one transaction. ```js const { Connection, PublicKey } = await import('@solana/web3.js'); const connection = new Connection('http://localhost:8899', 'confirmed'); const { stdout } = await __helpers.getCommandOutput( `solana address -k ${project.dashedName}/tic-tac-toe/player-two.json` ); const pubkey = new PublicKey(stdout.trim()); const transactions = await connection.getConfirmedSignaturesForAddress2(pubkey); // 2 is used because the first transaction is likely an airdrop assert.isAtLeast(transactions, 2, 'Try playing a game with player one'); ``` ## 61 ### --description-- Within `web3.js`, you create a new `Connection` with the default level of commitment, _confirmed_: ```js new Connetion('http://localhost:8899', 'confirmed'); ``` _Commitment_ refers to the level of confidence you have that a transaction will be included in a block. The higher the level of commitment, the longer it takes for a transaction to be confirmed. Play different games changing the level of commitment to see what affect that has on the speed the board updates propagate. Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 62 ### --description-- Congratulations on finishing this project! Feel free to play with your code. **Summary** Interacting with your Anchor program in the browser involves: - Building the program IDL - Attaching `Buffer` to the `window` - Connecting to a user's wallet - Creating and settings the provider - Defining your Anchor `Program` - For apps involving shared state between multiple users, it is important to understand the required commitment level for a transaction Minimal code example: ```javascript import { AnchorProvider, Program, setProvider } from '@coral-xyz/anchor'; import { Connection, PublicKey } from '@solana/web3.js'; import { IDL } from '../path/to/program/idl'; import { Buffer } from 'buffer'; window.Buffer = Buffer; const connection = new Connection('network', 'commitment'); const PROGRAM_ID = new PublicKey('program_publickey'); // Wallet is an interface defined by Anchor, but specific to different vendors const wallet = new Wallet(); const provider = new AnchorProvider(connection, wallet, {}); setProvider(provider); const program = new Program(IDL, PROGRAM_ID, provider); ``` 🎆 Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-how-to-build-a-client-side-app-part-2.md ================================================ # Solana - Learn How to Build a Client-Side App: Part 2 ## 1 ### --description-- In this project, you will learn how to use the Phantom wallet browser extension to connect to your local validator, connect your wallet to a dApp, and sign transactions. Change into the `learn-how-to-build-a-client-side-app-part-2/` directory in a new terminal. ### --tests-- You should be in the `learn-how-to-build-a-client-side-app-part-2/` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/?$`); assert.match(cwd, dirRegex); ``` ## 2 ### --description-- You have been started out with the same Tic-Tac-Toe Anchor program as the last project. Previously, you manually copy-pasted the player keypairs into the client app. This is both insecure and a poor user experience. Instead, install the `@solana/wallet-adapter-phantom` package in `app/` to handle connecting to the Phantom Wallet - a multi-chain wallet. ### --tests-- You should have `@solana/wallet-adapter-phantom` in `app/package.json`. ```js const packageJson = JSON.parse( await __helpers.getFile(join(project.dashedName, 'app/package.json')) ); assert.property( packageJson.dependencies, '@solana/wallet-adapter-phantom', 'The `package.json` file should have a `@solana/wallet-adapter-phantom` dependency.' ); ``` ## 3 ### --description-- Within `web3.js`, replace the `Wallet` import with `PhantomWalletAdapter` from `@solana/wallet-adapter-phantom`. ### --tests-- You should not import `Wallet` from `./wallet.js`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './wallet.js'; }); assert.notExists(importDeclaration, 'You should not import from `./wallet.js`'); ``` You should import `PhantomWalletAdapter` from `@solana/wallet-adapter-phantom`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/wallet-adapter-phantom'; }); assert.exists( importDeclaration, 'You should import from `@solana/wallet-adapter-phantom`' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'PhantomWalletAdapter', '`PhantomWalletAdapter` should be imported from `@solana/wallet-adapter-phantom`' ); ``` ## 4 ### --description-- Within the `connectWallet` function, delete all the keypair logic, and assign `wallet` a new instance of `PhantomWalletAdapter`. ### --tests-- You should remove the `keypairArr` declaration from `connectWallet`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'keypairArr'); assert.notExists( variableDeclaration, 'You should remove the `keypairArr` declaration from `connectWallet`' ); ``` You should remove the `uint` declaration from `connectWallet`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'uint'); assert.notExists( variableDeclaration, 'You should remove the `uint` declaration from `connectWallet`' ); ``` You should remove the `keypair` declaration from `connectWallet`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'keypair'); assert.notExists( variableDeclaration, 'You should remove the `keypair` declaration from `connectWallet`' ); ``` You should have `const wallet = new PhantomWalletAdapter()` within `connectWallet`. ```js const expectedCodeString = `const wallet=new PhantomWalletAdapter()`; const actualCodeString = babelisedCode.generate(babelisedCode.parsedCode, { compact: true }); assert.include(actualCodeString, expectedCodeString); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const babelisedConnectWallet = new __helpers.Babeliser( babelisedCode.generate( babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'connectWallet') ) ); global.babelisedCode = babelisedConnectWallet; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 5 ### --description-- Similarly, remove all the keypair logic within the `startGame` function. ### --tests-- You should remove the `keypairStr` declaration from `startGame`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'keypairStr'); assert.notExists( variableDeclaration, 'A `keypairStr` declaration should not exist in `startGame`' ); ``` You should remove the `keypairArr` declaration from `startGame`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'keypairArr'); assert.notExists( variableDeclaration, 'A `keypairArr` declaration should not exist in `startGame`' ); ``` You should remove the `uint8Array` declaration from `startGame`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'uint8Array'); assert.notExists( variableDeclaration, 'A `uint8Array` declaration should not exist in `startGame`' ); ``` You should remove the `keypair` declaration from `startGame`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'keypair'); assert.notExists( variableDeclaration, 'A `keypair` declaration should not exist in `startGame`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const babelisedConnectWallet = new __helpers.Babeliser( babelisedCode.generate( babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'connectWallet') ) ); global.babelisedCode = babelisedConnectWallet; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 6 ### --description-- The Phantom Browser Extension injects a global `window.phantom` object into the browser. This object contains the chains and public keys of the currently connected wallet. Within `startGame`, set the `playerOne` account public key to `window.phantom.solana.publicKey`. ### --tests-- You should have `.accounts({playerOne:window.phantom.solana.publicKey})` within `startGame`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const babelisedStartGame = new __helpers.Babeliser( babelisedCode.generate( babelisedCode.getFunctionDeclarations().find(f => f.id.name === 'startGame') ) ); const expectedCodeString = `.accounts({playerOne:window.phantom.solana.publicKey})`; const actualCodeString = babelisedStartGame.generate( babelisedStartGame.parsedCode, { compact: true } ); assert.include(actualCodeString, expectedCodeString); ``` ## 7 ### --description-- Within `startGame`, seeing as the wallet is handling the signing, remove the `keypair` as a `signer` to the rpc call. ### --tests-- You should remove `.signers([keypair])` from `startGame`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const babelisedStartGame = new __helpers.Babeliser( babelisedCode.generate( babelisedCode.getFunctionDeclarations().find(f => f.id.name === 'startGame') ) ); const expectedCodeString = `.signers([keypair])`; const actualCodeString = babelisedStartGame.generate( babelisedStartGame.parsedCode, { compact: true } ); assert.notInclude(actualCodeString, expectedCodeString); ``` ## 8 ### --description-- Within `startGame`, remove the keypair logic from the `handlePlay` function. ### --tests-- You should remove the `keypairStr` declaration from `handlePlay`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'keypairStr'); assert.notExists( variableDeclaration, 'A `keypairStr` declaration should not exist in `handlePlay`' ); ``` You should remove the `keypairArr` declaration from `handlePlay`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'keypairArr'); assert.notExists( variableDeclaration, 'A `keypairArr` declaration should not exist in `handlePlay`' ); ``` You should remove the `uint8Array` declaration from `handlePlay`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'uint8Array'); assert.notExists( variableDeclaration, 'A `uint8Array` declaration should not exist in `handlePlay`' ); ``` You should remove the `keypair` declaration from `handlePlay`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.id.name === 'keypair'); assert.notExists( variableDeclaration, 'A `keypair` declaration should not exist in `handlePlay`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const babelisedConnectWallet = new __helpers.Babeliser( babelisedCode.generate( babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'connectWallet') ) ); global.babelisedCode = babelisedConnectWallet; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 9 ### --description-- Within `handlePlay`, remove the `keypair` as a `signer` to the rpc call. ### --tests-- You should remove `.signers([keypair])` from `handlePlay`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const babelisedHandlePlay = new __helpers.Babeliser( babelisedCode.generate( babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'handlePlay') ) ); const expectedCodeString = `.signers([keypair])`; const actualCodeString = babelisedHandlePlay.generate( babelisedHandlePlay.parsedCode, { compact: true } ); assert.notInclude(actualCodeString, expectedCodeString); ``` ## 10 ### --description-- Within `handlePlay`, set the `player` account public key to the public key of the wallet. ### --tests-- You should have `.accounts({player:window.phantom.solana.publicKey})` within `handlePlay`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'app/web3.js') ); const babelisedCode = new __helpers.Babeliser(codeString); const babelisedHandlePlay = new __helpers.Babeliser( babelisedCode.generate( babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'handlePlay') ) ); const expectedCodeString = `.accounts({player:window.phantom.solana.publicKey})`; const actualCodeString = babelisedHandlePlay.generate( babelisedHandlePlay.parsedCode, { compact: true } ); assert.include(actualCodeString, expectedCodeString); ``` ## 11 ### --description-- The proceeding lessons do not have any tests. Follow the instructions, and once you are confident you have completed the task, type `done` in the terminal. Type `done` in the terminal to move on to the next lesson. ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 12 ### --description-- Within a browser, navigate to https://phantom.app/ to install the Phantom browser extension. ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 13 ### --description-- Click the "Download" button, and follow your browser's instructions to add the extension. ![Phantom browser extension download page](../../images/phantom/image.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 14 ### --description-- After installing the extension, click the Phantom icon in your browser's toolbar to set up your wallet. Create a password, and click "Continue". ![Phantom browser extension password creation](../../images/phantom/image-2.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 15 ### --description-- Take note of your secret recovery phrase. This is the only way to recover your wallet if you forget your password, and can be used to import your wallet into other browsers/platforms. Then, click "Continue". ![Phantom browser extension secret recovery phrase](../../images/phantom/image-3.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 16 ### --description-- Finish reading the setup information, and click the Phantom icon in your browser's toolbar to open the extension. ![Phantom browser extension setup information](../../images/phantom/image-4.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 17 ### --description-- You should a UI similar to: ![Phantom browser extension landing page](../../images/phantom/image-5.png) Open the menubar by clicking the three dots in the top left corner. ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 18 ### --description-- Open the settings page: ![Phantom browser extension settings button](../../images/phantom/image-6.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 19 ### --description-- Click the "Developer Settings" button. ![Phantom browser extension developer settings button](../../images/phantom/image-7.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 20 ### --description-- Enable the "Testnet Mode" in order to connect to your local validator. ![Phantom browser extension testnet mode](../../images/phantom/image-8.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 21 ### --description-- Click the Solana network button. ![Phantom browser extension solana network button](../../images/phantom/image-9.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 22 ### --description-- Select the "Solana Localnet" option. ![Phantom browser extension solana localnet option](../../images/phantom/image-10.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 23 ### --description-- From the menubar, click the account to edit it. ![Phantom browser extension account button](../../images/phantom/image-12.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 24 ### --description-- Change the account name to `Player 1` to help you keep track of it. ![Phantom browser extension account name](../../images/phantom/image-12.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 25 ### --description-- Within your terminal, start a local validator, being sure to deploy the `tic_tac_toe.so` program at the same time. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The mess program should be deployed. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getProgramAccounts", "params": [ "BPFLoader2111111111111111111111111111111111", { "encoding": "base64", "dataSlice": { "length": 0, "offset": 0 } } ] }'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); const programId = '5xGwZASoE5ZgxKgaisJNaGTGzMKzjyyBGv9FCUtu2m1c'; try { const jsonOut = JSON.parse(stdout); assert.exists(jsonOut.result.find(r => r.pubkey === programId)); } catch (e) { assert.fail( e, `Try running \`solana-test-validator --bpf-program ${programId} tic_tac_toe.so --reset\`` ); } ``` Within the Phantom browser extension, click on your account name to get your Solana public key: ![Phantom browser extension account public key](../../images/phantom/image-13.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 26 ### --description-- Within your terminal, airdrop to your account. ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 27 ### --description-- Within the `app/` directory, start the client app server with `yarn dev`. ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 28 ### --description-- Within your browser, navigate to http://localhost:5173/. You should see the client app. Connect your wallet to the app by clicking the "Connect Wallet" button. ![Phantom browser extension connect wallet button](../../images/phantom/image-14.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 29 ### --description-- Within the Phantom browser extension, create a second Solana account: ![Phantom browser extension create account button](../../images/phantom/image-15.png) ![Phantom browser extension create account button](../../images/phantom/image-16.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 30 ### --description-- Rename the second account to `Player 2`: ![Phantom browser extension rename account button](../../images/phantom/image-17.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 31 ### --description-- Within your terminal, airdrop to the second account. ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 32 ### --description-- Within the Phantom browser extension, ensure you are on your `Player 1` account. Within the client app, add the two public keys and any game id. ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 33 ### --description-- Within the client app, click the start game button. The Phantom browser extension should prompt you to approve the transaction. Approve it. ![Phantom browser extension approve transaction](../../images/phantom/image-19.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 34 ### --description-- Once the game is started, you can play the game by opening a second browser window, and connecting to the second account. ![Phantom browser extension connect wallet button](../../images/phantom/image-18.png) ![Phantom browser extension approve transaction](../../images/phantom/image-19.png) ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 35 ### --description-- Congratulations on finishing this project! Feel free to play with your code. **Summary**: 1. Install the wallet adapter/s: `@solana/wallet-adapter-` 2. Navigate to https://phantom.app/ 3. Install the Phantom browser extension 4. Start a local validator: ```bash solana-test-validator --bpf-program ./tic_tac_toe.so --reset ``` 5. Airdrop to your wallet account 6. Connect your wallet to your app 🎆 Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-how-to-build-for-mainnet.md ================================================ # Solana - Learn How to Build for Mainnet ## 1 ### --description-- You have been started off with an Anchor full-stack boilerplate. In this project, you will develop and prepare a program for deployment on the Solana blockchain. Start by changing into the `learn-how-to-build-for-mainnet/todo/` directory. ### --tests-- You should be in the `learn-how-to-build-for-mainnet/todo/` directory. ```js const cwd = await __helpers.getLastCWD(); const dirRegex = new RegExp(`${project.dashedName}/todo/?$`); assert.match(cwd, dirRegex); ``` ## 2 ### --description-- Within `programs/todo/src/lib.rs`, start by renaming the `initialize` instruction handle to `save_tasks`. ### --tests-- The `initialize` instruction handle should be renamed to `save_tasks`. ```js const librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); assert.match(librs, /pub fn save_tasks/); assert.notMatch(librs, /pub fn initialize/); ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn initialize(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct Initialize {} ``` ## 3 ### --description-- Rename the `Initialize` context to `SaveTasks`. ### --tests-- The `Initialize` struct should be renamed to `SaveTasks`. ```js const librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); assert.match(librs, /pub struct SaveTasks/); assert.notMatch(librs, /pub struct Initialize/); ``` The `Initialize` context should be renamed to `SaveTasks`. ```js const librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); assert.match(librs, /Context/); assert.notMatch(librs, /Context/); ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct Initialize {} ``` ## 4 ### --description-- A "task" will be saved into a new PDA. Within the `SaveTasks` struct, add a public `tasks` account with a type of `Account<'info, TasksAccount>`. ### --tests-- The `SaveTasks` struct should have a `pub tasks` field. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match(saveTasks, /pub\s+tasks/); ``` The `tasks` field should be annotated with `#[account()]`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match(saveTasks, /#\[\s*account\(/); ``` The `tasks` field should have a type of `Account<'info, TasksAccount>`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match( saveTasks, /pub\s+tasks\s*:\s*Account\s*<\s*'info\s*,\s*TasksAccount\s*>/ ); ``` The `SaveTasks` struct should be generic over a lifetime of `'info`. ```js assert.match(__librs, /struct\s+SaveTasks\s*<\s*'info\s*>/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct SaveTasks {} ``` ## 5 ### --description-- The `save_task` instruction handle will be the only instruction handle in the program. So, the data account should be initialized only _if needed_. Anchor provides an `init_if_needed` argument for this purpose. Pass it as an argument to the `account` attribute macro, and add `init-if-needed` as a feature to the `anchor-lang` dependency in the `Cargo.toml` file. ### --tests-- The `tasks` field should be annotated with `#[account(init_if_needed)]`. ```js const saveTasks = __librs.match( /struct\s+SaveTasks\s*<\s*'info\s*>\s*{([^}]*)}/s )?.[1]; assert.match(saveTasks, /#\[\s*account\s*\(\s*init_if_needed\s*\)\s*\]/); ``` The `anchor-lang` dependency should have a `features` array with a value of `["init-if-needed"]`. ```js const cargoToml = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/Cargo.toml` ); assert.match(cargoToml, /features\s*=\s*\[\s*"init-if-needed"\s*\]/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account()] pub tasks: Account<'info, TasksAccount> } ``` ## 6 ### --description-- Define a public `TasksAccount` struct with a `tasks` field of type `Vec`. Remember to annotate it as an account. ### --tests-- The `TasksAccount` struct should be annotated with `#[account]`. ```js assert.match(__librs, /#\[\s*account\s*\]\s*pub\s+struct\s+TasksAccount/); ``` The `TasksAccount` struct should have a public `tasks` field. ```js const tasksAccount = __librs.match( /pub\s+struct\s+TasksAccount[^{]*{([^}]*)}/s )?.[1]; assert.match(tasksAccount, /pub\s+tasks/); ``` The `tasks` field should have a type of `Vec`. ```js const tasksAccount = __librs.match( /pub\s+struct\s+TasksAccount[^{]*{([^}]*)}/s )?.[1]; assert.match(tasksAccount, /pub\s+tasks\s*:\s*Vec\s*<\s*Task\s*>/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account(init_if_needed)] pub tasks: Account<'info, TasksAccount> } ``` #### --"learn-how-to-build-for-mainnet/todo/programs/todo/Cargo.toml"-- ```toml [package] name = "todo" version = "0.1.0" description = "A todo list program" edition = "2021" [lib] crate-type = ["cdylib", "lib"] name = "todo" [features] no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] [dependencies] anchor-lang = { version = "0.28.0", features = ["init-if-needed"] } ``` ## 7 ### --description-- Define a public `Task` struct with an `id` file of type `u32`, a `name` field of type `String`, and a `completed` field of type `bool`. ### --tests-- The `Task` struct should have a public `id` field. ```js const task = __librs.match(/pub\s+struct\s+Task\s*{([^}]*)}/s)?.[1]; assert.match(task, /pub\s+id/); ``` The `id` field should have a type of `u32`. ```js const task = __librs.match(/pub\s+struct\s+Task\s*{([^}]*)}/s)?.[1]; assert.match(task, /pub\s+id\s*:\s*u32/); ``` The `Task` struct should have a public `name` field. ```js const task = __librs.match(/pub\s+struct\s+Task\s*{([^}]*)}/s)?.[1]; assert.match(task, /pub\s+name/); ``` The `name` field should have a type of `String`. ```js const task = __librs.match(/pub\s+struct\s+Task\s*{([^}]*)}/s)?.[1]; assert.match(task, /pub\s+name\s*:\s*String/); ``` The `Task` struct should have a public `completed` field. ```js const task = __librs.match(/pub\s+struct\s+Task\s*{([^}]*)}/s)?.[1]; assert.match(task, /pub\s+completed/); ``` The `completed` field should have a type of `bool`. ```js const task = __librs.match(/pub\s+struct\s+Task\s*{([^}]*)}/s)?.[1]; assert.match(task, /pub\s+completed\s*:\s*bool/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account(init_if_needed)] pub tasks: Account<'info, TasksAccount> } #[account] pub struct TasksAccount { pub tasks: Vec } ``` ## 8 ### --description-- Derive the necessary traits for the `Task` struct. ### --tests-- The `Task` struct should be annotated with `#[derive(AnchorSerialize, AnchorDeserialize, Clone)]`. ```js assert.match( __librs, /#\[\s*derive\s*\(\s*AnchorSerialize\s*,\s*AnchorDeserialize\s*,\s*Clone\s*\)\s*\]\s*pub\s+struct\s+Task/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account(init_if_needed)] pub tasks: Account<'info, TasksAccount>, } #[account] pub struct TasksAccount { pub tasks: Vec, } pub struct Task { pub id: u32, pub name: String, pub completed: bool, } ``` ## 9 ### --description-- The `save_tasks` instruction handle will take a `replacing_tasks` argument of type `Vec`. Add the expected argument. ### --tests-- The `save_tasks` instruction handle should have a `replacing_tasks` argument. ```js assert.match( __librs, /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks/s ); ``` The `replacing_tasks` argument should have a type of `Vec`. ```js assert.match( __librs, /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)/s ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account(init_if_needed)] pub tasks: Account<'info, TasksAccount>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } ``` ## 10 ### --description-- Create a public `ErrorCode` enum with the following variants: `TaskNameTooLong`, `TaskNameTooShort`, and `TaskIdNotUnique` ### --tests-- The `ErrorCode` enum should have a `TaskNameTooLong` variant. ```js const errorCode = __librs.match(/pub\s+enum\s+ErrorCode\s*{([^}]*)}/s)?.[1]; assert.match(errorCode, /TaskNameTooLong/); ``` The `ErrorCode` enum should have a `TaskNameTooShort` variant. ```js const errorCode = __librs.match(/pub\s+enum\s+ErrorCode\s*{([^}]*)}/s)?.[1]; assert.match(errorCode, /TaskNameTooShort/); ``` The `ErrorCode` enum should have a `TaskIdNotUnique` variant. ```js const errorCode = __librs.match(/pub\s+enum\s+ErrorCode\s*{([^}]*)}/s)?.[1]; assert.match(errorCode, /TaskIdNotUnique/); ``` The `ErrorCode` enum should be annotated with `#[error_code]`. ```js assert.match( __librs, /#\[\s*error_code\s*\]\s*pub\s+enum\s+ErrorCode\s*{([^}]*)}/s ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account(init_if_needed)] pub tasks: Account<'info, TasksAccount>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } ``` ## 11 ### --description-- Within the `save_tasks` instruction handle, return an error if any of the task names are greater than 32 characters. ### --tests-- The `save_tasks` instruction handle should return `Err(ErrorCode::TaskNameTooLong.into())` if any of the task names are greater than 32 characters. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /Err\s*\(\s*ErrorCode::TaskNameTooLong\s*\.\s*into\s*\(\s*\)/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account(init_if_needed)] pub tasks: Account<'info, TasksAccount>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 12 ### --description-- Within the `save_tasks` instruction handle, return an error if any of the task names are less than 1 character. ### --tests-- The `save_tasks` instruction handle should return `Err(ErrorCode::TaskNameTooShort.into())` if any of the task names are less than 1 character. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /Err\s*\(\s*ErrorCode::TaskNameTooShort\s*\.\s*into\s*\(\s*\)/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account(init_if_needed)] pub tasks: Account<'info, TasksAccount>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 13 ### --description-- Within the `save_tasks` instruction handle, return an error if any of the task ids are not unique. ### --tests-- The `save_tasks` instruction handle should return `Err(ErrorCode::TaskIdNotUnique.into())` if any of the task ids are not unique. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /Err\s*\(\s*ErrorCode::TaskIdNotUnique\s*\.\s*into\s*\(\s*\)/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account(init_if_needed)] pub tasks: Account<'info, TasksAccount>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 14 ### --description-- When initializing the `TasksAccount` account, set the `space` argument to: ```text + replacing_tasks.len() * ``` ### --tests-- The `SaveTasks` struct `tasks` field should be annotated with `#[account(space = ...)]`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match(saveTasks, /\s*space\s*=/); ``` The `space` argument should be set to `8 + replacing_tasks.len() * (4 + (4 + 32) + 1)`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match( saveTasks, /\s*space\s*=\s*8\s*\+\s*replacing_tasks\.len\s*\(\s*\)\s*\*\s*\(\s*4\s*\+\s*\(\s*4\s*\+\s*32\s*\)\s*\+\s*1\s*\)/ ); ``` The `SaveTasks` struct should be annotated with `#[instruction(replacing_tasks: Vec)]`. ```js assert.match( __librs, /#\[instruction\s*\(\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*\]/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } Ok(()) } } #[derive(Accounts)] pub struct SaveTasks<'info> { #[account(init_if_needed)] pub tasks: Account<'info, TasksAccount>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 15 ### --description-- Set the payer of the `TasksAccount` account to `user`, and declare a public `user` field of type `Signer<'info>` within the `SaveTasks` struct. ### --tests-- The `SaveTasks` struct should have a `pub user` field. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match(saveTasks, /pub\s+user/); ``` The `user` field should have a type of `Signer<'info>`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match(saveTasks, /pub\s+user\s*:\s*Signer\s*<\s*'\s*info\s*>\s*,/); ``` The `user` field should be annotated with `#[account(mut)]`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match(saveTasks, /#[^#]*account\s*\(\s*mut\s*\)/); ``` The `tasks` field should be annotated with `#[account(payer = user)]`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match(saveTasks, /payer\s*=\s*user/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1))] pub tasks: Account<'info, TasksAccount>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 16 ### --description-- Seeing as each ToDo will be related to a specific user, set the seeds of the `TasksAccount` account to the user public key, and tell Anchor to generate the bump seed. ### --tests-- The `tasks` field should be annotated with `#[account(seeds = [user.key().as_ref()])]`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match( saveTasks, /seeds\s*=\s*\[\s*user\.key\s*\(\s*\)\s*\.\s*as_ref\s*\(\s*\)\s*\]/ ); ``` The `tasks` field should be annotated with `#[account(bump)]`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match(saveTasks, /bump/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 17 ### --description-- Include the necessary program to initialize a data account owned by your program. ### --tests-- The `SaveTasks` struct should have a `pub system_program` field. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match(saveTasks, /pub\s+system_program/); ``` The `system_program` field should have a type of `Program<'info, System>`. ```js const saveTasks = __librs.match(/struct\s+SaveTasks[^{]*{([^}]*)}/s)?.[1]; assert.match( saveTasks, /pub\s+system_program\s*:\s*Program\s*<\s*'\s*info\s*,\s*System\s*>\s*,/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 18 ### --description-- As the number of tasks stored can vary between saves, the account size needs to reallocate to the correct amount of space. Within the `save_tasks` instruction handle, add an `if` statement to check if the `tasks` account `tasks` data length is not equal to the `replacing_tasks` length. ### --tests-- The `save_tasks` instruction handle should have `if tasks.tasks.len() != replacing_tasks.len() {}`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /if\s+tasks\.tasks\.len\s*\(\s*\)\s*!=\s*replacing_tasks\.len\s*\(\s*\)\s*{}/ ); ``` You should declare a `tasks` variable with value `ctx.accounts.tasks`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match(saveTasks, /let\s+tasks\s*=/); assert.match(saveTasks, /ctx\.accounts\.tasks\s*;/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 19 ### --description-- Within the `if` statement, declare a `new_space` variable with the new **total** size of the `tasks` account with the `replacing_tasks` data. ### --tests-- The `new_space` variable should be declared with a value of `8 + replacing_tasks.len() * (4 + (4 + 32) + 1)`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /let\s+new_space\s*=\s*8\s*\+\s*replacing_tasks\.len\s*\(\s*\)\s*\*\s*\(\s*4\s*\+\s*\(\s*4\s*\+\s*32\s*\)\s*\+\s*1\s*\)/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() != replacing_tasks.len() {} Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 20 ### --description-- With the new space, the new rent exemption can be calculated: ```rust let minimum_balance: u64 = Rent::get()?.minimum_balance(space); ``` Use the `new_space` to calculate the new rent exemption, and assign it to a `new_minimum_balance` variable. ### --tests-- The `new_minimum_balance` variable should be declared with a value of `Rent::get()?.minimum_balance(new_space)`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /let\s+new_minimum_balance\s*=\s*Rent\s*::\s*get\s*\(\s*\)\s*\?\s*\.\s*minimum_balance\s*\(\s*new_space\s*\)/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() != replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 21 ### --description-- An `Account` is just a wrapper around `AccountInfo`. The `tasks` account info is needed to get its current balance, and adjust the balance to the new minimum balance. ```rust let account_info = &ctx.accounts.account.to_account_info(); ``` Declare a `tasks_account_info` variable, and assign it the `tasks` account info. ### --tests-- The `tasks_account_info` variable should be declared with a value of `tasks.to_account_info()`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /let\s+tasks_account_info\s*=\s*tasks\s*\.to_account_info\s*\(\s*\)/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() != replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 22 ### --description-- To get the difference in balance, the `saturating_sub` method can be used on the `new_minimum_balance` to ensure the balance is not negative. Use `saturating_sub` to subtract `tasks_account_info.lamports()` from `new_minimum_balance`, and assign it to a `lamports_diff` variable. ### --tests-- The `lamports_diff` variable should be declared with a value of `new_minimum_balance.saturating_sub(tasks_account_info.lamports())`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /let\s+lamports_diff\s*=\s*new_minimum_balance\s*\.\s*saturating_sub\s*\(\s*tasks_account_info\s*\.\s*lamports\s*\(\s*\)\s*\)/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() != replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 23 ### --description-- A mutable account's balance can be adjusted by mutably borrowing its lamports and adjusting them: ```rust **ctx.accounts.mutable_account.to_account_info().try_borrow_mut_lamports()? = 1; ``` Subtract the `lamports_diff` from the `user` account. ### --tests-- You should have `**ctx.accounts.user.to_account_info().try_borrow_mut_lamports()? -= lamports_diff;`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /\*\*ctx\s*\.accounts\s*\.user\s*\.to_account_info\s*\(\s*\)\s*\.\s*try_borrow_mut_lamports\s*\(\s*\)\s*\?\s*-=\s*lamports_diff\s*;/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() != replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); let lamports_diff = new_minimum_balance.saturating_sub(tasks_account_info.lamports()); } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 24 ### --description-- Now, add the `lamports_diff` to the `tasks` account. ### --tests-- You should have `**tasks_account_info.try_borrow_mut_lamports()? += lamports_diff;`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /\*\*tasks_account_info\s*\.\s*try_borrow_mut_lamports\s*\(\s*\)\s*\?\s*\+=\s*lamports_diff\s*;/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() != replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); let lamports_diff = new_minimum_balance.saturating_sub(tasks_account_info.lamports()); **ctx .accounts .user .to_account_info() .try_borrow_mut_lamports()? -= lamports_diff; } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 25 ### --description-- Now that the `tasks` account has the correct balance, the `realloc` method can be called to reallocate the account's data: ```rust account_info.realloc(new_space, zero_initialize_memory)?; ``` The `realloc` method takes the new space, and a boolean to zero-initialize the new memory. Call `realloc` on the `tasks_account_info` with the `new_space` and `false`. ### --tests-- You should have `tasks_account_info.realloc(new_space, false)?;`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /tasks_account_info\s*\.\s*realloc\s*\(\s*new_space\s*,\s*false\s*\)\s*\?/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() != replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); let lamports_diff = new_minimum_balance.saturating_sub(tasks_account_info.lamports()); **ctx .accounts .user .to_account_info() .try_borrow_mut_lamports()? -= lamports_diff; **tasks_account_info.try_borrow_mut_lamports()? += lamports_diff; } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 26 ### --description-- After the `if` statement, set the `tasks` account's `tasks` data to `replacing_tasks`. ### --tests-- You should have `tasks.tasks = replacing_tasks;`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match(saveTasks, /tasks\s*\.\s*tasks\s*=\s*replacing_tasks\s*;/); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() != replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); let lamports_diff = new_minimum_balance.saturating_sub(tasks_account_info.lamports()); **ctx .accounts .user .to_account_info() .try_borrow_mut_lamports()? -= lamports_diff; **tasks_account_info.try_borrow_mut_lamports()? += lamports_diff; tasks_account_info.realloc(new_space, false)?; } Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 27 ### --description-- Lastly within the `save_tasks` instruction handle, adjust the `if` statement conditional to only realloc if the replacing tasks space is less than the currently allocated space. ### --tests-- The `if` statement conditional should be `if tasks.tasks.len() < replacing_tasks.len() {`. ```js const saveTasks = __librs.match( /pub\s+fn\s+save_tasks\s*\(\s*ctx:\s*Context<\s*SaveTasks\s*>,\s*replacing_tasks\s*:\s*Vec\s*<\s*Task\s*>\s*\)\s*->\s*Result\s*<\s*\(\)\s*>\s*{(.*?)Ok\(\(\)\)/s )?.[1]; assert.match( saveTasks, /if\s+tasks\s*\.\s*tasks\s*\.\s*len\s*\(\s*\)\s*<\s*replacing_tasks\s*\.\s*len\s*\(\s*\)\s*{/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() != replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); let lamports_diff = new_minimum_balance.saturating_sub(tasks_account_info.lamports()); **ctx .accounts .user .to_account_info() .try_borrow_mut_lamports()? -= lamports_diff; **tasks_account_info.try_borrow_mut_lamports()? += lamports_diff; tasks_account_info.realloc(new_space, false)?; } tasks.tasks = replacing_tasks; Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 28 ### --description-- Finally for the program, use the `msg` attribute macro to add human-readable error messages to the `ErrorCode` enum: ```rust #[error_code] enum MyErrorEnum { #[msg("My error message")] MyError, } ``` ### --tests-- The `TaskNameTooLong` variant should be annotated with a message. ```js const errorCode = __librs.match( /#\[error_code\]\s*pub\s+enum\s+ErrorCode\s*{([\s\S]*?)}/ )?.[1]; assert.match( errorCode, /#\s*\[\s*msg\s*\(\s*"[\w\s.]{1,}"\)\]\s*TaskNameTooLong\s*,/ ); ``` The `TaskNameTooShort` variant should be annotated with a message. ```js const errorCode = __librs.match( /#\[error_code\]\s*pub\s+enum\s+ErrorCode\s*{([\s\S]*?)}/ )?.[1]; assert.match( errorCode, /#\s*\[\s*msg\s*\(\s*"[\w\s.]{1,}"\)\]\s*TaskNameTooShort\s*,/ ); ``` The `TaskIdNotUnique` variant should be annotated with a message. ```js const errorCode = __librs.match( /#\[error_code\]\s*pub\s+enum\s+ErrorCode\s*{([\s\S]*?)}/ )?.[1]; assert.match( errorCode, /#\s*\[\s*msg\s*\(\s*"[\w\s.]{1,}"\)\]\s*TaskIdNotUnique\s*,/ ); ``` ### --before-all-- ```js const __librs = await __helpers.getFile( `${project.dashedName}/todo/programs/todo/src/lib.rs` ); global.__librs = __librs; ``` ### --after-all-- ```js delete global.__librs; ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() < replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); let lamports_diff = new_minimum_balance.saturating_sub(tasks_account_info.lamports()); **ctx .accounts .user .to_account_info() .try_borrow_mut_lamports()? -= lamports_diff; **tasks_account_info.try_borrow_mut_lamports()? += lamports_diff; tasks_account_info.realloc(new_space, false)?; } tasks.tasks = replacing_tasks; Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { TaskNameTooLong, TaskNameTooShort, TaskIdNotUnique, } ``` ## 29 ### --description-- Build the program to get the IDL for your client app. ### --tests-- You should build the program. ```js const { access, constants } = await import('fs/promises'); // See if `target/types/todo.ts` exists await access( join(project.dashedName, 'todo/target/types/todo.ts'), constants.F_OK ); ``` ### --seed-- #### --"learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs"-- ```rust use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } let tasks = &mut ctx.accounts.tasks; if tasks.tasks.len() < replacing_tasks.len() { let new_space = 8 + 4 + (4 + 32) + 1 * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); let lamports_diff = new_minimum_balance.saturating_sub(tasks_account_info.lamports()); **ctx .accounts .user .to_account_info() .try_borrow_mut_lamports()? -= lamports_diff; **tasks_account_info.try_borrow_mut_lamports()? += lamports_diff; tasks_account_info.realloc(new_space, false)?; } tasks.tasks = replacing_tasks; Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * (4 + (4 + 32) + 1), payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct TasksAccount { pub tasks: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { #[msg("The task name must be less than 32 characters.")] TaskNameTooLong, #[msg("The task name must be at least 1 character.")] TaskNameTooShort, #[msg("The task id must be unique.")] TaskIdNotUnique, } ``` ## 30 ### --description-- With the program complete, the client app can be wired to use it. The `app/` directory has a React app built with Vite. If you do not know React, do not worry; very little of the integration is React-specific. Within `app/src/app.tsx`, under the `TODO:1` comment, attach the `Buffer` to the `window`. ### --tests-- You should have `window.Buffer = Buffer`. ```js const expectedCodeString = 'window.Buffer=Buffer;'; const actualCodeString = __babelisedCode.generateCode( __babelisedCode.parsedCode, { compact: true } ); assert.include(actualCodeString, expectedCodeString); ``` You should import `Buffer` from `buffer`. ```js const importDeclaration = __babelisedCode.getImportDeclarations().find(i => { return i.source.value === 'buffer'; }); assert.exists(importDeclaration, 'You should import from `buffer`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'Buffer'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); global.__babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); ``` ### --after-all-- ```js delete global.__babelisedCode; ``` ## 31 ### --description-- Within `app/` use `yarn` to install the `@solana/web3.js` package. ### --tests-- You should have `@solana/web3.js` in your `package.json` dependencies. ```js const packageJson = JSON.parse( await __helpers.getFile(join(project.dashedName, 'todo/app/package.json')) ); assert.property( packageJson.dependencies, '@solana/web3.js', 'The `package.json` file should have a `@solana/web3.js` dependency.' ); ``` ## 32 ### --description-- Under the `TODO:2` comment, declare a `PROGRAM_ID` variable, and assign it the value of the program's id as a `PublicKey`. ### --tests-- You should have `const PROGRAM_ID = new PublicKey("...")`. ```js const { stdout } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/todo` ); const expectedProgramId = stdout.match(/[^\s]{44}/)?.[0]; const variableDeclaration = __babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'PROGRAM_ID'); const actualProgramId = variableDeclaration.declarations?.[0]?.init?.arguments?.[0]?.value; assert.equal(actualProgramId, expectedProgramId); ``` You should import `PublicKey` from `@solana/web3.js`. ```js const importDeclaration = __babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists(importDeclaration, 'You should import from `@solana/web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'PublicKey'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); global.__babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); ``` ### --after-all-- ```js delete global.__babelisedCode; ``` ## 33 ### --description-- Under the `TODO:3` comment, declare an `ENDPOINT` variable, and assign it the value of `import.meta.VITE_SOLANA_CONNECTION_URL` or default to `http://localhost:8899`. **Note:** `import.meta.VITE_*` is a way to access environemnt variables in a Vitejs app during build time. ### --tests-- You should have `const ENDPOINT = import.meta.VITE_SOLANA_CONNECTION_URL || "http://localhost:8899";`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /const\s+ENDPOINT\s*=\s*import\.meta\.VITE_SOLANA_CONNECTION_URL\s*\|\|/ ); ``` ## 34 ### --description-- Under the `TODO:4` comment, declare a `connection` variable, and assign it the value of a new `Connection` instance with the `ENDPOINT`, and choose a suitable commitment level for a production app. ### --tests-- You should have `const connection = new Connection(ENDPOINT, ...);`. ```js const variableDeclaration = __babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'connection'; }); assert.exists( variableDeclaration, 'A variable named `connection` should exist' ); const newExpression = variableDeclaration.declarations?.[0]?.init; const callee = newExpression?.callee; assert.equal(callee?.name, 'Connection'); const args = newExpression?.arguments; assert.equal(args?.[0]?.name, 'ENDPOINT'); ``` The commitment config should be `"confirmed"` or `"finalized"`. ```js const variableDeclaration = __babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'connection'; }); assert.exists( variableDeclaration, 'A variable named `connection` should exist' ); const newExpression = variableDeclaration.declarations?.[0]?.init; const args = newExpression?.arguments; assert.include(['confirmed', 'finalized'], args?.[1]?.value); ``` You should import `Connection` from `@solana/web3.js`. ```js const importDeclaration = __babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists(importDeclaration, 'You should import from `@solana/web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'Connection'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); global.__babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); ``` ### --after-all-- ```js delete global.__babelisedCode; ``` ## 35 ### --description-- Within `app/` use `yarn` to install the `@solana/wallet-adapter-phantom` package. ### --tests-- You should have `@solana/wallet-adapter-phantom` in your `package.json` dependencies. ```js const packageJson = JSON.parse( await __helpers.getFile(join(project.dashedName, 'todo/app/package.json')) ); assert.property( packageJson.dependencies, '@solana/wallet-adapter-phantom', 'The `package.json` file should have a `@solana/wallet-adapter-phantom` dependency.' ); ``` ## 36 ### --description-- Under the `TODO:5` comment, declare a `wallet` variable as a new instance of the Phantom wallet adapter. ### --tests-- You should have `const wallet = new PhantomWalletAdapter()`. ```js const variableDeclaration = __babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'wallet'; }); assert.exists(variableDeclaration, 'A variable named `wallet` should exist'); const newExpression = variableDeclaration.declarations?.[0]?.init; const callee = newExpression?.callee; assert.equal(callee?.name, 'PhantomWalletAdapter'); ``` You should import `PhantomWalletAdapter` from `@solana/wallet-adapter-phantom`. ```js const importDeclaration = __babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/wallet-adapter-phantom'; }); assert.exists( importDeclaration, 'There should be an import from `@solana/wallet-adapter-phantom`' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'PhantomWalletAdapter'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); global.__babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); ``` ### --after-all-- ```js delete global.__babelisedCode; ``` ## 37 ### --description-- Within `app/` use `yarn` to install the `@coral-xyz/anchor` package. ### --tests-- You should have `@coral-xyz/anchor` in your `package.json` dependencies. ```js const packageJson = JSON.parse( await __helpers.getFile(join(project.dashedName, 'todo/app/package.json')) ); assert.property( packageJson.dependencies, '@coral-xyz/anchor', 'The `package.json` file should have a `@coral-xyz/anchor` dependency.' ); ``` ## 38 ### --description-- Under the `TODO:6` comment, declare a React context variable, `ProgramContext`, for the program in order to access the program throughout the application components. ```typescript const MyContext = createContext(null); ``` ### --tests-- You should have `const ProgramContext = createContext | null>(null);`. ```js const variableDeclaration = __babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'ProgramContext'; }); assert.exists( variableDeclaration, 'A variable named `ProgramContext` should exist' ); const callExpression = variableDeclaration.declarations?.[0]?.init; const { callee, arguments: args, typeParameters } = callExpression; assert.equal(callee?.name, 'createContext'); assert.equal(args?.[0]?.value, null); ``` You should import `createContext` from `react`. ```js const importDeclaration = __babelisedCode.getImportDeclarations().find(i => { return i.source.value === 'react'; }); assert.exists(importDeclaration, 'There should be an import from `react`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'createContext'); ``` You should import `Program` from `@coral-xyz/anchor`. ```js const importDeclaration = __babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@coral-xyz/anchor'; }); assert.exists( importDeclaration, 'There should be an import from `@coral-xyz/anchor`' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'Program'); ``` You should import `Todo` from `../../target/types/todo`. ```js const importDeclaration = __babelisedCode.getImportDeclarations().find(i => { return i.source.value === '../../target/types/todo'; }); assert.exists( importDeclaration, 'There should be an import from `../../target/types/todo`' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'Todo'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); global.__babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); ``` ### --after-all-- ```js delete global.__babelisedCode; ``` ## 39 ### --description-- Under the `TODO:7` comment, declare a program state variable: ```typescript const [program, setProgram] = useState(null); ``` ### --tests-- You should have `const [program, setProgram] = useState | null>(null);`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /const\s*\[\s*program\s*,\s*setProgram\s*\]\s*=\s*useState\s*<\s*Program\s*<\s*Todo\s*>\s*\|\s*null\s*>\s*\(\s*null\s*\)/ ); ``` ## 40 ### --description-- Under the `TODO:8` comment, connect to the wallet. ### --tests-- You should have `await wallet.connect()`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match(codeString, /await\s+wallet\s*\.\s*connect\s*\(\s*\)/); ``` ## 41 ### --description-- Under the `TODO:9` comment, add an `if` statement with a condition to check if the wallet is actually connected. You can use the provided `isWalletConnected` function from `./utils.ts`. ### --tests-- You should have `if (isWalletConnected(wallet)) {}`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /if\s*\(\s*isWalletConnected\s*\(\s*wallet\s*\)\s*\)\s*{\s*}/ ); ``` You should import `isWalletConnected` from `./utils`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils'; }); assert.exists(importDeclaration, 'There should be an import from `./utils`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'isWalletConnected'); ``` ## 42 ### --description-- Under the `TODO:10` comment, and within the `if` statement, declare a `provider` variable as a new Anchor provider. ### --tests-- You should have `const provider = new AnchorProvider(connection, wallet, {})`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /const\s+provider\s*=\s*new\s+AnchorProvider\s*\(\s*connection\s*,\s*wallet\s*,\s*\{\s*\}\s*\)/ ); ``` You should import `AnchorProvider` from `@coral-xyz/anchor`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@coral-xyz/anchor'; }); assert.exists( importDeclaration, 'There should be an import from `@coral-xyz/anchor`' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'AnchorProvider'); ``` ## 43 ### --description-- Under the `TODO:11` comment, and within the `if` statement, declare a `program` variable as an instance of an Anchor program. ### --tests-- You should have `const program = new Program(IDL, PROGRAM_ID, provider)`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /const\s+program\s*=\s*new\s+Program\s*\(\s*IDL\s*,\s*PROGRAM_ID\s*,\s*provider\s*\)/ ); ``` You should import `IDL` from `../../target/types/todo`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '../../target/types/todo'; }); assert.exists( importDeclaration, 'There should be an import from `../../target/types/todo`' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'IDL'); ``` ## 44 ### --description-- Under the `TODO:12` comment, and within the `if` statment, use the `setProgram` function to set the `program` state variable to `program`. ### --tests-- You should have `setProgram(program)`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match(codeString, /setProgram\s*\(\s*program\s*\)/); ``` ## 45 ### --description-- Under the `TODO:13` comment, conditionally either render the `Landing` page or the `LogIn` page based on whether the `program` is set: ```tsx <>{condition ? : } ``` ### --tests-- You should have `<>{program ? : LogIn connectWallet={connectWallet} />}`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id?.name === 'App'; }); const returnStatement = functionDeclaration.body.body?.find( b => b.type === 'ReturnStatement' ); const expressionContainer = returnStatement.argument?.children?.find(c => { return c.expression?.test?.name === 'program'; }); const { test, consequent, alternate } = expressionContainer.expression; assert.equal(test.name, 'program'); assert.equal(consequent.openingElement.name?.name, 'Landing'); assert.equal(alternate.openingElement.name?.name, 'LogIn'); ``` ## 46 ### --description-- Under the `TODO:14` comment, replace the component fragment (`<>`) with a context provider for the program: ```tsx ``` ### --tests-- You should have `{program ? : LogIn connectWallet={connectWallet} />}`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /<\s*ProgramContext\s*\.\s*Provider\s*value\s*=\s*\{\s*program\s*\}\s*>/ ); assert.match(codeString, /<\/\s*ProgramContext\s*\.\s*Provider\s*>/); ``` ## 47 ### --description-- The `Landing` component fetches any tasks associated with a connected account, and handles the main logic for displaying ToDos. Under the `TODO:15` comment, declare a `program` variable with the program context hook: ```js const state = useContext(MyContext); ``` ### --tests-- You should have `const program = useContext(ProgramContext)`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /const\s+program\s*=\s*useContext\s*\(\s*ProgramContext\s*\)/ ); ``` You should import `useContext` from `react`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['typescript', 'jsx'] }); const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === 'react'; }); assert.exists(importDeclaration, 'There should be an import from `react`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include(importSpecifiers, 'useContext'); ``` ## 48 ### --description-- Under the `TODO:16` comment, add an `if` statement with a condition to check if the program exists. ### --tests-- You should have `if (program) {}`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match(codeString, /if\s*\(\s*program\s*\)\s*{\s*}/); ``` ## 49 ### --description-- Under the `TODO:17` comment, and within the `if` statement, declare a `tasksPublicKey` variable with a value of the program derived address. ### --tests-- You should have `const [tasksPublicKey, _] = PublicKey.findProgramAddressSync([program.provider.publicKey.toBuffer()], PROGRAM_ID)`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /const\s*\[\s*tasksPublicKey\s*,\s*_\s*\]\s*=\s*PublicKey\s*\.\s*findProgramAddressSync\s*\(\s*\[\s*program\s*\.\s*provider\s*\.\s*publicKey\s*\.\s*toBuffer\s*\(\s*\)\s*\]\s*,\s*PROGRAM_ID\s*\)/ ); ``` ## 50 ### --description-- Under the `TODO:18` comment, and within the `if` statement, declare a `tasks` variable with a value of the program's task account. ### --tests-- You should have `const tasks = await program.account.tasksAccount.fetch(tasksPublicKey)`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /const\s*tasks\s*=\s*await\s*program\s*\.\s*account\s*\.\s*tasksAccount\s*\.\s*fetch\s*\(\s*tasksPublicKey\s*\)/ ); ``` ## 51 ### --description-- Under the `TODO:19` comment, and within the `if` statement, call the `setTasks` function with the tasks data. ### --tests-- You should have `setTasks(tasks.tasks)`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match(codeString, /setTasks\s*\(\s*tasks\s*\.\s*tasks\s*\)/); ``` ## 52 ### --description-- Under the `TODO:20` comment, ensure the program exists, derive the program address, and save the `tasks` state to the program's task account. ### --tests-- You should have `if (program) { ... }`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match(codeString, /if\s*\(\s*program\s*\)\s*{/); ``` You should have `const [tasksPublicKey, _] = PublicKey.findProgramAddressSync([program.provider.publicKey.toBuffer()], PROGRAM_ID)`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /const\s*\[\s*tasksPublicKey\s*,\s*_\s*\]\s*=\s*PublicKey\s*\.\s*findProgramAddressSync\s*\(\s*\[\s*program\s*\.\s*provider\s*\.\s*publicKey\s*\.\s*toBuffer\s*\(\s*\)\s*\]\s*,\s*PROGRAM_ID\s*\)/ ); ``` You should have `await program.methods.saveTasks(tasks).accounts({ tasks: tasksPublicKey }).rpc()`. ```js const codeString = await __helpers.getFile( join(project.dashedName, 'todo/app/src/app.tsx') ); assert.match( codeString, /await\s*program\s*\.\s*methods\s*\.\s*saveTasks\s*\(\s*tasks\s*\)\s*\.\s*accounts\s*\(\s*\{\s*tasks\s*:\s*tasksPublicKey\s*\}\s*\)\s*\.\s*rpc\s*\(\s*\)/ ); ``` ## 53 ### --description-- Your client app is finished! Before testing, set the environment variables. Within `todo/` create a `.env` file, and add `VITE_SOLANA_CONNECTION_URL=http://localhost:8899`. ### --tests-- You should create a `todo/.env` file. ```js const { access, constants } = await import('fs/promises'); await access(join(project.dashedName, 'todo/.env'), constants.F_OK); ``` You should set the `VITE_SOLANA_CONNECTION_URL` variable to `http://localhost:8899`. ```js const { readFile } = await import('fs/promises'); const envFile = await readFile(join(project.dashedName, 'todo/.env'), 'utf8'); assert.match( envFile, /VITE_SOLANA_CONNECTION_URL\s*=\s*['`"]?http:\/\/localhost:8899/ ); ``` ## 54 ### --description-- Start a local Solana cluster with the program deployed. Then, start the client app with `yarn dev` in the `app/` directory. ### --tests-- You should start a local Solana cluster. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` You should deploy the program to the local cluster. ```js const { stdout: keys } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/todo` ); const expectedProgramId = keys.match(/[^\s]{44}/)?.[0]; const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getAccountInfo", "params": [ "${expectedProgramId}", { "encoding": "base64", "dataSlice": { "length": 0, "offset": 0 } } ] }'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.equal(jsonOut.result?.value?.executable, true); } catch (e) { assert.fail( e, `Try running \`solana-test-validator --bpf-program ${expectedProgramId} ./target/deploy/todo.so --reset\`` ); } ``` You should start the client app server. ```js const response = await fetch('http://localhost:5173'); assert.equal(response.status, 200, 'The server should be running.'); ``` ## 55 ### --description-- Open the client app in your browser, and connect your Phantom wallet, and play with your app making a few transactions. ### --tests-- You should perform a few transactions using the client interface. ```js const { Connection, PublicKey } = await import('@solana/web3.js'); const connection = new Connection('http://127.0.0.1:8899', 'confirmed'); const { stdout: keys } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/todo` ); const expectedProgramId = keys.match(/[^\s]{44}/)?.[0]; const pubkey = new PublicKey(expectedProgramId); const transactions = await connection.getConfirmedSignaturesForAddress2(pubkey); assert.isAtLeast( transactions, 2, 'Try using the client interface and your wallet to make a few transactions' ); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-how-to-deploy-to-devnet.md ================================================ # Solana - Learn How to Deploy to Devnet ## 1 ### --description-- You have been started with the same programa and app as the previous project. This time, you will deploy it to the public Devnet! First, start by ensuring it still works locally: 1. Install all dependencies 2. Build the program 3. Start a local Solana cluster - Deploy the program to the local cluster 4. Start the client server 5. Test the program ### --tests-- You should run `yarn` in the `learn-how-to-deploy-to-devnet/todo/` directory to install all dependencies. ```js const { access, constants } = await import('fs/promises'); await access(join(project.dashedName, 'todo/node_modules'), constants.F_OK); ``` You should run `anchor build` in the `learn-how-to-deploy-to-devnet/todo/` directory to build the program. ```js const { access, constants } = await import('fs/promises'); await access(join(project.dashedName, 'todo/target'), constants.F_OK); ``` You should have a local cluster running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` You should deploy the program to the local cluster. ```js const { stdout: keys } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/todo` ); const expectedProgramId = keys.match(/[^\s]{44}/)?.[0]; const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getAccountInfo", "params": [ "${expectedProgramId}", { "encoding": "base64", "dataSlice": { "length": 0, "offset": 0 } } ] }'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.equal(jsonOut.result?.value?.executable, true); } catch (e) { assert.fail( e, `Try running \`solana-test-validator --bpf-program ${expectedProgramId} ./target/deploy/todo.so --reset\`` ); } ``` You should run `yarn dev` in the `learn-how-to-deploy-to-devnet/todo/app/` directory to start the client server. ```js const response = await fetch('http://localhost:5173'); assert.equal(response.status, 200, 'The server should be running.'); ``` You should perform some transactions using the client app. ```js const { Connection, PublicKey } = await import('@solana/web3.js'); const connection = new Connection('http://127.0.0.1:8899', 'confirmed'); const { stdout: keys } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/todo` ); const expectedProgramId = keys.match(/[^\s]{44}/)?.[0]; const pubkey = new PublicKey(expectedProgramId); const transactions = await connection.getConfirmedSignaturesForAddress2(pubkey); assert.isAtLeast( transactions, 2, 'Try using the client interface and your wallet to make a few transactions' ); ``` ## 2 ### --description-- It is often good practice to _vet_ a public program befor using it. In the best case scenario, you can vet the source code yourself to ensure it is safe. With programs deployed as bytecode, it is difficult to determine what the program does. Anchor provides a tool to help you verify a program matches the source code: ```bash anchor verify ``` Provided you have the supposed source code for a program, you can run that command in the program directory to verify it is the source code for the public program id. Verify the program you deployed to the local cluster matches its source code. ### --tests-- You should run `anchor verify ` in the `learn-how-to-deploy-to-devnet/todo/programs/todo` directory to verify the program. ```js const lastCommand = await __helpers.getLastCommand(); const { stdout: keys } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/todo` ); const expectedProgramId = keys.match(/[^\s]{44}/)?.[0]; assert.include(lastCommand, `anchor verify "${expectedProgramId}"`); ``` ## 3 ### --description-- In order to work with the public Devnet, you will need to create a wallet. Instead of creating a wallet directly in Phantom, create a wallet using the Solana CLI. However, in order for this wallet to be compatible with Phantom, you will need to create the wallet using the `--derivation-path` flag. A derivation path is a way to create a wallet that is compatible with other wallets. Phantom uses the derivation path `m/44'/501'/0'/0'` to create wallets. Save this wallet to `learn-how-to-deploy-to-devnet/todo/wallet.json`. ### --tests-- You should run `solana-keygen new --outfile wallet.json --derivation-path` in the `learn-how-to-deploy-to-devnet/todo/` directory to create a wallet. ```js const { access, constants } = await import('fs/promises'); await access(join(project.dashedName, 'todo/wallet.json'), constants.F_OK); ``` ## 4 ### --description-- Now, to work on the Devnet, change your Solana config to use the Devnet URL. ### --tests-- You should run `solana config set --url devnet`. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'solana config set --url devnet'); ``` ## 5 ### --description-- In order to deploy to Devnet, you will need to have enough SOL to pay for the transaction fees. As Devnet is considered a testing network, you can get SOL by requesting an airdrop to your public key. Airdrop 2 SOL to your public key. ### --tests-- You should run `solana airdrop 2 --keypair wallet.json` in the `learn-how-to-deploy-to-devnet/todo/` directory to airdrop 2 SOL to your public key. ```js const { stdout } = await __helpers.getCommandOutput( `solana balance ${project.dashedName}/todo/wallet.json` ); const balance = stdout.trim()?.match(/\d+/)?.[0]; assert.isAtLeast( parseInt(balance), 2, 'Try running `solana airdrop 2 --keypair wallet.json` within `todo/`' ); ``` ## 6 ### --description-- Now that you have a wallet with SOL, you can deploy the program to Devnet. First, adjust the `Anchor.toml` file so the `provider.cluster` points to Devnet. ### --tests-- You should have `cluster = "devnet"` in the `learn-how-to-deploy-to-devnet/todo/Anchor.toml` file. ```js const codeString = await __helpers.readFile( join(project.dashedName, 'todo/Anchor.toml') ); assert.match(codeString, /cluster\s*=\s*"devnet"/); ``` ## 7 ### --description-- Adjust the wallet path in the `Anchor.toml` file to point to the wallet you created. ### --tests-- You should have `wallet = "./wallet.json"` in the `learn-how-to-deploy-to-devnet/todo/Anchor.toml` file. ```js const codeString = await __helpers.readFile( join(project.dashedName, 'todo/Anchor.toml') ); assert.match(codeString, /wallet\s*=\s*"\.\/wallet\.json"/); ``` ## 8 ### --description-- Add a `[programs.devnet]` section to the `Anchor.toml` file, and add a `todo` key with the value of the program id you will deploy to. ### --tests-- You should have a `[programs.devnet]` section in the `learn-how-to-deploy-to-devnet/todo/Anchor.toml` file. ```js const codeString = await __helpers.readFile( join(project.dashedName, 'todo/Anchor.toml') ); assert.match(codeString, /\[programs\.devnet\]/); ``` You should have a `todo = ""` key in the `[programs.devnet]` section. ```js const codeString = await __helpers.readFile( join(project.dashedName, 'todo/Anchor.toml') ); const { stdout: keys } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/todo` ); const expectedProgramId = keys.match(/[^\s]{44}/)?.[0]; const actualProgramId = codeString.match( /\[programs\.devnet\]\s*todo\s*=\s*"([^"]{44})"/ )?.[1]; assert.equal(expectedProgramId, actualProgramId); ``` ## 9 ### --description-- Use the Anchor CLI to deploy the program to Devnet. _This should fail_. ### --tests-- You should run `anchor deploy` in the `learn-how-to-deploy-to-devnet/todo/` directory and see an error. ```js // TODO const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, 'insufficient funds for fee'); ``` ## 10 ### --description-- You do not have enough funds to pay for the program deployment transactions. You will need to airdrop more SOL to your wallet. Airdrop 2 SOL to your `wallet.json` account. _This should fail_. ### --tests-- You should run `solana airdrop 2 --keypair wallet.json` in `learn-how-to-deploy-to-devnet/todo/`. ```js // TODO const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, ''); ``` ## 11 ### --description-- You have been rate limited by the Solana Devnet. You can wait a few minutes before airdropping again, or: ```bash # Create a temporary wallet todo/ $ solana-keygen new --outfile temp.json --derivation-path # Airdrop 2 SOL to the temporary wallet todo/ $ solana airdrop 2 --keypair temp.json # Set the Solana CLI to use the temporary wallet todo/ $ solana config set --keypair temp.json # Transfer 1.95 SOL from the temporary wallet to your wallet todo/ $ solana transfer 1.95 wallet.json ``` ### --tests-- Your `wallet.json` account should have `3.95` SOL. ```js const { stdout } = await __helpers.getCommandOutput( `solana balance ${project.dashedName}/todo/wallet.json` ); const balance = stdout.trim()?.match(/\d+/)?.[0]; assert.isAtLeast(parseInt(balance), 3.95); ``` ## 12 ### --description-- Deploy the program to Devnet. _This usually takes a few minutes to complete._ ### --tests-- You should run `anchor deploy` in the `learn-how-to-deploy-to-devnet/todo/` directory. ```js // TODO const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, 'Deploying program to chain'); ``` ## 13 ### --description-- Verify the program matches the source code. ### --tests-- You should run `anchor verify ""` in the `learn-how-to-deploy-to-devnet/todo/programs/todo` directory to verify the program. ```js const lastCommand = await __helpers.getLastCommand(); const { stdout: keys } = await __helpers.getCommandOutput( 'anchor keys list', `${project.dashedName}/todo` ); const expectedProgramId = keys.match(/[^\s]{44}/)?.[0]; assert.include(lastCommand, `anchor verify "${expectedProgramId}"`); ``` ## 14 ### --description-- With your program deployed, ensure your `.env` file has the correct program id and cluster endpoint. Then start the app client server. ### --tests-- You should run `yarn dev` in the `learn-how-to-deploy-to-devnet/todo/app/` directory. ```js const response = await fetch('http://localhost:5173'); assert.equal(response.status, 200, 'The client server should be running.'); ``` ## 15 ### --description-- In your browser, import your wallet into Phantom: ![add account](../../images/devnet/image.png) ![use your secret recovery phrase](../../images/devnet/image-1.png) After inputting your secret recovery phrase, you should see your wallet in Phantom. ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 16 ### --description-- Open the app in your browser, and perform some transactions. ### --tests-- You should type `done` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## 17 ### --description-- Congratulations! You have deployed your program to Devnet! **Summary** 1. Adjust all config files to use Devnet 2. Create a wallet using the Solana CLI 3. Airdrop SOL to your wallet 4. Use `anchor verify` to verify any programs you use match their source code 5. Deploy the program to Devnet 6. Share your app with others 🎆 Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-how-to-interact-with-on-chain-programs.md ================================================ # Solana - Learn How to Interact with On-Chain Programs ## 1 ### --description-- Welcome to the second Solana project! For the duration of this project, you will be working in the `learn-how-to-interact-with-on-chain-programs/` directory. Change into the above directory in your bash terminal. ### --tests-- You should use `cd` to change into the `learn-how-to-interact-with-on-chain-programs/` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include(cwd, 'learn-how-to-interact-with-on-chain-programs'); ``` ## 2 ### --description-- Previously, you developed a smart contract that kept count of the number of times it was invoked. Now, you will develop a client to call your smart contract. Start by building your smart contract with: ```bash npm run build ``` It will take a moment. ### --tests-- You should run `npm run build` in the terminal. ```js await new Promise(res => setTimeout(res, 1000)); const lastCommand = await __helpers.getLastCommand(); assert.equal(lastCommand.trim(), 'npm run build'); ``` You should be in the `learn-how-to-interact-with-on-chain-programs` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include(cwd, 'learn-how-to-interact-with-on-chain-programs'); ``` ## 3 ### --description-- Within the `src` directory, create a directory named `client` to hold your code. ### --tests-- You should have a `src/client` directory. ```js const pathExists = __helpers.fileExists( 'learn-how-to-interact-with-on-chain-programs/src/client' ); assert.isTrue(pathExists, 'You should have a `src/client` directory.'); ``` ### --seed-- #### --cmd-- ```bash npm run build ``` ## 4 ### --description-- Within the `src/client` directory, create a file named `main.js` which will be the entrypoint of your script. ### --tests-- You should have a `src/client/main.js` file. ```js const pathExists = __helpers.fileExists( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); assert.isTrue(pathExists, 'You should have a `src/client/main.js` file.'); ``` ### --seed-- #### --cmd-- ```bash mkdir src/client ``` ## 5 ### --description-- Within `src/client/main.js`, create an asynchronous function named `main`. ### --tests-- You should have an asynchronous function with the handle `main`. ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); const mainFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'main'); assert.exists( mainFunctionDeclaration, 'You should have a function named `main`' ); assert( mainFunctionDeclaration.async, 'main should be an asynchronous function' ); ``` ### --seed-- #### --cmd-- ```bash touch src/client/main.js ``` ## 6 ### --description-- Call your `main` function, awaiting the process before exiting. ### --tests-- You should call `main` with `await`. ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); const mainExpressionStatement = babelisedCode.getExpressionStatement('main'); assert.exists(mainExpressionStatement, 'You should call `main`'); assert.equal( mainExpressionStatement?.expression?.type, 'AwaitExpression', 'You should call `main` with `await`' ); ``` ### --seed-- #### --"src/client/main.js"-- ```js async function main() {} ``` ## 7 ### --description-- Within the `main` function, log to the console the string `Saying 'hello' to a Solana account`. ### --tests-- You should have `console.log("Saying 'hello' to a Solana account")` within `main`. ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); const consoleLogExpressionStatement = babelisedCode.getExpressionStatement('console.log'); assert.exists( consoleLogExpressionStatement, 'You should have a `console.log` statement' ); assert.equal( consoleLogExpressionStatement.scope.join('.'), 'global.main', 'The console.log should be within the main function' ); const arg = consoleLogExpressionStatement?.expression?.arguments?.[0]; const code = babelisedCode.generateCode(arg); assert.match( code, /("|'|`)Saying ("|'|`)hello\2 to a Solana account\1/, 'You should have `console.log("Saying \'hello\' to a Solana account")`' ); ``` ### --seed-- #### --"src/client/main.js"-- ```js async function main() {} await main(); ``` ## 8 ### --description-- Alongside the entrypoint for this program, you will need a module to hold all the methods needed to interact with your smart contract. Within the `src/client` directory, create a file named `hello-world.js`. ### --tests-- You should have a `src/client/hello-world.js` file. ```js const pathExists = __helpers.fileExists( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); assert.isTrue( pathExists, 'You should have a `src/client/hello-world.js` file.' ); ``` ### --seed-- #### --"src/client/main.js"-- ```js async function main() { console.log("Saying 'hello' to a Solana account"); } await main(); ``` ## 9 ### --description-- Within `hello-world.js`, export a function named `establishConnection`. ### --tests-- You should define a function named `establishConnection` in `src/client/hello-world.js`. ```js const establishConnectionFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'establishConnection'; }); assert.exists( establishConnectionFunctionDeclaration, 'You should define a function named `establishConnection` in `src/client/hello-world.js`' ); ``` You should export `establishConnection` as a named export. ```js const establishConnectionFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'establishConnection'; }); assert.exists( establishConnectionFunctionDeclaration, 'You should define a function named `establishConnection` in `src/client/hello-world.js`' ); const establishConnectionExportNamedDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => { const name = e.declaration?.id?.name; return name === 'establishConnection'; }); assert.exists( establishConnectionExportNamedDeclaration, 'You should export `establishConnection` as a named export' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash touch src/client/hello-world.js ``` ## 10 ### --description-- The `web3.js` module from Solana provides all the functionality you will need to interact with the Solana blockchain. In your `package.json` file, `@solana/web3.js` is included as a dependency, along with `borsh`. Run `npm install` to install them. ### --tests-- You should run `npm install` in your project folder to install the modules ```js await new Promise(res => setTimeout(res, 1000)); const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand.trim(), /npm\s+(i|install)/); ``` You should have a `node_modules/@solana/web3.js` folder as a result of installing the dependencies ```js const dir = await __helpers.getDirectory( 'learn-how-to-interact-with-on-chain-programs/node_modules/@solana' ); assert.include(dir, 'web3.js'); ``` You should have a `node_modules/borsh` folder as a result of installing the dependencies ```js const dir = await __helpers.getDirectory( 'learn-how-to-interact-with-on-chain-programs/node_modules' ); assert.include(dir, 'borsh'); ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js export function establishConnection() {} ``` ## 11 ### --description-- Within `establishConnection`, connect to your local cluster, using the `Connection` class from the `@solana/web3.js` module. Return this new connection from `establishConnection`. ### --tests-- You should import `Connection` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'You should import from `@solana/web3.js`' ); const connectionImportSpecifier = solanaWeb3ImportDeclaration.specifiers.find( s => { return s.imported.name === 'Connection'; } ); assert.exists( connectionImportSpecifier, 'You should import `Connection` from `@solana/web3.js`' ); ``` You should create a new connection with `new Connection('http://localhost:8899')`. ```js const newConnectionExpression = babelisedCode .getType('NewExpression') .find(e => { return e.callee.name === 'Connection'; }); assert.exists( newConnectionExpression, 'You should create a new connection with `new Connection()`' ); assert.equal( newConnectionExpression.scope.join(), 'global,establishConnection', 'You should create the new connection within the `establishConnection` function' ); assert.equal( newConnectionExpression.arguments[0].value, 'http://localhost:8899', "You should create a new connection with `new Connection('http://localhost:8899')`" ); ``` Your `establishConnection` function should return the new connection. ```js const { Connection } = await __helpers.importSansCache( './learn-how-to-interact-with-on-chain-programs/node_modules/@solana/web3.js/lib/index.cjs.js' ); const { establishConnection } = await __helpers.importSansCache( './learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const connection = establishConnection(); assert.instanceOf( connection, Connection, 'Your `establishConnection` function should return the new connection' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash npm install @solana/web3.js@1 ``` ## 12 ### --description-- Within the `main` function in `main.js`, make a call to `establishConnection`, and store the value in a variable named `connection`. ### --tests-- You should have `const connection = establishConnection()` in `main.js`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'connection'; }); assert.exists( connectionVariableDeclaration, 'You should declare a variable named `connection` in `main.js`' ); assert.equal( connectionVariableDeclaration.scope.join(), 'global,main', 'You should declare the `connection` variable within the `main` function' ); const init = connectionVariableDeclaration?.declarations?.[0]?.init; assert.exists( init, 'You should initialise the `connection` variable in `main.js`' ); assert.equal( init?.callee?.name, 'establishConnection', 'You should initialise the `connection` variable with `establishConnection()`' ); ``` You should import `establishConnection` from `./hello-world.js`. ```js const helloWorldImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === './hello-world.js'; }); assert.exists( helloWorldImportDeclaration, 'You should import from `./hello-world.js`' ); const establishConnectionImportSpecifier = helloWorldImportDeclaration.specifiers.find(s => { return s.imported.name === 'establishConnection'; }); assert.exists( establishConnectionImportSpecifier, 'You should import `establishConnection` from `./hello-world.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection } from '@solana/web3.js'; export function establishConnection() { return new Connection('http://localhost:8899'); } ``` ## 13 ### --description-- Creating transactions takes compute power. So, an account has to pay for any transaction made. Within `hello-world.js`, export a function named `establishPayer`. ### --tests-- You should define a function with the handle `establishPayer`. ```js const getProgramIdFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'establishPayer'; }); assert.exists( getProgramIdFunctionDeclaration, 'You should define a function named `establishPayer` in `src/client/hello-world.js`' ); ``` You should define `establishPayer` as asynchronous. ```js const getProgramIdFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'establishPayer'; }); assert.isTrue( getProgramIdFunctionDeclaration.async, 'You should define `establishPayer` as being asynchronous' ); ``` You should export `establishPayer` as a named export. ```js const getProgramIdExportNamedDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => { const name = e.declaration?.id?.name; return name === 'establishPayer'; }); assert.exists( getProgramIdExportNamedDeclaration, 'You should export `establishPayer` as a named export' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/main.js"-- ```js import { establishConnection } from './hello-world.js'; async function main() { console.log("Saying 'hello' to a Solana account"); const connection = establishConnection(); } await main(); ``` ## 14 ### --description-- Within `establishPayer`, declare a variable `secretKeyString`, and assign it the value of your local account keypair: ```js await readFile('../../../root/.config/solana/id.json', 'utf8'); ``` ### --tests-- You should import `readFile` from `fs/promises`. ```js const readFileImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === 'fs/promises'; }); assert.exists( readFileImportDeclaration, 'You should import `readFile` from `fs/promises`' ); ``` You should define a variable named `secretKeyString` within `establishPayer`. ```js const secretKeyStringVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'secretKeyString' && v.scope.join() === 'global,establishPayer' ); }); assert.exists( secretKeyStringVariableDeclaration, 'You should define a variable named `secretKeyString`' ); ``` `secretKeyString` should be assigned the result of `readFile`. ```js const secretKeyStringVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'secretKeyString' && v.scope.join() === 'global,establishPayer' ); }); const awaitExpression = secretKeyStringVariableDeclaration?.declarations?.[0]?.init; assert.exists( awaitExpression, '`secretKeyString` should be assigned the result of awaiting `readFile`' ); const readFileCallExpression = awaitExpression?.argument; assert.equal( readFileCallExpression?.callee?.name, 'readFile', '`secretKeyString` should be assigned the result of awaiting `readFile`' ); ``` You should pass `"../../../root/.config/solana/id.json"` as the first argument to `readFile`. ```js const readFileCallExpression = babelisedCode .getType('CallExpression') .find(c => { return ( c.callee.name === 'readFile' && c.scope.includes('global') && c.scope.includes('establishPayer') ); }); assert.exists( readFileCallExpression, 'You should pass `"../../../root/.config/solana/id.json"` as the first argument to `readFile`' ); const firstArgument = readFileCallExpression.arguments?.[0]?.value; assert.equal( readFileCallExpression?.arguments?.[0]?.value, '../../../root/.config/solana/id.json', 'You should pass `../../../root/.config/solana/id.json` as the first argument to `readFile`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection } from '@solana/web3.js'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() {} ``` ## 15 ### --description-- The payer keypair needs to be constructed from an array of 8-bit unsigned integers. Within `establishPayer`, define a variable `secretKey`, and assign it the value of: ```js Uint8Array.from(JSON.parse(secretKeyString)); ``` ### --tests-- You should define a variable named `secretKey` within `establishPayer`. ```js const secretKeyVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'secretKey' && v.scope.join() === 'global,establishPayer' ); }); assert.exists( secretKeyVariableDeclaration, 'You should define a variable named `secretKey`' ); ``` `secretKey` should be assigned the result of `Uint8Array.from`. ```js const secretKeyVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'secretKey' && v.scope.join() === 'global,establishPayer' ); }); const callExpression = secretKeyVariableDeclaration?.declarations?.[0]?.init; const uintMemberExpression = callExpression?.callee; assert.equal( uintMemberExpression?.object?.name, 'Uint8Array', '`secretKey` should be assigned the result of `Uint8Array.from`' ); assert.equal( uintMemberExpression?.property?.name, 'from', '`secretKey` should be assigned the result of `Uint8Array.from`' ); ``` You should pass the result of `JSON.parse(secretKeyString)` as the first argument to `Uint8Array.from`. ```js const secretKeyVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'secretKey' && v.scope.join() === 'global,establishPayer' ); }); const callExpression = secretKeyVariableDeclaration?.declarations?.[0]?.init; const jsonCallExpression = callExpression?.arguments?.[0]; const jsonMemberExpression = jsonCallExpression?.callee; assert.equal( jsonMemberExpression?.object?.name, 'JSON', 'You should pass the result of `JSON.parse(secretKeyString)` as the first argument to `Uint8Array.from`' ); assert.equal( jsonMemberExpression?.property?.name, 'parse', 'You should pass the result of `JSON.parse(secretKeyString)` as the first argument to `Uint8Array.from`' ); assert.equal( jsonCallExpression?.arguments?.[0]?.name, 'secretKeyString', 'You should pass the result of `JSON.parse(secretKeyString)` as the first argument to `Uint8Array.from`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); } ``` ## 16 ### --description-- Within `establishPayer`, return the value of calling the `fromSecretKey` method on `Keypair`, passing `secretKey` as the first argument. ### --tests-- You should return the result of calling the `fromSecretKey` method on `Keypair`. ```js const returnStatement = babelisedCode.getType('ReturnStatement').find(r => { return r.scope.join() === 'global,establishPayer'; }); assert.exists(returnStatement, 'No return statement found'); const callExpression = returnStatement?.argument; assert.equal( callExpression?.callee?.object?.name, 'Keypair', 'You should return the result of calling `Keypair.`' ); assert.equal( callExpression?.callee?.property?.name, 'fromSecretKey', 'You should return the result of calling `Keypair.fromSecretKey`' ); ``` You should pass `secretKey` as the first argument to `fromSecretKey`. ```js const returnStatement = babelisedCode.getType('ReturnStatement').find(r => { return r.scope.join() === 'global,establishPayer'; }); const callExpression = returnStatement?.argument; assert.equal( callExpression?.arguments?.[0]?.name, 'secretKey', 'You should pass `secretKey` as the first argument to `fromSecretKey`' ); ``` You should import `Keypair` from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); const specifier = importDeclaration?.specifiers?.find(s => { return s.imported.name === 'Keypair'; }); assert.exists(specifier, 'You should import `Keypair` from `@solana/web3.js`'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); } ``` ## 17 ### --description-- Interacting with an on-chain program requires its program id. Within `hello-world.js`, export an asynchronous function with the handle `getProgramId`. ### --tests-- You should define a function with the handle `getProgramId`. ```js const getProgramIdFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'getProgramId'; }); assert.exists( getProgramIdFunctionDeclaration, 'You should define a function named `getProgramId` in `src/client/hello-world.js`' ); ``` You should define `getProgramId` as asynchronous. ```js const getProgramIdFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'getProgramId'; }); assert.isTrue( getProgramIdFunctionDeclaration.async, 'You should define `getProgramId` as being asynchronous' ); ``` You should export `getProgramId` as a named export. ```js const getProgramIdExportNamedDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => { const name = e.declaration?.id?.name; return name === 'getProgramId'; }); assert.exists( getProgramIdExportNamedDeclaration, 'You should export `getProgramId` as a named export' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } ``` ## 18 ### --description-- The program id is its public key. You can derive it from the program's keypair. Within `getProgramId`, declare a variable `secretKeyString`, and assign it the value of: ```js await readFile(, 'utf8'); ``` Where `` is the keypair json file path (relative to this project's root) created when building the smart contract. ### --tests-- You should define a variable named `secretKeyString` within `getProgramId`. ```js const secretKeyStringVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'secretKeyString' && v.scope.join() === 'global,getProgramId' ); }); assert.exists( secretKeyStringVariableDeclaration, 'You should define a variable named `secretKeyString`' ); ``` `secretKeyString` should be assigned the result of `readFile`. ```js const secretKeyStringVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'secretKeyString' && v.scope.join() === 'global,getProgramId' ); }); const awaitExpression = secretKeyStringVariableDeclaration?.declarations?.[0]?.init; assert.exists( awaitExpression, '`secretKeyString` should be assigned the result of awaiting `readFile`' ); const readFileCallExpression = awaitExpression?.argument; assert.equal( readFileCallExpression?.callee?.name, 'readFile', '`secretKeyString` should be assigned the result of awaiting `readFile`' ); ``` You should pass `dist/program/helloworld-keypair.json` as the first argument to `readFile`. ```js const readFileCallExpression = babelisedCode .getType('CallExpression') .find(c => { return ( c.callee.name === 'readFile' && c.scope.includes('global') && c.scope.includes('getProgramId') ); }); assert.exists( readFileCallExpression, 'You should pass `dist/program/helloworld-keypair.json` as the first argument to `readFile`' ); const firstArgument = readFileCallExpression.arguments?.[0]?.value; const urlToAssert = new URL(firstArgument, 'file://'); assert.equal( readFileCallExpression?.arguments?.[0]?.value, 'dist/program/helloworld-keypair.json', 'You should pass `dist/program/helloworld-keypair.json` as the first argument to `readFile`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair } from '@solana/web3.js'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() {} ``` ## 19 ### --description-- Within `getProgramId`, define a variable `secretKey`, and assign it the value of: ```js Uint8Array.from(JSON.parse(secretKeyString)); ``` ### --tests-- You should define a variable named `secretKey`. ```js const secretKeyVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'secretKey'; }); assert.exists( secretKeyVariableDeclaration, 'You should define a variable named `secretKey`' ); ``` `secretKey` should be assigned the result of `Uint8Array.from`. ```js const secretKeyVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'secretKey'; }); const callExpression = secretKeyVariableDeclaration?.declarations?.[0]?.init; const uintMemberExpression = callExpression?.callee; assert.equal( uintMemberExpression?.object?.name, 'Uint8Array', '`secretKey` should be assigned the result of `Uint8Array.from`' ); assert.equal( uintMemberExpression?.property?.name, 'from', '`secretKey` should be assigned the result of `Uint8Array.from`' ); ``` You should pass the result of `JSON.parse(secretKeyString)` as the first argument to `Uint8Array.from`. ```js const secretKeyVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'secretKey'; }); const callExpression = secretKeyVariableDeclaration?.declarations?.[0]?.init; const jsonCallExpression = callExpression?.arguments?.[0]; const jsonMemberExpression = jsonCallExpression?.callee; assert.equal( jsonMemberExpression?.object?.name, 'JSON', 'You should pass the result of `JSON.parse(secretKeyString)` as the first argument to `Uint8Array.from`' ); assert.equal( jsonMemberExpression?.property?.name, 'parse', 'You should pass the result of `JSON.parse(secretKeyString)` as the first argument to `Uint8Array.from`' ); assert.equal( jsonCallExpression?.arguments?.[0]?.name, 'secretKeyString', 'You should pass the result of `JSON.parse(secretKeyString)` as the first argument to `Uint8Array.from`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); } ``` ## 20 ### --description-- Within `getProgramId`, define a variable `keypair`, and assign it the value of calling the `fromSecretKey` method on `Keypair`, passing `secretKey` as the first argument. ### --tests-- You should define a variable named `keypair`. ```js const keypairVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'keypair'; }); assert.exists( keypairVariableDeclaration, 'You should define a variable named `keypair`' ); assert.equal( keypairVariableDeclaration.scope.join(), 'global,getProgramId', 'You should define `keypair` within `getProgramId`' ); ``` `keypair` should be assigned the result of calling the `fromSecretKey` method on `Keypair`. ```js const keypairVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'keypair'; }); const callExpression = keypairVariableDeclaration?.declarations?.[0]?.init; const fromSecretKeyMemberExpression = callExpression?.callee; assert.equal( fromSecretKeyMemberExpression?.object?.name, 'Keypair', '`keypair` should be assigned the result of calling the `fromSecretKey` method on `Keypair`' ); assert.equal( fromSecretKeyMemberExpression?.property?.name, 'fromSecretKey', '`keypair` should be assigned the result of calling the `fromSecretKey` method on `Keypair`' ); ``` You should pass `secretKey` as the first argument to `fromSecretKey`. ```js const keypairVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'keypair'; }); const callExpression = keypairVariableDeclaration?.declarations?.[0]?.init; const secretKeyArgument = callExpression?.arguments?.[0]; assert.equal( secretKeyArgument?.name, 'secretKey', 'You should pass `secretKey` as the first argument to `fromSecretKey`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); } ``` ## 21 ### --description-- Within `getProgramId`, return the `publicKey` property of `keypair`. ### --tests-- You should return the `publicKey` property of `keypair`. ```js const returnStatement = babelisedCode.getFunctionDeclarations().find(f => { return f.id?.name === 'getProgramId'; }); const blockStatement = returnStatement?.body?.body; const returnExpression = blockStatement?.find(b => { return b.type === 'ReturnStatement'; }); assert.equal( returnExpression?.argument?.object?.name, 'keypair', 'You should return the `publicKey` property of `keypair`' ); assert.equal( returnExpression?.argument?.property?.name, 'publicKey', 'You should return the `publicKey` property of `keypair`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); } ``` ## 22 ### --description-- In Solana, program accounts (smart contracts) are stateless. As such, separate accounts (data accounts) need to be created to persist data. Within `hello-world.js`, export an asynchronous function with the handle `getAccountPubkey`. This function should expect two arguments: `payer` and `programId`. ### --tests-- You should define a function with the handle `getAccountPubkey`. ```js const getAccountPubkeyFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'getAccountPubkey'; }); assert.exists( getAccountPubkeyFunctionDeclaration, 'You should define a function with the handle `getAccountPubkey`' ); ``` You should define `getAccountPubkey` as asynchronous. ```js const getAccountPubkeyFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'getAccountPubkey'; }); assert.isTrue( getAccountPubkeyFunctionDeclaration?.async, 'You should define `getAccountPubkey` as asynchronous' ); ``` You should define `getAccountPubkey` with a first parameter `payer`. ```js const getAccountPubkeyFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'getAccountPubkey'; }); const firstParameter = getAccountPubkeyFunctionDeclaration?.params?.[0]; assert.exists( firstParameter, 'You should define `getAccountPubkey` to accept a first argument' ); assert.equal( firstParameter?.name, 'payer', 'You should define `getAccountPubkey` with a first parameter `payer`' ); ``` You should define `getAccountPubkey` with a second parameter `programId`. ```js const getAccountPubkeyFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id.name === 'getAccountPubkey'; }); const secondParameter = getAccountPubkeyFunctionDeclaration?.params?.[1]; assert.exists( secondParameter, 'You should define `getAccountPubkey` to accept a second argument' ); assert.equal( secondParameter?.name, 'programId', 'You should define `getAccountPubkey` with a second parameter `programId`' ); ``` You should export `getAccountPubkey` as a named export. ```js const getAccountPubkeyExportDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => { return e.declaration?.id?.name === 'getAccountPubkey'; }); assert.exists( getAccountPubkeyExportDeclaration, 'You should export `getAccountPubkey` as a named export' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } ``` ## 23 ### --description-- Within `getAccountPubkey`, use the `createWithSeed` function on the `PublicKey` class from `@solana/web3.js` to create a public key, passing in the following arguments: 1. `payer.publicKey` 2. Any string of your choosing which will act as the seed 3. `programId` Then, return the awaited result. ### --tests-- You should import `PublicKey` from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source?.value === '@solana/web3.js'; }); const specifier = importDeclaration?.specifiers?.find(s => { return s.local?.name === 'PublicKey'; }); assert.exists( specifier, 'You should import `PublicKey` from `@solana/web3.js`' ); ``` You should call `PublicKey.createWithSeed` within `getAccountPubkey`. ```js const createWithSeedCallExpression = babelisedCode .getType('CallExpression') .find(c => { return c.callee?.property?.name === 'createWithSeed'; }); assert.exists( createWithSeedCallExpression, 'You should call `PublicKey.createWithSeed`' ); assert.equal( createWithSeedCallExpression?.scope?.join(), 'global,getAccountPubkey', 'You should call `PublicKey.createWithSeed` within `getAccountPubkey`' ); assert.equal( createWithSeedCallExpression?.callee?.object?.name, 'PublicKey', "You should use `PublicKey`'s `createWithSeed` function" ); ``` You should pass `payer.publicKey` as the first argument to `createWithSeed`. ```js const publicKeyCallExpression = babelisedCode .getType('CallExpression') .find(c => { return c.callee?.property?.name === 'createWithSeed'; }); const payerPropertyAccessExpression = publicKeyCallExpression?.arguments?.[0]; assert.exists( payerPropertyAccessExpression, 'You should pass a the first argument to `createWithSeed`' ); assert.equal( payerPropertyAccessExpression?.object?.name, 'payer', 'You should pass `payer.publicKey` as the first argument to `createWithSeed`' ); assert.equal( payerPropertyAccessExpression?.property?.name, 'publicKey', 'You should call `publicKey` on `payer`' ); ``` You should pass a string as the second argument to `createWithSeed`. ```js const createWithSeedCallExpression = babelisedCode .getType('CallExpression') .find(c => { return c.callee?.property?.name === 'createWithSeed'; }); const secondArgument = createWithSeedCallExpression?.arguments?.[1]; assert.exists( secondArgument, 'You should pass a second argument to `createWithSeed`' ); assert.equal( secondArgument.type, 'StringLiteral', 'You should pass a string as the second argument to `createWithSeed`' ); ``` You should pass `programId` as the third argument to `createWithSeed`. ```js const createWithSeedCallExpression = babelisedCode .getType('CallExpression') .find(c => { return c.callee?.property?.name === 'createWithSeed'; }); const programIdPropertyAccessExpression = createWithSeedCallExpression?.arguments?.[2]; assert.exists( programIdPropertyAccessExpression, 'You should pass a third argument to `createWithSeed`' ); assert.equal( programIdPropertyAccessExpression?.name, 'programId', 'You should pass `programId` as the third argument to `createWithSeed`' ); ``` You should return the result of `await PublicKey.createWithSeed`. ```js const getAccountPubkeyFunctionDeclaration = babelisedCode .getType('FunctionDeclaration') .find(f => { return f.id?.name === 'getAccountPubkey'; }); const returnStatement = getAccountPubkeyFunctionDeclaration?.body?.body?.find( b => { return b.type === 'ReturnStatement'; } ); assert.exists(returnStatement, 'You should return within `getAccountPubkey`'); const awaitExpression = returnStatement?.argument; assert.equal( awaitExpression?.type, 'AwaitExpression', 'You should await the result of `PublicKey.createWithSeed`' ); const memberExpression = awaitExpression?.argument?.callee; assert.equal( memberExpression?.object?.name, 'PublicKey', 'You should return `await PublicKey...`' ); assert.equal( memberExpression?.property?.name, 'createWithSeed', 'You should return `PublicKey.createWithSeed(...)`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) {} ``` ## 24 ### --description-- Creating a transaction interacting with a non-existent account still costs _lamports_ (one lamport is the minimum token value divisible). So, it is best to double-check the program account exists. Within `hello-world.js`, define and export a function with the following signature: ```typescript function checkProgram( connection: Connection, payer: Keypair, programId: PublicKey, accountPubkey: PublicKey ): Promise; ``` ### --tests-- You should define a function with the handle `checkProgram`. ```js const checkProgramFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id?.name === 'checkProgram'; }); assert.exists( checkProgramFunctionDeclaration, 'You should define a function with the handle `checkProgram`' ); ``` You should define `checkProgram` with a first parameter named `connection`. ```js const checkProgramFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id?.name === 'checkProgram'; }); const firstParameter = checkProgramFunctionDeclaration?.params?.[0]; assert.exists( firstParameter, 'You should define `checkProgram` to expect at least one argument' ); assert.equal( firstParameter?.name, 'connection', 'You should define `checkProgram` with a first parameter named `connection`' ); ``` You should define `checkProgram` with a second parameter named `payer`. ```js const checkProgramFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id?.name === 'checkProgram'; }); const secondParameter = checkProgramFunctionDeclaration?.params?.[1]; assert.exists( secondParameter, 'You should define `checkProgram` to expect at least two arguments' ); assert.equal( secondParameter?.name, 'payer', 'You should define `checkProgram` with a second parameter named `payer`' ); ``` You should define `checkProgram` with a third parameter named `programId`. ```js const checkProgramFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id?.name === 'checkProgram'; }); const thirdParameter = checkProgramFunctionDeclaration?.params?.[2]; assert.exists( thirdParameter, 'You should define `checkProgram` to expect at least three arguments' ); assert.equal( thirdParameter?.name, 'programId', 'You should define `checkProgram` with a third parameter named `programId`' ); ``` You should define `checkProgram` with a fourth parameter named `accountPubkey`. ```js const checkProgramFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id?.name === 'checkProgram'; }); const fourthParameter = checkProgramFunctionDeclaration?.params?.[3]; assert.exists( fourthParameter, 'You should define `checkProgram` to expect at least four arguments' ); assert.equal( fourthParameter?.name, 'accountPubkey', 'You should define `checkProgram` with a fourth parameter named `accountPubkey`' ); ``` You should define `checkProgram` to be a named export. ```js const exportNamedDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(d => { return d.declaration?.id?.name === 'checkProgram'; }); assert.exists( exportNamedDeclaration, 'You should define `checkProgram` to be a named export' ); ``` You should define `checkProgram` to be asynchronous. ```js const checkProgramFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id?.name === 'checkProgram'; }); assert.isTrue( checkProgramFunctionDeclaration?.async, 'You should define `checkProgram` to be asynchronous' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } ``` ## 25 ### --description-- Within, `checkProgram`, use the `getAccountInfo` method on `connection` to get the **program account** information _if any exists_. The `getAccountInfo` method expects a `PublicKey` as an argument. If the result is equal to `null`, throw an `Error` with a string message. ### --tests-- `checkProgram` should throw an `Error` instance, if `await connection.getAccountInfo(programId)` returns `null`. ```js const { checkProgram } = await __helpers.importSansCache( './learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const payer = {}; const programId = 'programId'; const accountPubkey = 'accountPubkey'; const connection = { getAccountInfo: async a => a !== programId ? assert.fail('incorrect parameter passed to `getAccountInfo`') : null }; try { await checkProgram(connection, payer, programId, accountPubkey); assert.fail( '`checkProgram` should throw an `Error` instance, if `await connection.getAccountInfo(programId)` returns `null`' ); } catch (e) { if (e instanceof AssertionError) { throw e; } assert.instanceOf(e, Error); } ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) {} ``` ## 26 ### --description-- Within `checkProgram`, make use of the `executable` (boolean) property of the `getAccountInfo` result to throw an `Error` if the program account is not executable. ### --tests-- `checkProgram` should throw an `Error` instance, if the program account `executable` property equals `false`. ```js const { checkProgram } = await __helpers.importSansCache( './learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const connection = { getAccountInfo: async () => ({ executable: true }) }; const payer = {}; const programId = {}; const accountPubkey = {}; // Should NOT throw if is executable try { await checkProgram(connection, payer, programId, accountPubkey); } catch (e) { assert.fail( '`checkProgram` should NOT throw an `Error` instance, if the program account `executable` property IS `true`' ); } // Should throw if is NOT executable connection.getAccountInfo = async () => ({ executable: false }); try { await checkProgram(connection, payer, programId, accountPubkey); assert.fail( '`checkProgram` should throw an `Error` instance, if the program account `executable` property equals `false`' ); } catch (e) { if (e instanceof AssertionError) { throw e; } assert.instanceOf(e, Error); } ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } } ``` ## 27 ### --description-- If this is the first time the program account is being invoked, it will not own a _data account_ to store any state. Within `checkProgram`, get the account info of the program **data** account (`accountPubkey`), _if any exists_. If the result is equal to `null`, throw an `Error` with a string message. ### --tests-- `checkProgram` should throw an `Error` instance, if the program **data account** does not exist. ```js const { checkProgram } = await __helpers.importSansCache( './learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const connection = { getAccountInfo: async flip => flip ? null : { executable: true } }; const payer = {}; const programId = false; let accountPubkey = true; try { await checkProgram(connection, payer, programId, accountPubkey); assert.fail( '`checkProgram` should throw an `Error` instance, if the program data account does not exist' ); } catch (e) { if (e instanceof AssertionError) { throw e; } assert.instanceOf(e, Error); } // Should NOT throw if data account exists accountPubkey = false; try { await checkProgram(connection, payer, programId, accountPubkey); } catch (e) { assert.fail( '`checkProgram` should NOT throw an `Error` instance, if the program **data account** exists' ); } ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } } ``` ## 28 ### --description-- Instead of throwing when a program data account is not found, you can create the account. Within `hello-world.js`, define and export a function with the following signature: ```javascript function createAccount( connection: Connection, payer: Keypair, programId: PublicKey, accountPubkey: PublicKey ): Promise ``` ### --tests-- You should define a function with the handle `createAccount`. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id?.name === 'createAccount'; }); assert.exists( functionDeclaration, 'You should define a function with the handle `createAccount`' ); ``` You should define `createAccount` with a first parameter named `connection`. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id?.name === 'createAccount'; }); const firstParameter = functionDeclaration?.params?.[0]; assert.exists( firstParameter, 'You should define `createAccount` to expect at least one argument' ); assert.equal( firstParameter?.name, 'connection', 'You should define `createAccount` with a first parameter named `connection`' ); ``` You should define `createAccount` with a second parameter named `payer`. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id?.name === 'createAccount'; }); const secondParameter = functionDeclaration?.params?.[1]; assert.exists( secondParameter, 'You should define `createAccount` to expect at least two arguments' ); assert.equal( secondParameter?.name, 'payer', 'You should define `createAccount` with a second parameter named `payer`' ); ``` You should define `createAccount` with a third parameter named `programId`. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id?.name === 'createAccount'; }); const thirdParameter = functionDeclaration?.params?.[2]; assert.exists( thirdParameter, 'You should define `createAccount` to expect at least three arguments' ); assert.equal( thirdParameter?.name, 'programId', 'You should define `createAccount` with a third parameter named `programId`' ); ``` You should define `createAccount` with a fourth parameter named `accountPubkey`. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id?.name === 'createAccount'; }); const fourthParameter = functionDeclaration?.params?.[3]; assert.exists( fourthParameter, 'You should define `createAccount` to expect at least four arguments' ); assert.equal( fourthParameter?.name, 'accountPubkey', 'You should define `createAccount` with a fourth parameter named `accountPubkey`' ); ``` You should define `createAccount` to be a named export. ```js const exportNamedDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => { return e.declaration?.id?.name === 'createAccount'; }); assert.exists( exportNamedDeclaration, 'You should define `createAccount` to be a named export' ); ``` You should define `createAccount` to be asynchronous. ```js const functionDeclaration = babelisedCode.getFunctionDeclarations().find(f => { return f.id?.name === 'createAccount'; }); assert.isTrue( functionDeclaration?.async, 'You should define `createAccount` to be asynchronous' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } ``` ## 29 ### --description-- Storing data on accounts costs a _rent_ fee. This fee is paid in _lamports_ and is calculated based on the size of the account. The `getMinimumBalanceForRentExemption` method on the `Connection` class can be used to calculate the rent fee payable to prevent an account from being purged. Within `createAccount`, use the `getMinimumBalanceForRentExemption` method on `connection` to get the minimum balance required to create an account with a size of `10000` bytes. Store this in a variable named `lamports`. ### --tests-- You should call `connection.getMinimumBalanceForRentExemption` within `createAccount`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.object?.name === 'connection' && c.callee?.property?.name === 'getMinimumBalanceForRentExemption' ); }); assert.exists( callExpression, 'You should call `connection.getMinimumBalanceForRentExemption`' ); assert.include( callExpression?.scope?.join(), 'global,createAccount', '`connection.getMinimumBalanceForRentExemption()` should be within `createAccount`' ); ``` You should pass `10000` as the first argument to `getMinimumBalanceForRentExemption`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.object?.name === 'connection' && c.callee?.property?.name === 'getMinimumBalanceForRentExemption' ); }); assert.equal( callExpression?.arguments?.[0]?.value, 10000, 'You should pass `10000` as the first argument to `getMinimumBalanceForRentExemption`' ); ``` You should await the result of `getMinimumBalanceForRentExemption`. ```js const awaitExpression = babelisedCode.getType('AwaitExpression').find(a => { return ( a.argument?.callee?.object?.name === 'connection' && a.scope?.join().includes('global,createAccount') ); }); assert.exists( awaitExpression, 'You should await the result of `connection.getMinimumBalanceForRentExemption(10000)`' ); assert.equal( awaitExpression.type, 'AwaitExpression', 'You should await the result of `connection.getMinimumBalanceForRentExemption(10000)`' ); ``` You should assign the value to a variable named `lamports`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'lamports'; }); assert.exists( variableDeclaration, 'You should assign the value to a variable named `lamports`' ); assert.equal( variableDeclaration?.scope?.join(), 'global,createAccount', '`lamports` should be defined within `createAccount`' ); const awaitExpression = variableDeclaration?.declarations?.[0]?.init; assert.equal( awaitExpression?.argument?.callee?.object?.name, 'connection', '`lamports` should be assigned the result of `connection.getMinimumBalanceForRentExemption(10000)`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } export async function createAccount( connection, payer, programId, accountPubkey ) {} ``` ## 30 ### --description-- You can estimate the cost of creating a program data account of size `10000` bytes by using the following CLI command: ```bash solana rent 10000 ``` ### --tests-- You should run `solana rent 10000` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal(lastCommand?.trim(), 'solana rent 10000'); ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption(10000); } ``` ## 31 ### --description-- Randomly guessing the size of your account might not always be best. Within `hello-world.js`, define an `ACCOUNT_SIZE` constant, and set its value to: ```javascript borsh.serialize(HelloWorldSchema, new HelloWorldAccount()).length; ``` Then, at the top of the file import `*` as `borsh` from the `borsh` module. ### --tests-- You should define a constant named `ACCOUNT_SIZE` in the `hello-world.js` file. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'ACCOUNT_SIZE'; }); assert.exists( variableDeclaration, 'You should define a constant named `ACCOUNT_SIZE`' ); assert.equal( variableDeclaration?.scope?.join(), 'global', '`ACCOUNT_SIZE` should be defined in the global scope' ); assert.equal( variableDeclaration?.kind, 'const', '`ACCOUNT_SIZE` should be defined as a constant' ); ``` You should set the value of `ACCOUNT_SIZE` to `borsh.serialize(HelloWorldSchema, new HelloWorldAccount()).length`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'ACCOUNT_SIZE'; }); const memberExpression = variableDeclaration?.declarations?.[0]?.init; const callExpression = memberExpression?.object; assert.equal( callExpression?.callee?.object?.name, 'borsh', '`ACCOUNT_SIZE` should use `borsh`' ); assert.equal( callExpression?.callee?.property?.name, 'serialize', '`ACCOUNT_SIZE` should use `borsh.serialize`' ); assert.equal( callExpression?.arguments?.[0]?.name, 'HelloWorldSchema', '`ACCOUNT_SIZE` should use `borsh.serialize(HelloWorldSchema, ...)`' ); const newExpression = callExpression?.arguments?.[1]; assert.equal( newExpression?.callee?.name, 'HelloWorldAccount', '`ACCOUNT_SIZE` should use `borsh.serialize(..., new HelloWorldAccount())`' ); assert.equal( newExpression?.type, 'NewExpression', '`HelloWorldAccount()` should be instantiated with `new`' ); assert.equal( memberExpression?.property?.name, 'length', '`ACCOUNT_SIZE` should use `borsh.serialize(...).length`' ); ``` You should `import * as borsh from borsh`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source?.value === 'borsh'; }); assert.exists(importDeclaration, 'You should import from `borsh`'); const importNamespaceSpecifier = importDeclaration?.specifiers?.find(s => { return s.type === 'ImportNamespaceSpecifier'; }); assert.exists( importNamespaceSpecifier, 'You should import `borsh` as a namespace: `import * as borsh from "borsh"`' ); assert.equal( importNamespaceSpecifier?.local?.name, 'borsh', 'You should import `borsh` as a namespace named `borsh`: `import * as borsh from "borsh"`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 32 ### --description-- Define a class named `HelloWorldAccount` whose constructor takes a single parameter named `fields`. Then, assign the value of `fields.counter` to a property named `counter` on the instance, on the condition `fields` exists. ### --tests-- You should define a class named `HelloWorldAccount`. ```js const classDeclaration = babelisedCode.getType('ClassDeclaration').find(c => { return c.id?.name === 'HelloWorldAccount'; }); assert.exists( classDeclaration, 'You should define a class named `HelloWorldAccount`' ); assert.equal( classDeclaration?.scope?.join(), 'global', '`HelloWorldAccount` should be defined in the global scope' ); ``` You should define a constructor for `HelloWorldAccount` with a single parameter named `fields`. ```js const classDeclaration = babelisedCode.getType('ClassDeclaration').find(c => { return c.id?.name === 'HelloWorldAccount'; }); const constructor = classDeclaration?.body?.body?.find(m => { return m.kind === 'constructor'; }); assert.exists( constructor, 'You should define a constructor for `HelloWorldAccount`' ); assert.equal( constructor?.params?.length, 1, 'The constructor for `HelloWorldAccount` should take a single parameter' ); assert.equal( constructor?.params?.[0]?.name, 'fields', 'The constructor for `HelloWorldAccount` should take a single parameter named `fields`' ); ``` You should assign the value of `fields.counter` to `this.counter` only if `fields` is not `undefined`. ```js const classDeclaration = babelisedCode.getType('ClassDeclaration').find(c => { return c.id?.name === 'HelloWorldAccount'; }); const constructor = classDeclaration?.body?.body?.find(m => { return m.kind === 'constructor'; }); const code = babelisedCode.generateCode(classDeclaration); const testCode = ` ${code} const t = new HelloWorldAccount(); `; try { const res = eval(testCode); } catch (e) { assert.fail( `The constructor for \`HelloWorldAccount\` should not throw an error when called without any arguments.` ); } ``` You should declare `HelloWorldAccount` before `ACCOUNT_SIZE`. ```js const classDeclaration = babelisedCode.getType('ClassDeclaration').find(c => { return c.id?.name === 'HelloWorldAccount'; }); const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'ACCOUNT_SIZE'; }); assert.isBelow( classDeclaration?.end, variableDeclaration?.start, '`HelloWorldAccount` should be declared before `ACCOUNT_SIZE`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption(10000); } const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; ``` ## 33 ### --description-- Define a variable named `HelloWorldSchema` and set its value to: ```javascript new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); ``` This is a _schema_ that matches the definition of the `GreetingAccount` struct in `src/program-rust/src/lib.rs`. ### --tests-- You should define a variable named `HelloWorldSchema`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'HelloWorldSchema'; }); assert.exists( variableDeclaration, 'You should define a variable named `HelloWorldSchema`' ); assert.equal( variableDeclaration?.scope?.join(), 'global', '`HelloWorldSchema` should be defined in the global scope' ); ``` You should set the value of `HelloWorldSchema` to the given schema. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'HelloWorldSchema'; }); const newExpression = variableDeclaration?.declarations?.[0]?.init; assert.equal( newExpression?.callee?.name, 'Map', '`HelloWorldSchema` should use `Map(...)`' ); assert.equal( newExpression?.type, 'NewExpression', '`HelloWorldSchema` should be set to `new Map(...)`' ); const arrayExpressionOne = newExpression?.arguments?.[0]; assert.equal( arrayExpressionOne?.type, 'ArrayExpression', '`HelloWorldSchema` should use `Map([ ... ])`' ); const arrayExpressionTwo = arrayExpressionOne?.elements?.[0]; assert.equal( arrayExpressionTwo?.type, 'ArrayExpression', '`HelloWorldSchema` should use `Map([ [ ... ] ])`' ); assert.equal( arrayExpressionTwo?.elements?.[0]?.name, 'HelloWorldAccount', '`HelloWorldSchema` should use `Map([ [ HelloWorldAccount, ... ] ])`' ); const objectExpression = arrayExpressionTwo?.elements?.[1]; assert.equal( objectExpression?.type, 'ObjectExpression', '`HelloWorldSchema` should use `Map([ [ HelloWorldAccount, { ... } ] ])`' ); const kindObjectProperty = objectExpression?.properties?.[0]; assert.equal( kindObjectProperty?.key?.name, 'kind', '`HelloWorldSchema` should use `Map([ [ HelloWorldAccount, { kind: ... } ] ])`' ); assert.equal( kindObjectProperty?.value?.value, 'struct', '`HelloWorldSchema` should use `Map([ [ HelloWorldAccount, { kind: "struct" } ] ])`' ); const fieldsObjectProperty = objectExpression?.properties?.[1]; assert.equal( fieldsObjectProperty?.key?.name, 'fields', '`HelloWorldSchema` should use `Map([ [ HelloWorldAccount, { fields: ... } ] ])`' ); assert.equal( fieldsObjectProperty?.value?.type, 'ArrayExpression', '`HelloWorldSchema` should use `Map([ [ HelloWorldAccount, { fields: [ ... ] } ] ])`' ); const arrayExpressionThree = fieldsObjectProperty?.value?.elements?.[0]; assert.equal( arrayExpressionThree?.type, 'ArrayExpression', '`HelloWorldSchema` should use `Map([ [ HelloWorldAccount, { fields: [ [ ... ] ] } ] ])`' ); assert.equal( arrayExpressionThree?.elements?.[0]?.value, 'counter', '`HelloWorldSchema` should use `Map([ [ HelloWorldAccount, { fields: [ [ "counter", ... ] ] } ] ])`' ); assert.equal( arrayExpressionThree?.elements?.[1]?.value, 'u32', '`HelloWorldSchema` should use `Map([ [ HelloWorldAccount, { fields: [ [ "counter", "u32" ] ] } ] ])`' ); ``` You should declare `HelloWorldSchema` before `ACCOUNT_SIZE`. ```js const hello = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'HelloWorldSchema'; }); const account = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'ACCOUNT_SIZE'; }); assert.isBelow( hello?.end, account?.start, '`HelloWorldSchema` should be declared before `ACCOUNT_SIZE`' ); ``` You should declare `HelloWorldSchema` after `HelloWorldAccount`. ```js const schema = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'HelloWorldSchema'; }); const clas = babelisedCode.getType('ClassDeclaration').find(c => { return c.id?.name === 'HelloWorldAccount'; }); assert.isAbove( schema?.start, clas?.end, '`HelloWorldSchema` should be declared after `HelloWorldAccount`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```javascript import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption(10000); } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; ``` ## 34 ### --description-- Within `createAccount`, replace the hard-coded value of `10000` with the `ACCOUNT_SIZE` constant. ### --tests-- You should replace the hard-coded value of `10000` with the `ACCOUNT_SIZE` constant. ```js const createAccountFunctionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => { return f.id?.name === 'createAccount'; }); const variableDeclaration = createAccountFunctionDeclaration?.body?.body?.find( v => { return v.declarations?.[0]?.id?.name === 'lamports'; } ); const awaitExpression = variableDeclaration?.declarations?.[0]?.init; assert.equal( awaitExpression?.argument?.arguments?.[0]?.name, 'ACCOUNT_SIZE', 'You should replace the hard-coded value of `10000` with the `ACCOUNT_SIZE` constant' ); ``` You should declare `ACCOUNT_SIZE` before `createAccount`. ```js const account = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'ACCOUNT_SIZE'; }); const createAccount = babelisedCode.getFunctionDeclarations().find(f => { return f.id?.name === 'createAccount'; }); const { end } = account; const { start } = createAccount; const { line: accountLine } = babelisedCode.getLineAndColumnFromIndex(end); const { line: createAccountLine } = babelisedCode.getLineAndColumnFromIndex(start); assert.isBelow( accountLine, createAccountLine, `'ACCOUNT_SIZE' declared on line ${accountLine}, but should be declared before ${createAccountLine}` ); // Check HelloWorldSchema and HelloWorldAccount are declared before ACCOUNT_SIZE const schema = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'HelloWorldSchema'; }); const clas = babelisedCode.getType('ClassDeclaration').find(c => { return c.id?.name === 'HelloWorldAccount'; }); assert.isBelow( schema?.end, account.start, '`HelloWorldSchema` should be declared before `ACCOUNT_SIZE`' ); assert.isBelow( clas?.end, account.start, '`HelloWorldAccount` should be declared before `ACCOUNT_SIZE`' ); // HelloWorldAccount should be declared before HelloWorldSchema assert.isBelow( clas?.end, schema?.start, '`HelloWorldAccount` should be declared before `HelloWorldSchema`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```javascript import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption(10000); } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; ``` ## 35 ### --description-- In order to create the program data account, you need to define a `Transaction` that will be signed by the `payer` and sent to the network. Within `createAccount`, create a new `Transaction` instance and store it in a variable named `transaction`. _Be sure to import the `Transaction` class from `@solana/web3.js`_ ### --tests-- You should create a new `Transaction` instance within `createAccount`. ```js const transactionNewExpression = babelisedCode .getType('NewExpression') .find(n => { return n.callee?.name === 'Transaction'; }); assert.exists( transactionNewExpression, 'You should create a new `Transaction`' ); assert.equal( transactionNewExpression?.scope?.join(), 'global,createAccount,transaction', 'You should create a new `Transaction` instance within `createAccount`' ); ``` You should store the result in a variable named `transaction`. ```js const transactionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'transaction' && v.scope?.join() === 'global,createAccount' ); }); assert.exists( transactionVariableDeclaration, 'You should define a variable named `transaction`' ); const newExpression = transactionVariableDeclaration?.declarations?.[0]?.init; assert.equal( newExpression?.callee?.name, 'Transaction', '`transaction` should be initialised with `new Transaction()`' ); ``` You should import `Transaction` from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(e => { return ( e.source?.value === '@solana/web3.js' && e.specifiers?.find(s => s.imported?.name === 'Transaction') ); }); assert.exists( importDeclaration, '`Transaction` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); } ``` ## 36 ### --description-- Within `createAccount`, create a new variable named `instruction`, and set its value to: ```javascript { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: , space: ACCOUNT_SIZE, } ``` ### --tests-- You should create a new variable named `instruction` within `createAccount`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return ( v.declarations?.[0]?.id?.name === 'instruction' && v.scope?.join() === 'global,createAccount' ); }); assert.exists( variableDeclaration, 'You should define a variable named `instruction`' ); ``` You should set the value of `instruction` to the given object. ```js const instructionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'instruction' && v.scope?.join() === 'global,createAccount' ); }); const instructionCode = babelisedCode.generateCode( instructionVariableDeclaration ); const testCode = ` const ACCOUNT_SIZE = 1; const payer = { publicKey: 'payer-public-key' }; const programId = 'program-id'; const accountPubkey = 'account-pubkey'; const lamports = 123; ${instructionCode}; return instruction; `; const instruction = new Function(testCode)(); const expected = { basePubkey: 'payer-public-key', fromPubkey: 'payer-public-key', lamports: 123, newAccountPubkey: 'account-pubkey', programId: 'program-id', space: 1 }; assert.deepInclude(instruction, expected); ``` You should use the same seed you used in the `getAccountPubkey` function. ```js const instructionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return ( v.declarations?.[0]?.id?.name === 'instruction' && v.scope?.join() === 'global,createAccount' ); }); const instructionSeedValue = instructionVariableDeclaration?.declarations?.[0]?.init?.properties?.find( p => p.key?.name === 'seed' )?.value?.value; const createWithSeedCall = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.object?.name === 'PublicKey' && c.callee?.property?.name === 'createWithSeed' ); }); const seedValue = createWithSeedCall?.arguments?.[1]?.value; assert.equal( instructionSeedValue, seedValue, 'You should use the same seed you used in the `getAccountPubkey` function.' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); } ``` ## 37 ### --description-- Solana has a native program called the _System Program_. It provides functionality to create accounts, allocate account data, assign an account to programs, work with nonce accounts, and transfer lamports. Within `createAccount`, use the `createAccountWithSeed` method on the `SystemProgram` class from `@solana/web3.js`. Pass it your `instruction` variable and store the return value in a variable named `tx`. ### --tests-- You should call `SystemProgram.createAccountWithSeed` within `createAccount`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.object?.name === 'SystemProgram' && c.callee?.property?.name === 'createAccountWithSeed' ); }); assert.exists( callExpression, 'You should call `SystemProgram.createAccountWithSeed`' ); assert.include( callExpression?.scope?.join(), 'global,createAccount', '`SystemProgram.CreateAccountWithSeed()` should be within `createAccount`' ); ``` You should pass `instruction` as the first argument to `createAccountWithSeed`. ```js const callExpression = babelisedCode.getType('CallExpression').find(c => { return ( c.callee?.object?.name === 'SystemProgram' && c.callee?.property?.name === 'createAccountWithSeed' ); }); assert.equal( callExpression?.arguments?.[0]?.name, 'instruction', '`instruction` should be the first argument to `createAccountWithSeed`' ); ``` You should assign the value to a variable named `tx`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'tx'; }); assert.exists(variableDeclaration, 'A `tx` variable declaration should exist'); assert.equal( variableDeclaration?.scope?.join(), 'global,createAccount', '`tx` should be defined within `createAccount`' ); const expression = variableDeclaration?.declarations?.[0]?.init; assert.equal( expression?.callee?.object?.name, 'SystemProgram', '`lamports` should be assigned the result of `SystemProgram.createAccountWithSeed(instruction)`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; } ``` ## 38 ### --description-- Within `createAccount`, use the `add` method on `transaction` to add the transaction with the instruction to create the program data account. ### --tests-- You should call `transaction.add` within `createAccount`. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.callee?.property?.name === 'add' && e.expression?.callee?.object?.name === 'transaction' && e.scope?.join() === 'global,createAccount' ); }); assert.exists( expressionStatement, 'You should call `transaction.add` within `createAccount`' ); ``` You should pass `tx` as the first argument to `transaction.add`. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.callee?.property?.name === 'add' && e.expression?.callee?.object?.name === 'transaction' && e.scope?.join() === 'global,createAccount' ); }); const callExpression = expressionStatement?.expression?.arguments?.[0]; assert.equal( callExpression?.name, 'tx', '`tx` should be the first argument to `transaction.add`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); } ``` ## 39 ### --description-- You have created a transaction, but need to send it to the network. Await the `sendAndConfirmTransaction` function from `@solana/web3.js` to send the transaction to the network. This function expects at least three arguments: - `connection` - `transaction` - An array of _signers_ (use the `payer` as the only signer) ### --tests-- You should call `sendAndConfirmTransaction` within `createAccount`. ```js const callExpression = babelisedCode.getType('CallExpression').find(e => { return ( e?.callee?.name === 'sendAndConfirmTransaction' && e.scope.join() === 'global,createAccount' ); }); assert.exists( callExpression, 'You should call `sendAndConfirmTransaction` within `createAccount`' ); ``` You should pass `connection` as the first argument. ```js const callExpression = babelisedCode.getType('CallExpression').find(e => { return ( e?.callee?.name === 'sendAndConfirmTransaction' && e.scope.join() === 'global,createAccount' ); }); const ident = callExpression?.arguments?.[0]; assert.equal( ident?.name, 'connection', '`connection` should be the first argument to `sendAndConfirmTransaction`' ); ``` You should pass `transaction` as the second argument. ```js const callExpression = babelisedCode.getType('CallExpression').find(e => { return ( e?.callee?.name === 'sendAndConfirmTransaction' && e.scope.join() === 'global,createAccount' ); }); const ident = callExpression?.arguments?.[1]; assert.equal( ident?.name, 'transaction', '`transaction` should be the second argument to `sendAndConfirmTransaction`' ); ``` You should pass `[payer]` as the third argument. ```js const callExpression = babelisedCode.getType('CallExpression').find(e => { return ( e?.callee?.name === 'sendAndConfirmTransaction' && e.scope.join() === 'global,createAccount' ); }); const arrayExpression = callExpression?.arguments?.[2]; assert.equal( arrayExpression?.elements?.[0]?.name, 'payer', '`[payer]` should be the third argument to `sendAndConfirmTransaction`' ); ``` You should await the result of `sendAndConfirmTransaction`. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.argument?.callee?.name === 'sendAndConfirmTransaction' && e.scope?.join() === 'global,createAccount' ); }); assert.equal( expressionStatement?.expression?.type, 'AwaitExpression', 'You should await the result of `sendAndConfirmTransaction`' ); ``` You should import `sendAndConfirmTransaction` from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(e => { return ( e.source?.value === '@solana/web3.js' && e.specifiers?.find(s => s.imported?.name === 'sendAndConfirmTransaction') ); }); assert.exists( importDeclaration, 'You should import `sendAndConfirmTransaction` from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); } ``` ## 40 ### --description-- Within `checkProgram`, instead of throwing an error when the program data account is not found, use `createAccount` to create the program data account. ### --tests-- You should no longer throw an error when the program data account is not found. ```js const { checkProgram } = await __helpers.importSansCache( './learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const connection = { getAccountInfo: a => (a === 'accountPubkey' ? null : { executable: true }), getMinimumBalanceForRentExemption: s => 10 }; const payer = { publicKey: 'payer' }; const programId = 'programId'; const accountPubkey = 'accountPubkey'; try { await checkProgram(connection, payer, programId, accountPubkey); } catch (e) { if (!(e instanceof TypeError)) { assert.fail( 'You should no longer throw an error when the program data account is not found' ); } } ``` You should call `createAccount` within `checkProgram`. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.argument?.callee?.name === 'createAccount' && e.scope?.join() === 'global,checkProgram' ); }); assert.exists( expressionStatement, 'You should call `createAccount` within `checkProgram`' ); ``` You should pass `connection` as the first argument. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.argument?.callee?.name === 'createAccount' && e.scope?.join() === 'global,checkProgram' ); }); const callExpression = expressionStatement?.expression?.argument; assert.equal( callExpression?.arguments?.[0]?.name, 'connection', 'You should pass `connection` as the first argument' ); ``` You should pass `payer` as the second argument. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.argument?.callee?.name === 'createAccount' && e.scope?.join() === 'global,checkProgram' ); }); const callExpression = expressionStatement?.expression?.argument; assert.equal( callExpression?.arguments?.[1]?.name, 'payer', 'You should pass `payer` as the second argument' ); ``` You should pass `programId` as the third argument. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.argument?.callee?.name === 'createAccount' && e.scope?.join() === 'global,checkProgram' ); }); const callExpression = expressionStatement?.expression?.argument; assert.equal( callExpression?.arguments?.[2]?.name, 'programId', 'You should pass `programId` as the third argument' ); ``` You should pass `accountPubkey` as the fourth argument. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.argument?.callee?.name === 'createAccount' && e.scope?.join() === 'global,checkProgram' ); }); const callExpression = expressionStatement?.expression?.argument; assert.equal( callExpression?.arguments?.[3]?.name, 'accountPubkey', 'You should pass `accountPubkey` as the fourth argument' ); ``` You should await the result of `createAccount`. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.argument?.callee?.name === 'createAccount' && e.scope?.join() === 'global,checkProgram' ); }); const callExpression = expressionStatement?.expression; assert.equal( callExpression?.type, 'AwaitExpression', 'You should await the result of `createAccount`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { throw new Error('Data account info not found'); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } ``` ## 41 ### --description-- Within `hello-world.js`, export an asynchronous function named `sayHello` with the following signature: ```javascript function sayHello( connection, payer, programId, accountPubkey, ): Promise ``` ### --tests-- You should define a function with the handle `sayHello`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'sayHello'); assert.exists( functionDeclaration, 'You should define a function with the handle `sayHello`' ); ``` You should define `sayHello` with a first parameter named `connection`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'sayHello'); const parameter = functionDeclaration?.params?.[0]; assert.equal( parameter?.name, 'connection', 'You should define `sayHello` with a first parameter named `connection`' ); ``` You should define `sayHello` with a second parameter named `payer`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'sayHello'); const parameter = functionDeclaration?.params?.[1]; assert.equal( parameter?.name, 'payer', 'You should define `sayHello` with a second parameter named `payer`' ); ``` You should define `sayHello` with a third parameter named `programId`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'sayHello'); const parameter = functionDeclaration?.params?.[2]; assert.equal( parameter?.name, 'programId', 'You should define `sayHello` with a third parameter named `programId`' ); ``` You should define `sayHello` with a fourth parameter named `accountPubkey`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'sayHello'); const parameter = functionDeclaration?.params?.[3]; assert.equal( parameter?.name, 'accountPubkey', 'You should define `sayHello` with a fourth parameter named `accountPubkey`' ); ``` You should define `sayHello` as an asynchronous function. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'sayHello'); assert.isTrue( functionDeclaration?.async, 'You should define `sayHello` as an asynchronous function' ); ``` You should define `sayHello` to be a named export. ```js const exportNamedDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => e.declaration?.id?.name === 'sayHello'); assert.exists( exportNamedDeclaration, 'You should define `sayHello` to be a named export' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } ``` ## 42 ### --description-- To say hello to your smart contract, you need to send a transaction with some data. Within `sayHello`, create a `transaction` variable with a value of: ```javascript { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0), } ``` _The `data` field is empty because the program does not do anything with it._ ### --tests-- You should define a variable named `transaction`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return ( v.declarations?.[0]?.id?.name === 'transaction' && v.scope.join() === 'global,sayHello' ); }); assert.exists( variableDeclaration, 'You should define a variable named `transaction`, within `sayHello`' ); ``` You should give `transaction` a value of the above object literal. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return ( v.declarations?.[0]?.id?.name === 'transaction' && v.scope.join() === 'global,sayHello' ); }); const txCode = babelisedCode.generateCode(variableDeclaration); const testCode = ` const accountPubkey = '123'; const programId = '456'; ${txCode} return transaction; `; let transaction = new Function(testCode)(); assert.deepEqual( transaction, { keys: [{ pubkey: '123', isSigner: false, isWritable: true }], programId: '456', data: Buffer.alloc(0) }, 'You should give `transaction` a value of the above object literal' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } export async function sayHello(connection, payer, programId, accountPubkey) {} ``` ## 43 ### --description-- Within `sayHello`, define an `instruction` variable to be a new instance `TransactionInstruction` from `@solana/web3.js`. The constructor expects your transaction object as an argument. ### --tests-- You should define a variable named `instruction`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'instruction' && v.scope.join() === 'global,sayHello' ); assert.exists( variableDeclaration, 'You should define a variable named `instruction`, within `sayHello`' ); ``` You should give `instruction` a value of `new TransactionInstruction(transaction)`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'instruction' && v.scope.join() === 'global,sayHello' ); const newExpression = variableDeclaration?.declarations?.[0]?.init; assert.exists( newExpression, 'You should give `instruction` a value of `new ...`' ); assert.equal( newExpression?.callee?.name, 'TransactionInstruction', 'You should give `instruction` a value of `new TransactionInstruction`' ); const transactionArgument = newExpression?.arguments?.[0]; assert.equal( transactionArgument?.name, 'transaction', 'You should give `instruction` a value of `new TransactionInstruction(transaction)`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } export async function sayHello(connection, payer, programId, accountPubkey) { const transaction = { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0) }; } ``` ## 44 ### --description-- Now, send and confirm the transaction. ### --tests-- You should call `sendAndConfirmTransaction` within `sayHello`. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return ( e.expression?.argument?.callee?.name === 'sendAndConfirmTransaction' && e.scope.join() === 'global,sayHello' ); }); assert.exists( expressionStatement, 'You should call `sendAndConfirmTransaction` within `sayHello`' ); ``` Calling `sayHello` should send the correct transaction with `sendAndConfirmTransaction(connection, new Transaction().add(instruction), [payer])`. ```js const sayHelloDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id?.name === 'sayHello'); const helloCode = babelisedCode.generateCode(sayHelloDeclaration); const testCode = ` class TransactionInstruction { constructor(transaction) { this.transaction = '5'; } } class Transaction { add(instruction) { this.instruction = instruction; return this; } } let error; const sendAndConfirmTransaction = (a, b, c) => { try { assert.equal(a, '1', 'You should call sendAndConfirmTransaction with connection as the first argument'); assert.equal(b.instruction.transaction, '5', 'You should call sendAndConfirmTransaction with new Transaction().add(instruction) as the second argument'); assert.include(c, '2', 'You should call sendAndConfirmTransaction with [payer] as the third argument'); } catch (e) { error = e; } }; ${helloCode} sayHello('1', '2', '3', '4'); return error; `; try { const t = eval(`(() => {${testCode}})()`); if (t) { throw t; } } catch (e) { if (e instanceof AssertionError) { throw e; } } ``` Your `sayHello` function should create a new `Transaction` instance ```js const { sayHello } = await __helpers.importSansCache( './learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); assert.match(sayHello.toString(), /new\s+Transaction\s*\(/); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction, TransactionInstruction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } export async function sayHello(connection, payer, programId, accountPubkey) { const transaction = { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0) }; const instruction = new TransactionInstruction(transaction); } ``` ## 45 ### --description-- Within `main.js` in the `main` function, create a variable named `programId` and use the function you created to assign it the program id. ### --tests-- You should define a variable named `programId`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'programId' && v.scope.join() === 'global,main' ); assert.exists( variableDeclaration, 'You should define a variable named `programId`, within `main`' ); ``` You should assign `programId` the value of `await getProgramId()`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'programId' && v.scope.join() === 'global,main' ); const awaitExpression = variableDeclaration?.declarations?.[0]?.init; assert.equal(awaitExpression?.type, 'AwaitExpression'); assert.equal( awaitExpression?.argument?.callee?.name, 'getProgramId', 'You should assign `programId` the value of `await getProgramId()`' ); ``` You should import `getProgramId` from `./hello-world.js`. ```js const importDeclaration = babelisedCode .getImportDeclarations() .find(i => i.source.value === './hello-world.js'); assert.exists(importDeclaration, 'You should import from `./hello-world.js`'); const importSpecifier = importDeclaration?.specifiers?.find( s => s.imported.name === 'getProgramId' ); assert.exists( importSpecifier, 'You should import `getProgramId` from `./hello-world.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction, TransactionInstruction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } export async function sayHello(connection, payer, programId, accountPubkey) { const transaction = { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0) }; const instruction = new TransactionInstruction(transaction); await sendAndConfirmTransaction( connection, new Transaction().add(instruction), [payer] ); } ``` ## 46 ### --description-- Within `main`, create a variable named `payer` and use the function you created to assign it the payer. ### --tests-- You should define a variable named `payer`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'payer' && v.scope.join() === 'global,main' ); assert.exists( variableDeclaration, 'You should define a variable named `payer`, within `main`' ); ``` You should assign `payer` the value of `await establishPayer()`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'payer' && v.scope.join() === 'global,main' ); const awaitExpression = variableDeclaration?.declarations?.[0]?.init; assert.equal( awaitExpression?.argument?.callee?.name, 'establishPayer', 'You should assign `payer` the value of `establishPayer()`' ); ``` You should import `establishPayer` from `./hello-world.js`. ```js const importDeclaration = babelisedCode .getImportDeclarations() .find(i => i.source.value === './hello-world.js'); assert.exists(importDeclaration, 'You should import from `./hello-world.js`'); const importSpecifier = importDeclaration?.specifiers?.find( s => s.imported.name === 'establishPayer' ); assert.exists( importSpecifier, 'You should import `establishPayer` from `./hello-world.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/main.js"-- ```js import { establishConnection, getProgramId } from './hello-world.js'; async function main() { console.log(`Saying 'hello' to a Solana account`); const connection = establishConnection(); const programId = await getProgramId(); } await main(); ``` ## 47 ### --description-- Within `main`, create a variable named `accountPubkey` and use the function you created to assign it the account pubkey. ### --tests-- You should define a variable named `accountPubkey`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'accountPubkey' && v.scope.join() === 'global,main' ); assert.exists( variableDeclaration, 'You should define a variable named `accountPubkey`, within `main`' ); ``` You should assign `accountPubkey` the value of `await getAccountPubkey(payer, programId)`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'accountPubkey' && v.scope.join() === 'global,main' ); const awaitExpression = variableDeclaration?.declarations?.[0]?.init; assert.equal(awaitExpression?.type, 'AwaitExpression'); assert.equal( awaitExpression?.argument?.callee?.name, 'getAccountPubkey', 'You should assign `accountPubkey` the value of `await getAccountPubkey(payer, programId)`' ); assert.equal(awaitExpression?.argument?.arguments[0]?.name, 'payer'); assert.equal(awaitExpression?.argument?.arguments[1]?.name, 'programId'); ``` You should import `getAccountPubkey` from `./hello-world.js`. ```js const importDeclaration = babelisedCode .getImportDeclarations() .find(i => i.source.value === './hello-world.js'); assert.exists(importDeclaration, 'You should import from `./hello-world.js`'); const importSpecifier = importDeclaration?.specifiers?.find( s => s.imported.name === 'getAccountPubkey' ); assert.exists( importSpecifier, 'You should import `getAccountPubkey` from `./hello-world.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/main.js"-- ```js import { establishConnection, establishPayer, getProgramId } from './hello-world.js'; async function main() { console.log(`Saying 'hello' to a Solana account`); const connection = establishConnection(); const programId = await getProgramId(); const payer = await establishPayer(); } await main(); ``` ## 48 ### --description-- Within `main`, ensure the program account is deployed, and the program data account is created. ### --tests-- You should call `await checkProgram(connection, payer, programId, accountPubkey)` within `main`. ```js const expressionStatement = babelisedCode .getExpressionStatements() .find(e => e.expression?.argument?.callee?.name === 'checkProgram'); assert.exists(expressionStatement, 'You should call `checkProgram`'); assert.equal( expressionStatement?.scope?.join(), 'global,main', 'You should call `checkProgram` within `main`' ); const awaitExpression = expressionStatement?.expression; assert.equal(awaitExpression?.type, 'AwaitExpression'); const args = awaitExpression?.argument?.arguments; const [connection, payer, programId, accountPubkey] = args; assert.equal( connection?.name, 'connection', '`connection` should be the first argument' ); assert.equal(payer?.name, 'payer', '`payer` should be the second argument'); assert.equal( programId?.name, 'programId', '`programId` should be the third argument' ); assert.equal( accountPubkey?.name, 'accountPubkey', '`accountPubkey` should be the fourth argument' ); ``` You should import `checkProgram` from `./hello-world.js`. ```js const importDeclaration = babelisedCode .getImportDeclarations() .find(i => i.source.value === './hello-world.js'); assert.exists(importDeclaration, 'You should import from `./hello-world.js`'); const importSpecifier = importDeclaration?.specifiers?.find( s => s.imported.name === 'checkProgram' ); assert.exists( importSpecifier, 'You should import `checkProgram` from `./hello-world.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/main.js"-- ```js import { establishConnection, establishPayer, getAccountPubkey, getProgramId } from './hello-world.js'; async function main() { console.log(`Saying 'hello' to a Solana account`); const connection = establishConnection(); const programId = await getProgramId(); const payer = await establishPayer(); const accountPubkey = await getAccountPubkey(payer, programId); } await main(); ``` ## 49 ### --description-- Within `main`, say hello to the program. ### --tests-- You should call `await sayHello(connection, payer, programId, accountPubkey)` within `main`. ```js const expressionStatement = babelisedCode .getExpressionStatements() .find(e => e.expression?.argument?.callee?.name === 'sayHello'); assert.exists(expressionStatement, 'You should call `sayHello`'); assert.equal( expressionStatement?.scope?.join(), 'global,main', 'You should call `sayHello` within `main`' ); const awaitExpression = expressionStatement?.expression; assert.equal(awaitExpression?.type, 'AwaitExpression'); const args = awaitExpression?.argument?.arguments; const [connection, payer, programId, accountPubkey] = args; assert.equal( connection?.name, 'connection', '`connection` should be the first argument' ); assert.equal(payer?.name, 'payer', '`payer` should be the second argument'); assert.equal( programId?.name, 'programId', '`programId` should be the third argument' ); assert.equal( accountPubkey?.name, 'accountPubkey', '`accountPubkey` should be the fourth argument' ); ``` You should import `sayHello` from `./hello-world.js`. ```js const importDeclaration = babelisedCode .getImportDeclarations() .find(i => i.source.value === './hello-world.js'); assert.exists(importDeclaration, 'You should import from `./hello-world.js`'); const importSpecifier = importDeclaration?.specifiers?.find( s => s.imported.name === 'sayHello' ); assert.exists( importSpecifier, 'You should import `sayHello` from `./hello-world.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/main.js"-- ```js import { checkProgram, establishConnection, establishPayer, getAccountPubkey, getProgramId } from './hello-world.js'; async function main() { console.log(`Saying 'hello' to a Solana account`); const connection = establishConnection(); const programId = await getProgramId(); const payer = await establishPayer(); const accountPubkey = await getAccountPubkey(payer, programId); await checkProgram(connection, payer, programId, accountPubkey); } await main(); ``` ## 50 ### --description-- Run `solana-test-validator` to start a local Solana cluster if you do not already have one running. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --"src/client/main.js"-- ```js import { checkProgram, establishConnection, establishPayer, getAccountPubkey, getProgramId, sayHello } from './hello-world.js'; async function main() { console.log(`Saying 'hello' to a Solana account`); const connection = establishConnection(); const programId = await getProgramId(); const payer = await establishPayer(); const accountPubkey = await getAccountPubkey(payer, programId); await checkProgram(connection, payer, programId, accountPubkey); await sayHello(connection, payer, programId, accountPubkey); } await main(); ``` ## 51 ### --description-- Test your script by using `node` to run it. It should produce an error. ### --tests-- You should run `node src/client/main.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand.trim(), 'node src/client/main.js', 'You should run `node src/client/main.js` in the terminal' ); ``` You should be in the `learn-how-to-interact-with-on-chain-programs` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include(cwd, 'learn-how-to-interact-with-on-chain-programs'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 52 ### --description-- Your script does not work as intended, because the program has not been deployed to the cluster. In order to deploy a program, you need to create an account. Use the terminal to create a new local keypair. ### --tests-- You should run `solana-keygen new` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand.trim(), 'solana-keygen new', 'You should run `solana-keygen new` in the terminal' ); ``` You should have an `id.json` file in the `/root/.config/solana/` directory as a result of generating a key ```js const solanaDir = await __helpers.getDirectory('../../root/.config/solana'); assert.exists(solanaDir, 'id.json'); ``` ## 53 ### --description-- Set your Solana config RPC URL to the local cluster. ### --tests-- You should run `solana config set --url localhost` in the terminal. ```js const command = `solana config get`; const { stdout, stderr } = await __helpers.getCommandOutput(command); assert.include( stdout, 'http://localhost:8899', 'You should run `solana config set --url localhost` in the terminal' ); ``` ## 54 ### --description-- Deploy the program to the cluster. ### --tests-- You should run `solana program deploy dist/program/helloworld.so` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand.trim(), 'solana program deploy dist/program/helloworld.so', 'You should run `solana program deploy dist/program/helloworld.so` in the terminal' ); ``` You should be in the `learn-how-to-interact-with-on-chain-programs` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include(cwd, 'learn-how-to-interact-with-on-chain-programs'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 55 ### --description-- Your program might not have deployed, because you do not have enough SOL in your account. Airdrop some SOL to your account. ### --tests-- You should run `solana airdrop ` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include( lastCommand, 'solana airdrop', 'You should run `solana airdrop 1` in the terminal' ); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 56 ### --description-- Deploy the program to the cluster. ### --tests-- You should run `solana program deploy dist/program/helloworld.so` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand.trim(), 'solana program deploy dist/program/helloworld.so', 'You should run `solana program deploy dist/program/helloworld.so` in the terminal' ); ``` You should be in the `learn-how-to-interact-with-on-chain-programs` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include(cwd, 'learn-how-to-interact-with-on-chain-programs'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 57 ### --description-- Test your script by using `node` to run it. ### --tests-- You should run `node src/client/main.js` from the `learn-how-to-interact-with-on-chain-programs` directory. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand.trim(), 'node src/client/main.js', 'You should run `node src/client/main.js` from the `learn-how-to-interact-with-on-chain-programs` directory' ); ``` You should be in the `learn-how-to-interact-with-on-chain-programs` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include(cwd, 'learn-how-to-interact-with-on-chain-programs'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 58 ### --description-- Now that you can say hello to the program, you will want to find out how many times the program has been said "hello" to. Within `hello-world.js`, export an asynchronous function named `getHelloCount` with the following signature: ```javascript function getHelloCount( connection, accountPubkey, ): Promise ``` ### --tests-- You should define a function with the handle `getHelloCount`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'getHelloCount'); assert.exists( functionDeclaration, 'You should define a function with the handle `getHelloCount`' ); ``` You should define `getHelloCount` with a first parameter named `connection`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'getHelloCount'); const [connection] = functionDeclaration?.params; assert.equal( connection?.name, 'connection', 'You should define `getHelloCount` with a first parameter named `connection`' ); ``` You should define `getHelloCount` with a second parameter named `accountPubkey`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'getHelloCount'); const [, accountPubkey] = functionDeclaration?.params; assert.equal( accountPubkey?.name, 'accountPubkey', 'You should define `getHelloCount` with a second parameter named `accountPubkey`' ); ``` You should define `getHelloCount` as an asynchronous function. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'getHelloCount'); assert.isTrue( functionDeclaration?.async, 'You should define `getHelloCount` as an asynchronous function' ); ``` You should define `getHelloCount` to be a named export. ```js const exportNamedDeclaration = babelisedCode .getType('ExportNamedDeclaration') .find(e => e.declaration?.id?.name === 'getHelloCount'); assert.exists( exportNamedDeclaration, 'You should define `getHelloCount` to be a named export' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 59 ### --description-- Within `getHelloCount`, create an `accountInfo` variable with a value of the account info for the public key passed as a parameter. ### --tests-- You should define a variable named `accountInfo`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'accountInfo' && v.scope.join() === 'global,getHelloCount' ); assert.exists( variableDeclaration, 'You should define a variable named `accountInfo`' ); ``` You should assign `accountInfo` the value of `await connection.getAccountInfo(accountPubkey)`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'accountInfo' && v.scope.join() === 'global,getHelloCount' ); const awaitExpression = variableDeclaration?.declarations[0]?.init; const callExpression = awaitExpression?.argument; assert.equal( callExpression?.callee?.object?.name, 'connection', 'You should assign `accountInfo` the value of `await connection.getAccountInfo(accountPubkey)`' ); assert.equal( callExpression?.callee?.property?.name, 'getAccountInfo', 'You should assign `accountInfo` the value of `await connection.getAccountInfo(accountPubkey)`' ); assert.equal( callExpression?.arguments?.[0]?.name, 'accountPubkey', 'You should assign `accountInfo` the value of `await connection.getAccountInfo(accountPubkey)`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction, TransactionInstruction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } export async function sayHello(connection, payer, programId, accountPubkey) { const transaction = { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0) }; const instruction = new TransactionInstruction(transaction); await sendAndConfirmTransaction( connection, new Transaction().add(instruction), [payer] ); } export async function getHelloCount(connection, accountPubkey) {} ``` ## 60 ### --description-- In order to read the data from an account, you need to deserialize it based on the program's schema. Within `getHelloCount`, create a `greeting` variable with a value of: ```javascript borsh.deserialize(, , ) ``` ### --tests-- You should define a variable named `greeting`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations[0].id.name === 'greeting' && v.scope.join() === 'global,getHelloCount' ); assert.exists( variableDeclaration, 'You should define a variable named `greeting`' ); ``` You should give `greeting` a value of `borsh.deserialize(HelloWorldSchema, HelloWorldAccount, accountInfo.data)`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations[0].id.name === 'greeting' && v.scope.join() === 'global,getHelloCount' ); const callExpression = variableDeclaration?.declarations[0]?.init; assert.equal( callExpression?.callee?.object?.name, 'borsh', 'You should give `greeting` a value of `borsh...()`' ); assert.equal( callExpression?.callee?.property?.name, 'deserialize', 'You should give `greeting` a value of `borsh.deserialize()`' ); assert.equal( callExpression?.arguments[0]?.name, 'HelloWorldSchema', 'You should give `greeting` a value of `borsh.deserialize(HelloWorldSchema)`' ); assert.equal( callExpression?.arguments[1]?.name, 'HelloWorldAccount', 'You should give `greeting` a value of `borsh.deserialize(, HelloWorldAccount)`' ); assert.equal( callExpression?.arguments[2]?.object?.name, 'accountInfo', 'You should give `greeting` a value of `borsh.deserialize(, , accountInfo.data)`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction, TransactionInstruction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } export async function sayHello(connection, payer, programId, accountPubkey) { const transaction = { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0) }; const instruction = new TransactionInstruction(transaction); await sendAndConfirmTransaction( connection, new Transaction().add(instruction), [payer] ); } export async function getHelloCount(connection, accountPubkey) { const accountInfo = await connection.getAccountInfo(accountPubkey); } ``` ## 61 ### --description-- Within `getHelloCount`, return the `counter` property of the `greeting` variable. ### --tests-- You should return the `counter` property of `greeting`. ```js const functionDeclaration = babelisedCode .getFunctionDeclarations() .find(f => f.id.name === 'getHelloCount'); const returnStatement = functionDeclaration?.body?.body?.find( b => b.type === 'ReturnStatement' ); assert.exists(returnStatement, '`getHelloCount` should return'); assert.equal( returnStatement?.argument?.object?.name, 'greeting', 'You should return the `counter` property of `greeting`' ); assert.equal( returnStatement?.argument?.property?.name, 'counter', 'You should return the `counter` property of `greeting`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/hello-world.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction, TransactionInstruction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } export async function sayHello(connection, payer, programId, accountPubkey) { const transaction = { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0) }; const instruction = new TransactionInstruction(transaction); await sendAndConfirmTransaction( connection, new Transaction().add(instruction), [payer] ); } export async function getHelloCount(connection, accountPubkey) { const accountInfo = await connection.getAccountInfo(accountPubkey); const greeting = borsh.deserialize( HelloWorldSchema, HelloWorldAccount, accountInfo.data ); } ``` ## 62 ### --description-- Within `main.js` in the `main` function, get the hello count, and store it in a variable named `helloCount`. ### --tests-- You should define a variable named `helloCount`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'helloCount' && v.scope.join() === 'global,main' ); assert.exists( variableDeclaration, 'You should define a variable named `helloCount`' ); ``` You should assign `helloCount` the value of `await getHelloCount(connection, accountPubkey)`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find( v => v.declarations?.[0]?.id?.name === 'helloCount' && v.scope.join() === 'global,main' ); const awaitExpression = variableDeclaration?.declarations[0]?.init; const callExpression = awaitExpression?.argument; assert.equal( callExpression?.callee?.name, 'getHelloCount', 'You should assign `helloCount` the value of `await getHelloCount`' ); assert.equal( callExpression?.arguments[0]?.name, 'connection', 'You should assign `helloCount` the value of `await getHelloCount(connection, ...)`' ); assert.equal( callExpression?.arguments[1]?.name, 'accountPubkey', 'You should assign `helloCount` the value of `await getHelloCount(..., accountPubkey)`' ); ``` You should import `getHelloCount` from `./hello-world.js`. ```js const importDeclaration = babelisedCode .getImportDeclarations() .find(i => i.source.value === './hello-world.js'); const importSpecifier = importDeclaration?.specifiers?.find( s => s.imported.name === 'getHelloCount' ); assert.exists( importSpecifier, 'You should import `getHelloCount` from `./hello-world.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/hello-world.js"-- ```js import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction, TransactionInstruction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } export async function sayHello(connection, payer, programId, accountPubkey) { const transaction = { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0) }; const instruction = new TransactionInstruction(transaction); await sendAndConfirmTransaction( connection, new Transaction().add(instruction), [payer] ); } export async function getHelloCount(connection, accountPubkey) { const accountInfo = await connection.getAccountInfo(accountPubkey); const greeting = borsh.deserialize( HelloWorldSchema, HelloWorldAccount, accountInfo.data ); return greeting.counter; } ``` ## 63 ### --description-- Within `main`, log the `helloCount` variable value. ### --tests-- You should log the `helloCount` variable value. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { const callExpression = e.expression; const object = callExpression?.callee?.object; const property = callExpression?.callee?.property; const helloCountInArgs = callExpression?.arguments?.some( a => a.name === 'helloCount' || a.expressions?.find(e => e.name === 'helloCount') ); return ( object?.name === 'console' && ['log', 'info', 'error', 'debug', 'table'].includes(property?.name) && helloCountInArgs ); }); assert.exists( expressionStatement, 'You should log the `helloCount` variable value' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-how-to-interact-with-on-chain-programs/src/client/main.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"src/client/main.js"-- ```js import { checkProgram, establishConnection, establishPayer, getAccountPubkey, getHelloCount, getProgramId, sayHello } from './hello-world.js'; async function main() { console.log(`Saying 'hello' to a Solana account`); const connection = establishConnection(); const programId = await getProgramId(); const payer = await establishPayer(); const accountPubkey = await getAccountPubkey(payer, programId); await checkProgram(connection, payer, programId, accountPubkey); await sayHello(connection, payer, programId, accountPubkey); const helloCount = await getHelloCount(connection, accountPubkey); } await main(); ``` ## 64 ### --description-- Use Nodejs to execute the `main.js` script. ### --tests-- You should run `node src/client/main.js` in the terminal. ```js await new Promise(res => setTimeout(res, 1000)); const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'node src/client/main.js', 'You should run `node src/client/main.js` in the terminal' ); ``` Your terminal should print `Hello count: ` ```js const output = await __helpers.getTerminalOutput(); const splitOutput = output.split('node src/client/main.js'); const lastOutput = splitOutput[splitOutput.length - 1]; assert.match(lastOutput, /Hello count: \d+/); ``` ### --seed-- #### --"src/client/main.js"-- ```js import { checkProgram, establishConnection, establishPayer, getAccountPubkey, getHelloCount, getProgramId, sayHello } from './hello-world.js'; async function main() { console.log(`Saying 'hello' to a Solana account`); const connection = establishConnection(); const programId = await getProgramId(); const payer = await establishPayer(); const accountPubkey = await getAccountPubkey(payer, programId); await checkProgram(connection, payer, programId, accountPubkey); await sayHello(connection, payer, programId, accountPubkey); const helloCount = await getHelloCount(connection, accountPubkey); console.log(`Hello count: ${helloCount}`); } await main(); ``` ## 65 ### --description-- Contratulations on finishing this project! Feel free to play with your code. 🎆 Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract.md ================================================ # Solana - Learn How to Set Up Solana by Building a Hello World Smart Contract ## 1 ### --description-- Welcome to the Solana curriculum! For the duration of this project, you will be working in the `learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/` directory. Open a new terminal, and change into the above directory. _Note: Do not change the existing terminal_ ### --tests-- You should use `cd` to change into the `learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include( cwd, 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract' ); ``` ## 2 ### --description-- You will be using the Solana CLI to: - Configure your cluster - Create Keypairs - Log useful information - Deploy your on-chain program The Solana CLI can be installed with: ```bash sh -c "$(curl -sSfL https://release.solana.com/v1.17.18/install)" ``` Solana is already installed in this environment. So, run `solana --version` to confirm it is installed. ### --tests-- You should run `solana --version` in the console. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /solana --version/); ``` The version should be printed to the console. ```js const terminalOut = await __helpers.getTerminalOutput(); assert.include(terminalOut, "1.17.18"); ``` ## 3 ### --description-- The Solana CLI is feature rich and has many commands. View the list of commands with: ```bash solana --help ``` ### --tests-- You should see the list of commands. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /solana --help/); ``` ## 4 ### --description-- See the default Solana configuration by running: ```bash solana config get ``` ### --tests-- You should run `solana config get` in the console. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /solana config get/); ``` ## 5 ### --description-- The Solana network consists of multiple clusters: - Devnet - Testnet - Mainnet During the initial stages of development, you are most likely to be working on a local cluster. Change your configuration to use `localhost` as the cluster: ```bash solana config set --url localhost ``` ### --tests-- You should set the configuration with `solana config set --url localhost`. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /solana config set --url localhost/); ``` ## 6 ### --description-- View the your changed config settings. ### --tests-- You should view the config with `solana config get`. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /solana config get/); ``` ## 7 ### --description-- `solana config` reads and writes to the `.config/solana` directory. View the config file contents in the terminal with: ```bash cat ~/.config/solana/cli/config.yml ``` ### --tests-- You should use `cat` to view the config file contents. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /cat ~\/\.config\/solana\/cli\/config.yml/); ``` ## 8 ### --description-- As this is your first time using the Solana CLI, you should generate a new keypair with: ```bash solana-keygen new ``` _Note:_ When prompted, hit _ENTER_ in the terminal to generate a keypair with an empty passphrase. ### --tests-- You should run `solana-keygen new` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /solana-keygen new/); ``` ## 9 ### --description-- The `keypair_path` is the path to your Solana keypair used when making transactions. View your keypair in the terminal with: ```bash cat ~/.config/solana/id.json ``` ### --tests-- You should use `cat` to view your keypair in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /cat ~\/\.config\/solana\/id\.json/); ``` ## 10 ### --description-- View/get your wallet public key with: ```bash solana address ``` ### --tests-- You should use `solana address` to view your public key. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /solana address/); ``` ## 11 ### --description-- You have set your Solana config to use a locally hosted cluster, but do not have one running yet. Open a new terminal, and start a Solana test validator with: ```bash solana-test-validator ``` **Note:** You need to manually click the _Run Tests_ button. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl -s -S http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout.trim()); assert.include( jsonOut, { result: 'ok' }, 'The validator should have a "health" result of "ok"' ); } catch (e) { assert.fail(e); } ``` ## 12 ### --description-- Manually make an RPC call with: ```bash curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"getBalance","params":["your_address_public_key",{"commitment":"finalized"}]}' http://localhost:8899 ``` _Remember to replace `your_address_public_key`_ ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should make an RPC call using `curl`. ```js const { stdout } = await __helpers.getCommandOutput('solana address'); const camperPublicKey = stdout.trim(); const toMatch = `curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getBalance", "params": ["${camperPublicKey}", { "commitment": "finalized" }]}' http://localhost:8899`; const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand.replace(/\s/g, ''), toMatch.replace(/\s/g, '')); ``` The RPC call should return a successful response. _Try again_ ```js const terminalOut = await __helpers.getTerminalOutput(); assert.match(terminalOut, /"result":/); ``` ## 13 ### --description-- It is not the best user experience using curl commands to interact with the network. View your wallet's balance with: ```bash solana balance ``` _Remember to replace ``_ ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should use the command `solana balance `. ```js const { stdout } = await __helpers.getCommandOutput('solana address'); const accountAddress = stdout.trim(); const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, `solana balance ${accountAddress}`); ``` ## 14 ### --description-- You can see your balance is `500000000` 😲. Maybe that is not enough for yourself 😨. Request an _airdrop_ of 1 SOL to your account with: ```bash solana airdrop 1 ``` _Remember to replace `` with your public key_ ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should use the above command to request an airdrop. ```js const { stdout } = await __helpers.getCommandOutput('solana address'); const accountAddress = stdout.trim(); const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, `solana airdrop 1 ${accountAddress}`); ``` Your account should have at least `500000001` SOL. ```js const { stdout: stdout1 } = await __helpers.getCommandOutput('solana address'); const accountAddress = stdout1.trim(); const { stdout } = await __helpers.getCommandOutput( `solana balance ${accountAddress}` ); const balance = stdout.trim()?.match(/\d+/)[0]; assert.isAtLeast(Number(balance), 500000001); ``` ## 15 ### --description-- Check out your balance. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should use `solana balance ` to check your balance. ```js const { stdout } = await __helpers.getCommandOutput('solana address'); const accountAddress = stdout.trim(); const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, `solana balance ${accountAddress}`); ``` ## 16 ### --description-- Open the `src/program-rust/src/lib.rs` file. This is where you will be developing your first Solana smart contract. Start by importing the `solana_program` crate. ### --tests-- You should have `use solana_program;` in `src/program-rust/src/lib.rs`. ```js const file = await __helpers.getFile( 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs' ); assert.include(file, 'use solana_program;'); ``` ## 17 ### --description-- When your smart contract is called, a function needs to be run. Define a public function with the handle `process_instruction`. ### --tests-- You should define a **PUBLIC** function with the handle `process_instruction`. ```js const file = await __helpers.getFile( 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs' ); assert.match(file, /pub\s+fn\s+process_instruction\s*\(/s); ``` ## 18 ### --description-- In order to tell your program which function is the entrypoint for the contract, import the `entrypoint` macro from the `solana_program` crate, and pass `process_instruction` as the argument. ### --tests-- You should import `solana_program::entrypoint`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); const variants = [ /solana_program\s*::\s*entrypoint/, /solana_program\s*::\s*\{\s*entrypoint\s*\}/, /solana_program::prelude::\*/ ]; const someMatch = variants.some(r => file.match(r)); assert.isTrue(someMatch, `Your code should match one of ${variants}`); ``` You should call the `entrypoint` macro in the root of your program. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /entrypoint!\(/); ``` You should pass `process_instruction` as an argument: `entrypoint!(process_instruction);`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /entrypoint!\(\s*process_instruction\s*\)/s); ``` ## 19 ### --description-- To make debugging your application easier, import the `msg` macro from `solana_program`, and use it to log the string slice `Hello World` to the console whenever `process_instruction` is called. ### --tests-- You should import `solana_program::msg;`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); const variants = [ /solana_program\s*::\s*msg/, /solana_program\s*::\s*\{[\s\S]*msg/, /solana_program::prelude::\*/ ]; const someMatch = variants.some(r => file.match(r)); assert.isTrue(someMatch, `Your code should match one of ${variants}`); ``` You should call the `msg` macro within the `process_instruction` function. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /msg!\(/); ``` You should pass `"Hello World"` to `msg` as an argument: `msg!("Hello World");`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /msg!\(\s*"Hello World"\s*\)/s); ``` ## 20 ### --description-- Within `src/program-rust/`, run `cargo build` to try build your library. You should see an error, because an entrypoint function is supposed to take 3 arguments. ### --tests-- You should run `cargo build` within the `src/program-rust/` directory. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /cargo build/, 'Last command was incorrect'); const dir = await __helpers.getCWD(); const cwd = dir.split('\n').filter(Boolean).pop(); assert.match( cwd, /src\/program-rust\/?$/, "You should be in the 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust' dir" ); ``` ## 21 ### --description-- The first argument an entrypoint function takes is a reference to a `Pubkey` which is the public key of the account the program was loaded into. Add a parameter to the function definition named `program_id` with the correct type. _Import the `Pubkey` struct from the `pubkey` module of `solana_program`._ ### --tests-- You should have `use solana_program::pubkey::Pubkey;` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); const variants = [ /solana_program\s*::\s*pubkey::Pubkey/, /solana_program\s*::\s*\{[\s\S]*pubkey::Pubkey/, /solana_program::prelude::\*/ ]; const someMatch = variants.some(r => file.match(r)); assert.isTrue(someMatch, `Your code should match one of ${variants}`); ``` You should define `process_instruction` to have one parameter named `program_id`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /process_instruction\s*\(\s*program_id\s*/s); ``` You should type `program_id` with `&Pubkey`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /program_id\s*:\s*&Pubkey/s); ``` ## 22 ### --description-- The second argument an entrypoint function takes is a slice of accounts with which the program can interact. Add a parameter to the function definition named `accounts` with the type `&[AccountInfo]`. _Import the `AccountInfo` struct from the `account_info` module of `solana_program`._ ### --tests-- You should have `use solana_program::account_info::AccountInfo;` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); const variants = [ /solana_program\s*::\s*account_info::AccountInfo/, /solana_program\s*::\s*\{[\s\S]*account_info::AccountInfo/, /solana_program::prelude::\*/ ]; const someMatch = variants.some(r => file.match(r)); assert.isTrue(someMatch, `Your code should match one of ${variants}`); ``` You should define `process_instruction` to have a second parameter named `accounts`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /process_instruction\s*\(.*?,\s*accounts/s); ``` You should type `accounts` with `&[AccountInfo]`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /accounts\s*:\s*&\[\s*AccountInfo\s*\]/s); ``` ## 23 ### --description-- The third argument an entrypoint function takes is instruction data from the smart contract call. Add a parameter to the function definition named `instruction_data` with the type `&[u8]`. ### --tests-- You should define `process_instruction` to have a third parameter named `instruction_data`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /process_instruction\s*\(.*?,\s*instruction_data/s); ``` You should type `instruction_data` with `&[u8]`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /instruction_data\s*:\s*&\[\s*u8\s*\]/s); ``` ## 24 ### --description-- Give your entrypoint function a return of `ProgramResult`. This type comes from the `entrypoint` module of `solana_program`. Also, return an empty tuple wrapped in the `Ok` variant of the `Result` enum. ### --tests-- You should have `use solana_program::entrypoint::ProgramResult;` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); const variants = [ /solana_program\s*::\s*entrypoint::ProgramResult/, /solana_program\s*::\s*\{[\s\S]*entrypoint::ProgramResult/, /solana_program::prelude::\*/ ]; const someMatch = variants.some(r => file.match(r)); assert.isTrue(someMatch, `Your code should match one of ${variants}`); ``` You should define `process_instruction` to return `ProgramResult`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /\)\s*->\s*ProgramResult\s*\{/s); ``` You should return `Ok(())` from `process_instruction`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /Ok\s*\(\s*\(\s*\)\s*\)/s); ``` ## 25 ### --description-- Now that the entrypoint function definition is correct, rebuild your program to see if it compiles. ### --tests-- You should run `cargo build` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'cargo build'); ``` You should be in the `src/program-rust` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match(cwd, /src\/program-rust\/?$/); ``` ## 26 ### --description-- Before deploying, build your program with: ```bash cargo build-sbf --sbf-out-dir=../../dist/program ``` ### --tests-- You should run the above command to build your program. ```js const lastCommand = await __helpers.getLastCommand(); assert.match(lastCommand, /cargo build-sbf --sbf-out-dir=.*?/s); ``` You should be in the `src/program-rust` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match(cwd, /src\/program-rust\/?$/); ``` ## 27 ### --description-- Your program is located in `dist/program/helloworld.so`. You can deploy it to your localnet with: ```bash solana program deploy ``` **NOTE:** `solana deploy ` will **not** work, because that deploys a non-upgradeable program. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should run `solana program deploy dist/program/helloworld.so` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'solana program deploy dist/program/helloworld.so'); ``` You should be in the `learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match( cwd, /learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract\/?$/ ); ``` ## 28 ### --description-- After deploying, your program id should be printed to the console. View the program account with: ```bash solana program show ``` ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should run `solana program show ` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'solana program show'); ``` ## 29 ### --description-- To view the logs from an on-chain program, open a new terminal, and run: ```bash solana logs ``` ### --tests-- You should run `solana logs` in a new terminal. ```js const temp = await __helpers.getTemp(); assert.include(temp, 'Streaming transaction logs'); ``` ## 30 ### --description-- To call your program, run: ```bash npm run call:hello-world ``` This will run the code in `src/client/`. Watch the `solana logs` terminal for the _'Program log'_ output. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should run `npm run call:hello-world` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'npm run call:hello-world'); ``` You should be in the `/learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match( cwd, /learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract\/?$/ ); ``` ## 31 ### --description-- Having a program that prints `Hello World` to the console is fun, but it is not very useful. Make it do something more interesting. Within the `process_instruction` function, create an iterator over the `accounts`, and store the iterator in a variable named `accounts_iter`. _Note: Make `accounts_iter` mutable_ ### --tests-- You should have `let mut accounts_iter = accounts.iter();` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /let\s+mut\s+accounts_iter\s*=\s*accounts\.iter\(\s*\)\s*;/s ); ``` ## 32 ### --description-- Safely access the next element of the `accounts_iter` collection with: ```rust if let Some(account) = accounts_iter.next() { } ``` Also, add an `else` clause to the `if let`, and use `msg` to log an appropriate message to the console. ### --tests-- You should have `if let Some(account) = accounts_iter.next() {}` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /if\s+let\s+Some\s*\(\s*account\s*\)\s*=\s*accounts_iter\.next\s*\(\s*\)\s*\{\s*\}/s ); ``` You should add an `else` clause with a call to the `msg` macro. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /\}\s*else\s*\{\s*msg!\s*\(/s); ``` ## 33 ### --description-- Import the `ProgramError` enum from the `program_error` module of `solana_program`. ### --tests-- You should have `use solana_program::program_error::ProgramError;` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); const variants = [ /solana_program\s*::\s*program_error::ProgramError/, /solana_program\s*::\s*\{[\s\S]*program_error::ProgramError/, /solana_program::prelude::\*/ ]; const someMatch = variants.some(r => file.match(r)); assert.isTrue(someMatch, `Your code should match one of ${variants}`); ``` ## 34 ### --description-- Be sure to call `Ok(())` when your function succeeds. Otherwise use the `NotEnoughAccountKeys` variant of `ProgramError` as the return for the `Err` variant of your function. ### --tests-- You should return `Ok(())` in the `if let` block of `process_instruction`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /(?<=\.next\s*\(\s*\)\s*\{).*?Ok\(\s*\(\s*\)\s*\);?\s*\}/s); ``` You should return `Err(ProgramError::NotEnoughAccountKeys)` in the `else` block of `process_instruction`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /(?<=else\s*\{).*?Err\s*\(\s*ProgramError::NotEnoughAccountKeys\s*\)/s ); ``` ## 35 ### --description-- Each `AccountInfo` element has an `owner` field which is the public key of the program that owns the account. Add an `if` statement checking for the case where this field's value does **not** match the `program_id` value. ### --tests-- You should have `if account.owner != program_id {}` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /if\s+(account\.owner\s*!=\s*program_id)|(program_id\s*!=\s*account\.owner)\s*\{/s ); ``` ## 36 ### --description-- Within the `if` statement, use `msg` to log `Account info does not match program id` to the console. ### --tests-- You should have `msg!("Account info does not match program id");` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /msg!\s*\(\s*"Account info does not match program id"\s*\)\s*;/s ); ``` ## 37 ### --description-- Within the `if` statement, return the `IncorrectProgramId` variant of `ProgramError` as the `Err`. ### --tests-- You should return `Err(ProgramError::IncorrectProgramId)` in the `if` statement. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /(?<=if.*?\{).*?Err\s*\(\s*ProgramError::IncorrectProgramId\s*\);?\s*\}/s ); ``` ## 38 ### --description-- In order to interact with the data associated with this smart contract account, you need to define a struct resembling the account data. Define a struct named `GreetingAccount` with a public field named `counter` with a value of `u32`. ### --tests-- You should have `pub struct GreetingAccount { pub counter: u32 }` in the root of `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /pub\s+struct\s+GreetingAccount\s*\{\s*pub\s+counter\s*:\s*u32\s*\}/s ); ``` ## 39 ### --description-- Derive `BorshSerialize` and `BorshDeserialize` for your `GreetingAccount` struct to be able to serialize and deserilize the data in the account. ### --tests-- You should add `#[derive(BorshSerialize, BorshDeserialize)]` above `GreetingAccount`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file.replace(/\s+/g, ''), /#\[derive\((BorshSerialize,BorshDeserialize)|(BorshDeserialize,BorshSerialize)\)\]pubstructGreetingAccount/s ); ``` ## 40 ### --description-- Bring `BorshSerialize` and `BorshDeserialize` in scope from the `borsh` crate. ### --tests-- You should have `use borsh::BorshSerialize;` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); const variants = [ /borsh\s*::\s*BorshSerialize/, /borsh\s*::\s*\{[\s\S]*BorshSerialize/, /borsh::prelude::\*/ ]; const someMatch = variants.some(r => file.match(r)); assert.isTrue(someMatch, `Your code should match one of ${variants}`); ``` You should have `use borsh::BorshDeserialize;` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); const variants = [ /borsh\s*::\s*BorshDeserialize/, /borsh\s*::\s*\{[\s\S]*BorshDeserialize/, /borsh::prelude::\*/ ]; const someMatch = variants.some(r => file.match(r)); assert.isTrue(someMatch, `Your code should match one of ${variants}`); ``` ## 41 ### --description-- Deserialize the `data` in the `account` variable with: ```rust GreetingAccount::try_from_slice(&account.data.borrow())?; ``` Assign this value to a mutable variable named `greeting_account`. ### --tests-- You should declare a mutable variable named `greeting_account`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /let\s+mut\s+greeting_account\s*=/s); ``` You should assign `GreetingAccount::try_from_slice(&account.data.borrow())?;` to `greeting_account`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /greeting_account\s*=\s*GreetingAccount::try_from_slice\s*\(\s*&account.data.borrow\s*\(\s*\)\s*\)\s*\?\s*;/s ); ``` ## 42 ### --description-- Increment the `counter` field of `greeting_account` by one. ### --tests-- You should have `greeting_account.counter += 1;` in `src/program-rust/src/lib.rs`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /greeting_account\.counter\s*\+=\s*1\s*;/s); ``` ## 43 ### --description-- Declare a new variable named `acc_data`, and assign it the value of `&mut account.data.borrow_mut()[..]`. ### --tests-- You should declare a new variable named `acc_data`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /let\s+acc_data\s*=/s); ``` You should assign `&mut account.data.borrow_mut()[..]` to `acc_data`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /acc_data\s*=\s*&mut\s+account\.data\.borrow_mut\s*\(\s*\)\s*\[\s*\.\.\s*\]\s*;/s ); ``` ## 44 ### --description-- Serialize the account data into your program with: ```rust greeting_account.serialize(&mut acc_data.as_mut())?; ``` ### --tests-- You should serialize the mutated data with `greeting_account.serialize(&mut acc_data.as_mut())?`. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match( file, /greeting_account\.serialize\s*\(\s*&mut\s+acc_data\.as_mut\s*\(\s*\)\s*\)\s*\?\s*;/s ); ``` ## 45 ### --description-- Log the number of times the account has been greeted, using the `msg` macro. ### --tests-- You should use `msg` to log the number of times the account has been greeted. ```js const filePath = 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs'; const file = await __helpers.getFile(filePath); assert.match(file, /msg!\(/); ``` ## 46 ### --description-- Now that your program is complete, rebuild it. ### --tests-- You should run `cargo build-sbf --sbf-out-dir=../../dist/program` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'cargo build-sbf --sbf-out-dir=../../dist/program'); ``` You should be in the `src/program-rust` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match(cwd, /src\/program-rust\/?$/); ``` ## 47 ### --description-- Re-deploy your program to your local Solana cluster. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should run `solana program deploy dist/program/helloworld.so` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'solana program deploy dist/program/helloworld.so'); ``` You should be in the `learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match( cwd, /learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract\/?$/ ); ``` ## 48 ### --description-- If you read closely, you should see your program failed to deploy. When you initially deployed, Solana allocated twice the amount of data it needed to store your account. Use `solana program show ` to view the data allocated for your program account. ### --tests-- You should run `solana program show ` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'solana program show'); ``` ## 49 ### --description-- The `Data Length` field returned shows the size (in bytes) allocated for your program account. Check your current program account size by running: ```bash du -b dist/program/helloworld.so ``` ### --tests-- You should run `du -b dist/program/helloworld.so` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'du -b dist/program/helloworld.so'); ``` You should be in the `learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match( cwd, /learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract\/?$/ ); ``` ## 50 ### --description-- Deploy a new program, by deleting the `dist/` directory, building again, then deploying. ### --tests-- You should rebuild your program with `cargo build-sbf`. ```js const isFileExists = __helpers.fileExists( 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/dist/program/helloworld.so' ); assert.isTrue( isFileExists, 'The `dist/program/helloworld.so` file should exist' ); ``` You should deploy your new program with `solana program deploy `. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'solana program deploy'); ``` ## 51 ### --description-- Send a message to your program to increment the counter by running: ```bash npm run call:hello-world ``` ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should run `npm run call:hello-world` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'npm run call:hello-world'); ``` You should be in the `learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match( cwd, /learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract\/?$/ ); ``` ## 52 ### --description-- Solana does not store smart contract state using the program account. Instead, a new account is used solely for the program state (data). View the account address for this _data account_ by running: ```bash npm run call:hello-world ``` See which address the program says hello to. ### --tests-- The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should run `npm run call:hello-world` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'npm run call:hello-world'); ``` You should be in the `learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match( cwd, /learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract\/?$/ ); ``` ## 53 ### --description-- View the updated account state associated with your program account: ```bash solana account ``` ### --tests-- You should run `solana account ` in the terminal. ```js const { stdout } = await __helpers.getCommandOutput( 'npm run call:hello-world', 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract' ); const programStateAccount = stdout.match(/Saying hello to: (\w+)/)?.[1]; const toMatch = `solana account ${programStateAccount}`; const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, toMatch); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` ## 54 ### --description-- Notice the final line of output with the name `0000`. This is the data you are storing! Take note of its value. Then, run `npm run call:hello-world` again. ### --tests-- You should run `npm run call:hello-world` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'npm run call:hello-world'); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` You should be in the `learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/` directory. ```js const wds = await __helpers.getCWD(); const cwd = wds.split('\n').filter(Boolean).pop(); assert.match( cwd, /learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract\/?$/ ); ``` ## 55 ### --description-- Now, get the data account state again, and look at the change in the data value. ### --tests-- You should run `solana account ` in the terminal. ```js const { stdout } = await __helpers.getCommandOutput( 'npm run call:hello-world', 'learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract' ); const programStateAccount = stdout.match(/Saying hello to: (\w+)/)?.[1]; const toMatch = `solana account ${programStateAccount}`; const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, toMatch); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e); } ``` ## 56 ### --description-- Contratulations on finishing this project! Feel free to play with your code. 🎆 Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-solanas-token-program-by-minting-a-fungible-token.md ================================================ # Solana - Learn Solana's Token Program by Minting a Fungible Token ## 1 ### --description-- In this project, you will learn how to create a fungible token on the Solana blockchain. You will be able to create your own token, and mint an unlimited supply of that token. You will be able to send your tokens to other Solana wallets. For the duration of this project, you will be working in the `learn-solanas-token-program-by-minting-a-fungible-token/` directory. Change into the above directory in a new bash terminal. ### --tests-- You can use `cd` to change into the `learn-solanas-token-program-by-minting-a-fungible-token/` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include(cwd, 'learn-solanas-token-program-by-minting-a-fungible-token'); ``` ## 2 ### --description-- Previously, you interacted with the _System Program_ to create accounts. Now, you will interact with the _Token Program_ to work with fungible tokens. Creating a fungible token on Solana requires the following steps: 1. Creating a _Mint_ account 2. Creating a _Token_ account 3. Minting tokens Create a file named `create-mint-account.js`. ### --tests-- You should have a `create-mint-account.js` file. ```js const fileExists = __helpers.fileExists( 'learn-solanas-token-program-by-minting-a-fungible-token/create-mint-account.js' ); assert.isTrue(fileExists); ``` ## 3 ### --description-- Within `create-mint-account.js`, declare a variable `connection`, and assign it a new instance of the `Connection` class. Pass in your local Solana RPC URL as the first argument. ### --tests-- You should have `const connection = new Connection('http://localhost:8899');` in `create-mint-account.js`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'connection'); assert.exists( connectionVariableDeclaration, 'You should declare a variable named `connection`' ); const newExpression = connectionVariableDeclaration.declarations[0].init; assert.equal( newExpression.callee.name, 'Connection', 'You should initialise `connection` with a new `Connection`' ); assert.equal( newExpression.arguments[0].value, 'http://localhost:8899', "You should create a new connection with `new Connection('http://localhost:8899')`" ); ``` You should import `Connection` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'You should import from `@solana/web3.js`' ); const connectionImportSpecifier = solanaWeb3ImportDeclaration.specifiers.find( s => { return s.imported.name === 'Connection'; } ); assert.exists( connectionImportSpecifier, 'You should import `Connection` from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-mint-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash touch create-mint-account.js ``` ## 4 ### --description-- You will need the `@solana/spl-token` package, as well as the `@solana/web3.js` package to interact with the _Token Program_. Install both packages using `npm`. ### --tests-- You should have at least version `1.70.0` of `@solana/web3.js` added to the `package.json` dependencies. ```js const packageJson = JSON.parse( await __helpers.getFile( './learn-solanas-token-program-by-minting-a-fungible-token/package.json' ) ); const web3Version = packageJson.dependencies?.['@solana/web3.js']; assert.exists( web3Version, 'You should have `@solana/web3.js` in your dependencies' ); // Manually check SemVer is at least 1.70.0 const web3VersionParts = web3Version.split('.').map(p => p.replace(/\D/g, '')); assert.isAtLeast( parseInt(web3VersionParts[0]), 1, '`@solana/web3.js` should have a major version of at least 1' ); assert.isAtLeast( parseInt(web3VersionParts[1]), 70, '`@solana/web3.js` should have a minor version of at least 70' ); assert.isAtLeast( parseInt(web3VersionParts[2]), 0, '`@solana/web3.js` should have a patch version of at least 0' ); ``` You should have at least version `0.3.6` of `@solana/spl-token` added to the `package.json` dependencies. ```js const packageJson = JSON.parse( await __helpers.getFile( './learn-solanas-token-program-by-minting-a-fungible-token/package.json' ) ); const splTokenVersion = packageJson.dependencies?.['@solana/spl-token']; assert.exists( splTokenVersion, 'You should have `@solana/spl-token` in your dependencies' ); // Manually check SemVer is at least 0.3.6 const splTokenVersionParts = splTokenVersion .split('.') .map(p => p.replace(/\D/g, '')); assert.isAtLeast( parseInt(splTokenVersionParts[0]), 0, '`@solana/spl-token` should have a major version of at least 0' ); assert.isAtLeast( parseInt(splTokenVersionParts[1]), 3, '`@solana/spl-token` should have a minor version of at least 3' ); assert.isAtLeast( parseInt(splTokenVersionParts[2]), 6, '`@solana/spl-token` should have a patch version of at least 6' ); ``` ### --seed-- #### --"create-mint-account.js"-- ```js import { Connection } from '@solana/web3.js'; const connection = new Connection('http://localhost:8899'); ``` ## 5 ### --description-- Creating a Mint Account requires another account to pay the fees. Import the `payer` variable from `utils.js`. ### --tests-- You should have `import { payer } from './utils.js';` in `create-mint-account.js`. ```js const utilsImportDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(utilsImportDeclaration, 'You should import from `./utils.js`'); const payerImportSpecifier = utilsImportDeclaration.specifiers.find(s => { return s.imported.name === 'payer'; }); assert.exists( payerImportSpecifier, 'You should import `payer` from `./utils.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-mint-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash npm install @solana/web3.js@1 @solana/spl-token ``` ## 6 ### --description-- The `payer` variable is a `Keypair` constructed from a `wallet.json` file. Use `solana-keygen` to create a new keypair, and save it to a file named `wallet.json`. You will be prompted to enter a passphrase. You can leave this blank. ### --tests-- You should have a `wallet.json` file in the `learn-solanas-token-program-by-minting-a-fungible-token/` directory. ```js const walletJsonExists = __helpers.fileExists( 'learn-solanas-token-program-by-minting-a-fungible-token/wallet.json' ); assert.isTrue(walletJsonExists, 'The `wallet.json` file should exist'); const walletJson = JSON.parse( await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/wallet.json' ) ); assert.isArray( walletJson, 'The `wallet.json` file should be an array of numbers.\nRun `solana-keygen new --outfile wallet.json` to create a new keypair.' ); ``` ### --seed-- #### --"create-mint-account.js"-- ```js import { Connection } from '@solana/web3.js'; import { payer } from './utils.js'; const connection = new Connection('http://localhost:8899'); ``` ## 7 ### --description-- A Mint Account also requires a _mint authority_ which is an account that controls the minting of the token. You will set the payer to be the mint authority. Within `create-mint-account.js`, declare a variable `mintAuthority`, and set it equal to the `payer` public key. ### --tests-- You should have `const mintAuthority = payer.publicKey;` in `create-mint-account.js`. ```js const mintAuthorityDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'mintAuthority'; }); assert.exists( mintAuthorityDeclaration, 'A variable named `mintAuthority` should exist' ); const mintAuthorityMemberExpression = mintAuthorityDeclaration.declarations?.[0]?.init; assert.exists( mintAuthorityMemberExpression, 'The `mintAuthority` variable should have an initialiser' ); assert.equal( mintAuthorityMemberExpression.object?.name, 'payer', 'The `mintAuthority` variable should be initialised with `payer.publicKey`' ); assert.equal( mintAuthorityMemberExpression.property?.name, 'publicKey', 'The `mintAuthority` variable should be initialised with `payer.publicKey`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-mint-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash solana-keygen new --no-bip39-passphrase --silent --outfile wallet.json ``` ## 8 ### --description-- A Mint Account requires a _freeze authority_ which is an account that controls the freezing of token accounts. You will set the payer to be the freeze authority. Within `create-mint-account.js`, declare a variable `freezeAuthority`, and set it equal to the `payer` public key. ### --tests-- You should have `const freezeAuthority = payer.publicKey;` in `create-mint-account.js`. ```js const freezeAuthorityDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.name === 'freezeAuthority'; }); assert.exists( freezeAuthorityDeclaration, 'A variable named `freezeAuthority` should exist' ); const freezeAuthorityMemberExpression = freezeAuthorityDeclaration.declarations?.[0]?.init; assert.exists( freezeAuthorityMemberExpression, 'The `freezeAuthority` variable should have an initialiser' ); assert.equal( freezeAuthorityMemberExpression.object?.name, 'payer', 'The `freezeAuthority` variable should be initialised with `payer.publicKey`' ); assert.equal( freezeAuthorityMemberExpression.property?.name, 'publicKey', 'The `freezeAuthority` variable should be initialised with `payer.publicKey`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-mint-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"create-mint-account.js"-- ```js import { Connection } from '@solana/web3.js'; import { payer } from './utils.js'; const connection = new Connection('http://localhost:8899'); const mintAuthority = payer.publicKey; ``` ## 9 ### --description-- Import the `createMint` function from `@solana/spl-token`, to create and initialize a new Mint Account: ```typescript createMint( connection: Connection, payer: Signer, mintAuthority: PublicKey, freezeAuthority: PublicKey | null, decimals: number ): Promise ``` Call the `createMint` function, passing in logical arguments. Store the awaited result in a variable named `mint`. The _decimals_ value is the number of decimal places the token will have. Set the decimals to `9` - the same as the native SOL token:
| Decimals | Smallest Token Unit | | :------: | :-----------------: | | 0 | 1 | | 1 | 0.1 | | ... | ... | | 9 | 0.000000001 |
### --tests-- You should import `createMint` from `@solana/spl-token`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( importDeclaration, 'An import from `@solana/spl-token` should exist' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'createMint', '`createMint` should be imported from `@solana/spl-token`' ); ``` You should have `const mint = await createMint(connection, payer, mintAuthority, freezeAuthority, 9);` in `create-mint-account.js`. ```js const mintDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'mint'; }); assert.exists(mintDeclaration, 'A variable named `mint` should exist'); const mintAwaitExpression = mintDeclaration.declarations?.[0]?.init; assert.equal( mintAwaitExpression.type, 'AwaitExpression', 'The `createMint` call should be awaited' ); const createMintCallExpression = mintAwaitExpression.argument; assert.equal( createMintCallExpression.callee.name, 'createMint', 'The `mint` variable should be initialised with the `createMint` function' ); const createMintArguments = createMintCallExpression.arguments; assert.equal( createMintArguments.length, 5, 'The `createMint` function should be called with 5 arguments' ); const [ connectionArgument, payerArgument, mintAuthorityArgument, freezeAuthorityArgument, decimalsArgument ] = createMintArguments; assert.equal( connectionArgument.name, 'connection', 'The first argument to `createMint` should be `connection`' ); assert.equal( payerArgument.name, 'payer', 'The second argument to `createMint` should be `payer`' ); assert.equal( mintAuthorityArgument.name, 'mintAuthority', 'The third argument to `createMint` should be `mintAuthority`' ); assert.equal( freezeAuthorityArgument.name, 'freezeAuthority', 'The fourth argument to `createMint` should be `freezeAuthority`' ); assert.equal( decimalsArgument.value, 9, 'The fifth argument to `createMint` should be `9`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-mint-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"create-mint-account.js"-- ```js import { Connection } from '@solana/web3.js'; import { payer } from './utils.js'; const connection = new Connection('http://localhost:8899'); const mintAuthority = payer.publicKey; const freezeAuthority = payer.publicKey; ``` ## 10 ### --description-- The `createMint` function returns the public key of the newly-created Mint Account. Log the base-58 representation of `mint` to the console. ### --tests-- You should use the `toBase58` method on `mint`. ```js const mintMemberExpression = babelisedCode .getType('MemberExpression') .find(m => { return m.object?.name === 'mint' && m.property?.name === 'toBase58'; }); assert.exists( mintMemberExpression, 'The `mint` variable should have a `toBase58` method called on it' ); ``` You can have `console.log(mint.toBase58());` in `create-mint-account.js`. ```js const consoleLogCallExpression = babelisedCode .getType('CallExpression') .find(c => { return ( c.callee?.object?.name === 'console' && c.callee?.property?.name === 'log' ); }); assert.exists(consoleLogCallExpression, 'A `console.log` call should exist'); const consoleLogArguments = consoleLogCallExpression.arguments; // Assert one of the arguments is the `mint.toBase58()` call const mintToBase58CallExpression = consoleLogArguments.find(a => { return ( a.type === 'CallExpression' && a.callee.object.name === 'mint' && a.callee.property.name === 'toBase58' ); }); assert.exists( mintToBase58CallExpression, 'One of the arguments to `console.log` should be `mint.toBase58()`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-mint-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"create-mint-account.js"-- ```js import { Connection } from '@solana/web3.js'; import { payer } from './utils.js'; import { createMint } from '@solana/spl-token'; const connection = new Connection('http://localhost:8899'); const mintAuthority = payer.publicKey; const freezeAuthority = payer.publicKey; const mint = await createMint( connection, payer, mintAuthority, freezeAuthority, 9 ); ``` ## 11 ### --description-- Start a local Solana cluster. Ensure the RPC URL is set to `http://localhost:8899`. ### --tests-- Your Solana config RPC URL should be set to `http://localhost:8899`. ```js const command = `solana config get json_rpc_url`; await new Promise(res => setTimeout(() => res(), 2000)); const { stdout, stderr } = await __helpers.getCommandOutput(command); assert.include( stdout, 'http://localhost:8899', 'Try running `solana config set --url localhost`' ); ``` You should run `solana-test-validator` in a separate terminal. ```js await new Promise(res => setTimeout(() => res(), 2000)); const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --"create-mint-account.js"-- ```js import { Connection } from '@solana/web3.js'; import { payer } from './utils.js'; import { createMint } from '@solana/spl-token'; const connection = new Connection('http://localhost:8899'); const mintAuthority = payer.publicKey; const freezeAuthority = payer.publicKey; const mint = await createMint( connection, payer, mintAuthority, freezeAuthority, 9 ); console.log('Token Unique Identifier:', mint.toBase58()); ``` ## 12 ### --description-- Airdrop some SOL to the payer account to pay for the transaction fees: ```bash solana airdrop ./wallet.json ``` ### --tests-- The `wallet.json` account should have at least 2 SOL. ```js const { stdout } = await __helpers.getCommandOutput( `solana balance ./learn-solanas-token-program-by-minting-a-fungible-token/wallet.json` ); const balance = stdout.trim()?.match(/\d+/)?.[0]; assert.isAtLeast( parseInt(balance), 2, 'The `wallet.json` account should have at least 2 SOL' ); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 13 ### --description-- Run the `create-mint-account.js` script: ```bash node create-mint-account.js ```
NOTE: Error If this command fails with the below error, wait a few seconds and run it again - transactions (e.g. Airdrops) take a few seconds to be finalised. ```bash /workspace/solana-curriculum/learn-solanas-token-program-by-minting-a-fungible-token/node_modules/@solana/web3.js/lib/index.cjs.js:9754 throw new SendTransactionError('failed to send transaction: ' + res.error.message, logs); ^ SendTransactionError: failed to send transaction: Transaction simulation failed: Attempt to debit an account but found no record of a prior credit. ```
### --tests-- You should run `node create-mint-account.js` in a terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include( lastCommand, 'node create-mint-account.js', 'You should run `node create-mint-account.js` in a terminal' ); ``` The output of `node create-mint-account.js` should include the base-58 representation of the Mint Account. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.match(terminalOutput, /[A-z0-9]{44}/); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 14 ### --description-- The output should include the base-58 representation of the Mint Account. In other words, the mint account's public key address. Copy this address, and paste it into the `MINT_ADDRESS_58` variable in `utils.js`. Then, uncomment the `mintAddress` export statement. ### --tests-- You should have `const MINT_ADDRESS_58 = '...';` in `utils.js`. ```js // TODO: Note that seed cannot add this... const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'MINT_ADDRESS_58'); assert.exists( variableDeclaration, 'The `MINT_ADDRESS_58` variable is should exist' ); const value = variableDeclaration.declarations?.[0]?.init?.value; assert.isString(value, 'The `MINT_ADDRESS_58` value should be a string'); assert.isNotEmpty(value, 'The `MINT_ADDRESS_58` value should not be empty'); ``` You should uncomment `export const mintAddress = new PublicKey(MINT_ADDRESS_58);` in `utils.js`. ```js const exportStatement = babelisedCode .getType('ExportNamedDeclaration') .find(e => e.declaration?.declarations?.[0]?.id?.name === 'mintAddress'); assert.exists( exportStatement, 'The `mintAddress` export statement should be UNCOMMENTED' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/utils.js' ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['importAssertions'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 15 ### --description-- Now that you have created a _Mint Account_, you need to create a _Token Account_. A _Token Account_ is owned by another account, and holds tokens of a specific mint. Create a new file named `create-token-account.js`. ### --tests-- You can use `touch` to create a file named `create-token-account.js`. ```js const fileExists = await __helpers.fileExists( 'learn-solanas-token-program-by-minting-a-fungible-token/create-token-account.js' ); assert.isTrue(fileExists); ``` ## 16 ### --description-- Within `create-token-account.js`, declare a variable `connection`, and assign it a new instance of the `Connection` class. Pass in your local Solana RPC URL as the first argument. ### --tests-- You should have `const connection = new Connection('http://localhost:8899');` in `create-token-account.js`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'connection'); assert.exists( connectionVariableDeclaration, 'A `connection` variable should be declared' ); const newExpression = connectionVariableDeclaration.declarations[0].init; assert.equal( newExpression.callee?.name, 'Connection', '`connection` should be initialised with a new `Connection`' ); assert.equal( newExpression.arguments?.[0]?.value, 'http://localhost:8899', "A new connection should be created with `new Connection('http://localhost:8899')`" ); ``` You should import `Connection` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'An import from `@solana/web3.js` should exist' ); const connectionImportSpecifier = solanaWeb3ImportDeclaration.specifiers.find( s => { return s.imported.name === 'Connection'; } ); assert.exists( connectionImportSpecifier, '`Connection` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-token-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash touch create-token-account.js ``` ## 17 ### --description-- Within `create-token-account.js`, import `payer` and `mintAddress` from `utils.js`. ### --tests-- You should import `payer` from `utils.js`. ```js const utilsImportDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists( utilsImportDeclaration, 'An import from `./utils.js` should exist' ); const payerImportSpecifiers = utilsImportDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( payerImportSpecifiers, 'payer', '`payer` should be imported from `./utils.js`' ); ``` You should import `mintAddress` from `utils.js`. ```js const utilsImportDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists( utilsImportDeclaration, 'An import from `./utils.js` should exist' ); const mintAddressImportSpecifiers = utilsImportDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( mintAddressImportSpecifiers, 'mintAddress', '`mintAddress` should be imported from `./utils.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-token-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"create-token-account.js"-- ```js import { Connection } from '@solana/web3.js'; const connection = new Connection('http://localhost:8899'); ``` ## 18 ### --description-- Import the `getOrCreateAssociatedTokenAccount` function from `@solana/spl-token`, to retrieve an associated token account, or create it if it does not exist: ```typescript getOrCreateAssociatedTokenAccount( connection: Connection, payer: Signer, // Payer for this transaction mint: PublicKey, // Address of the associated token mint owner: PublicKey // Address of the account to own the token account ): Promise ``` Call the `getOrCreateAssociatedTokenAccount` function, passing in logical arguments. Store the awaited result in a variable named `tokenAccount`. Use the `payer` account as the owner of the token account. ### --tests-- You should have `const tokenAccount = await getOrCreateAssociatedTokenAccount(...);` in `create-token-account.js`. ```js const tokenAccountVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'tokenAccount'); assert.exists( tokenAccountVariableDeclaration, 'A `tokenAccount` variable should be declared' ); const awaitExpression = tokenAccountVariableDeclaration.declarations[0].init; assert.exists( awaitExpression, 'The `tokenAccount` variable should be initialised with an `await` expression' ); const callExpression = awaitExpression.argument; assert.exists( callExpression, 'The `tokenAccount` variable should be initialised with a call expression' ); const callee = callExpression.callee; assert.equal( callee.name, 'getOrCreateAssociatedTokenAccount', 'The `tokenAccount` variable should be initialised with a call expression to `getOrCreateAssociatedTokenAccount`' ); ``` You should import `getOrCreateAssociatedTokenAccount` from `@solana/spl-token`. ```js const splTokenImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( splTokenImportDeclaration, 'An import from `@solana/spl-token` should exist' ); const getOrCreateAssociatedTokenAccountImportSpecifiers = splTokenImportDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( getOrCreateAssociatedTokenAccountImportSpecifiers, 'getOrCreateAssociatedTokenAccount', '`getOrCreateAssociatedTokenAccount` should be imported from `@solana/spl-token`' ); ``` You should pass in order: `connection`, `payer`, `mintAddress`, and `payer.publicKey` as arguments to `getOrCreateAssociatedTokenAccount`. ```js const getOrCreateAssociatedTokenAccountCallExpression = babelisedCode .getType('CallExpression') .find(c => c.callee.name === 'getOrCreateAssociatedTokenAccount'); assert.exists( getOrCreateAssociatedTokenAccountCallExpression, 'A call expression to `getOrCreateAssociatedTokenAccount` should exist' ); const [arg1, arg2, arg3, arg4] = getOrCreateAssociatedTokenAccountCallExpression.arguments; assert.equal( arg1.name, 'connection', 'The first argument to `getOrCreateAssociatedTokenAccount` should be `connection`' ); assert.equal( arg2.name, 'payer', 'The second argument to `getOrCreateAssociatedTokenAccount` should be `payer`' ); assert.equal( arg3.name, 'mintAddress', 'The third argument to `getOrCreateAssociatedTokenAccount` should be `mintAddress`' ); assert.nestedPropertyVal(arg4, 'object.name', 'payer'); assert.nestedPropertyVal(arg4, 'property.name', 'publicKey'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-token-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"create-token-account.js"-- ```js import { Connection } from '@solana/web3.js'; import { payer, mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); ``` ## 19 ### --description-- Within `create-token-account.js`, log the base-58 representation of the `tokenAccount` address to the console. ### --tests-- You should have `console.log(tokenAccount.address.toBase58());` in `create-token-account.js`. ```js const consoleLogCallExpression = babelisedCode .getType('CallExpression') .find(c => { return ( c.callee.object?.name === 'console' && c.callee.property?.name === 'log' ); }); assert.exists(consoleLogCallExpression, 'A `console.log` call should exist'); const consoleLogArguments = consoleLogCallExpression.arguments; // Assert one of the arguments is the `mint.toBase58()` call const mintToBase58CallExpression = consoleLogArguments.find(a => { return ( a.type === 'CallExpression' && a.callee?.object?.object?.name === 'tokenAccount' && a.callee?.object?.property?.name === 'address' && a.callee?.property?.name === 'toBase58' ); }); assert.exists( mintToBase58CallExpression, 'One of the arguments to `console.log` should be `tokenAccount.publicKey.toBase58()`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/create-token-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"create-token-account.js"-- ```js import { Connection } from '@solana/web3.js'; import { payer, mintAddress } from './utils.js'; import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; const connection = new Connection('http://localhost:8899'); const tokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, payer.publicKey ); ``` ## 20 ### --description-- Run the `create-token-account.js` script: ```bash node create-token-account.js ``` ### --tests-- You should run `node create-token-account.js` in a terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include( lastCommand, 'node create-token-account.js', 'Try running `node create-token-account.js` in a terminal' ); ``` The output of `node create-token-account.js` should include the base-58 representation of the Token Account. ```js const terminalOutput = await __helpers.getTerminalOutput(); assert.match(terminalOutput, /[A-z0-9]{44}/); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --"create-token-account.js"-- ```js import { Connection } from '@solana/web3.js'; import { payer, mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const tokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, payer.publicKey ); console.log('Token Account Address:', tokenAccount.publicKey.toBase58()); ``` ## 21 ### --description-- The output should include the base-58 representation of the Token Account. In other words, the token account's public key address. Copy this address, and paste it into the `TOKEN_ACCOUNT_58` variable in `utils.js`. Then, uncomment the `tokenAccount` variable. ### --tests-- You should have `const TOKEN_ACCOUNT_58 = '...';` in `utils.js`. ```js // TODO: Note that seed cannot add this... const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'TOKEN_ACCOUNT_58'); assert.exists( variableDeclaration, 'The `TOKEN_ACCOUNT_58` variable is should exist' ); const value = variableDeclaration.declarations?.[0]?.init?.value; assert.isString(value, 'The `TOKEN_ACCOUNT_58` value should be a string'); assert.isNotEmpty(value, 'The `TOKEN_ACCOUNT_58` value should not be empty'); ``` You should uncomment `export const tokenAccount = new PublicKey(TOKEN_ACCOUNT_58);` in `utils.js`. ```js const exportStatement = babelisedCode .getType('ExportNamedDeclaration') .find(e => e.declaration?.declarations?.[0]?.id?.name === 'tokenAccount'); assert.exists( exportStatement, 'The `tokenAccount` export statement should be UNCOMMENTED' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/utils.js' ); const babelisedCode = new __helpers.Babeliser(codeString, { plugins: ['importAssertions'] }); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 22 ### --description-- Run the `create-token-account.js` script again: ```bash node create-token-account.js ``` ### --tests-- You should run `node create-token-account.js` in a terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include( lastCommand, 'node create-token-account.js', 'Try running `node create-token-account.js` in a terminal' ); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 23 ### --description-- Seeing as there is already an associated token account for the `payer` account, the `getOrCreateAssociatedTokenAccount` function will return the existing token account. To see what a token account looks like, create a new file called `get-token-account.js`: ### --tests-- You should have a `get-token-account.js` file. ```js const fileExists = await __helpers.fileExists( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-account.js' ); assert.isTrue(fileExists); ``` ## 24 ### --description-- Within `get-token-account.js`, declare a variable `connection`, and assign it a new instance of the `Connection` class. Pass in your local Solana RPC URL as the first argument. ### --tests-- You should have `const connection = new Connection('http://localhost:8899');` in `get-token-account.js`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'connection'); assert.exists( connectionVariableDeclaration, 'A `connection` variable should be declared' ); const newExpression = connectionVariableDeclaration.declarations[0].init; assert.equal( newExpression.callee.name, 'Connection', '`connection` should be initialised with a new `Connection`' ); assert.equal( newExpression.arguments[0].value, 'http://localhost:8899', "A new connection should be created with `new Connection('http://localhost:8899')`" ); ``` You should import `Connection` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'An import from `@solana/web3.js` should exist' ); const connectionImportSpecifier = solanaWeb3ImportDeclaration.specifiers.find( s => { return s.imported.name === 'Connection'; } ); assert.exists( connectionImportSpecifier, '`Connection` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash touch get-token-account.js ``` ## 25 ### --description-- Within `get-token-account.js`, create a new `PublicKey` from the first command-line argument, and assign it to a variable called `userPublicKey`. ### --tests-- You should have `const userPublicKey = new PublicKey(process.argv[2]);` in `get-token-account.js`. ```js const userPublicKeyVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'userPublicKey'); assert.exists( userPublicKeyVariableDeclaration, 'A `userPublicKey` variable should be declared' ); const newExpression = userPublicKeyVariableDeclaration.declarations?.[0]?.init; assert.equal( newExpression?.callee?.name, 'PublicKey', '`userPublicKey` should be initialised with a new `PublicKey`' ); const processArgvMemberExpression = newExpression?.arguments?.[0]; assert.equal( processArgvMemberExpression?.object?.object?.name, 'process', '`PublicKey` should be initialised with `process...`' ); assert.equal( processArgvMemberExpression?.object?.property?.name, 'argv', '`PublicKey` should be initialised with `process.argv...`' ); assert.equal( processArgvMemberExpression?.property?.value, '2', '`PublicKey` should be initialised with `process.argv[2]`' ); ``` You should import `PublicKey` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'An import from `@solana/web3.js` should exist' ); const specifiers = solanaWeb3ImportDeclaration.specifiers.map( s => s.imported.name ); assert.include( specifiers, 'PublicKey', '`PublicKey` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"get-token-account.js"-- ```js import { Connection } from '@solana/web3.js'; const connection = new Connection('http://localhost:8899'); ``` ## 26 ### --description-- Import the `getAssociatedTokenAddress` function from `@solana/spl-token`, to get the address of the associated token account for a given mint and owner: ```typescript getAssociatedTokenAddress( mint: PublicKey, owner: PublicKey ): Promise; ``` Call the `getAssociatedTokenAddress` function, passing in the `mintAddress` (from `utils.js`) and `userPublicKey` variables as arguments. Assign the result to a variable called `tokenAddress`. ### --tests-- You should have `const tokenAddress = await getAssociatedTokenAddress(mintAddress, userPublicKey);` in `get-token-account.js`. ```js const tokenAddressVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'tokenAddress'); assert.exists( tokenAddressVariableDeclaration, 'A `tokenAddress` variable should be declared' ); const awaitExpression = tokenAddressVariableDeclaration.declarations?.[0]?.init; assert.equal( awaitExpression?.type, 'AwaitExpression', '`tokenAddress` should be initialised with an `await` expression' ); const getAssociatedTokenAddressCallExpression = awaitExpression?.argument; assert.equal( getAssociatedTokenAddressCallExpression?.callee?.name, 'getAssociatedTokenAddress', '`tokenAddress` should be initialised with `getAssociatedTokenAddress(...)`' ); const [mintAddressIdentifier, userPublicKeyIdentifier] = getAssociatedTokenAddressCallExpression?.arguments; assert.equal( mintAddressIdentifier?.name, 'mintAddress', '`getAssociatedTokenAddress` should be called with `mintAddress` as the first argument' ); assert.equal( userPublicKeyIdentifier?.name, 'userPublicKey', '`getAssociatedTokenAddress` should be called with `userPublicKey` as the second argument' ); ``` You should import `getAssociatedTokenAddress` from `@solana/spl-token`. ```js const splTokenImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( splTokenImportDeclaration, 'An import from `@solana/spl-token` should exist' ); const specifiers = splTokenImportDeclaration.specifiers.map( s => s.imported.name ); assert.include( specifiers, 'getAssociatedTokenAddress', '`getAssociatedTokenAddress` should be imported from `@solana/spl-token`' ); ``` You should import `mintAddress` from `./utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'An import from `./utils.js` should exist'); const specifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( specifiers, 'mintAddress', '`mintAddress` should be imported from `./utils.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"get-token-account.js"-- ```js import { Connection, PublicKey } from '@solana/web3.js'; const connection = new Connection('http://localhost:8899'); const tokenAccountPublicKey = new PublicKey(process.argv[2]); ``` ## 27 ### --description-- Import the `getAccount` function from `@solana/spl-token`, to get the account information for a given token account: ```typescript getAccount( connection: Connection, address: PublicKey ): Promise; ``` Call the `getAccount` function, passing in the `connection` and `tokenAddress` variables as arguments. Assign the result to a variable called `tokenAccount`. ### --tests-- You should have `const tokenAccount = await getAccount(connection, tokenAddress);` in `get-token-account.js`. ```js const tokenAccountVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'tokenAccount'); assert.exists( tokenAccountVariableDeclaration, 'A `tokenAccount` variable should be declared' ); const awaitExpression = tokenAccountVariableDeclaration.declarations?.[0]?.init; assert.equal( awaitExpression?.type, 'AwaitExpression', '`tokenAccount` should be initialised with an `await` expression' ); const getAccountCallExpression = awaitExpression?.argument; assert.equal( getAccountCallExpression?.callee?.name, 'getAccount', '`tokenAccount` should be initialised with `getAccount(...)`' ); const [connectionIdentifier, tokenAddressIdentifier] = getAccountCallExpression?.arguments; assert.equal( connectionIdentifier?.name, 'connection', '`getAccount` should be called with `connection` as the first argument' ); assert.equal( tokenAddressIdentifier?.name, 'tokenAddress', '`getAccount` should be called with `tokenAddress` as the second argument' ); ``` You should import `getAccount` from `@solana/spl-token`. ```js const splTokenImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( splTokenImportDeclaration, 'An import from `@solana/spl-token` should exist' ); const specifiers = splTokenImportDeclaration.specifiers.map( s => s.imported.name ); assert.include( specifiers, 'getAccount', '`getAccount` should be imported from `@solana/spl-token`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"get-token-account.js"-- ```js import { Connection, PublicKey } from '@solana/web3.js'; import { getAssociatedTokenAddress } from '@solana/spl-token'; import { mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const userPublicKey = new PublicKey(process.argv[2]); const tokenAddress = await getAssociatedTokenAddress( mintAddress, userPublicKey ); ``` ## 28 ### --description-- Print the `tokenAccount` variable to the console. ### --tests-- You should have `console.log(tokenAccount);` in `get-token-account.js`. ```js const consoleLogCallExpression = babelisedCode .getType('CallExpression') .find(c => { return ( c.callee.object?.name === 'console' && c.callee.property?.name === 'log' ); }); assert.exists(consoleLogCallExpression, 'A `console.log` call should exist'); const consoleLogArguments = consoleLogCallExpression.arguments; // Assert one of the arguments is `tokenAccount` const tokenAccountIdent = consoleLogArguments.find(a => { return a.name === 'tokenAccount'; }); assert.exists( tokenAccountIdent, 'One of the arguments to `console.log` should be `tokenAccount`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"get-token-account.js"-- ```js import { Connection, PublicKey } from '@solana/web3.js'; import { getAssociatedTokenAddress, getAccount } from '@solana/spl-token'; import { mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const userPublicKey = new PublicKey(process.argv[2]); const tokenAddress = await getAssociatedTokenAddress( mintAddress, userPublicKey ); const tokenAccount = await getAccount(connection, tokenAddress); ``` ## 29 ### --description-- Run the `get-token-account.js` script, passing in the public key of the `payer` account as the first argument: ```bash solana address --keypair ./wallet.json # to get the public key node get-token-account.js ``` ### --tests-- You should run `node get-token-account.js ` in the terminal. ```js const wallet = JSON.parse( await __helpers.getFile( './learn-solanas-token-program-by-minting-a-fungible-token/wallet.json' ) ); const secretKey = Uint8Array.from(wallet); const { Keypair } = await import('@solana/web3.js'); const payer = Keypair.fromSecretKey(secretKey); const command = `node get-token-account.js ${payer.publicKey.toString()}`; const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, command, `The last command should be '${command}'`); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --"get-token-account.js"-- ```js import { Connection, PublicKey } from '@solana/web3.js'; import { getAssociatedTokenAddress, getAccount } from '@solana/spl-token'; import { mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const userPublicKey = new PublicKey(process.argv[2]); const tokenAddress = await getAssociatedTokenAddress( mintAddress, userPublicKey ); const tokenAccount = await getAccount(connection, tokenAddress); console.log('Token Account:', tokenAccount); ``` ## 30 ### --description-- The `tokenAccount` variable should have an `amount` property, which is the number of tokens held by the account. Currently, the token account has no tokens, because none have been minted into the account. Create a file named `mint.js`. ### --tests-- You should have a file named `mint.js`. ```js const mintFileExists = await __helpers.fileExists( 'learn-solanas-token-program-by-minting-a-fungible-token/mint.js' ); assert.isTrue(mintFileExists, 'A file named `mint.js` should exist'); ``` ## 31 ### --description-- Within `mint.js`, declare a variable `connection`, and assign it a new instance of the `Connection` class. Pass in your local Solana RPC URL as the first argument. ### --tests-- You should have `const connection = new Connection('http://localhost:8899');` in `mint.js`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'connection'); assert.exists( connectionVariableDeclaration, 'A `connection` variable should be declared' ); const newExpression = connectionVariableDeclaration.declarations[0].init; assert.equal( newExpression.callee.name, 'Connection', '`connection` should be initialised with a new `Connection`' ); assert.equal( newExpression.arguments[0].value, 'http://localhost:8899', "A new connection should be created with `new Connection('http://localhost:8899')`" ); ``` You should import `Connection` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'An import from `@solana/web3.js` should exist' ); const connectionImportSpecifier = solanaWeb3ImportDeclaration.specifiers.find( s => { return s.imported.name === 'Connection'; } ); assert.exists( connectionImportSpecifier, '`Connection` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/mint.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash touch mint.js ``` ## 32 ### --description-- Import the `mintTo` function from `@solana/spl-token`, to mint tokens into a token account: ```typescript mintTo( connection: Connection, payer: Signer, mint: PublicKey, destination: PublicKey, authority: PublicKey | Signer, amount: number | bigint ): Promise ``` Call and await the `mintTo` function, passing in the `connection`, `payer`, `mintAddress`, `tokenAccount`, `mintAuthority`, and `1_000_000_000` variables as arguments. ### --tests-- You should have `await mintTo(connection, payer, mintAddress, tokenAccount, mintAuthority, 1_000_000_000);` in `mint.js`. ```js const expressionStatement = babelisedCode .getExpressionStatements() .find( e => e.expression.type === 'AwaitExpression' && e.expression.argument?.callee?.name === 'mintTo' ); assert.exists( expressionStatement, 'An `await mintTo(...)` expression should exist' ); const mintToArguments = expressionStatement.expression.argument.arguments; const [ connectionArgument, payerArgument, mintAddressArgument, tokenAccountArgument, mintAuthorityArgument, amountArgument ] = mintToArguments; assert.equal( connectionArgument?.name, 'connection', 'The first argument to `mintTo` should be `connection`' ); assert.equal( payerArgument?.name, 'payer', 'The second argument to `mintTo` should be `payer`' ); assert.equal( mintAddressArgument?.name, 'mintAddress', 'The third argument to `mintTo` should be `mintAddress`' ); assert.equal( tokenAccountArgument?.name, 'tokenAccount', 'The fourth argument to `mintTo` should be `tokenAccount`' ); assert.equal( mintAuthorityArgument?.name, 'mintAuthority', 'The fifth argument to `mintTo` should be `mintAuthority`' ); assert.equal( amountArgument?.value, 1_000_000_000, 'The sixth argument to `mintTo` should be `1_000_000_000`' ); ``` You should import `mintTo` from `@solana/spl-token`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( importDeclaration, 'An import from `@solana/spl-token` should exist' ); const importSpecifiers = importDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( importSpecifiers, 'mintTo', '`mintTo` should be imported from `@solana/spl-token`' ); ``` You should import `payer` from `./utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'An import from `./utils.js` should exist'); const specifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( specifiers, 'payer', '`payer` should be imported from `./utils.js`' ); ``` You should import `mintAddress` from `./utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'An import from `./utils.js` should exist'); const specifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( specifiers, 'mintAddress', '`mintAddress` should be imported from `./utils.js`' ); ``` You should import `tokenAccount` from `./utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'An import from `./utils.js` should exist'); const specifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( specifiers, 'tokenAccount', '`tokenAccount` should be imported from `./utils.js`' ); ``` You should import `mintAuthority` from `./utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'An import from `./utils.js` should exist'); const specifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( specifiers, 'mintAuthority', '`mintAuthority` should be imported from `./utils.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/mint.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"mint.js"-- ```js import { Connection } from '@solana/web3.js'; const connection = new Connection('http://localhost:8899'); ``` ## 33 ### --description-- Run the `mint.js` script: ```bash node mint.js ``` ### --tests-- You should run `node mint.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include( lastCommand, 'node mint.js', 'Try running `node mint.js` in the terminal' ); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --"mint.js"-- ```js import { Connection } from '@solana/web3.js'; import { mintTo } from '@solana/spl-token'; import { payer, mintAddress, tokenAccount, mintAuthority } from './utils.js'; const connection = new Connection('http://localhost:8899'); await mintTo( connection, payer, mintAddress, tokenAccount, mintAuthority, 1_000_000_000 ); ``` ## 34 ### --description-- Run the `get-token-account.js` script again, passing in the public key of the `payer` account as the first argument. ### --tests-- You should run `node get-token-account.js ` in the terminal. ```js const wallet = JSON.parse( await __helpers.getFile( './learn-solanas-token-program-by-minting-a-fungible-token/wallet.json' ) ); const secretKey = Uint8Array.from(wallet); const { Keypair } = await import('@solana/web3.js'); const payer = Keypair.fromSecretKey(secretKey); const command = `node get-token-account.js ${payer.publicKey.toString()}`; const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, command, `The last command should be '${command}'`); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 35 ### --description-- The token account should have an `amount` of `1000000000n`. Note that this means the account has a total of 1 token, because the token has 9 decimals - the same way an account with `1_000_000_000 lamports` means it has `1 SOL`. To see the total number of tokens minted, create a file named `get-token-info.js`. ### --tests-- You should have a file named `get-token-info.js`. ```js const fileExists = await __helpers.fileExists( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-info.js' ); assert.isTrue(fileExists, 'A file named `get-token-info.js` should exist'); ``` ## 36 ### --description-- Within `get-token-info.js`, declare a variable `connection`, and assign it a new instance of the `Connection` class. Pass in your local Solana RPC URL as the first argument. ### --tests-- You should have `const connection = new Connection('http://localhost:8899');` in `get-token-info.js`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'connection'); assert.exists( connectionVariableDeclaration, 'A `connection` variable should be declared' ); const newExpression = connectionVariableDeclaration.declarations[0].init; assert.equal( newExpression.callee.name, 'Connection', '`connection` should be initialised with a new `Connection`' ); assert.equal( newExpression.arguments[0].value, 'http://localhost:8899', "A new connection should be created with `new Connection('http://localhost:8899')`" ); ``` You should import `Connection` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'An import from `@solana/web3.js` should exist' ); const connectionImportSpecifier = solanaWeb3ImportDeclaration.specifiers.find( s => { return s.imported.name === 'Connection'; } ); assert.exists( connectionImportSpecifier, '`Connection` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-info.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash touch get-token-info.js ``` ## 37 ### --description-- Import the `getMint` function from `@solana/spl-token`, to get the token information: ```typescript getMint( connection: Connection, address: PublicKey ): Promise ``` Call the `getMint` function, passing in the `connection` and `mintAddress` variables as arguments, and assign the awaited result to a variable `mint`. ### --tests-- You should have `const mint = await getMint(connection, mintAddress);` in `get-token-info.js`. ```js const mintVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'mint'); assert.exists(mintVariableDeclaration, 'A `mint` variable should be declared'); const awaitExpression = mintVariableDeclaration.declarations[0].init; assert.equal( awaitExpression.type, 'AwaitExpression', '`mint` should be initialised with an await expression' ); const callExpression = awaitExpression.argument; assert.equal( callExpression.callee.name, 'getMint', '`mint` should be initialised with a call to `getMint`' ); assert.equal( callExpression.arguments[0].name, 'connection', '`getMint` should be called with `connection` as the first argument' ); assert.equal( callExpression.arguments[1].name, 'mintAddress', '`getMint` should be called with `mintAddress` as the second argument' ); ``` You should import `getMint` from `@solana/spl-token`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( importDeclaration, 'An import from `@solana/spl-token` should exist' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'getMint', '`getMint` should be imported from `@solana/spl-token`' ); ``` You should import `mintAddress` from `./utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'An import from `./utils.js` should exist'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'mintAddress', '`mintAddress` should be imported from `./utils.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-info.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"get-token-info.js"-- ```js import { Connection } from '@solana/web3.js'; const connection = new Connection('http://localhost:8899'); ``` ## 38 ### --description-- Log the `mint` variable to the console. ### --tests-- You should have `console.log(mint);` in `get-token-info.js`. ```js const consoleLogCallExpression = babelisedCode .getType('CallExpression') .find(c => { return ( c.callee.object?.name === 'console' && c.callee.property?.name === 'log' ); }); assert.exists(consoleLogCallExpression, 'A `console.log` call should exist'); const consoleLogArguments = consoleLogCallExpression.arguments; // Assert one of the arguments is `mint` const mintIdent = consoleLogArguments.find(a => { return a.name === 'mint'; }); assert.exists( mintIdent, 'One of the arguments to `console.log` should be `mint`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/get-token-info.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"get-token-info.js"-- ```js import { Connection } from '@solana/web3.js'; import { getMint } from '@solana/spl-token'; const connection = new Connection('http://localhost:8899'); const mint = await getMint(connection, mintAddress); ``` ## 39 ### --description-- Run the `get-token-info.js` script: ```bash node get-token-info.js ``` ### --tests-- You should run `node get-token-info.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.include( lastCommand, 'node get-token-info.js', 'Try running `node get-token-info.js` in the terminal' ); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --"get-token-info.js"-- ```js import { Connection } from '@solana/web3.js'; import { getMint } from '@solana/spl-token'; import { mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const mint = await getMint(connection, mintAddress); console.log(mint); ``` ## 40 ### --description-- The output should show a `supply` property of `1000000000n`, which means the total number of tokens minted is `1_000_000_000 / 9 = 1`. Mint at least 2 more tokens to the `payer` account. ### --tests-- You should run `node mint.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand, 'node mint.js', 'Try running `node mint.js` in the terminal' ); ``` The `payer` account should have a balance of at least `3_000_000_000`. ```js const wallet = JSON.parse( await __helpers.getFile( './learn-solanas-token-program-by-minting-a-fungible-token/wallet.json' ) ); const secretKey = Uint8Array.from(wallet); const { Keypair } = await import('@solana/web3.js'); const payer = Keypair.fromSecretKey(secretKey); const { Connection } = await import('@solana/web3.js'); const { TOKEN_PROGRAM_ID } = await import('@solana/spl-token'); const connection = new Connection('http://localhost:8899'); const tokenAccounts = await connection.getParsedTokenAccountsByOwner( payer.publicKey, { programId: TOKEN_PROGRAM_ID } ); const { tokenAmount } = tokenAccounts?.value?.[0]?.account?.data?.parsed?.info; assert.isAtLeast(tokenAmount?.uiAmount, 3); ``` The total supply of tokens should be at least `3_000_000_000`. ```js const { mintAddress } = await __helpers.importSansCache( './learn-solanas-token-program-by-minting-a-fungible-token/utils.js' ); const { getMint } = await import('@solana/spl-token'); const { Connection } = await import('@solana/web3.js'); const connection = new Connection('http://localhost:8899'); const mint = await getMint(connection, mintAddress); assert.isAtLeast(Number(mint.supply), 3_000_000_000); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 41 ### --description-- Now that your token has been minted and is in circulation, you can transfer tokens from one account to another. Create a file named `transfer.js`. ### --tests-- You should have a file named `transfer.js`. ```js const transferFileExists = __helpers.fileExists( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); assert.isTrue(transferFileExists, 'A `transfer.js` file should exist'); ``` ## 42 ### --description-- Within `transfer.js`, declare a variable `connection`, and assign it a new instance of the `Connection` class. Pass in your local Solana RPC URL as the first argument. ### --tests-- You should have `const connection = new Connection('http://localhost:8899');` in `transfer.js`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'connection'); assert.exists( connectionVariableDeclaration, 'A `connection` variable should be declared' ); const newExpression = connectionVariableDeclaration.declarations[0].init; assert.equal( newExpression.callee.name, 'Connection', '`connection` should be initialised with a new `Connection`' ); assert.equal( newExpression.arguments[0].value, 'http://localhost:8899', "A new connection should be created with `new Connection('http://localhost:8899')`" ); ``` You should import `Connection` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'An import from `@solana/web3.js` should exist' ); const connectionImportSpecifier = solanaWeb3ImportDeclaration.specifiers.find( s => { return s.imported.name === 'Connection'; } ); assert.exists( connectionImportSpecifier, '`Connection` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --cmd-- ```bash touch transfer.js ``` ## 43 ### --description-- From the first command-line argument, create a new `PublicKey` instance, and assign it to a variable `fromTokenAccountPublicKey`. ### --tests-- You should have `const fromTokenAccountPublicKey = new PublicKey(process.argv[2]);` in `transfer.js`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'fromTokenAccountPublicKey'); assert.exists( variableDeclaration, 'A `fromTokenAccountPublicKey` variable should be declared' ); const newExpression = variableDeclaration.declarations?.[0]?.init; assert.equal( newExpression?.callee?.name, 'PublicKey', '`fromTokenAccountPublicKey` should be initialised with a new `PublicKey`' ); const processArgvMemberExpression = newExpression?.arguments?.[0]; assert.equal( processArgvMemberExpression?.object?.object?.name, 'process', '`PublicKey` should be initialised with `process...`' ); assert.equal( processArgvMemberExpression?.object?.property?.name, 'argv', '`PublicKey` should be initialised with `process.argv...`' ); assert.equal( processArgvMemberExpression?.property?.value, '2', '`PublicKey` should be initialised with `process.argv[2]`' ); ``` You should import `PublicKey` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'An import from `@solana/web3.js` should exist' ); const importSpecifiers = solanaWeb3ImportDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( importSpecifiers, 'PublicKey', '`PublicKey` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"transfer.js"-- ```js import { Connection } from '@solana/web3.js'; const connection = new Connection('http://localhost:8899'); ``` ## 44 ### --description-- Generate a new keypair, and assign it to a variable `toWallet`. ### --tests-- You should have `const toWallet = Keypair.generate();` in `transfer.js`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'toWallet'); assert.exists(variableDeclaration, 'A `toWallet` variable should be declared'); const generateMemberExpression = variableDeclaration.declarations?.[0]?.init?.callee; assert.equal( generateMemberExpression?.object?.name, 'Keypair', '`toWallet` should be initialised with `Keypair.generate()`' ); assert.equal( generateMemberExpression?.property?.name, 'generate', '`toWallet` should be initialised with `Keypair.generate()`' ); ``` You should import `Keypair` from `@solana/web3.js`. ```js const solanaWeb3ImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists( solanaWeb3ImportDeclaration, 'An import from `@solana/web3.js` should exist' ); const importSpecifiers = solanaWeb3ImportDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( importSpecifiers, 'Keypair', '`Keypair` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"transfer.js"-- ```js import { Connection, PublicKey } from '@solana/web3.js'; const connection = new Connection('http://localhost:8899'); const fromTokenAccountPublicKey = new PublicKey(process.argv[2]); ``` ## 45 ### --description-- Import the `getOrCreateAssociatedTokenAccount` function from `@solana/spl-token`, to get (create) the token account of the `toWallet` address: ```typescript getOrCreateAssociatedTokenAccount( connection: Connection, payer: Signer, mint: PublicKey, owner: PublicKey ): Promise ``` Call the function, passing in order: `connection`, `payer`, `mintAddress`, and the public key of `toWallet` as arguments. Assign the awaited result to a variable `toTokenAccount`. ### --tests-- You should have `const toTokenAccount = await getOrCreateAssociatedTokenAccount(connection, payer, mintAddress, toWallet.publicKey);` in `transfer.js`. ```js const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'toTokenAccount'); assert.exists( variableDeclaration, 'A `toTokenAccount` variable should be declared' ); const awaitExpression = variableDeclaration.declarations?.[0]?.init; assert.equal( awaitExpression?.type, 'AwaitExpression', '`toTokenAccount` should be initialised with an `await` expression' ); const callExpression = awaitExpression?.argument; assert.equal( callExpression?.callee?.name, 'getOrCreateAssociatedTokenAccount', '`toTokenAccount` should be initialised with `getOrCreateAssociatedTokenAccount()`' ); const [ connectionArgument, payerArgument, mintAddressArgument, toWalletPublicKeyArgument ] = callExpression?.arguments; assert.equal( connectionArgument?.name, 'connection', 'The first argument of `getOrCreateAssociatedTokenAccount()` should be `connection`' ); assert.equal( payerArgument?.name, 'payer', 'The second argument of `getOrCreateAssociatedTokenAccount()` should be `payer`' ); assert.equal( mintAddressArgument?.name, 'mintAddress', 'The third argument of `getOrCreateAssociatedTokenAccount()` should be `mintAddress`' ); assert.equal( toWalletPublicKeyArgument?.object?.name, 'toWallet', 'The fourth argument of `getOrCreateAssociatedTokenAccount()` should be `toWallet.publicKey`' ); assert.equal( toWalletPublicKeyArgument?.property?.name, 'publicKey', 'The fourth argument of `getOrCreateAssociatedTokenAccount()` should be `toWallet.publicKey`' ); ``` You should import `getOrCreateAssociatedTokenAccount` from `@solana/spl-token`. ```js const splTokenImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( splTokenImportDeclaration, 'An import from `@solana/spl-token` should exist' ); const importSpecifiers = splTokenImportDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( importSpecifiers, 'getOrCreateAssociatedTokenAccount', '`getOrCreateAssociatedTokenAccount` should be imported from `@solana/spl-token`' ); ``` You should import `payer` from `./utils.js`. ```js const utilsImportDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists( utilsImportDeclaration, 'An import from `./utils.js` should exist' ); const importSpecifiers = utilsImportDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( importSpecifiers, 'payer', '`payer` should be imported from `./utils.js`' ); ``` You should import `mintAddress` from `./utils.js`. ```js const utilsImportDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists( utilsImportDeclaration, 'An import from `./utils.js` should exist' ); const importSpecifiers = utilsImportDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( importSpecifiers, 'mintAddress', '`mintAddress` should be imported from `./utils.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"transfer.js"-- ```js import { Connection, PublicKey, Keypair } from '@solana/web3.js'; const connection = new Connection('http://localhost:8899'); const fromTokenAccountPublicKey = new PublicKey(process.argv[2]); const toWallet = Keypair.generate(); ``` ## 46 ### --description-- Import the `getAccount` function from `@solana/spl-token`, to get the owner of the `fromTokenAccountPublicKey` account: ```typescript getAccount( connection: Connection, address: PublicKey ): Promise ``` Call the function, passing in order: `connection`, and `fromTokenAccountPublicKey` as arguments. Assign the awaited result to a variable `fromWallet`. ### --tests-- You should have `const fromWallet = await getAccount(connection, fromTokenAccountPublicKey);` in `transfer.js`. ```js const variableDeclarations = babelisedCode.getVariableDeclarations(); const fromWalletVariableDeclaration = variableDeclarations.find(v => { return v.declarations?.[0]?.id?.name === 'fromWallet'; }); assert.exists(fromWalletVariableDeclaration, '`fromWallet` should be declared'); const awaitExpression = fromWalletVariableDeclaration.declarations?.[0]?.init; assert.equal( awaitExpression?.type, 'AwaitExpression', '`fromWallet` should be initialised with `await`' ); const callExpression = awaitExpression?.argument; assert.equal( callExpression?.callee?.name, 'getAccount', '`fromWallet` should be initialised with `await getAccount()`' ); const [connectionArgument, fromTokenAccountPublicKeyArgument] = callExpression?.arguments; assert.equal( connectionArgument?.name, 'connection', 'The first argument of `getAccount()` should be `connection`' ); assert.equal( fromTokenAccountPublicKeyArgument?.name, 'fromTokenAccountPublicKey', 'The second argument of `getAccount()` should be `fromTokenAccountPublicKey`' ); ``` You should import `getAccount` from `@solana/spl-token`. ```js const splTokenImportDeclaration = babelisedCode .getImportDeclarations() .find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( splTokenImportDeclaration, 'An import from `@solana/spl-token` should exist' ); const importSpecifiers = splTokenImportDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( importSpecifiers, 'getAccount', '`getAccount` should be imported from `@solana/spl-token`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"transfer.js"-- ```js import { Connection, PublicKey, Keypair } from '@solana/web3.js'; import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; import { payer, mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const fromTokenAccountPublicKey = new PublicKey(process.argv[2]); const toWallet = Keypair.generate(); const toTokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, toWallet.publicKey ); ``` ## 47 ### --description-- Assign the `owner` property of `fromWallet` to a variable `owner`. ### --tests-- You should have `const owner = fromWallet.owner;` in `transfer.js`. ```js const variableDeclarations = babelisedCode.getVariableDeclarations(); const ownerVariableDeclaration = variableDeclarations.find(v => { return v.declarations?.[0]?.id?.name === 'owner'; }); assert.exists(ownerVariableDeclaration, '`owner` should be declared'); const memberExpression = ownerVariableDeclaration.declarations?.[0]?.init; assert.equal( memberExpression?.object?.name, 'fromWallet', '`owner` should be initialised with `fromWallet`' ); assert.equal( memberExpression?.property?.name, 'owner', '`owner` should be initialised with `fromWallet.owner`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"transfer.js"-- ```js import { Connection, PublicKey, Keypair } from '@solana/web3.js'; import { getOrCreateAssociatedTokenAccount, getAccount } from '@solana/spl-token'; import { payer, mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const fromTokenAccountPublicKey = new PublicKey(process.argv[2]); const toWallet = Keypair.generate(); const toTokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, toWallet.publicKey ); const fromWallet = await getAccount(connection, fromTokenAccountPublicKey); ``` ## 48 ### --description-- Cast the second command-line argument to a number, and assign it to a variable `amount`. This will be used as the amount of tokens to transfer. ### --tests-- You should have `const amount = Number(process.argv[3]);` in `transfer.js`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'amount'; }); assert.exists(variableDeclaration, '`amount` should be declared'); const callExpression = variableDeclaration.declarations?.[0]?.init; assert.equal( callExpression?.callee?.name, 'Number', '`amount` should be initialised with `Number()`' ); const processArgvArgument = callExpression?.arguments?.[0]; assert.equal( processArgvArgument?.object?.object?.name, 'process', '`amount` should be initialised with `Number(process.argv[3])`' ); assert.equal( processArgvArgument?.object?.property?.name, 'argv', '`amount` should be initialised with `Number(process.argv[3])`' ); assert.equal( processArgvArgument?.property?.value, 3, '`amount` should be initialised with `Number(process.argv[3])`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"transfer.js"-- ```js import { Connection, PublicKey, Keypair } from '@solana/web3.js'; import { getOrCreateAssociatedTokenAccount, getAccount } from '@solana/spl-token'; import { payer, mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const fromTokenAccountPublicKey = new PublicKey(process.argv[2]); const toWallet = Keypair.generate(); const toTokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, toWallet.publicKey ); const fromWallet = await getAccount(connection, fromTokenAccountPublicKey); const owner = fromWallet.owner; ``` ## 49 ### --description-- Import the `transfer` function from `@solana/spl-token`, to transfer tokens from the `fromTokenAccountPublicKey` account to the `toTokenAccount` account: ```typescript transfer( connection: Connection, payer: Signer, source: PublicKey, destination: PublicKey, owner: PublicKey | Signer, amount: number | bigint ): Promise ``` Call the function, passing in order: `connection`, `payer`, `fromTokenAccountPublicKey`, the `address` of `toTokenAccount`, `owner`, and `amount` as arguments. ### --tests-- You should have `await transfer(connection, payer, fromTokenAccountPublicKey, toTokenAccount.address, owner, amount);` in `transfer.js`. ```js const expressionStatement = babelisedCode .getExpressionStatements() .find( e => e.expression.type === 'AwaitExpression' && e.expression.argument.callee.name === 'transfer' ); assert.exists( expressionStatement, 'An `await transfer(...)` expression should exist' ); const transferArgs = expressionStatement.expression.argument.arguments; const [ connectionArgument, payerArgument, fromTokenAccountPublicKeyArgument, toTokenAccountMemberExpression, ownerArgument, amountArgument ] = transferArgs; assert.equal( connectionArgument.name, 'connection', 'The first argument to `transfer` should be `connection`' ); assert.equal( payerArgument.name, 'payer', 'The second argument to `transfer` should be `payer`' ); assert.equal( fromTokenAccountPublicKeyArgument.name, 'fromTokenAccountPublicKey', 'The third argument to `transfer` should be `fromTokenAccountPublicKey`' ); assert.equal( toTokenAccountMemberExpression.object.name, 'toTokenAccount', 'The fourth argument to `transfer` should be `toTokenAccount.address`' ); assert.equal( toTokenAccountMemberExpression.property.name, 'address', 'The fourth argument to `transfer` should be `toTokenAccount.address`' ); assert.equal( ownerArgument.name, 'owner', 'The fifth argument to `transfer` should be `owner`' ); assert.equal( amountArgument.name, 'amount', 'The sixth argument to `transfer` should be `amount`' ); ``` You should import `transfer` from `@solana/spl-token`. ```js const importDeclaration = babelisedCode .getImportDeclarations() .find(i => i.source.value === '@solana/spl-token'); assert.exists( importDeclaration, 'An import from `@solana/spl-token` should exist' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'transfer', 'An import from `@solana/spl-token` should import `transfer`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"transfer.js"-- ```js import { Connection, PublicKey, Keypair } from '@solana/web3.js'; import { getOrCreateAssociatedTokenAccount, getAccount } from '@solana/spl-token'; import { payer, mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const fromTokenAccountPublicKey = new PublicKey(process.argv[2]); const toWallet = Keypair.generate(); const toTokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, toWallet.publicKey ); const fromWallet = await getAccount(connection, fromTokenAccountPublicKey); const owner = fromWallet.owner; const amount = Number(process.argv[3]); ``` ## 50 ### --description-- Add the following to the end of the file, to log the result of the transfer: ```js console.log( `Transferred ${amount} tokens from ${fromTokenAccountPublicKey.toBase58()} to ${toTokenAccount.address.toBase58()}` ); ``` ### --tests-- You should add the above code to the end of `transfer.js`. ```js const transferFile = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); assert.match( transferFile, /console\.log\s*\(\s*`Transferred \${\s*amount\s*} tokens from \${\s*fromTokenAccountPublicKey\.toBase58\s*\(\s*\)\s*} to \${\s*toTokenAccount\.address\.toBase58\s*\(\s*\)\s*}`\s*\)\s*;?/, 'The code to log the result of the transfer should exist' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-solanas-token-program-by-minting-a-fungible-token/transfer.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ### --seed-- #### --"transfer.js"-- ```js import { Connection, PublicKey, Keypair } from '@solana/web3.js'; import { getOrCreateAssociatedTokenAccount, getAccount, transfer } from '@solana/spl-token'; import { payer, mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const fromTokenAccountPublicKey = new PublicKey(process.argv[2]); const toWallet = Keypair.generate(); const toTokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, toWallet.publicKey ); const fromWallet = await getAccount(connection, fromTokenAccountPublicKey); const owner = fromWallet.owner; const amount = Number(process.argv[3]); await transfer( connection, payer, fromTokenAccountPublicKey, toTokenAccount.address, owner, amount ); ``` ## 51 ### --description-- Run the `transfer.js` script passing in the token account public key for `./wallet.json`, and any amount of tokens to transfer: ```bash $ node get-token-account.js # Get wallet.json token account address $ node transfer.js ``` ### --tests-- You should run the `transfer.js` script. ```js const { tokenAccount } = await __helpers.importSansCache( './learn-solanas-token-program-by-minting-a-fungible-token/utils.js' ); const commandRe = new RegExp(`node transfer.js ${tokenAccount.toBase58()} \\d+`); const lastCommand = await __helpers.getLastCommand(); assert.match( lastCommand, commandRe, `The last command should match '${commandRe}'` ); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ### --seed-- #### --"transfer.js"-- ```js import { Connection, PublicKey, Keypair } from '@solana/web3.js'; import { getOrCreateAssociatedTokenAccount, getAccount, transfer } from '@solana/spl-token'; import { payer, mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const fromTokenAccountPublicKey = new PublicKey(process.argv[2]); const toWallet = Keypair.generate(); const toTokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, toWallet.publicKey ); const fromWallet = await getAccount(connection, fromTokenAccountPublicKey); const owner = fromWallet.owner; const amount = Number(process.argv[3]); await transfer( connection, payer, fromTokenAccountPublicKey, toTokenAccount.address, owner, amount ); console.log( `Transferred ${amount} tokens from ${fromTokenAccountPublicKey.toBase58()} to ${toTokenAccount.address.toBase58()}` ); ``` ## 52 ### --description-- Contratulations on finishing this project! Feel free to play with your code. **Summary** - A _Mint Account_ is an account typically owned by an organisation who commissions the token, and keeps track of the supply of tokens - A _Token Account_ is an account typically owned by an individual, and keeps track of the tokens held by that individual - Minting tokens involves the _minting authority_ authorising a payer to create tokens and transfer them to a _Token Account_ 🎆 Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: curriculum/locales/english/learn-the-metaplex-sdk-by-minting-an-nft.md ================================================ # Solana - Learn the Metaplex SDK by Minting an NFT ## 1 ### --description-- In this project, you will learn how to use the Metaplex JavaScript SDK to mint a Non-Fungible Token (NFT) on the Solana blockchain. You will learn how to add metadata to your NFT, and how to upload your NFT somewhere it can be viewed by others. For the duration of this project, you will be working in the `learn-the-metaplex-sdk-by-minting-an-nft/` directory. Change into the above directory in a new bash terminal. ### --tests-- You can use `cd` to change into the `learn-the-metaplex-sdk-by-minting-an-nft/` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include(cwd, 'learn-the-metaplex-sdk-by-minting-an-nft'); ``` ## 2 ### --description-- So far, you have interacted with the _System Program_, created and deployed your own program, and interacted with the _Token Program_. Now, you will interact with the _Metaplex Token Program_ to mint your NFT. Starting from the same code as the previous project, you will change the the way the mint account is created, and the way the token is minted such that it is an NFT. Create a new keypair in a file called `wallet.json`. ### --tests-- You should have a `wallet.json` file in the `learn-the-metaplex-sdk-my-minting-an-nft/` directory. ```js const walletJsonExists = __helpers.fileExists( 'learn-the-metaplex-sdk-by-minting-an-nft/wallet.json' ); assert.isTrue(walletJsonExists, 'The `wallet.json` file should exist'); const walletJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/wallet.json' ) ); assert.isArray( walletJson, 'The `wallet.json` file should be an array of numbers.\nRun `solana-keygen new --outfile wallet.json` to create a new keypair.' ); ``` ## 3 ### --description-- Start a local Solana cluster. Ensure the RPC URL is set to `http://localhost:8899`. ### --tests-- Your Solana config RPC URL should be set to `http://localhost:8899`. ```js const command = `solana config get json_rpc_url`; await new Promise(res => setTimeout(() => res(), 2000)); const { stdout, stderr } = await __helpers.getCommandOutput(command); assert.include( stdout, 'http://localhost:8899', 'Try running `solana config set --url localhost`' ); ``` You should run `solana-test-validator` in a separate terminal. ```js await new Promise(res => setTimeout(() => res(), 2000)); const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 4 ### --description-- Use the Solana CLI to get the public key of the wallet you created. Put this public key in the `env.WALLET_ADDRESS` property of the `package.json` file. ### --tests-- The `package.json` file should have a `env.WALLET_ADDRESS` property that is not empty. ```js const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson, 'env', 'The `package.json` file should have an `env` property.' ); assert.property( packageJson.env, 'WALLET_ADDRESS', 'The `package.json` file should have an `env.WALLET_ADDRESS` property.' ); assert.notEmpty( packageJson.env.WALLET_ADDRESS, 'The `env.WALLET_ADDRESS` property should have a value.' ); ``` The `env.WALLET_ADDRESS` property should match the public key of `wallet.json`. ```js const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); const command = 'solana address -k learn-the-metaplex-sdk-by-minting-an-nft/wallet.json'; const { stdout, stderr } = await __helpers.getCommandOutput(command); const walletAddress = stdout.trim(); console.debug('walletAddress', walletAddress, packageJson.env.WALLET_ADDRESS); assert.notEmpty( walletAddress, 'The `wallet.json` file should have a public key.' ); assert.notEmpty( packageJson.env.WALLET_ADDRESS, 'The `env.WALLET_ADDRESS` property should have a value.' ); assert.equal( packageJson.env.WALLET_ADDRESS, walletAddress, 'The `env.WALLET_ADDRESS` property should match the public key of `wallet.json`.' ); ``` ## 5 ### --description-- Following the same steps to create a fungible token, now a mint account for your NFT needs to be created. Seeing as NFT's are not divisible - you cannot own 0.1 of an NFT - the decimal argument of the `createMint` call needs to be changed. Within `spl-program/create-mint-account.js`, change the decimal place argument to `0`. ### --tests-- The `createMint` call within `spl-program/create-mint-account.js` should have a decimal place argument of `0`. ```js const mintDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'mint'; }); assert.exists(mintDeclaration, 'A variable named `mint` should exist'); const mintAwaitExpression = mintDeclaration.declarations?.[0]?.init; assert.equal( mintAwaitExpression.type, 'AwaitExpression', 'The `createMint` call should be awaited' ); const createMintCallExpression = mintAwaitExpression.argument; assert.equal( createMintCallExpression.callee.name, 'createMint', 'The `mint` variable should be initialised with the `createMint` function' ); const createMintArguments = createMintCallExpression.arguments; assert.equal( createMintArguments.length, 5, 'The `createMint` function should be called with 5 arguments' ); const [ connectionArgument, payerArgument, mintAuthorityArgument, freezeAuthorityArgument, decimalsArgument ] = createMintArguments; assert.equal( connectionArgument.name, 'connection', 'The first argument to `createMint` should be `connection`' ); assert.equal( payerArgument.name, 'payer', 'The second argument to `createMint` should be `payer`' ); assert.equal( mintAuthorityArgument.name, 'mintAuthority', 'The third argument to `createMint` should be `mintAuthority`' ); assert.equal( freezeAuthorityArgument.name, 'freezeAuthority', 'The fourth argument to `createMint` should be `freezeAuthority`' ); assert.equal( decimalsArgument.value, 0, 'The fifth argument to `createMint` should be `0`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/spl-program/create-mint-account.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 6 ### --description-- Another properties of NFT's is that there is only one of each NFT. This means that the `mintTo` call needs to be changed to mint only one token. Within `spl-program/mint.js`, change the `amount` argument of the `mintTo` call to `1`. ### --tests-- The `mintTo` call within `spl-program/mint.js` should have an `amount` argument of `1`. ```js const expressionStatement = babelisedCode .getExpressionStatements() .find( e => e.expression.type === 'AwaitExpression' && e.expression.argument?.callee?.name === 'mintTo' ); assert.exists( expressionStatement, 'An `await mintTo(...)` expression should exist' ); const mintToArguments = expressionStatement.expression.argument.arguments; const [ connectionArgument, payerArgument, mintAddressArgument, tokenAccountArgument, mintAuthorityArgument, amountArgument ] = mintToArguments; assert.equal( connectionArgument?.name, 'connection', 'The first argument to `mintTo` should be `connection`' ); assert.equal( payerArgument?.name, 'payer', 'The second argument to `mintTo` should be `payer`' ); assert.equal( mintAddressArgument?.name, 'mintAddress', 'The third argument to `mintTo` should be `mintAddress`' ); assert.equal( tokenAccountArgument?.name, 'tokenAccount', 'The fourth argument to `mintTo` should be `tokenAccount`' ); assert.equal( mintAuthorityArgument?.name, 'mintAuthority', 'The fifth argument to `mintTo` should be `mintAuthority`' ); assert.equal( amountArgument?.value, 1, 'The sixth argument to `mintTo` should be `1`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/spl-program/mint.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 7 ### --description-- Once the NFT has been minted to an account, the mint authority needs to be removed from the mint account. The mint authority is the only account that can mint more tokens. If the mint authority is removed, then no more tokens can be minted. Within `spl-program/mint.js`, import the `setAuthority` function from `@solana/spl-token`, to change the mint authority. ```typescript setAuthority( connection: Connection, payer: Signer, account: PublicKey, currentAuthority: Signer | PublicKey, authorityType: AuthorityType, // Import this enum from @solana/spl-token! newAuthority: PublicKey | null ): Promise ``` Call and await the `setAuthority` function, passing in the `connection`, `payer`, `mintAddress`, `mintAuthority`, `AuthorityType.MintTokens`, and `null` arguments. ### --tests-- You should have `await setAuthority(connection, payer, mintAddress, mintAuthority, AuthorityType.MintTokens, null);` in `spl-program/mint.js`. ```js const expressionStatement = babelisedCode .getExpressionStatements() .find( e => e.expression.type === 'AwaitExpression' && e.expression.argument?.callee?.name === 'setAuthority' ); assert.exists( expressionStatement, 'An `await setAuthority(...)` expression should exist' ); const setAuthorityArguments = expressionStatement.expression.argument.arguments; const [ connectionArgument, payerArgument, mintAddressArgument, mintAuthorityArgument, authorityTypeMemberExpressionArgument, nullArgument ] = setAuthorityArguments; assert.equal( connectionArgument?.name, 'connection', 'The first argument to `setAuthority` should be `connection`' ); assert.equal( payerArgument?.name, 'payer', 'The second argument to `setAuthority` should be `payer`' ); assert.equal( mintAddressArgument?.name, 'mintAddress', 'The third argument to `setAuthority` should be `mintAddress`' ); assert.equal( mintAuthorityArgument?.name, 'mintAuthority', 'The fourth argument to `setAuthority` should be `mintAuthority`' ); assert.equal( authorityTypeMemberExpressionArgument?.object?.name, 'AuthorityType', 'The fifth argument to `setAuthority` should be `AuthorityType.MintTokens`' ); assert.equal( authorityTypeMemberExpressionArgument?.property?.name, 'MintTokens', 'The fifth argument to `setAuthority` should be `AuthorityType.MintTokens`' ); assert.equal( nullArgument?.type, 'NullLiteral', 'The sixth argument to `setAuthority` should be `null`' ); ``` You should import `setAuthority` from `@solana/spl-token`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( importDeclaration, 'An import from `@solana/spl-token` should exist' ); const importSpecifiers = importDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( importSpecifiers, 'setAuthority', '`setAuthority` should be imported from `@solana/spl-token`' ); ``` You should import `AuthorityType` from `@solana/spl-token`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/spl-token'; }); assert.exists( importDeclaration, 'An import from `@solana/spl-token` should exist' ); const importSpecifiers = importDeclaration.specifiers.map(s => { return s.imported.name; }); assert.include( importSpecifiers, 'AuthorityType', '`AuthorityType` should be imported from `@solana/spl-token`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/spl-program/mint.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 8 ### --description-- With those small changes, you can now mint an NFT! Run the following to create the mint account: ```bash node spl-program/create-mint-account.js ``` **Note:** You should see an error (similar to below) in the terminal.
Error ```bash FailedToSendTransactionError: The transaction could not be sent successfully to the network. Please check the underlying error below for more details. Source: RPC Caused By: Error: failed to send transaction: Transaction simulation failed: Attempt to debit an account but found no record of a prior credit. ```
### --tests-- You should run `node spl-program/create-mint-account.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'node spl-program/create-mint-account.js', 'Try running `node spl-program/create-mint-account.js` in the terminal' ); ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 9 ### --description-- Remember to airdrop some SOL to the wallet 🤦‍♂️ ### --tests-- The `wallet.json` account should have at least 2 SOL. ```js const { stdout } = await __helpers.getCommandOutput( `solana balance ./learn-the-metaplex-sdk-by-minting-an-nft/wallet.json` ); const balance = stdout.trim()?.match(/\d+/)?.[0]; assert.isAtLeast( parseInt(balance), 2, 'Try running `solana airdrop 2 ./wallet.json`' ); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 10 ### --description-- You can now mint an NFT! Run the following to create the mint account: ```bash node spl-program/create-mint-account.js ``` ### --tests-- You should run `node spl-program/create-mint-account.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'node spl-program/create-mint-account.js', 'Try running `node spl-program/create-mint-account.js` in the terminal' ); ``` The command should succeed. ```js // Test terminal-out includes value that can be parsed as a PublicKey. const terminalOut = await __helpers.getTerminalOutput(); const base58String = terminalOut.match(/[\w\d]{32,44}/)?.[0]; assert.exists( base58String, 'The terminal output should include a base58 string' ); try { console.log('TEST: ', terminalOut.match(/[\w\d]{32,44}/)); const { PublicKey } = await import('@solana/web3.js'); new PublicKey(base58String); } catch (e) { assert.fail(e); } ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 11 ### --description-- Put the mint address in `env.MINT_ACCOUNT_ADDRESS` in the `package.json` file. ### --tests-- The `package.json` file should have a `env.MINT_ACCOUNT_ADDRESS` property. ```js const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson, 'env', 'The `package.json` file should have an `env` property.' ); assert.property( packageJson.env, 'MINT_ACCOUNT_ADDRESS', 'The `package.json` file should have an `env.MINT_ACCOUNT_ADDRESS` property.' ); ``` The `env.MINT_ACCOUNT_ADDRESS` property should match an NFT owned by `wallet.json`. ```js const command = 'solana address -k learn-the-metaplex-sdk-by-minting-an-nft/wallet.json'; const { stdout, stderr } = await __helpers.getCommandOutput(command); const walletAddress = stdout.trim(); const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson, 'env', 'The `package.json` file should have an `env` property.' ); assert.equal( packageJson.env.WALLET_ADDRESS, walletAddress, 'The `env.WALLET_ADDRESS` property should match the public key of `wallet.json`.' ); try { const { Connection } = await import('@solana/web3.js'); const { TOKEN_PROGRAM_ID } = await import('@solana/spl-token'); const connection = new Connection('http://127.0.0.1:8899'); const mintAccounts = await connection.getParsedProgramAccounts( TOKEN_PROGRAM_ID, { filters: [ { dataSize: 82 }, { memcmp: { offset: 4, bytes: packageJson.env.WALLET_ADDRESS } } ] } ); const mintAccount = mintAccounts.find( ({ pubkey }) => pubkey?.toBase58() === packageJson.env.MINT_ACCOUNT_ADDRESS ); assert.exists( mintAccount, 'The `env.MINT_ACCOUNT_ADDRESS` property should match an NFT owned by `wallet.json`.' ); } catch (e) { assert.fail(e); } ``` ## 12 ### --description-- Run the following to create the token account: ```bash node spl-program/create-token-account.js ``` ### --tests-- You should run `node spl-program/create-token-account.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'node spl-program/create-token-account.js', 'Try running `node spl-program/create-token-account.js` in the terminal' ); ``` The command should succeed. ```js // Test terminal-out includes value that can be parsed as a PublicKey. const terminalOut = await __helpers.getTerminalOutput(); const base58String = terminalOut.match(/[\w\d]{32,44}/)?.[0]; const error = terminalOut.match('Error')?.[0]; assert.notExists(error); assert.exists( base58String, 'The terminal output should include a base58 string' ); try { const { PublicKey } = await import('@solana/web3.js'); new PublicKey(base58String); } catch (e) { assert.fail(e, 'Public key should be printed to the terminal'); } ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 13 ### --description-- Put the token account address in `env.TOKEN_ACCOUNT_ADDRESS` in the `package.json` file. ### --tests-- The `package.json` file should have a `env` property. ```js const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson, 'env', 'The `package.json` file should have an `env` property.' ); ``` The `package.json` file should have a `env.TOKEN_ACCOUNT_ADDRESS` property. ```js const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson.env, 'TOKEN_ACCOUNT_ADDRESS', 'The `package.json` file should have an `env` property.' ); assert.notEmpty(packageJson.env.TOKEN_ACCOUNT_ADDRESS); ``` The `env.TOKEN_ACCOUNT_ADDRESS` property should match an NFT owned by `wallet.json`. ```js const command = 'solana address -k learn-the-metaplex-sdk-by-minting-an-nft/wallet.json'; const { stdout, stderr } = await __helpers.getCommandOutput(command); const walletAddress = stdout.trim(); const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson, 'env', 'The `package.json` file should have an `env` property.' ); assert.equal( packageJson.env.WALLET_ADDRESS, walletAddress, 'The `env.WALLET_ADDRESS` property should match the public key of `wallet.json`.' ); try { const { Connection } = await import('@solana/web3.js'); const { TOKEN_PROGRAM_ID } = await import('@solana/spl-token'); const connection = new Connection('http://127.0.0.1:8899'); const tokenAccounts = await connection.getParsedProgramAccounts( TOKEN_PROGRAM_ID, { filters: [ { dataSize: 165 }, { memcmp: { offset: 32, bytes: packageJson.env.WALLET_ADDRESS } } ] } ); const tokenAccount = tokenAccounts.find( ({ pubkey }) => pubkey.toBase58() === packageJson.env.TOKEN_ACCOUNT_ADDRESS ); assert.exists( tokenAccount, 'The `env.TOKEN_ACCOUNT_ADDRESS` property should match an NFT owned by `wallet.json`.' ); assert.equal( tokenAccount.account.data.parsed.info.mint, packageJson.env.MINT_ACCOUNT_ADDRESS, 'The `env.TOKEN_ACCOUNT_ADDRESS` should be associated with the `env.MINT_ACCOUNT_ADDRESS`.' ); } catch (e) { assert.fail(e); } ``` ## 14 ### --description-- Run the following to mint the NFT to the token account: ```bash node spl-program/mint.js ``` ### --tests-- You should run `node spl-program/mint.js` in the terminal. ```js // Test command is run in correct directory const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'node spl-program/mint.js', 'Try running `node spl-program/mint.js` in the terminal' ); ``` The command should succeed. ```js // Test authority is null const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson, 'env', 'The `package.json` file should have an `env` property.' ); assert.notEmpty( packageJson.env.MINT_ACCOUNT_ADDRESS, 'The `env.MINT_ACCOUNT_ADDRESS` property should be set.' ); try { const { Connection, PublicKey } = await import('@solana/web3.js'); const { getMint } = await import('@solana/spl-token'); const connection = new Connection('http://127.0.0.1:8899'); const mint = await getMint( connection, new PublicKey(packageJson.env.MINT_ACCOUNT_ADDRESS) ); assert.equal(mint.mintAuthority, null, 'The mint authority should be null.'); } catch (e) { assert.fail(e); } ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 15 ### --description-- Run the following to confirm the NFT has no mint authority, and has a total supply of 1: ```bash node spl-program/get-token-info.js ``` ### --tests-- You should run `node spl-program/get-token-info.js` in the terminal. ```js // Test command is run in correct directory const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'node spl-program/get-token-info.js', 'Try running `node spl-program/get-token-info.js` in the terminal' ); ``` The command should succeed. ```js // Script should be idempotent const command = `node spl-program/get-token-info.js`; const { stdout, stderr } = await __helpers.getCommandOutput( command, 'learn-the-metaplex-sdk-by-minting-an-nft' ); assert.include(stdout, 'mintAuthority: null'); ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 16 ### --description-- You often hear about NFTs being odd pictures of a cat, but what if you wanted to associate more information with the NFT? For example, you could associate a name, description, and image with the NFT. This is where metadata comes in. The metadata file is stored on IPFS, and the NFT is associated with the metadata file by storing the IPFS hash in the NFT's data field. There are many accounts and files involved in the process of creating an NFT with metadata. Metaplex has a JavaScript SDK which provides common functionality for creating NFTs with metadata. Install version `0.18.1` of `@metaplex-foundation/js`. ### --tests-- You should install `@metaplex-foundation/js` version `0.18.1`. ```js const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson.dependencies, '@metaplex-foundation/js', 'The `package.json` file should have a `@metaplex-foundation/js` dependency.' ); assert.equal( packageJson.dependencies['@metaplex-foundation/js'], '0.18.1', 'Try running `npm install --save-exact @metaplex-foundation/js@0.18.1` in the terminal.' ); ``` ## 17 ### --description-- Create a file called `create-nft.js`. ### --tests-- You should create a file called `create-nft.js`. ```js const fileExists = __helpers.fileExists( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); assert.isTrue( fileExists, 'The `learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js` file should exist.' ); ``` ## 18 ### --description-- Within `create-nft.js`, create a `connection` variable set to a new `Connection` of your local Solana validator. ### --tests-- You should have `const connection = new Connection('http://127.0.0.1:8899');` in `create-nft.js`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'connection'); assert.exists( connectionVariableDeclaration, 'You should declare a variable named `connection`' ); const newExpression = connectionVariableDeclaration.declarations[0].init; assert.equal( newExpression.callee.name, 'Connection', 'You should initialise `connection` with a new `Connection`' ); assert.equal( newExpression.arguments[0].value, 'http://127.0.0.1:8899', "You should create a new connection with `new Connection('http://127.0.0.1:8899')`" ); ``` You should import `Connection` from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists(importDeclaration, 'You should import from `@solana/web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'Connection', '`Connection` should be imported from `@solana/spl-token`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 19 ### --description-- Import the `Metaplex` class from the Metaplex SDK, create a `metaplex` variable, and set it to: ```js Metaplex.make(); ``` ### --tests-- You should have `const metaplex = Metaplex.make();` in `create-nft.js`. ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const variableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'metaplex'); assert.exists( variableDeclaration, 'You should declare a variable named `metaplex`' ); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match(minifiedCode, /metaplex=Metaplex\.make\(\)/); ``` You should import `Metaplex` from `@metaplex-foundation/js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@metaplex-foundation/js'; }); assert.exists( importDeclaration, 'An import from `@metaplex-foundation/js` should exist' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'Metaplex', '`Metaplex` should be imported from `@metaplex-foundation/js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 20 ### --description-- The `make` method on the `Metaplex` class expects a `Connection` that will be used to communicate with the cluster. Pass the `connection` variable to the `make` method. ### --tests-- You should have `const metaplex = Metaplex.make(connection);` in `create-nft.js`. ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'metaplex'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match(minifiedCode, /metaplex=Metaplex\.make\(connection\)/); ``` ## 21 ### --description-- The `Metaplex` class has chainable `use` methods that allow you to configure the `Metaplex` instance with `MetaplexPlugin` implementations. Configure the `Metaplex` instance to use the wallet keypair as the primary identity for transactions: ```js Metaplex.make(connection).use(keypairIdentity(WALLET_KEYPAIR)); ``` The `keypairIdentity` function takes a `Keypair` and returns a `MetaplexPlugin`, and is exported from `@metaplex-foundation/js`. The `WALLET_KEYPAIR` variable is a `Keypair` created from the `wallet.json` file, and is already exported from `utils.js`. ### --tests-- You should have `const metaplex = Metaplex.make(connection).use(keypairIdentity(WALLET_KEYPAIR));` in `create-nft.js`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'metaplex'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /metaplex=Metaplex\.make\(connection\)\.use\(keypairIdentity\(WALLET_KEYPAIR\)\)/ ); ``` You should import `keypairIdentity` from `@metaplex-foundation/js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@metaplex-foundation/js'; }); assert.exists( importDeclaration, 'An import from `@metaplex-foundation/js` should exist' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'keypairIdentity', '`keypairIdentity` should be imported from `@metaplex-foundation/js`' ); ``` You should import `WALLET_KEYPAIR` from `utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'An import from `./utils.js` should exist'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'WALLET_KEYPAIR', '`WALLET_KEYPAIR` should be imported from `./utils.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 22 ### --description-- The final configuration required is the _storage driver_ that will be used to store the NFT's metadata. This is because the NFT's metadata is not stored on-chain, but rather in a separate, often cheaper, storage system. Some common metadata storage locations are: - IPFS - Arweave - AWS For the purpose of testing, and to avoid having to pay for storage, you can use the `localStorage` driver which will generate random URLs and keep track of their content in a local server. Configure the `Metaplex` instance to use the `localStorage` driver. ### --tests-- You should have `const metaplex = Metaplex.make(connection).use(keypairIdentity(WALLET_KEYPAIR)).use(localStorage());` in `create-nft.js`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'metaplex'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /metaplex=Metaplex\.make\(connection\)\.use\(keypairIdentity\(WALLET_KEYPAIR\)\)\.use\(localStorage\(\)\)/ ); ``` You should import `localStorage` from `utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'An import from `./utils.js` should exist'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'localStorage', '`localStorage` should be imported from `./utils.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 23 ### --description-- The storage driver takes an object with a `baseUrl` property, which is the base URL that will be used to generate the metadata URLs. Set the `baseUrl` to `http://127.0.0.1:3001/`. ### --tests-- You should have `const metaplex = Metaplex.make(connection).use(keypairIdentity(WALLET_KEYPAIR)).use(localStorage({ baseUrl: 'http://127.0.0.1:3001/' }));` in `create-nft.js`. ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'metaplex'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /metaplex=Metaplex\.make\(connection\)\.use\(keypairIdentity\(WALLET_KEYPAIR\)\)\.use\(localStorage\(\{('|"|`)?baseUrl\1:('|"|`)http:\/\/127\.0\.0\.1:3001\/\2\}\)\)/ ); ``` ## 24 ### --description-- You can use any image you want for your NFT, but one is provided for you in the `assets` folder. Declare a variable `imageBuffer`, and set it to the contents of the `assets/pic.png` file. You can use the `fs/promises` module to read the file. ### --tests-- You can use `const imageBuffer = await readFile('assets/pic.png');` in `create-nft.js`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'imageBuffer'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /imageBuffer=(await readFile|readFileSync|read)\(('|"|`)(\.\/)?assets\/pic\.png\2\)/ ); ``` You should import one of the file-reading API. ```js // one of fs, fs/promises // one of readFileSync | readFile | read const importDeclarations = babelisedCode.getImportDeclarations().filter(i => { return i.source.value === 'fs' || i.source.value === 'fs/promises'; }); assert.notEmpty( importDeclarations, 'An import from `fs` or `fs/promises` should exist' ); const importSpecifiers = importDeclarations.flatMap(i => i.specifiers.map(s => s.imported.name) ); const fileReadingApis = ['readFile', 'readFileSync', 'read']; let found = false; out: { for (const i of importSpecifiers) { for (const f of fileReadingApis) { if (i === f) { found = true; break out; } } } } assert.isTrue(found, 'One of the file-reading APIs should be imported'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 25 ### --description-- The `@metaplex-foundation/js` module exports a `toMetaplexFile` function: ```typescript toMetaplexFile( content: MetaplexFileContent, fileName: string ): MetaplexFile ``` The `MetaplexFile` type contains useful metadata about the file which is useful for your NFT's metadata. Declare a `file` variable, and set it to the result of calling `toMetaplexFile` with the `imageBuffer` and the filename `pic.png`. ### --tests-- You should have `const file = toMetaplexFile(imageBuffer, 'pic.png');` in `create-nft.js`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'file'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /file=toMetaplexFile\(imageBuffer,('|"|`)?pic\.png\1\)/ ); ``` You should import `toMetaplexFile` from `@metaplex-foundation/js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@metaplex-foundation/js'; }); assert.exists( importDeclaration, 'An import from `@metaplex-foundation/js` should exist' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'toMetaplexFile', 'The `toMetaplexFile` function should be imported' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 26 ### --description-- The `Metaplex` class has a `storage` method that returns the storage driver configured earlier. The storage driver has an `upload` method that takes a `MetaplexFile` and returns a `Promise` that resolves to a `string` containing the URL of the uploaded file. Declare an `image` variable, and set it to the result of awaiting `metaplex.storage().upload(file)`. ### --tests-- You should have `const image = await metaplex.storage().upload(file);` in `create-nft.js`. ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'image'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /\s+image=await metaplex\.storage\(\)\.upload\(file\)/ ); ``` ## 27 ### --description-- With the URL to the uploaded image, you can upload the NFT's metadata. The `Metaplex` class has an `nfts` method that returns many useful methods for working with NFT's. One of these methods is `uploadMetadata`:
uploadMetadata Signature ```typescript uploadMetadata( input: { name?: string; symbol?: string; description?: string; seller_fee_basis_points?: number; image?: MetaplexFile; external_url?: MetaplexFile; attributes?: Array<{ trait_type?: string; value?: string; [key: string]: unknown; }>; properties?: { creators?: Array<{ address?: string; share?: number; [key: string]: unknown; }>; files?: Array<{ type?: string; uri?: MetaplexFile; [key: string]: unknown; }>; [key: string]: unknown; }; collection?: { name?: string; family?: string; [key: string]: unknown; }; [key: string]: unknown; } ): Promise ```
That is a lot of metadata! For now, you can just set the `name`, `description`, and `image` properties. Destructure the `uri` property from the result of awaiting `uploadMetadata`. Pass the `image` variable as the `image` property, and give the `name` and `description` properties some sane values. ### --tests-- You should have `const { uri } = await metaplex.nfts().uploadMetadata({ name: 'any string', description: 'any string', image });` in `create-nft.js`. ```js const uriVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => { return v.declarations?.[0]?.id?.properties?.find(p => p.key.name === 'uri'); }); assert.exists( uriVariableDeclaration, 'A variable declaration with a `uri` property should exist' ); const codeString = babelisedCode.generateCode(uriVariableDeclaration); const metaplex = ` const metaplex = { nfts: () => ({ uploadMetadata: ({name, description, image}) => { if (typeof name !== 'string') { throw new Error('name must be a string'); } if (typeof description !== 'string') { throw new Error('description must be a string'); } return Promise.resolve({ uri: 'any string' }); } }) }; let image, name, description; `; const testString = `${metaplex}\n${codeString}`; try { await eval(`(async () => {${testString}})()`); } catch (e) { assert.fail(e.message); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 28 ### --description-- Another method on the `nfts` object is `create`:
create Signature ````typescript create( input: { /** * The authority that will be able to make changes * to the created NFT. * * This is required as a Signer because creating the master * edition account requires the update authority to sign * the transaction. * * @defaultValue `metaplex.identity()` */ updateAuthority?: Signer; /** * The authority that is currently allowed to mint new tokens * for the provided mint account. * * Note that this is only relevant if the `useExistingMint` parameter * if provided. * * @defaultValue `metaplex.identity()` */ mintAuthority?: Signer; /** * The address of the new mint account as a Signer. * This is useful if you already have a generated Keypair * for the mint account of the NFT to create. * * @defaultValue `Keypair.generateCode()` */ useNewMint?: Signer; /** * The address of the existing mint account that should be converted * into an NFT. The account at this address should have the right * requirements to become an NFT, e.g. its supply should contains * exactly 1 token. * * @defaultValue Defaults to creating a new mint account with the * right requirements. */ useExistingMint?: PublicKey; /** * Whether or not we should mint one token for the new NFT. * * @defaultValue `true` */ mintTokens?: boolean; /** * The owner of the NFT to create. * * @defaultValue `metaplex.identity().publicKey` */ tokenOwner?: PublicKey; /** * The token account linking the mint account and the token owner * together. By default, the associated token account will be used. * * If the provided token account does not exist, it must be passed as * a Signer as we will need to create it before creating the NFT. * * @defaultValue Defaults to creating a new associated token account * using the `mintAddress` and `tokenOwner` parameters. */ tokenAddress?: PublicKey | Signer; /** * Describes the asset class of the token. * It can be one of the following: * - `TokenStandard.NonFungible`: A traditional NFT (master edition). * - `TokenStandard.FungibleAsset`: A fungible token with metadata that can also have attrributes. * - `TokenStandard.Fungible`: A fungible token with simple metadata. * - `TokenStandard.NonFungibleEdition`: A limited edition NFT "printed" from a master edition. * - `TokenStandard.ProgrammableNonFungible`: A master edition NFT with programmable configuration. * * @defaultValue `TokenStandard.NonFungible` */ tokenStandard?: TokenStandard; /** The URI that points to the JSON metadata of the asset. */ uri: string; /** The on-chain name of the asset, e.g. "My NFT #123". */ name: string; /** * The royalties in percent basis point (i.e. 250 is 2.5%) that * should be paid to the creators on each secondary sale. */ sellerFeeBasisPoints: number; /** * The on-chain symbol of the asset, stored in the Metadata account. * E.g. "MYNFT". * * @defaultValue `""` */ symbol?: string; /** * {@inheritDoc CreatorInput} * @defaultValue * Defaults to using the provided `updateAuthority` as the only verified creator. * ```ts * [{ * address: updateAuthority.publicKey, * authority: updateAuthority, * share: 100, * }] * ``` */ creators?: CreatorInput[]; /** * Whether or not the NFT's metadata is mutable. * When set to `false` no one can update the Metadata account, * not even the update authority. * * @defaultValue `true` */ isMutable?: boolean; /** * Whether or not selling this asset is considered a primary sale. * Once flipped from `false` to `true`, this field is immutable and * all subsequent sales of this asset will be considered secondary. * * @defaultValue `false` */ primarySaleHappened?: boolean; /** * The maximum supply of printed editions. * When this is `null`, an unlimited amount of editions * can be printed from the original edition. * * @defaultValue `toBigNumber(0)` */ maxSupply?: Option; /** * When this field is not `null`, it indicates that the NFT * can be "used" by its owner or any approved "use authorities". * * @defaultValue `null` */ uses?: Option; /** * Whether the created NFT is a Collection NFT. * When set to `true`, the NFT will be created as a * Sized Collection NFT with an initial size of 0. * * @defaultValue `false` */ isCollection?: boolean; /** * The Collection NFT that this new NFT belongs to. * When `null`, the created NFT will not be part of a collection. * * @defaultValue `null` */ collection?: Option; /** * The collection authority that should sign the created NFT * to prove that it is part of the provided collection. * When `null`, the provided `collection` will not be verified. * * @defaultValue `null` */ collectionAuthority?: Option; /** * Whether or not the provided `collectionAuthority` is a delegated * collection authority, i.e. it was approved by the update authority * using `metaplex.nfts().approveCollectionAuthority()`. * * @defaultValue `false` */ collectionAuthorityIsDelegated?: boolean; /** * Whether or not the provided `collection` is a sized collection * and not a legacy collection. * * @defaultValue `true` */ collectionIsSized?: boolean; /** * The ruleset account that should be used to configure the * programmable NFT. * * This is only relevant for programmable NFTs, i.e. if the * `tokenStandard` is set to `TokenStandard.ProgrammableNonFungible`. * * @defaultValue `null` */ ruleSet?: Option; } ): Promise ````
The only required properties are `name`, `uri`, and `sellerFeeBasisPoints`. On top of those, you should also provide a `maxSupply` of `1` if you want to create a limited edition NFT. The `create` method will take care of creating the mint account, the associated token account, the metadata PDA and the original edition PDA (a.k.a. the master edition) for you. Declare a variable `createResponse`, and assign to it the result of awaiting the `create` method. Pass in the previously mentioned properties with sane values. ### --tests-- You should have `const createResponse = await metaplex.nfts().create({ name: "any string", uri, sellerFeeBasisPoints: , maxSupply: 1 });` in `create-nft.js`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id.name === 'createResponse'; }); assert.exists(variableDeclaration, 'A variable `createResponse` should exist'); const codeString = babelisedCode.generateCode(variableDeclaration); const metaplex = ` const metaplex = { nfts: () => ({ create: ({name, sellerFeeBasisPoints, maxSupply}) => { if (typeof name !== 'string') { throw new Error('name must be a string'); } if (typeof sellerFeeBasisPoints !== 'number') { throw new Error('sellerFeeBasisPoints must be a number'); } if (typeof maxSupply !== 'number') { throw new Error('maxSupply must be a number'); } else { if (maxSupply !== 1) { throw new Error('maxSupply must be 1'); } } return Promise.resolve('createResponse'); } }) }; let name, sellerFeeBasisPoints, maxSupply, uri; `; const testString = `${metaplex}\n${codeString}`; try { await eval(`(async () => {${testString}})()`); } catch (e) { assert.fail(e.message); } ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 29 ### --description-- Log the `createResponse` variable to the console. ### --tests-- You should have `console.log(createResponse);` in `create-nft.js`. ```js const consoleLogCallExpression = babelisedCode .getType('CallExpression') .find(c => { return ( c.callee.object?.name === 'console' && c.callee.property?.name === 'log' ); }); assert.exists(consoleLogCallExpression, 'A `console.log` call should exist'); const consoleLogArguments = consoleLogCallExpression.arguments; const ident = consoleLogArguments.find(a => { return a.name === 'createResponse'; }); assert.exists( ident, 'One of the arguments to `console.log` should be `createResponse`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/create-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 30 ### --description-- The local storage driver points to a simple REST API that stores the metadata on your local machine. It is useful for testing purposes. Start the local storage driver in a new terminal: ```bash npm run start:server ``` ### --tests-- The local storage driver should be running at `http://127.0.0.1:3001`. ```js try { const res = await fetch('http://127.0.0.1:3001/status/ping'); // Response should be 200 with text "pong" if (res.status === 200) { const text = await res.text(); if (text !== 'pong') { throw new Error(`Expected response text "pong", got ${text}`); } } else { throw new Error(`Expected status code 200, got ${res.status}`); } } catch (e) { assert.fail(e); } ``` ## 31 ### --description-- Run the script in the terminal: ```bash node create-nft.js ``` **Note:** You should see an error (similar to below) in the terminal.
Error ```bash FailedToSendTransactionError: The transaction could not be sent successfully to the network. Please check the underlying error below for more details. Source: RPC Caused By: Error: failed to send transaction: Transaction simulation failed: Attempt to load a program that does not exist ```
### --tests-- You should run `node create-nft.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'node create-nft.js', 'Run `node create-nft.js` in the terminal' ); ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The local storage driver should be running at `http://127.0.0.1:3001`. ```js try { const res = await fetch('http://127.0.0.1:3001/status/ping'); // Response should be 200 with text "pong" if (res.status === 200) { const text = await res.text(); if (text !== 'pong') { throw new Error(`Expected response text "pong", got ${text}`); } } else { throw new Error(`Expected status code 200, got ${res.status}`); } } catch (e) { assert.fail(e); } ``` ## 32 ### --description-- The error comes about because the default Solana test validator does not come with the Metaplex Token program deployed. So, the program needs to be manually deployed to the local cluster. To do this, first a dump of the program from mainnet needs to be created: ```bash solana program dump --url mainnet-beta metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s ./mlp_token.so ```
About the Command The above command: - Takes a dump of the program at the provided address - Uses the mainnet-beta cluster (the cluster that the Metaplex Token program is deployed to) - Outputs the dump to `mlp_token.so` The address of the Metaplex Token program is `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`. You can find the address of any program you want by searching for it on the Solana Explorer: `https://explorer.solana.com/address/metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`
### --tests-- You should have a file called `mlp_token.so` in the root of the project. ```js const fileExists = __helpers.fileExists( 'learn-the-metaplex-sdk-by-minting-an-nft/mlp_token.so' ); assert.isTrue(fileExists, 'A file called `mlp_token.so` should exist.'); ``` ## 33 ### --description-- Deploy the Metaplex Token program to your local cluster: ```bash solana program deploy --keypair wallet.json ./mlp_token.so ``` ### --tests-- You should run `solana program deploy ./mlp_token.so` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'solana program deploy --keypair wallet.json ./mlp_token.so', 'Run `solana program deploy ./mlp_token.so` in the terminal' ); ``` ## 34 ### --description-- Re-run the script to create the NFT. **Note:** You should see an error in the terminal. ### --tests-- You should run `node create-nft.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'node create-nft.js', 'Run `node create-nft.js` in the terminal' ); ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The local storage driver should be running at `http://127.0.0.1:3001`. ```js try { const res = await fetch('http://127.0.0.1:3001/status/ping'); // Response should be 200 with text "pong" if (res.status === 200) { const text = await res.text(); if (text !== 'pong') { throw new Error(`Expected response text "pong", got ${text}`); } } else { throw new Error(`Expected status code 200, got ${res.status}`); } } catch (e) { assert.fail(e); } ``` ## 35 ### --description-- The error comes about because, internally, the Metaplex SDK is expecting a program address of `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`. However, when you deployed the `.so` file, it was deployed at a random public address. Instead, you can start the local cluster with the program pre-deployed at a specific address: ```bash solana-test-validator --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s ./mlp_token.so --reset ``` Stop your existing local cluster, and start a new one with the above command.
About the Command The `--bpf-program` flag is used to specify the address of the program. This is the same address that the program is deployed to on mainnet-beta. Internally, the Metaplex SDK uses this address in the transaction instructions to tell the cluster which program to use. The `--reset` flag is used to clear the `test-ledger` directory. This is where the local cluster stores its data. If you don't clear the directory, the cluster will use the old data.
### --tests-- You should be in the `learn-the-metaplex-sdk-by-minting-an-nft` directory. ```js const cwdFile = await __helpers.getCWD(); const cwd = cwdFile.split('\n').filter(Boolean).pop(); assert.include(cwd, 'learn-the-metaplex-sdk-by-minting-an-nft'); ``` You should run `solana-test-validator --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s ./mlp_token.so --reset` in the terminal. ```js await new Promise(res => setTimeout(() => res(), 2000)); const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getProgramAccounts", "params": [ "BPFLoader2111111111111111111111111111111111", { "encoding": "base64", "dataSlice": { "length": 0, "offset": 0 } } ] }'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.exists( jsonOut.result.find( r => r.pubkey === 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s' ) ); } catch (e) { assert.fail( e, 'Try running `solana-test-validator --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s ./mlp_token.so --reset`' ); } ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; await new Promise(res => setTimeout(() => res(), 2000)); const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 36 ### --description-- For ease, this command has been added to the `package.json` file as a script. Stop your existing local cluster, and start a new one with the following command: ```bash npm run start:validator ``` **Note:** You might need to manually run the tests. ### --tests-- You should run `npm run start:validator` in the terminal. ```js const lastCommand = await __helpers.getTemp(); assert.include( lastCommand, 'npm run start:validator', 'Run `npm run start:validator` in the terminal' ); ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 37 ### --description-- Restarting the validator with the `--reset` flag deletes any previous ledger data. This means you will need to airdrop some SOL into the account associated with `wallet.json` again. Do so. ### --tests-- The `wallet.json` account should have at least 2 SOL. ```js const { stdout } = await __helpers.getCommandOutput( `solana balance ./learn-the-metaplex-sdk-by-minting-an-nft/wallet.json` ); const balance = stdout.trim()?.match(/\d+/)?.[0]; assert.isAtLeast( parseInt(balance), 2, 'Try running `solana airdrop 2 ./wallet.json`' ); ``` The validator should be running at `http://localhost:8899`. ```js const command = `curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` ## 38 ### --description-- Create a new NFT again. Pay attention to the output in the terminal. ### --tests-- You should run `node create-nft.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal( lastCommand?.trim(), 'node create-nft.js', 'Run `node create-nft.js` in the terminal' ); ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The local storage driver should be running at `http://127.0.0.1:3001`. ```js try { const res = await fetch('http://127.0.0.1:3001/status/ping'); // Response should be 200 with text "pong" if (res.status === 200) { const text = await res.text(); if (text !== 'pong') { throw new Error(`Expected response text "pong", got ${text}`); } } else { throw new Error(`Expected status code 200, got ${res.status}`); } } catch (e) { assert.fail(e); } ``` ## 39 ### --description-- Part of the output should be the mint account's public key. Copy the base58 string and paste it into the `env.MINT_ACCOUNT_ADDRESS` property of the `package.json` file. _As this is a new NFT, the `env.MINT_ACCOUNT_ADDRESS` property will be different to the previous one._ ### --tests-- The `package.json` file should have a `env.MINT_ACCOUNT_ADDRESS` property. ```js const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson, 'env', 'The `package.json` file should have an `env` property.' ); assert.property( packageJson.env, 'MINT_ACCOUNT_ADDRESS', 'The `package.json` file should have an `env.MINT_ACCOUNT_ADDRESS` property.' ); ``` The `env.MINT_ACCOUNT_ADDRESS` property should match an NFT owned by `wallet.json`. ```js const command = 'solana address -k learn-the-metaplex-sdk-by-minting-an-nft/wallet.json'; const { stdout, stderr } = await __helpers.getCommandOutput(command); const walletAddress = stdout.trim(); const packageJson = JSON.parse( await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/package.json' ) ); assert.property( packageJson, 'env', 'The `package.json` file should have an `env` property.' ); assert.equal( packageJson.env.WALLET_ADDRESS, walletAddress, 'The `env.WALLET_ADDRESS` property should match the public key of `wallet.json`.' ); try { const { Connection } = await import('@solana/web3.js'); const { TOKEN_PROGRAM_ID } = await import('@solana/spl-token'); const connection = new Connection('http://127.0.0.1:8899'); const mintAccounts = await connection.getParsedProgramAccounts( TOKEN_PROGRAM_ID, { filters: [ { dataSize: 165 }, { memcmp: { offset: 32, bytes: packageJson.env.WALLET_ADDRESS } } ] } ); const mintAccount = mintAccounts.find( ({ account }) => account?.data?.parsed?.info?.mint === packageJson.env.MINT_ACCOUNT_ADDRESS ); assert.exists( mintAccount, 'The `env.MINT_ACCOUNT_ADDRESS` property should match an NFT owned by `wallet.json`.' ); } catch (e) { assert.fail(e); } ``` ## 40 ### --description-- Now that your NFT is deployed, and you have the mint account's public key, you can always find it using the public key. Create a new file called `get-nft.js`. ### --tests-- You should have a `get-nft.js` file. ```js const fileExists = __helpers.fileExists( 'learn-the-metaplex-sdk-by-minting-an-nft/get-nft.js' ); assert.isTrue(fileExists, 'You should have a `get-nft.js` file.'); ``` ## 41 ### --description-- Within `get-nft.js`, declare a `connection` variable with a configuration pointing to your local cluster. ### --tests-- You should have `const connection = new Connection('http://127.0.0.1:8899');` in `get-nft.js`. ```js const connectionVariableDeclaration = babelisedCode .getVariableDeclarations() .find(v => v.declarations?.[0]?.id?.name === 'connection'); assert.exists( connectionVariableDeclaration, 'You should declare a variable named `connection`' ); const newExpression = connectionVariableDeclaration.declarations[0].init; assert.equal( newExpression.callee.name, 'Connection', 'You should initialise `connection` with a new `Connection`' ); assert.equal( newExpression.arguments[0].value, 'http://127.0.0.1:8899', "You should create a new connection with `new Connection('http://127.0.0.1:8899')`" ); ``` You should import `Connection` from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists(importDeclaration, 'You should import from `@solana/web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'Connection', '`Connection` should be imported from `@solana/spl-token`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/get-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 42 ### --description-- Within `get-nft.js`, declare a `metaplex` variable with the same configuration as the `metaplex` variable in `create-nft.js`. ### --tests-- You should have `const metaplex = Metaplex.make(connection).use(keypairIdentity(WALLET_KEYPAIR)).use(localStorage({ baseUrl: 'http://127.0.0.1:3001/' }));` in `get-nft.js`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'metaplex'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /metaplex=Metaplex\.make\(connection\)\.use\(keypairIdentity\(WALLET_KEYPAIR\)\)\.use\(localStorage\(\{('|"|`)?baseUrl\1:('|"|`)http:\/\/127\.0\.0\.1:3001\/\2\}\)\)/ ); ``` You should import `Metaplex` from `@metaplex-foundation/js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@metaplex-foundation/js'; }); assert.exists( importDeclaration, 'You should import from `@metaplex-foundation/js`' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'Metaplex', '`Metaplex` should be imported from `@metaplex-foundation/js`' ); ``` You should import `keypairIdentity` from `@metaplex-foundation/js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@metaplex-foundation/js'; }); assert.exists( importDeclaration, 'You should import from `@metaplex-foundation/js`' ); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'keypairIdentity', '`keypairIdentity` should be imported from `@metaplex-foundation/js`' ); ``` You should import `WALLET_KEYPAIR` from `utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'You should import from `./utils.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'WALLET_KEYPAIR', '`WALLET_KEYPAIR` should be imported from `./utils.js`' ); ``` You should import `localStorage` from `utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'You should import from `./utils.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'localStorage', '`localStorage` should be imported from `./utils.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/get-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 43 ### --description-- Within `get-nft.js`, declare a `mintAddress` variable, and assign it the value of an instance of `PublicKey` constructed from the `MINT_ACCOUNT_ADDRESS` property from `pkg.env`. **Hint:** The `pkg` object is exported from `utils.js`. ### --tests-- You should have `const mintAddress = new PublicKey(pkg.env.MINT_ACCOUNT_ADDRESS);` in `get-nft.js`. ```js const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'mintAddress'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /mintAddress=new PublicKey\(pkg\.env\.MINT_ACCOUNT_ADDRESS\)/ ); ``` You should import `pkg` from `utils.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === './utils.js'; }); assert.exists(importDeclaration, 'You should import from `./utils.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'pkg', '`pkg` should be imported from `./utils.js`' ); ``` You should import `PublicKey` from `@solana/web3.js`. ```js const importDeclaration = babelisedCode.getImportDeclarations().find(i => { return i.source.value === '@solana/web3.js'; }); assert.exists(importDeclaration, 'You should import from `@solana/web3.js`'); const importSpecifiers = importDeclaration.specifiers.map(s => s.imported.name); assert.include( importSpecifiers, 'PublicKey', '`PublicKey` should be imported from `@solana/web3.js`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/get-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 44 ### --description-- To get the NFT, use the `findByMint` method on the `Metaplex.nfts()` class: ```typescript findByMint( { mintAddress: PublicKey; tokenAddress?: PublicKey; tokenOwner?: PublicKey; loadJsonMetadata?: boolean; } ): Promise ``` Pass in the required `mintAddress` property, and assign the awaited result to an `nft` variable. ### --tests-- You should have `const nft = await metaplex.nfts().findByMint({ mintAddress });` in `get-nft.js`. ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/get-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'nft'; }); assert.exists(variableDeclaration, 'An `nft` variable should exist'); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /nft=await metaplex\.nfts\(\)\.findByMint\(\{mintAddress\}\)/ ); ``` ## 45 ### --description-- Log the `nft` variable to the console. ### --tests-- You should have `console.log(nft);` in `get-nft.js`. ```js const consoleLogCallExpression = babelisedCode .getType('CallExpression') .find(c => { return ( c.callee.object?.name === 'console' && c.callee.property?.name === 'log' ); }); assert.exists(consoleLogCallExpression, 'A `console.log` call should exist'); const consoleLogArguments = consoleLogCallExpression.arguments; const ident = consoleLogArguments.find(a => { return a.name === 'nft'; }); assert.exists(ident, 'One of the arguments to `console.log` should be `nft`'); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/get-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 46 ### --description-- Run the `get-nft.js` script. ```bash node get-nft.js ``` ### --tests-- You should run `node get-nft.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal(lastCommand?.trim(), 'node get-nft.js'); ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The local storage driver should be running at `http://127.0.0.1:3001`. ```js try { const res = await fetch('http://127.0.0.1:3001/status/ping'); // Response should be 200 with text "pong" if (res.status === 200) { const text = await res.text(); if (text !== 'pong') { throw new Error(`Expected response text "pong", got ${text}`); } } else { throw new Error(`Expected status code 200, got ${res.status}`); } } catch (e) { assert.fail(e); } ``` ## 47 ### --description-- Notice the output includes a `json` property. This is the metadata for the NFT. The metadata includes the image URL, but not the image data itself. To get the image data, you can use the `download` method on the `Metaplex.storage()` class: ```typescript download( uri: string ): Promise ``` Declare a variable `imageData`, and assign it the awaited result of calling the `download` method, passing in the `image` property of the NFT metadata. ### --tests-- You should have `const imageData = await metaplex.storage().download(nft.json.image);` in `get-nft.js`. ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/get-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); const variableDeclaration = babelisedCode.getVariableDeclarations().find(v => { return v.declarations?.[0]?.id?.name === 'imageData'; }); const minifiedCode = babelisedCode.generateCode(variableDeclaration, { minified: true }); assert.match( minifiedCode, /imageData=await metaplex\.storage\(\)\.download\(nft\.json\.image\)/ ); ``` ## 48 ### --description-- Log the `imageData` variable to the console. ### --tests-- You should have `console.log(imageData);` in `get-nft.js`. ```js const consoleLogCallExpressions = babelisedCode .getType('CallExpression') .filter(c => { return ( c.callee.object?.name === 'console' && c.callee.property?.name === 'log' ); }); assert.exists(consoleLogCallExpressions, 'A `console.log` call should exist'); const is_ident = consoleLogCallExpressions.some(c => { const consoleLogArguments = c.arguments; const ident = consoleLogArguments.find(a => { return a.name === 'imageData'; }); return ident; }); assert.exists( is_ident, 'One of the arguments to `console.log` should be `imageData`' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/get-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 49 ### --description-- Run the `get-nft.js` script. ### --tests-- You should run `node get-nft.js` in the terminal. ```js const lastCommand = await __helpers.getLastCommand(); assert.equal(lastCommand?.trim(), 'node get-nft.js'); ``` The validator should be running at `http://127.0.0.1:8899`. ```js const command = `curl http://127.0.0.1:8899 -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getHealth"}'`; const { stdout, stderr } = await __helpers.getCommandOutput(command); try { const jsonOut = JSON.parse(stdout); assert.deepInclude(jsonOut, { result: 'ok' }); } catch (e) { assert.fail(e, 'Try running `solana-test-validator` in a separate terminal'); } ``` The local storage driver should be running at `http://127.0.0.1:3001`. ```js try { const res = await fetch('http://127.0.0.1:3001/status/ping'); // Response should be 200 with text "pong" if (res.status === 200) { const text = await res.text(); if (text !== 'pong') { throw new Error(`Expected response text "pong", got ${text}`); } } else { throw new Error(`Expected status code 200, got ${res.status}`); } } catch (e) { assert.fail(e); } ``` ## 50 ### --description-- Now, with the buffer data, use the `writeFile` function from the `fs/promises` module to reconstruct the image file. Re-run the script to generate the image file. ### --tests-- You should have `await writeFile(, imageData.buffer);` in `get-nft.js`. ```js const expressionStatement = babelisedCode.getExpressionStatements().find(e => { return e.expression?.argument?.callee?.name === 'writeFile'; }); assert.exists(expressionStatement, 'An `await writeFile()` call should exist'); const [fileName, imageBufferMemberExpression] = expressionStatement.expression.argument.arguments; assert.isString(fileName?.value, 'The first argument should be a string'); assert.equal( imageBufferMemberExpression?.object?.name, 'imageData', 'The second argument should be `imageData.buffer`' ); assert.equal( imageBufferMemberExpression?.property?.name, 'buffer', 'The second argument should be `imageData.buffer`' ); ``` You should have a `.png` file in the project root. ```js const dir = await __helpers.getDirectory( 'learn-the-metaplex-sdk-by-minting-an-nft' ); assert.exists( dir.find(f => f.endsWith('.png')), 'A .png file should exist in the project root' ); ``` ### --before-all-- ```js const codeString = await __helpers.getFile( 'learn-the-metaplex-sdk-by-minting-an-nft/get-nft.js' ); const babelisedCode = new __helpers.Babeliser(codeString); global.babelisedCode = babelisedCode; ``` ### --after-all-- ```js delete global.babelisedCode; ``` ## 51 ### --description-- Contratulations on finishing this project! Feel free to play with your code. **Summary** The main differences between NFT's and fungible tokens are: - NFT's are not divisible - Only one NFT of a mint can exist - No more NFT's can be minted once the mint authority is removed The Metaplex SDK provides a simple interface for minting NFT's and managing the metadata associated with them: ```javascript // Create a new Metaplex instance const metaplex = Metaplex.make(connection); // Attach a wallet to use for transactions metaplex.use(keypairIdentity(wallet_keypair)); // Attach a storage driver to use for uploading and downloading metadata metaplex.use(storage_driver); // Create a MetaplexFile const file = toMetaplexFile(image_buffer); // Upload the file to the storage driver const image_uri = await metaplex.storage().upload(file); // Upload the metadata to the storage driver const { uri } = await metaplex.nfts().uploadMetadata({ name: 'fCC', description: 'An image of the freeCodeCamp logo', image: image_uri }); // Create a new NFT mint const nftInfo = await metaplex.nfts().create({ name: 'fCC', uri, sellerFeeBasisPoints: 1000, maxSupply: 1 }); // Get nft data const nft = await metaplex.nfts().findByMint({ mintAddress: nftInfo.mintAddress }); // Download image from storage driver const image = await metaplex.storage().download(nft.json.image); ``` 🎆 Once you are done, enter `done` in the terminal. ### --tests-- You should enter `done` in the terminal ```js const lastCommand = await __helpers.getLastCommand(); assert.include(lastCommand, 'done'); ``` ## --fcc-end-- ================================================ FILE: freecodecamp.conf.json ================================================ { "path": ".", "version": "1.0.3", "scripts": { "develop-course": "NODE_ENV=development node ./node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/server.js", "run-course": "NODE_ENV=production node ./node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/server.js", "test": { "functionName": "handleMessage", "arguments": [ { "message": "Hello World!", "type": "info" } ] } }, "workspace": { "previews": [ { "open": true, "url": "http://localhost:8080", "showLoader": true, "timeout": 4000 } ] }, "bash": { ".bashrc": "./bash/.bashrc", "sourcerer.sh": "./bash/sourcerer.sh" }, "client": { "assets": { "header": "./client/assets/fcc_primary_large.svg", "favicon": "./client/assets/fcc_primary_small.svg" }, "landing": { "description": "Learn the Solana toolchain, and test your new-found skills.", "faq-link": "#", "faq-text": "https://github.com/freeCodeCamp/solana-curriculum/issues" }, "static": { "/images/phantom": "./curriculum/images/phantom", "/images/devnet": "./curriculum/images/devnet" } }, "config": { "projects.json": "./config/projects.json", "state.json": "./config/state.json" }, "curriculum": { "locales": { "english": "./curriculum/locales/english" } }, "tooling": { "helpers": "./tooling/helpers.js" } } ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-1/.gitkeep ================================================ ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/.prettierignore ================================================ .anchor .DS_Store target node_modules dist build test-ledger ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/Anchor.toml ================================================ [features] seeds = false skip-lint = false [programs.localnet] tic_tac_toe = "BUfb6FXLkiSpMnJnMR4Q5uGZYZkaNGytjhLwiiJQsE8F" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/Cargo.toml ================================================ [workspace] members = [ "programs/*" ] [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/migrations/deploy.ts ================================================ // Migrations are an early feature. Currently, they're nothing more than this // single deploy script that's invoked from the CLI, injecting a provider // configured from the workspace's Anchor.toml. const anchor = require("@coral-xyz/anchor"); module.exports = async function (provider) { // Configure client to use the provider. anchor.setProvider(provider); // Add your deploy script here. }; ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/package.json ================================================ { "scripts": { "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" }, "dependencies": { "@coral-xyz/anchor": "^0.27.0" }, "devDependencies": { "chai": "^4.3.4", "mocha": "^9.0.3", "ts-mocha": "^10.0.0", "@types/bn.js": "^5.1.0", "@types/chai": "^4.3.0", "@types/mocha": "^9.0.0", "typescript": "^4.3.5", "prettier": "^2.6.2" } } ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/programs/tic-tac-toe/Cargo.toml ================================================ [package] name = "tic-tac-toe" version = "0.1.0" description = "Created with Anchor" edition = "2021" [lib] crate-type = ["cdylib", "lib"] name = "tic_tac_toe" [features] no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] [dependencies] anchor-lang = "0.27.0" num-traits = "0.2" num-derive = "0.3" ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/programs/tic-tac-toe/Xargo.toml ================================================ [target.bpfel-unknown-unknown.dependencies.std] features = [] ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/programs/tic-tac-toe/src/lib.rs ================================================ use anchor_lang::prelude::*; use num_derive; use num_traits::FromPrimitive; declare_id!("BUfb6FXLkiSpMnJnMR4Q5uGZYZkaNGytjhLwiiJQsE8F"); #[program] pub mod tic_tac_toe { use super::*; pub fn setup_game( ctx: Context, player_two_pubkey: Pubkey, _game_id: String, ) -> Result<()> { let player_one = &ctx.accounts.player_one; let player_one_pubkey = player_one.key(); let game = &mut ctx.accounts.game; game.start([player_one_pubkey, player_two_pubkey]) } pub fn play(ctx: Context, tile: Tile) -> Result<()> { let game = &mut ctx.accounts.game; require_keys_eq!( game.current_player(), ctx.accounts.player.key(), TicTacToeError::NotPlayersTurn ); game.play(&tile) } } #[derive(Accounts)] #[instruction(player_two_pubkey: Pubkey, _game_id: String)] pub struct SetupGame<'info> { #[account( init, payer = player_one, space = 8 + Game::MAXIMUM_SIZE, seeds = [b"game", player_one.key().as_ref(), _game_id.as_bytes()], bump )] pub game: Account<'info, Game>, #[account(mut)] pub player_one: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct Game { players: [Pubkey; 2], // (32 * 2) turn: u8, // 1 board: [[Option; 3]; 3], // 9 * (1 + 1) = 18 state: GameState, // 32 + 1 } #[derive(Accounts)] pub struct Play<'info> { #[account(mut)] pub game: Account<'info, Game>, pub player: Signer<'info>, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)] pub enum GameState { Active, Tie, Won { winner: Pubkey }, } #[derive(AnchorSerialize, AnchorDeserialize, num_derive::FromPrimitive, Copy, Clone, PartialEq)] pub enum Sign { X, O, } impl Game { pub const MAXIMUM_SIZE: usize = (32 * 2) + 1 + (9 * (1 + 1)) + (32 + 1); pub fn start(&mut self, players: [Pubkey; 2]) -> Result<()> { require_eq!(self.turn, 0, TicTacToeError::GameAlreadyStarted); self.players = players; self.turn = 1; Ok(()) } pub fn is_active(&self) -> bool { self.state == GameState::Active } fn current_player_index(&self) -> usize { ((self.turn - 1) % 2) as usize } pub fn current_player(&self) -> Pubkey { self.players[self.current_player_index()] } pub fn play(&mut self, tile: &Tile) -> Result<()> { require!(self.is_active(), TicTacToeError::GameAlreadyOver); match tile { tile @ Tile { row: 0..=2, column: 0..=2, } => match self.board[tile.row as usize][tile.column as usize] { Some(_) => return Err(TicTacToeError::TileAlreadySet.into()), None => { self.board[tile.row as usize][tile.column as usize] = Some(Sign::from_usize(self.current_player_index()).unwrap()); } }, _ => return Err(TicTacToeError::TileOutOfBounds.into()), } self.update_state(); if GameState::Active == self.state { self.turn += 1; } Ok(()) } fn is_winning_trio(&self, trio: [(usize, usize); 3]) -> bool { let [first, second, third] = trio; self.board[first.0][first.1].is_some() && self.board[first.0][first.1] == self.board[second.0][second.1] && self.board[first.0][first.1] == self.board[third.0][third.1] } fn update_state(&mut self) { for i in 0..=2 { // three of the same in one row if self.is_winning_trio([(i, 0), (i, 1), (i, 2)]) { self.state = GameState::Won { winner: self.current_player(), }; return; } // three of the same in one column if self.is_winning_trio([(0, i), (1, i), (2, i)]) { self.state = GameState::Won { winner: self.current_player(), }; return; } } // three of the same in one diagonal if self.is_winning_trio([(0, 0), (1, 1), (2, 2)]) || self.is_winning_trio([(0, 2), (1, 1), (2, 0)]) { self.state = GameState::Won { winner: self.current_player(), }; return; } // reaching this code means the game has not been won, // so if there are unfilled tiles left, it's still active for row in 0..=2 { for column in 0..=2 { if self.board[row][column].is_none() { return; } } } // game has not been won // game has no more free tiles // -> game ends in a tie self.state = GameState::Tie; } } #[derive(AnchorSerialize, AnchorDeserialize)] pub struct Tile { row: u8, column: u8, } #[error_code] pub enum TicTacToeError { TileOutOfBounds, TileAlreadySet, GameAlreadyOver, NotPlayersTurn, GameAlreadyStarted, } ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/tests/tic-tac-toe.ts ================================================ import { AnchorProvider, workspace, setProvider, Program } from '@coral-xyz/anchor'; import { TicTacToe } from '../target/types/tic_tac_toe'; describe('TicTacToe', () => { // Configure the client to use the local cluster. setProvider(AnchorProvider.env()); const program = workspace.TicTacToe as Program; it('Is initialized!', async () => { // Add your test here. const tx = await program.methods.initialize().rpc(); console.log('Your transaction signature', tx); }); }); ================================================ FILE: learn-anchor-by-building-tic-tac-toe-part-2/tic-tac-toe/tsconfig.json ================================================ { "compilerOptions": { "types": ["mocha", "chai"], "typeRoots": ["./node_modules/@types"], "lib": ["es2015"], "module": "commonjs", "target": "es6", "esModuleInterop": true } } ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/.gitkeep ================================================ ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/.gitignore ================================================ .anchor .DS_Store target **/*.rs.bk node_modules test-ledger .yarn ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/Anchor.toml ================================================ [features] seeds = false skip-lint = false [programs.localnet] tic_tac_toe = "5xGwZASoE5ZgxKgaisJNaGTGzMKzjyyBGv9FCUtu2m1c" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/Cargo.toml ================================================ [workspace] members = [ "programs/*" ] [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/app/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/app/index.css ================================================ body { background: black; width: 100%; height: 100vh; margin: 0; padding: 0; } form, table { color: whitesmoke; margin: 1em auto; text-align: center; } form > fieldset > button:hover { cursor: pointer; background-color: #cd3737; } tbody > tr { border: solid 1px whitesmoke; } td { width: 100px; height: 100px; text-align: center; vertical-align: middle; font-size: 50px; border: solid 1px whitesmoke; } td:hover { cursor: pointer; background-color: #4b8270; } #errors { color: red; font-size: 1.5em; margin: 0 auto; text-align: center; } #keypairs { color: greenyellow; } /* Wrap text in #keypairs > li */ #keypairs > li > pre { text-wrap: wrap; word-wrap: break-word; color: #2ca08d; background-color: #2e3136; margin: 1rem; padding: 0.5rem; } #spinner { margin: 0 auto; text-align: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .lds-ripple { display: inline-block; position: relative; width: 80px; height: 80px; } .lds-ripple div { position: absolute; border: 4px solid #fff; opacity: 1; border-radius: 50%; animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; } .lds-ripple div:nth-child(2) { animation-delay: -0.5s; } @keyframes lds-ripple { 0% { top: 36px; left: 36px; width: 0; height: 0; opacity: 1; } 100% { top: 0px; left: 0px; width: 72px; height: 72px; opacity: 0; } } .hidden { display: none; } ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/app/index.html ================================================ Tic-Tac-Toe

OR

Turn:
Player:

    ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/app/index.js ================================================ import { Keypair, PublicKey } from '@solana/web3.js'; import player_one_keypair from '../player-one.json'; import player_two_keypair from '../player-two.json'; import { displayError, removeLoader, showLoader } from './utils'; import { PROGRAM_ID, deriveGamePublicKey } from './web3'; const gameIdEl = document.getElementById('game-id'); const startGameBtnEl = document.getElementById('start-game'); const tableBodyEl = document.querySelector('tbody'); const tdEls = tableBodyEl.querySelectorAll('td'); const joinGameBtnEl = document.getElementById('join-game'); const playerOnePublicKeyEl = document.getElementById('player-one-public-key'); const playerTwoPublicKeyEl = document.getElementById('player-two-public-key'); const connectWalletBtnEl = document.getElementById('connect-wallet'); const keypairEl = document.getElementById('keypair'); const keypairsEl = document.getElementById('keypairs'); connectWalletBtnEl.addEventListener('click', ev => { ev.preventDefault(); displayError(); showLoader(); try { const keypair = keypairEl.value; sessionStorage.setItem('keypair', keypair); // TODO: Connect to wallet connectWalletBtnEl.style.backgroundColor = 'green'; } catch (e) { displayError(e); } finally { removeLoader(); } }); startGameBtnEl.addEventListener('click', async event => { event.preventDefault(); displayError(); showLoader(); try { // TODO: Create a new game } catch (e) { displayError(e); } finally { removeLoader(); } }); joinGameBtnEl.addEventListener('click', async event => { event.preventDefault(); displayError(); showLoader(); try { // TODO: Join an existing game } catch (e) { displayError(e); } finally { removeLoader(); } }); tdEls.forEach(tdEl => { tdEl.addEventListener('click', async event => { event.preventDefault(); displayError(); showLoader(); try { // TODO: Play tile } catch (e) { displayError(e); } finally { removeLoader(); } }); }); document.addEventListener('DOMContentLoaded', async _event => { startWithPossibleValues(); const interval = setInterval(async () => { showLoader(); try { // TODO: If program and gamePublicKey exist, update board } catch (e) { console.debug(e); } finally { removeLoader(); } }, 3000); // A game of tic-tac-toe should not last long, // but for development this is commented out // setTimeout(() => { // clearInterval(interval); // }, 300_000); return () => { clearInterval(interval); }; }); // --------------------- // CONVENIENCE FUNCTIONS // --------------------- function startWithPossibleValues() { const player_one_publicKey = Keypair.fromSecretKey( new Uint8Array(player_one_keypair) ).publicKey.toBase58(); const player_two_publicKey = Keypair.fromSecretKey( new Uint8Array(player_two_keypair) ).publicKey.toBase58(); playerOnePublicKeyEl.value = player_one_publicKey; playerTwoPublicKeyEl.value = player_two_publicKey; const keypairs = [player_one_keypair, player_two_keypair]; keypairs.forEach((keypair, i) => { const li = document.createElement('li'); const code = document.createElement('code'); const pre = document.createElement('pre'); pre.appendChild(code); code.textContent = JSON.stringify(keypair); li.textContent = `Player ${i + 1} Public Key: `; li.appendChild(pre); keypairsEl.appendChild(li); }); } playerOnePublicKeyEl.addEventListener('change', e => { const playerOnePublicKey = e.target.value; sessionStorage.setItem('playerOnePublicKey', playerOnePublicKey); }); playerTwoPublicKeyEl.addEventListener('change', e => { const playerTwoPublicKey = e.target.value; sessionStorage.setItem('playerTwoPublicKey', playerTwoPublicKey); }); gameIdEl.addEventListener('change', e => { const gameId = e.target.value; sessionStorage.setItem('gameId', gameId); const gamePublicKey = deriveGamePublicKey( new PublicKey(playerOnePublicKeyEl.value), gameId, PROGRAM_ID ); sessionStorage.setItem('gamePublicKey', gamePublicKey.toBase58()); }); ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/app/package.json ================================================ { "name": "app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "devDependencies": { "vite": "4.5.6" }, "dependencies": {} } ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/app/utils.js ================================================ const errorsEl = document.getElementById('errors'); const spinnerEl = document.getElementById('spinner'); const tableBodyEl = document.querySelector('tbody'); const tdEls = tableBodyEl.querySelectorAll('td'); /** * @param {{ x: {} } | { o: {} } | null} tile * @returns 'X' | 'O' | '' */ export function tileToString(tile) { if (tile?.x) { return 'X'; } if (tile?.o) { return 'O'; } return ''; } /** * @param {({ x: {} } | { o: {} } | null)[][]} board */ export function setTiles(board) { for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { const tile = board[i][j]; const tileEl = tdEls[i * 3 + j]; tileEl.textContent = tileToString(tile); } } } /** * @param {Error | undefined} error */ export function displayError(error) { if (error) { console.error(error); errorsEl.innerText = error.message; } else { errorsEl.innerText = ''; } } /** * @param {number} id * @returns {{ row: number; column: number }} */ export function idToTile(id) { for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { const tile = tdEls[i * 3 + j]; if (tile.id === id) { return { row: i, column: j }; } } } } export function showLoader() { spinnerEl.classList.remove('hidden'); } export function removeLoader() { spinnerEl.classList.add('hidden'); } ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/app/wallet.js ================================================ export class Wallet { constructor(keypair) { this.keypair = keypair; this._publicKey = keypair.publicKey; } async signTransaction(tx) { tx.sign(this.keypair); return tx; } async signAllTransactions(txs) { txs.map(tx => tx.sign(this.keypair)); return txs; } get publicKey() { return this._publicKey; } } ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/migrations/deploy.ts ================================================ // Migrations are an early feature. Currently, they're nothing more than this // single deploy script that's invoked from the CLI, injecting a provider // configured from the workspace's Anchor.toml. const anchor = require("@coral-xyz/anchor"); module.exports = async function (provider) { // Configure client to use the provider. anchor.setProvider(provider); // Add your deploy script here. }; ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/package.json ================================================ { "scripts": { "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" }, "dependencies": { "@coral-xyz/anchor": "^0.28.0" }, "devDependencies": { "@types/bn.js": "^5.1.0", "@types/chai": "^4.3.0", "@types/mocha": "^9.0.0", "chai": "^4.3.4", "mocha": "^9.0.3", "ts-mocha": "^10.0.0", "typescript": "^4.3.5" } } ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/programs/tic-tac-toe/Cargo.toml ================================================ [package] name = "tic-tac-toe" version = "0.1.0" description = "Created with Anchor" edition = "2021" [lib] crate-type = ["cdylib", "lib"] name = "tic_tac_toe" [features] no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] [dependencies] anchor-lang = "0.28.0" num-traits = "0.2" num-derive = "0.3" ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/programs/tic-tac-toe/Xargo.toml ================================================ [target.bpfel-unknown-unknown.dependencies.std] features = [] ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/programs/tic-tac-toe/src/lib.rs ================================================ use anchor_lang::prelude::*; use num_derive; use num_traits::FromPrimitive; declare_id!("5xGwZASoE5ZgxKgaisJNaGTGzMKzjyyBGv9FCUtu2m1c"); #[program] pub mod tic_tac_toe { use super::*; pub fn setup_game( ctx: Context, player_two_pubkey: Pubkey, _game_id: String, ) -> Result<()> { let player_one = &ctx.accounts.player_one; let player_one_pubkey = player_one.key(); let game = &mut ctx.accounts.game; game.start([player_one_pubkey, player_two_pubkey]) } pub fn play(ctx: Context, tile: Tile) -> Result<()> { let game = &mut ctx.accounts.game; require_keys_eq!( game.current_player(), ctx.accounts.player.key(), TicTacToeError::NotPlayersTurn ); game.play(&tile) } } #[derive(Accounts)] #[instruction(player_two_pubkey: Pubkey, _game_id: String)] pub struct SetupGame<'info> { #[account( init, payer = player_one, space = 8 + Game::MAXIMUM_SIZE, seeds = [b"game", player_one.key().as_ref(), _game_id.as_bytes()], bump )] pub game: Account<'info, Game>, #[account(mut)] pub player_one: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct Game { players: [Pubkey; 2], // (32 * 2) turn: u8, // 1 board: [[Option; 3]; 3], // 9 * (1 + 1) = 18 state: GameState, // 32 + 1 } #[derive(Accounts)] pub struct Play<'info> { #[account(mut)] pub game: Account<'info, Game>, pub player: Signer<'info>, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)] pub enum GameState { Active, Tie, Won { winner: Pubkey }, } #[derive(AnchorSerialize, AnchorDeserialize, num_derive::FromPrimitive, Copy, Clone, PartialEq)] pub enum Sign { X, O, } impl Game { pub const MAXIMUM_SIZE: usize = (32 * 2) + 1 + (9 * (1 + 1)) + (32 + 1); pub fn start(&mut self, players: [Pubkey; 2]) -> Result<()> { require_eq!(self.turn, 0, TicTacToeError::GameAlreadyStarted); self.players = players; self.turn = 1; Ok(()) } pub fn is_active(&self) -> bool { self.state == GameState::Active } fn current_player_index(&self) -> usize { ((self.turn - 1) % 2) as usize } pub fn current_player(&self) -> Pubkey { self.players[self.current_player_index()] } pub fn play(&mut self, tile: &Tile) -> Result<()> { require!(self.is_active(), TicTacToeError::GameAlreadyOver); match tile { tile @ Tile { row: 0..=2, column: 0..=2, } => match self.board[tile.row as usize][tile.column as usize] { Some(_) => return Err(TicTacToeError::TileAlreadySet.into()), None => { self.board[tile.row as usize][tile.column as usize] = Some(Sign::from_usize(self.current_player_index()).unwrap()); } }, _ => return Err(TicTacToeError::TileOutOfBounds.into()), } self.update_state(); if GameState::Active == self.state { self.turn += 1; } Ok(()) } fn is_winning_trio(&self, trio: [(usize, usize); 3]) -> bool { let [first, second, third] = trio; self.board[first.0][first.1].is_some() && self.board[first.0][first.1] == self.board[second.0][second.1] && self.board[first.0][first.1] == self.board[third.0][third.1] } fn update_state(&mut self) { for i in 0..=2 { // three of the same in one row if self.is_winning_trio([(i, 0), (i, 1), (i, 2)]) { self.state = GameState::Won { winner: self.current_player(), }; return; } // three of the same in one column if self.is_winning_trio([(0, i), (1, i), (2, i)]) { self.state = GameState::Won { winner: self.current_player(), }; return; } } // three of the same in one diagonal if self.is_winning_trio([(0, 0), (1, 1), (2, 2)]) || self.is_winning_trio([(0, 2), (1, 1), (2, 0)]) { self.state = GameState::Won { winner: self.current_player(), }; return; } // reaching this code means the game has not been won, // so if there are unfilled tiles left, it's still active for row in 0..=2 { for column in 0..=2 { if self.board[row][column].is_none() { return; } } } // game has not been won // game has no more free tiles // -> game ends in a tie self.state = GameState::Tie; } } #[derive(AnchorSerialize, AnchorDeserialize)] pub struct Tile { row: u8, column: u8, } #[error_code] pub enum TicTacToeError { TileOutOfBounds, TileAlreadySet, GameAlreadyOver, NotPlayersTurn, GameAlreadyStarted, } ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/tests/tic-tac-toe.ts ================================================ import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { TicTacToe } from "../target/types/tic_tac_toe"; describe("tic-tac-toe", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.TicTacToe as Program; it("Is initialized!", async () => { // Add your test here. const tx = await program.methods.initialize().rpc(); console.log("Your transaction signature", tx); }); }); ================================================ FILE: learn-how-to-build-a-client-side-app-part-1/tic-tac-toe/tsconfig.json ================================================ { "compilerOptions": { "types": ["mocha", "chai"], "typeRoots": ["./node_modules/@types"], "lib": ["es2015"], "module": "commonjs", "target": "es6", "esModuleInterop": true } } ================================================ FILE: learn-how-to-build-a-client-side-app-part-2/app/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: learn-how-to-build-a-client-side-app-part-2/app/index.css ================================================ body { background: black; width: 100%; height: 100vh; margin: 0; padding: 0; } form, table { color: whitesmoke; margin: 1em auto; text-align: center; } form > fieldset > button:hover { cursor: pointer; background-color: #cd3737; } tbody > tr { border: solid 1px whitesmoke; } td { width: 100px; height: 100px; text-align: center; vertical-align: middle; font-size: 50px; border: solid 1px whitesmoke; } td:hover { cursor: pointer; background-color: #4b8270; } #errors { color: red; font-size: 1.5em; margin: 0 auto; text-align: center; } #keypairs { color: greenyellow; } /* Wrap text in #keypairs > li */ #keypairs > li > pre { text-wrap: wrap; word-wrap: break-word; color: #2ca08d; background-color: #2e3136; margin: 1rem; padding: 0.5rem; } #spinner { margin: 0 auto; text-align: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .lds-ripple { display: inline-block; position: relative; width: 80px; height: 80px; } .lds-ripple div { position: absolute; border: 4px solid #fff; opacity: 1; border-radius: 50%; animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; } .lds-ripple div:nth-child(2) { animation-delay: -0.5s; } @keyframes lds-ripple { 0% { top: 36px; left: 36px; width: 0; height: 0; opacity: 1; } 100% { top: 0px; left: 0px; width: 72px; height: 72px; opacity: 0; } } .hidden { display: none; } ================================================ FILE: learn-how-to-build-a-client-side-app-part-2/app/index.html ================================================ Tic-Tac-Toe

    OR

    Turn:
    Player:

      ================================================ FILE: learn-how-to-build-a-client-side-app-part-2/app/index.js ================================================ import { Keypair, PublicKey } from '@solana/web3.js'; import player_one_keypair from '../player-one.json'; import player_two_keypair from '../player-two.json'; import { displayError, removeLoader, showLoader } from './utils'; import { PROGRAM_ID, connectWallet, deriveGamePublicKey, handlePlay, startGame, updateBoard } from './web3'; const gameIdEl = document.getElementById('game-id'); const startGameBtnEl = document.getElementById('start-game'); const tableBodyEl = document.querySelector('tbody'); const tdEls = tableBodyEl.querySelectorAll('td'); const joinGameBtnEl = document.getElementById('join-game'); const playerOnePublicKeyEl = document.getElementById('player-one-public-key'); const playerTwoPublicKeyEl = document.getElementById('player-two-public-key'); const connectWalletBtnEl = document.getElementById('connect-wallet'); const keypairEl = document.getElementById('keypair'); const keypairsEl = document.getElementById('keypairs'); connectWalletBtnEl.addEventListener('click', ev => { ev.preventDefault(); displayError(); showLoader(); try { const keypair = keypairEl.value; sessionStorage.setItem('keypair', keypair); // TODO: Connect to wallet connectWallet(); connectWalletBtnEl.style.backgroundColor = 'green'; } catch (e) { displayError(e); } finally { removeLoader(); } }); startGameBtnEl.addEventListener('click', async event => { event.preventDefault(); displayError(); showLoader(); try { // TODO: Create a new game await startGame(); } catch (e) { displayError(e); } finally { removeLoader(); } }); joinGameBtnEl.addEventListener('click', async event => { event.preventDefault(); displayError(); showLoader(); try { // TODO: Join an existing game await updateBoard(); } catch (e) { displayError(e); } finally { removeLoader(); } }); tdEls.forEach(tdEl => { tdEl.addEventListener('click', async event => { event.preventDefault(); displayError(); showLoader(); try { // TODO: Play tile await handlePlay(event.target.id); } catch (e) { displayError(e); } finally { removeLoader(); } }); }); document.addEventListener('DOMContentLoaded', async _event => { startWithPossibleValues(); const interval = setInterval(async () => { showLoader(); try { // TODO: If program and gamePublicKey exist, update board const gamePublicKey = sessionStorage.getItem('gamePublicKey'); if (program && gamePublicKey) { await updateBoard(); } } catch (e) { console.debug(e); } finally { removeLoader(); } }, 3000); // A game of tic-tac-toe should not last long, // but for development this is commented out // setTimeout(() => { // clearInterval(interval); // }, 300_000); return () => { clearInterval(interval); }; }); // --------------------- // CONVENIENCE FUNCTIONS // --------------------- function startWithPossibleValues() { const player_one_publicKey = Keypair.fromSecretKey( new Uint8Array(player_one_keypair) ).publicKey.toBase58(); const player_two_publicKey = Keypair.fromSecretKey( new Uint8Array(player_two_keypair) ).publicKey.toBase58(); playerOnePublicKeyEl.value = player_one_publicKey; playerTwoPublicKeyEl.value = player_two_publicKey; const keypairs = [player_one_keypair, player_two_keypair]; keypairs.forEach((keypair, i) => { const li = document.createElement('li'); const code = document.createElement('code'); const pre = document.createElement('pre'); pre.appendChild(code); code.textContent = JSON.stringify(keypair); li.textContent = `Player ${i + 1} Public Key: `; li.appendChild(pre); keypairsEl.appendChild(li); }); } playerOnePublicKeyEl.addEventListener('change', e => { const playerOnePublicKey = e.target.value; sessionStorage.setItem('playerOnePublicKey', playerOnePublicKey); }); playerTwoPublicKeyEl.addEventListener('change', e => { const playerTwoPublicKey = e.target.value; sessionStorage.setItem('playerTwoPublicKey', playerTwoPublicKey); }); gameIdEl.addEventListener('change', e => { const gameId = e.target.value; sessionStorage.setItem('gameId', gameId); const gamePublicKey = deriveGamePublicKey( new PublicKey(playerOnePublicKeyEl.value), gameId, PROGRAM_ID ); sessionStorage.setItem('gamePublicKey', gamePublicKey.toBase58()); }); ================================================ FILE: learn-how-to-build-a-client-side-app-part-2/app/package.json ================================================ { "name": "app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "devDependencies": { "vite": "4.5.6" }, "dependencies": { "@solana/web3.js": "1.87.7", "@coral-xyz/anchor": "0.28.1-beta.1" } } ================================================ FILE: learn-how-to-build-a-client-side-app-part-2/app/utils.js ================================================ const errorsEl = document.getElementById('errors'); const spinnerEl = document.getElementById('spinner'); const tableBodyEl = document.querySelector('tbody'); const tdEls = tableBodyEl.querySelectorAll('td'); /** * @param {{ x: {} } | { o: {} } | null} tile * @returns 'X' | 'O' | '' */ export function tileToString(tile) { if (tile?.x) { return 'X'; } if (tile?.o) { return 'O'; } return ''; } /** * @param {({ x: {} } | { o: {} } | null)[][]} board */ export function setTiles(board) { for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { const tile = board[i][j]; const tileEl = tdEls[i * 3 + j]; tileEl.textContent = tileToString(tile); } } } /** * @param {Error | undefined} error */ export function displayError(error) { if (error) { console.error(error); errorsEl.innerText = error.message; } else { errorsEl.innerText = ''; } } /** * @param {number} id * @returns {{ row: number; column: number }} */ export function idToTile(id) { for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { const tile = tdEls[i * 3 + j]; if (tile.id === id) { return { row: i, column: j }; } } } } export function showLoader() { spinnerEl.classList.remove('hidden'); } export function removeLoader() { spinnerEl.classList.add('hidden'); } ================================================ FILE: learn-how-to-build-a-client-side-app-part-2/app/wallet.js ================================================ export class Wallet { constructor(keypair) { this.keypair = keypair; this._publicKey = keypair.publicKey; } async signTransaction(tx) { tx.sign(this.keypair); return tx; } async signAllTransactions(txs) { txs.map(tx => tx.sign(this.keypair)); return txs; } get publicKey() { return this._publicKey; } } ================================================ FILE: learn-how-to-build-a-client-side-app-part-2/app/web3.js ================================================ import { AnchorProvider, Program, setProvider } from '@coral-xyz/anchor'; import { Wallet } from './wallet.js'; import { IDL } from '../tic_tac_toe'; import { Connection, PublicKey, Keypair } from '@solana/web3.js'; import { Buffer } from 'buffer'; import { idToTile, setTiles } from './utils.js'; const turnEl = document.getElementById('turn'); const playerTurnEl = document.getElementById('player-turn'); window.Buffer = Buffer; window.program = null; export const PROGRAM_ID = new PublicKey( '5xGwZASoE5ZgxKgaisJNaGTGzMKzjyyBGv9FCUtu2m1c' ); export const connection = new Connection('http://localhost:8899', 'processed'); export function connectWallet() { const keypairArr = sessionStorage.getItem('keypair'); const uint = new Uint8Array(JSON.parse(keypairArr)); const keypair = Keypair.fromSecretKey(uint); const wallet = new Wallet(keypair); const provider = new AnchorProvider(connection, wallet, {}); setProvider(provider); const program = new Program(IDL, PROGRAM_ID, provider); window.program = program; } export async function startGame() { const gameId = sessionStorage.getItem('gameId'); const playerOnePublicKey = new PublicKey( sessionStorage.getItem('playerOnePublicKey') ); const playerTwoPublicKey = new PublicKey( sessionStorage.getItem('playerTwoPublicKey') ); const gamePublicKey = deriveGamePublicKey( playerOnePublicKey, gameId, PROGRAM_ID ); sessionStorage.setItem('gamePublicKey', gamePublicKey.toString()); const keypairStr = sessionStorage.getItem('keypair'); const keypairArr = JSON.parse(keypairStr); const uint8Arr = new Uint8Array(keypairArr); const keypair = Keypair.fromSecretKey(uint8Arr); await program.methods .setupGame(playerTwoPublicKey, gameId) .accounts({ playerOne: keypair.publicKey, game: gamePublicKey }) .signers([keypair]) .rpc(); await updateBoard(); } export async function handlePlay(id) { const tile = idToTile(id); await updateBoard(); const keypairStr = sessionStorage.getItem('keypair'); const keypairArr = JSON.parse(keypairStr); const uint8Arr = new Uint8Array(keypairArr); const keypair = Keypair.fromSecretKey(uint8Arr); const gamePublicKey = new PublicKey(sessionStorage.getItem('gamePublicKey')); await program.methods .play(tile) .accounts({ player: keypair.publicKey, game: gamePublicKey }) .signers([keypair]) .rpc(); await updateBoard(); } export function deriveGamePublicKey(playerOnePublicKey, gameId, programId) { const [gamePublicKey, _] = PublicKey.findProgramAddressSync( [Buffer.from('game'), playerOnePublicKey.toBuffer(), Buffer.from(gameId)], programId ); return gamePublicKey; } export async function getGameAccount() { const gamePublicKey = new PublicKey(sessionStorage.getItem('gamePublicKey')); const gameData = await program.account.game.fetch(gamePublicKey); turnEl.textContent = gameData.turn; playerTurnEl.textContent = gameData.turn % 2 === 0 ? 'O' : 'X'; return gameData; } export async function updateBoard() { const gameData = await getGameAccount(); const board = gameData.board; setTiles(board); } ================================================ FILE: learn-how-to-build-a-client-side-app-part-2/tic_tac_toe.ts ================================================ export type TicTacToe = { "version": "0.1.0", "name": "tic_tac_toe", "instructions": [ { "name": "setupGame", "accounts": [ { "name": "game", "isMut": true, "isSigner": false }, { "name": "playerOne", "isMut": true, "isSigner": true }, { "name": "systemProgram", "isMut": false, "isSigner": false } ], "args": [ { "name": "playerTwoPubkey", "type": "publicKey" }, { "name": "gameId", "type": "string" } ] }, { "name": "play", "accounts": [ { "name": "game", "isMut": true, "isSigner": false }, { "name": "player", "isMut": false, "isSigner": true } ], "args": [ { "name": "tile", "type": { "defined": "Tile" } } ] } ], "accounts": [ { "name": "game", "type": { "kind": "struct", "fields": [ { "name": "players", "type": { "array": [ "publicKey", 2 ] } }, { "name": "turn", "type": "u8" }, { "name": "board", "type": { "array": [ { "array": [ { "option": { "defined": "Sign" } }, 3 ] }, 3 ] } }, { "name": "state", "type": { "defined": "GameState" } } ] } } ], "types": [ { "name": "Tile", "type": { "kind": "struct", "fields": [ { "name": "row", "type": "u8" }, { "name": "column", "type": "u8" } ] } }, { "name": "GameState", "type": { "kind": "enum", "variants": [ { "name": "Active" }, { "name": "Tie" }, { "name": "Won", "fields": [ { "name": "winner", "type": "publicKey" } ] } ] } }, { "name": "Sign", "type": { "kind": "enum", "variants": [ { "name": "X" }, { "name": "O" } ] } } ], "errors": [ { "code": 6000, "name": "TileOutOfBounds" }, { "code": 6001, "name": "TileAlreadySet" }, { "code": 6002, "name": "GameAlreadyOver" }, { "code": 6003, "name": "NotPlayersTurn" }, { "code": 6004, "name": "GameAlreadyStarted" } ] }; export const IDL: TicTacToe = { "version": "0.1.0", "name": "tic_tac_toe", "instructions": [ { "name": "setupGame", "accounts": [ { "name": "game", "isMut": true, "isSigner": false }, { "name": "playerOne", "isMut": true, "isSigner": true }, { "name": "systemProgram", "isMut": false, "isSigner": false } ], "args": [ { "name": "playerTwoPubkey", "type": "publicKey" }, { "name": "gameId", "type": "string" } ] }, { "name": "play", "accounts": [ { "name": "game", "isMut": true, "isSigner": false }, { "name": "player", "isMut": false, "isSigner": true } ], "args": [ { "name": "tile", "type": { "defined": "Tile" } } ] } ], "accounts": [ { "name": "game", "type": { "kind": "struct", "fields": [ { "name": "players", "type": { "array": [ "publicKey", 2 ] } }, { "name": "turn", "type": "u8" }, { "name": "board", "type": { "array": [ { "array": [ { "option": { "defined": "Sign" } }, 3 ] }, 3 ] } }, { "name": "state", "type": { "defined": "GameState" } } ] } } ], "types": [ { "name": "Tile", "type": { "kind": "struct", "fields": [ { "name": "row", "type": "u8" }, { "name": "column", "type": "u8" } ] } }, { "name": "GameState", "type": { "kind": "enum", "variants": [ { "name": "Active" }, { "name": "Tie" }, { "name": "Won", "fields": [ { "name": "winner", "type": "publicKey" } ] } ] } }, { "name": "Sign", "type": { "kind": "enum", "variants": [ { "name": "X" }, { "name": "O" } ] } } ], "errors": [ { "code": 6000, "name": "TileOutOfBounds" }, { "code": 6001, "name": "TileAlreadySet" }, { "code": 6002, "name": "GameAlreadyOver" }, { "code": 6003, "name": "NotPlayersTurn" }, { "code": 6004, "name": "GameAlreadyStarted" } ] }; ================================================ FILE: learn-how-to-build-for-mainnet/_answer/Anchor.toml ================================================ [features] seeds = false skip-lint = false skip-preflight = true [programs.localnet] todo = "9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" ================================================ FILE: learn-how-to-build-for-mainnet/_answer/Cargo.toml ================================================ [workspace] members = [ "programs/*" ] [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/index.html ================================================ TODO
      ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/package.json ================================================ { "name": "todo", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "@coral-xyz/anchor": "0.28.1-beta.1", "@solana/wallet-adapter-phantom": "0.9.24", "@solana/web3.js": "1.87.7", "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { "@types/react": "18.2.31", "@types/react-dom": "18.2.14", "@typescript-eslint/eslint-plugin": "6.8.0", "@typescript-eslint/parser": "6.8.0", "@vitejs/plugin-react": "4.0.4", "eslint": "8.52.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.3", "typescript": "5.1.6", "vite": "4.5.6" } } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/src/app.css ================================================ /* RESETS */ *, *::before, *::after { box-sizing: border-box; } *:focus { outline: 3px dashed #228bec; outline-offset: 0; } html { font: 62.5% / 1.15 sans-serif; } h1, h2 { margin-bottom: 0; } ul { list-style: none; padding: 0; } button { border: none; margin: 0; padding: 0; width: auto; overflow: visible; background: transparent; color: inherit; font: inherit; line-height: normal; -webkit-font-smoothing: inherit; -moz-osx-font-smoothing: inherit; appearance: none; } button::-moz-focus-inner { border: 0; } button:hover { cursor: pointer; } button, input { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { overflow: visible; } input[type='text'] { border-radius: 0; } body { width: 100%; max-width: 68rem; margin: 0 auto; font: 1.6rem/1.25 Arial, sans-serif; background-color: #f5f5f5; color: #4d4d4d; } @media screen and (min-width: 620px) { body { font-size: 1.9rem; line-height: 1.31579; } } /*END RESETS*/ /* GLOBAL STYLES */ .btn { padding: 0.8rem 1rem 0.7rem; border: 0.2rem solid #4d4d4d; cursor: pointer; text-transform: capitalize; } .btn.toggle-btn { border-width: 1px; border-color: #d3d3d3; } .btn.toggle-btn[aria-pressed='true'] { text-decoration: underline; border-color: #4d4d4d; } .btn__danger { color: #fff; background-color: #ca3c3c; border-color: #bd2130; } .btn__primary { color: #fff; background-color: #000; } .btn-group { display: flex; justify-content: space-between; } .btn-group > * { flex: 1 1 49%; } .btn-group > * + * { margin-left: 0.8rem; } .label-wrapper { margin: 0; flex: 0 0 100%; text-align: center; } .visually-hidden { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); white-space: nowrap; } [class*='stack'] > * { margin-top: 0; margin-bottom: 0; } .stack-small > * + * { margin-top: 1.25rem; } .stack-large > * + * { margin-top: 2.5rem; } @media screen and (min-width: 550px) { .stack-small > * + * { margin-top: 1.4rem; } .stack-large > * + * { margin-top: 2.8rem; } } .stack-exception { margin-top: 1.2rem; } /* END GLOBAL STYLES */ .todoapp { background: #fff; margin: 2rem 0 4rem 0; padding: 1rem; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1); } @media screen and (min-width: 550px) { .todoapp { padding: 4rem; } } .todoapp > * { max-width: 50rem; margin-left: auto; margin-right: auto; } .todoapp > form { max-width: 100%; } .todoapp > h1 { display: block; max-width: 100%; text-align: center; margin: 0; margin-bottom: 1rem; } .label__lg { line-height: 1.01567; font-weight: 300; padding: 0.8rem; margin-bottom: 1rem; text-align: center; } .input__lg { padding: 2rem; border: 2px solid #000; } .input__lg:focus { border-color: #4d4d4d; box-shadow: inset 0 0 0 2px; } [class*='__lg'] { display: inline-block; width: 100%; font-size: 1.9rem; } [class*='__lg']:not(:last-child) { margin-bottom: 1rem; } @media screen and (min-width: 620px) { [class*='__lg'] { font-size: 2.4rem; } } /* Todo item styles */ .todo { display: flex; flex-direction: row; flex-wrap: wrap; } .todo > * { flex: 0 0 100%; } .todo-text { width: 100%; min-height: 4.4rem; padding: 0.4rem 0.8rem; border: 2px solid #565656; } .todo-text:focus { box-shadow: inset 0 0 0 2px; } /* CHECKBOX STYLES */ .c-cb { box-sizing: border-box; font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; font-weight: 400; font-size: 1.6rem; line-height: 1.25; display: block; position: relative; min-height: 44px; padding-left: 40px; clear: left; } .c-cb > label::before, .c-cb > input[type='checkbox'] { box-sizing: border-box; top: -2px; left: -2px; width: 44px; height: 44px; } .c-cb > input[type='checkbox'] { -webkit-font-smoothing: antialiased; cursor: pointer; position: absolute; z-index: 1; margin: 0; opacity: 0; } .c-cb > label { font-size: inherit; font-family: inherit; line-height: inherit; display: inline-block; margin-bottom: 0; padding: 8px 15px 5px; cursor: pointer; touch-action: manipulation; } .c-cb > label::before { content: ''; position: absolute; border: 2px solid currentcolor; background: transparent; } .c-cb > input[type='checkbox']:focus + label::before { border-width: 4px; outline: 3px dashed #228bec; } .c-cb > label::after { box-sizing: content-box; content: ''; position: absolute; top: 11px; left: 9px; width: 18px; height: 7px; transform: rotate(-45deg); border: solid; border-width: 0 0 5px 5px; border-top-color: transparent; opacity: 0; background: transparent; } .c-cb > input[type='checkbox']:checked + label::after { opacity: 1; } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/src/app.tsx ================================================ import { ChangeEvent, FormEvent, MouseEventHandler, createContext, useContext, useEffect, useRef, useState } from 'react'; import { IDL, Todo } from '../../target/types/todo'; import './app.css'; import { AnchorProvider, Program } from '@coral-xyz/anchor'; import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom'; import { Connection, PublicKey } from '@solana/web3.js'; import { Buffer } from 'buffer'; import { isWalletConnected, Task, Filters, FILTER_MAP, usePrevious, FILTER_NAMES, TodoT, FormT, FilterButtonT } from './utils'; window.Buffer = Buffer; const PROGRAM_ID = new PublicKey( '9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2' ); const ENDPOINT = import.meta.env.VITE_SOLANA_CONNECTION_URL || 'http://localhost:8899'; const connection = new Connection(ENDPOINT, 'confirmed'); const wallet = new PhantomWalletAdapter(); const ProgramContext = createContext | null>(null); export function App() { const [program, setProgram] = useState | null>(null); const connectWallet: MouseEventHandler = async e => { e.preventDefault(); await wallet.connect(); console.log('Connected to: ', wallet.publicKey); if (isWalletConnected(wallet)) { const provider = new AnchorProvider(connection, wallet, {}); const program = new Program(IDL, PROGRAM_ID, provider); setProgram(program); } }; return ( {program ? : } ); } function LogIn({ connectWallet }: { connectWallet: MouseEventHandler; }) { return (

      Connect and Login

      ); } function Landing() { const [tasks, setTasks] = useState([]); const [filter, setFilter] = useState('All'); const program = useContext(ProgramContext); async function loadTasksFromChain() { if (program && program.provider.publicKey) { const [tasksPublicKey, _] = PublicKey.findProgramAddressSync( [program.provider.publicKey.toBuffer()], PROGRAM_ID ); try { const tasks = await program.account.tasksAccount.fetch(tasksPublicKey); setTasks(tasks.tasks); } catch (e) { console.warn( 'Your account does not yet exist. Save your ToDos to Solana to create an account.', e ); } } } useEffect(() => { loadTasksFromChain(); }, []); function toggleTaskCompleted(id: number) { const updatedTasks = tasks.map(task => { if (id === task.id) { return { ...task, completed: !task.completed }; } return task; }); setTasks(updatedTasks); } function deleteTask(id: number) { const remainingTasks = tasks.filter(task => id !== task.id); setTasks(remainingTasks); } function editTask(id: number, newName: string) { const editedTaskList = tasks.map(task => { if (id === task.id) { return { ...task, name: newName }; } return task; }); setTasks(editedTaskList); } function addTask(name: string) { const id = Math.floor(Math.random() * 1_000_000); const newTask = { id, name, completed: false }; setTasks([...tasks, newTask]); } async function saveTasksToChain() { if (program && program.provider.publicKey) { const [tasksPublicKey, _] = PublicKey.findProgramAddressSync( [program.provider.publicKey.toBuffer()], PROGRAM_ID ); await program.methods .saveTasks(tasks) .accounts({ tasks: tasksPublicKey }) .rpc(); } } const taskList = tasks .filter(FILTER_MAP[filter]) .map(task => ( )); const tasksNoun = taskList.length !== 1 ? 'tasks' : 'task'; const headingText = `${taskList.length} ${tasksNoun} remaining`; const listHeadingRef = useRef(null); const prevTaskLength = usePrevious(tasks.length) ?? 0; useEffect(() => { if (tasks.length - prevTaskLength === -1) { listHeadingRef?.current?.focus(); } }, [tasks.length, prevTaskLength]); return (

      ToDos

      {FILTER_NAMES.map(name => ( ))}

      {headingText}

        {taskList}
      ); } function TodoC({ name, id, editTask, toggleTaskCompleted, deleteTask, completed }: TodoT) { const [isEditing, setEditing] = useState(false); const [newName, setNewName] = useState(''); const editFieldRef = useRef(null); const editButtonRef = useRef(null); const wasEditing = usePrevious(isEditing); function handleChange(e: ChangeEvent) { setNewName(e.target.value); } function handleSubmit(e: FormEvent) { e.preventDefault(); if (!newName.trim()) { return; } editTask(id, newName); setNewName(''); setEditing(false); } const editingTemplate = (
      ); const viewTemplate = (
      toggleTaskCompleted(id)} />
      ); useEffect(() => { if (!wasEditing && isEditing) { editFieldRef?.current?.focus(); } if (wasEditing && !isEditing) { editButtonRef?.current?.focus(); } }, [wasEditing, isEditing]); return
    • {isEditing ? editingTemplate : viewTemplate}
    • ; } function Form({ addTask }: FormT) { const [name, setName] = useState(''); function handleSubmit(e: FormEvent) { e.preventDefault(); if (!name) return; addTask(name); setName(''); } function handleChange(e: ChangeEvent) { setName(e.target.value); } return (

      ); } function FilterButton({ name, isPressed, setFilter }: FilterButtonT) { return ( ); } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/src/index.css ================================================ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/src/main.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import { App } from './app'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( ); ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/src/utils.ts ================================================ import { Wallet } from '@coral-xyz/anchor'; import { useEffect, useRef } from 'react'; export type Task = { id: number; name: string; completed: boolean; }; export type TodoT = { name: string; id: number; completed: boolean; toggleTaskCompleted: (id: number) => void; deleteTask: (id: number) => void; editTask: (id: number, newName: string) => void; }; export type FormT = { addTask: (name: string) => void; }; export type FilterButtonT = { name: Filters; isPressed: boolean; setFilter: (name: Filters) => void; }; export type Filters = keyof typeof FILTER_MAP; export const FILTER_MAP = { All: () => true, Active: (task: Task) => !task.completed, Completed: (task: Task) => task.completed } as const; export const FILTER_NAMES = Object.keys(FILTER_MAP) as Array< keyof typeof FILTER_MAP >; export function usePrevious(value: T) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } export function isWalletConnected(wallet: any): wallet is Wallet { return wallet.publicKey !== null; } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/src/vite-env.d.ts ================================================ /// ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/app/vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], envDir: '../' }); ================================================ FILE: learn-how-to-build-for-mainnet/_answer/migrations/deploy.ts ================================================ // Migrations are an early feature. Currently, they're nothing more than this // single deploy script that's invoked from the CLI, injecting a provider // configured from the workspace's Anchor.toml. const anchor = require("@coral-xyz/anchor"); module.exports = async function (provider) { // Configure client to use the provider. anchor.setProvider(provider); // Add your deploy script here. }; ================================================ FILE: learn-how-to-build-for-mainnet/_answer/package.json ================================================ { "scripts": { "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" }, "dependencies": { "@coral-xyz/anchor": "0.28.1-beta.1" }, "devDependencies": { "chai": "4.3.10", "mocha": "10.2.0", "ts-mocha": "10.0.0", "@types/bn.js": "5.1.3", "@types/chai": "4.3.9", "@types/mocha": "10.0.1", "typescript": "5.1.6", "prettier": "3.0.1" } } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/programs/todo/Cargo.toml ================================================ [package] name = "todo" version = "0.1.0" description = "A todo list program" edition = "2021" [lib] crate-type = ["cdylib", "lib"] name = "todo" [features] no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] [dependencies] anchor-lang = { version = "0.28.0", features = ["init-if-needed"] } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/programs/todo/Xargo.toml ================================================ [target.bpfel-unknown-unknown.dependencies.std] features = [] ================================================ FILE: learn-how-to-build-for-mainnet/_answer/programs/todo/src/lib.rs ================================================ use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); const TASK_SIZE: usize = 4 + (4 + 32) + 1; #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { let tasks = &mut ctx.accounts.tasks; // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } // If length of tasks is not equal to the length of replacing_tasks, then // reallocate the tasks account. if tasks.tasks.len() < replacing_tasks.len() { let new_space = 8 + TASK_SIZE * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); let lamports_diff = new_minimum_balance.saturating_sub(tasks_account_info.lamports()); **ctx .accounts .user .to_account_info() .try_borrow_mut_lamports()? -= lamports_diff; **tasks_account_info.try_borrow_mut_lamports()? += lamports_diff; // Allocate the new space for the tasks account. tasks_account_info.realloc(new_space, false)?; } tasks.tasks = replacing_tasks; Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * TASK_SIZE, payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] #[derive(Debug)] pub struct TasksAccount { tasks: Vec, } #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, /// The name of the task. Max 32 characters, min 1 character. pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { #[msg("The task name must be less than 32 characters.")] TaskNameTooLong, #[msg("The task name must be at least 1 character.")] TaskNameTooShort, #[msg("The task id must be unique.")] TaskIdNotUnique, } ================================================ FILE: learn-how-to-build-for-mainnet/_answer/tests/todo.ts ================================================ import * as anchor from '@coral-xyz/anchor'; import { Program } from '@coral-xyz/anchor'; import { Todo } from '../target/types/todo'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; describe('todo', () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.Todo as Program; const connection = new Connection('http://localhost:8899', 'confirmed'); it('saves a new task', async () => { const user = Keypair.generate(); const sig = await connection.requestAirdrop(user.publicKey, 10_000_000_000); await connection.confirmTransaction(sig); const [tasksPublicKey, _] = PublicKey.findProgramAddressSync( [user.publicKey.toBuffer()], program.programId ); const _tx = await program.methods .saveTasks([ { id: 1, name: 'example', completed: false } ]) .accounts({ user: user.publicKey, tasks: tasksPublicKey }) .signers([user]) .rpc({ skipPreflight: true }); const tasks = await program.account.tasksAccount.fetch(tasksPublicKey); console.log('tasks', tasks); }); }); ================================================ FILE: learn-how-to-build-for-mainnet/_answer/tsconfig.json ================================================ { "compilerOptions": { "types": ["mocha", "chai"], "typeRoots": ["./node_modules/@types"], "lib": ["es2015"], "module": "commonjs", "target": "es6", "esModuleInterop": true } } ================================================ FILE: learn-how-to-build-for-mainnet/todo/.gitignore ================================================ .anchor .DS_Store target **/*.rs.bk node_modules test-ledger .yarn ================================================ FILE: learn-how-to-build-for-mainnet/todo/.prettierignore ================================================ .anchor .DS_Store target node_modules dist build test-ledger ================================================ FILE: learn-how-to-build-for-mainnet/todo/Anchor.toml ================================================ [features] seeds = false skip-lint = false skip-preflight = true [programs.localnet] todo = "9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" ================================================ FILE: learn-how-to-build-for-mainnet/todo/Cargo.toml ================================================ [workspace] members = [ "programs/*" ] [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/index.html ================================================ TODO
      ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/package.json ================================================ { "name": "todo", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { "@types/react": "18.2.31", "@types/react-dom": "18.2.14", "@typescript-eslint/eslint-plugin": "6.8.0", "@typescript-eslint/parser": "6.8.0", "@vitejs/plugin-react": "4.0.4", "eslint": "8.52.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.3", "typescript": "5.1.6", "vite": "4.5.6" } } ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/src/app.css ================================================ /* RESETS */ *, *::before, *::after { box-sizing: border-box; } *:focus { outline: 3px dashed #228bec; outline-offset: 0; } html { font: 62.5% / 1.15 sans-serif; } h1, h2 { margin-bottom: 0; } ul { list-style: none; padding: 0; } button { border: none; margin: 0; padding: 0; width: auto; overflow: visible; background: transparent; color: inherit; font: inherit; line-height: normal; -webkit-font-smoothing: inherit; -moz-osx-font-smoothing: inherit; appearance: none; } button::-moz-focus-inner { border: 0; } button:hover { cursor: pointer; } button, input { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { overflow: visible; } input[type='text'] { border-radius: 0; } body { width: 100%; max-width: 68rem; margin: 0 auto; font: 1.6rem/1.25 Arial, sans-serif; background-color: #f5f5f5; color: #4d4d4d; } @media screen and (min-width: 620px) { body { font-size: 1.9rem; line-height: 1.31579; } } /*END RESETS*/ /* GLOBAL STYLES */ .btn { padding: 0.8rem 1rem 0.7rem; border: 0.2rem solid #4d4d4d; cursor: pointer; text-transform: capitalize; } .btn.toggle-btn { border-width: 1px; border-color: #d3d3d3; } .btn.toggle-btn[aria-pressed='true'] { text-decoration: underline; border-color: #4d4d4d; } .btn__danger { color: #fff; background-color: #ca3c3c; border-color: #bd2130; } .btn__primary { color: #fff; background-color: #000; } .btn-group { display: flex; justify-content: space-between; } .btn-group > * { flex: 1 1 49%; } .btn-group > * + * { margin-left: 0.8rem; } .label-wrapper { margin: 0; flex: 0 0 100%; text-align: center; } .visually-hidden { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); white-space: nowrap; } [class*='stack'] > * { margin-top: 0; margin-bottom: 0; } .stack-small > * + * { margin-top: 1.25rem; } .stack-large > * + * { margin-top: 2.5rem; } @media screen and (min-width: 550px) { .stack-small > * + * { margin-top: 1.4rem; } .stack-large > * + * { margin-top: 2.8rem; } } .stack-exception { margin-top: 1.2rem; } /* END GLOBAL STYLES */ .todoapp { background: #fff; margin: 2rem 0 4rem 0; padding: 1rem; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1); } @media screen and (min-width: 550px) { .todoapp { padding: 4rem; } } .todoapp > * { max-width: 50rem; margin-left: auto; margin-right: auto; } .todoapp > form { max-width: 100%; } .todoapp > h1 { display: block; max-width: 100%; text-align: center; margin: 0; margin-bottom: 1rem; } .label__lg { line-height: 1.01567; font-weight: 300; padding: 0.8rem; margin-bottom: 1rem; text-align: center; } .input__lg { padding: 2rem; border: 2px solid #000; } .input__lg:focus { border-color: #4d4d4d; box-shadow: inset 0 0 0 2px; } [class*='__lg'] { display: inline-block; width: 100%; font-size: 1.9rem; } [class*='__lg']:not(:last-child) { margin-bottom: 1rem; } @media screen and (min-width: 620px) { [class*='__lg'] { font-size: 2.4rem; } } /* Todo item styles */ .todo { display: flex; flex-direction: row; flex-wrap: wrap; } .todo > * { flex: 0 0 100%; } .todo-text { width: 100%; min-height: 4.4rem; padding: 0.4rem 0.8rem; border: 2px solid #565656; } .todo-text:focus { box-shadow: inset 0 0 0 2px; } /* CHECKBOX STYLES */ .c-cb { box-sizing: border-box; font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; font-weight: 400; font-size: 1.6rem; line-height: 1.25; display: block; position: relative; min-height: 44px; padding-left: 40px; clear: left; } .c-cb > label::before, .c-cb > input[type='checkbox'] { box-sizing: border-box; top: -2px; left: -2px; width: 44px; height: 44px; } .c-cb > input[type='checkbox'] { -webkit-font-smoothing: antialiased; cursor: pointer; position: absolute; z-index: 1; margin: 0; opacity: 0; } .c-cb > label { font-size: inherit; font-family: inherit; line-height: inherit; display: inline-block; margin-bottom: 0; padding: 8px 15px 5px; cursor: pointer; touch-action: manipulation; } .c-cb > label::before { content: ''; position: absolute; border: 2px solid currentcolor; background: transparent; } .c-cb > input[type='checkbox']:focus + label::before { border-width: 4px; outline: 3px dashed #228bec; } .c-cb > label::after { box-sizing: content-box; content: ''; position: absolute; top: 11px; left: 9px; width: 18px; height: 7px; transform: rotate(-45deg); border: solid; border-width: 0 0 5px 5px; border-top-color: transparent; opacity: 0; background: transparent; } .c-cb > input[type='checkbox']:checked + label::after { opacity: 1; } ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/src/app.tsx ================================================ import { ChangeEvent, FormEvent, MouseEventHandler, useEffect, useRef, useState } from 'react'; import './app.css'; import { FILTER_MAP, FILTER_NAMES, FilterButtonT, Filters, FormT, Task, TodoT, usePrevious } from './utils'; // TODO:1 Attach Buffer to window // TODO:2 Create PROGRAM_ID // TODO:3 Get ENDPOINT from env // TODO:4 Create Connection // TODO:5 Create wallet adapter // TODO:6 Create ProgramContext from IDL export function App() { // TODO:7 Initialise program state const connectWallet: MouseEventHandler = async e => { e.preventDefault(); // TODO:8 Connect wallet // TODO:9 Check if wallet is connected // TODO:10 Create provider // TODO:11 Create program // TODO:12 Set program }; return ( // TODO:14 Wrap page in program context provider <> {/* TODO:13 If program is set, show landing page, otherwise show login page */} ); } function LogIn({ connectWallet }: { connectWallet: MouseEventHandler; }) { return (

      Connect and Login

      ); } function Landing() { const [tasks, setTasks] = useState([]); const [filter, setFilter] = useState('All'); // TODO:15 Use program context async function loadTasksFromChain() { // TODO:16 If program is connected, // TODO:17 Derive tasks account public key // TODO:18 Fetch the tasksAccount data // TODO:19 Set tasks state } useEffect(() => { loadTasksFromChain(); }, []); function toggleTaskCompleted(id: number) { const updatedTasks = tasks.map(task => { if (id === task.id) { return { ...task, completed: !task.completed }; } return task; }); setTasks(updatedTasks); } function deleteTask(id: number) { const remainingTasks = tasks.filter(task => id !== task.id); setTasks(remainingTasks); } function editTask(id: number, newName: string) { const editedTaskList = tasks.map(task => { if (id === task.id) { return { ...task, name: newName }; } return task; }); setTasks(editedTaskList); } function addTask(name: string) { const id = Math.floor(Math.random() * 1_000_000); const newTask = { id, name, completed: false }; setTasks([...tasks, newTask]); } async function saveTasksToChain() { // TODO:20 If program exists, derive tasks account public key, and initiate `save_tasks` instruction } const taskList = tasks .filter(FILTER_MAP[filter]) .map(task => ( )); const tasksNoun = taskList.length !== 1 ? 'tasks' : 'task'; const headingText = `${taskList.length} ${tasksNoun} remaining`; const listHeadingRef = useRef(null); const prevTaskLength = usePrevious(tasks.length) ?? 0; useEffect(() => { if (tasks.length - prevTaskLength === -1) { listHeadingRef?.current?.focus(); } }, [tasks.length, prevTaskLength]); return (

      ToDos

      {FILTER_NAMES.map(name => ( ))}

      {headingText}

        {taskList}
      ); } function TodoC({ name, id, editTask, toggleTaskCompleted, deleteTask, completed }: TodoT) { const [isEditing, setEditing] = useState(false); const [newName, setNewName] = useState(''); const editFieldRef = useRef(null); const editButtonRef = useRef(null); const wasEditing = usePrevious(isEditing); function handleChange(e: ChangeEvent) { setNewName(e.target.value); } function handleSubmit(e: FormEvent) { e.preventDefault(); if (!newName.trim()) { return; } editTask(id, newName); setNewName(''); setEditing(false); } const editingTemplate = (
      ); const viewTemplate = (
      toggleTaskCompleted(id)} />
      ); useEffect(() => { if (!wasEditing && isEditing) { editFieldRef?.current?.focus(); } if (wasEditing && !isEditing) { editButtonRef?.current?.focus(); } }, [wasEditing, isEditing]); return
    • {isEditing ? editingTemplate : viewTemplate}
    • ; } function Form({ addTask }: FormT) { const [name, setName] = useState(''); function handleSubmit(e: FormEvent) { e.preventDefault(); if (!name) return; addTask(name); setName(''); } function handleChange(e: ChangeEvent) { setName(e.target.value); } return (

      ); } function FilterButton({ name, isPressed, setFilter }: FilterButtonT) { return ( ); } ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/src/index.css ================================================ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/src/main.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import { App } from './app'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( ); ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/src/utils.ts ================================================ import { Wallet } from '@coral-xyz/anchor'; import { useEffect, useRef } from 'react'; export type Task = { id: number; name: string; completed: boolean; }; export type TodoT = { name: string; id: number; completed: boolean; toggleTaskCompleted: (id: number) => void; deleteTask: (id: number) => void; editTask: (id: number, newName: string) => void; }; export type FormT = { addTask: (name: string) => void; }; export type FilterButtonT = { name: Filters; isPressed: boolean; setFilter: (name: Filters) => void; }; export type Filters = keyof typeof FILTER_MAP; export const FILTER_MAP = { All: () => true, Active: (task: Task) => !task.completed, Completed: (task: Task) => task.completed } as const; export const FILTER_NAMES = Object.keys(FILTER_MAP) as Array< keyof typeof FILTER_MAP >; export function usePrevious(value: T) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } export function isWalletConnected(wallet: any): wallet is Wallet { return wallet.publicKey !== null; } ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/src/vite-env.d.ts ================================================ /// ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: learn-how-to-build-for-mainnet/todo/app/vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], envDir: '../' }); ================================================ FILE: learn-how-to-build-for-mainnet/todo/migrations/deploy.ts ================================================ // Migrations are an early feature. Currently, they're nothing more than this // single deploy script that's invoked from the CLI, injecting a provider // configured from the workspace's Anchor.toml. const anchor = require("@coral-xyz/anchor"); module.exports = async function (provider) { // Configure client to use the provider. anchor.setProvider(provider); // Add your deploy script here. }; ================================================ FILE: learn-how-to-build-for-mainnet/todo/package.json ================================================ { "scripts": { "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" }, "dependencies": { "@coral-xyz/anchor": "0.28.1-beta.1" }, "devDependencies": { "chai": "4.3.10", "mocha": "10.2.0", "ts-mocha": "10.0.0", "@types/bn.js": "5.1.3", "@types/chai": "4.3.9", "@types/mocha": "10.0.1", "typescript": "5.1.6", "prettier": "3.0.1" } } ================================================ FILE: learn-how-to-build-for-mainnet/todo/programs/todo/Cargo.toml ================================================ [package] name = "todo" version = "0.1.0" description = "A todo list program" edition = "2021" [lib] crate-type = ["cdylib", "lib"] name = "todo" [features] no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] [dependencies] anchor-lang = { version = "0.28.0", features = [] } ================================================ FILE: learn-how-to-build-for-mainnet/todo/programs/todo/Xargo.toml ================================================ [target.bpfel-unknown-unknown.dependencies.std] features = [] ================================================ FILE: learn-how-to-build-for-mainnet/todo/programs/todo/src/lib.rs ================================================ use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); #[program] pub mod todo { use super::*; pub fn initialize(ctx: Context) -> Result<()> { Ok(()) } } #[derive(Accounts)] pub struct Initialize {} ================================================ FILE: learn-how-to-build-for-mainnet/todo/tests/todo.ts ================================================ import * as anchor from '@coral-xyz/anchor'; import { Program } from '@coral-xyz/anchor'; import { Todo } from '../target/types/todo'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; describe('todo', () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.Todo as Program; const connection = new Connection('http://localhost:8899', 'confirmed'); it('saves a new task', async () => { const user = Keypair.generate(); const sig = await connection.requestAirdrop(user.publicKey, 10_000_000_000); await connection.confirmTransaction(sig); const [tasksPublicKey, _] = PublicKey.findProgramAddressSync( [user.publicKey.toBuffer()], program.programId ); const _tx = await program.methods .saveTasks([ { id: 1, name: 'example', completed: false } ]) .accounts({ user: user.publicKey, tasks: tasksPublicKey }) .signers([user]) .rpc({ skipPreflight: true }); const tasks = await program.account.tasksAccount.fetch(tasksPublicKey); console.log('tasks', tasks); }); }); ================================================ FILE: learn-how-to-build-for-mainnet/todo/tsconfig.json ================================================ { "compilerOptions": { "types": ["mocha", "chai"], "typeRoots": ["./node_modules/@types"], "lib": ["es2015"], "module": "commonjs", "target": "es6", "esModuleInterop": true } } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/Anchor.toml ================================================ [features] seeds = false skip-lint = false [programs.localnet] todo = "9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2" [registry] url = "https://api.apr.dev" [provider] cluster = "Localnet" wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" ================================================ FILE: learn-how-to-deploy-to-devnet/todo/Cargo.toml ================================================ [workspace] members = [ "programs/*" ] [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 [profile.release.build-override] opt-level = 3 incremental = false codegen-units = 1 ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/index.html ================================================ TODO
      ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/package.json ================================================ { "name": "todo", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "@coral-xyz/anchor": "0.28.1-beta.1", "@solana/wallet-adapter-phantom": "0.9.24", "@solana/web3.js": "1.87.7", "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { "@types/react": "18.2.31", "@types/react-dom": "18.2.14", "@typescript-eslint/eslint-plugin": "6.8.0", "@typescript-eslint/parser": "6.8.0", "@vitejs/plugin-react": "4.0.4", "eslint": "8.52.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.3", "typescript": "5.1.6", "vite": "4.5.6" } } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/src/app.css ================================================ /* RESETS */ *, *::before, *::after { box-sizing: border-box; } *:focus { outline: 3px dashed #228bec; outline-offset: 0; } html { font: 62.5% / 1.15 sans-serif; } h1, h2 { margin-bottom: 0; } ul { list-style: none; padding: 0; } button { border: none; margin: 0; padding: 0; width: auto; overflow: visible; background: transparent; color: inherit; font: inherit; line-height: normal; -webkit-font-smoothing: inherit; -moz-osx-font-smoothing: inherit; appearance: none; } button::-moz-focus-inner { border: 0; } button:hover { cursor: pointer; } button, input { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { overflow: visible; } input[type='text'] { border-radius: 0; } body { width: 100%; max-width: 68rem; margin: 0 auto; font: 1.6rem/1.25 Arial, sans-serif; background-color: #f5f5f5; color: #4d4d4d; } @media screen and (min-width: 620px) { body { font-size: 1.9rem; line-height: 1.31579; } } /*END RESETS*/ /* GLOBAL STYLES */ .btn { padding: 0.8rem 1rem 0.7rem; border: 0.2rem solid #4d4d4d; cursor: pointer; text-transform: capitalize; } .btn.toggle-btn { border-width: 1px; border-color: #d3d3d3; } .btn.toggle-btn[aria-pressed='true'] { text-decoration: underline; border-color: #4d4d4d; } .btn__danger { color: #fff; background-color: #ca3c3c; border-color: #bd2130; } .btn__primary { color: #fff; background-color: #000; } .btn-group { display: flex; justify-content: space-between; } .btn-group > * { flex: 1 1 49%; } .btn-group > * + * { margin-left: 0.8rem; } .label-wrapper { margin: 0; flex: 0 0 100%; text-align: center; } .visually-hidden { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); white-space: nowrap; } [class*='stack'] > * { margin-top: 0; margin-bottom: 0; } .stack-small > * + * { margin-top: 1.25rem; } .stack-large > * + * { margin-top: 2.5rem; } @media screen and (min-width: 550px) { .stack-small > * + * { margin-top: 1.4rem; } .stack-large > * + * { margin-top: 2.8rem; } } .stack-exception { margin-top: 1.2rem; } /* END GLOBAL STYLES */ .todoapp { background: #fff; margin: 2rem 0 4rem 0; padding: 1rem; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1); } @media screen and (min-width: 550px) { .todoapp { padding: 4rem; } } .todoapp > * { max-width: 50rem; margin-left: auto; margin-right: auto; } .todoapp > form { max-width: 100%; } .todoapp > h1 { display: block; max-width: 100%; text-align: center; margin: 0; margin-bottom: 1rem; } .label__lg { line-height: 1.01567; font-weight: 300; padding: 0.8rem; margin-bottom: 1rem; text-align: center; } .input__lg { padding: 2rem; border: 2px solid #000; } .input__lg:focus { border-color: #4d4d4d; box-shadow: inset 0 0 0 2px; } [class*='__lg'] { display: inline-block; width: 100%; font-size: 1.9rem; } [class*='__lg']:not(:last-child) { margin-bottom: 1rem; } @media screen and (min-width: 620px) { [class*='__lg'] { font-size: 2.4rem; } } /* Todo item styles */ .todo { display: flex; flex-direction: row; flex-wrap: wrap; } .todo > * { flex: 0 0 100%; } .todo-text { width: 100%; min-height: 4.4rem; padding: 0.4rem 0.8rem; border: 2px solid #565656; } .todo-text:focus { box-shadow: inset 0 0 0 2px; } /* CHECKBOX STYLES */ .c-cb { box-sizing: border-box; font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; font-weight: 400; font-size: 1.6rem; line-height: 1.25; display: block; position: relative; min-height: 44px; padding-left: 40px; clear: left; } .c-cb > label::before, .c-cb > input[type='checkbox'] { box-sizing: border-box; top: -2px; left: -2px; width: 44px; height: 44px; } .c-cb > input[type='checkbox'] { -webkit-font-smoothing: antialiased; cursor: pointer; position: absolute; z-index: 1; margin: 0; opacity: 0; } .c-cb > label { font-size: inherit; font-family: inherit; line-height: inherit; display: inline-block; margin-bottom: 0; padding: 8px 15px 5px; cursor: pointer; touch-action: manipulation; } .c-cb > label::before { content: ''; position: absolute; border: 2px solid currentcolor; background: transparent; } .c-cb > input[type='checkbox']:focus + label::before { border-width: 4px; outline: 3px dashed #228bec; } .c-cb > label::after { box-sizing: content-box; content: ''; position: absolute; top: 11px; left: 9px; width: 18px; height: 7px; transform: rotate(-45deg); border: solid; border-width: 0 0 5px 5px; border-top-color: transparent; opacity: 0; background: transparent; } .c-cb > input[type='checkbox']:checked + label::after { opacity: 1; } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/src/app.tsx ================================================ import { ChangeEvent, FormEvent, MouseEventHandler, createContext, useContext, useEffect, useRef, useState } from 'react'; import { IDL, Todo } from '../../target/types/todo'; import './app.css'; import { AnchorProvider, Program } from '@coral-xyz/anchor'; import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom'; import { Connection, PublicKey } from '@solana/web3.js'; import { Buffer } from 'buffer'; import { isWalletConnected, Task, Filters, FILTER_MAP, usePrevious, FILTER_NAMES, TodoT, FormT, FilterButtonT } from './utils'; window.Buffer = Buffer; const PROGRAM_ID = new PublicKey( import.meta.env.VITE_PROGRAM_ID || '9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2' ); const ENDPOINT = import.meta.env.VITE_SOLANA_CONNECTION_URL || 'http://localhost:8899'; const connection = new Connection(ENDPOINT, 'confirmed'); const wallet = new PhantomWalletAdapter(); const ProgramContext = createContext | null>(null); export function App() { const [program, setProgram] = useState | null>(null); const connectWallet: MouseEventHandler = async e => { e.preventDefault(); await wallet.connect(); console.log('Connected to: ', wallet.publicKey); if (isWalletConnected(wallet)) { const provider = new AnchorProvider(connection, wallet, {}); const program = new Program(IDL, PROGRAM_ID, provider); setProgram(program); } }; return ( {program ? : } ); } function LogIn({ connectWallet }: { connectWallet: MouseEventHandler; }) { return (

      Connect and Login

      ); } function Landing() { const [tasks, setTasks] = useState([]); const [filter, setFilter] = useState('All'); const program = useContext(ProgramContext); async function loadTasksFromChain() { if (program && program.provider.publicKey) { const [tasksPublicKey, _] = PublicKey.findProgramAddressSync( [program.provider.publicKey.toBuffer()], PROGRAM_ID ); try { const tasks = await program.account.tasksAccount.fetch(tasksPublicKey); setTasks(tasks.tasks); } catch (e) { console.warn( 'Your account does not yet exist. Save your ToDos to Solana to create an account.', e ); } } } useEffect(() => { loadTasksFromChain(); }, []); function toggleTaskCompleted(id: number) { const updatedTasks = tasks.map(task => { if (id === task.id) { return { ...task, completed: !task.completed }; } return task; }); setTasks(updatedTasks); } function deleteTask(id: number) { const remainingTasks = tasks.filter(task => id !== task.id); setTasks(remainingTasks); } function editTask(id: number, newName: string) { const editedTaskList = tasks.map(task => { if (id === task.id) { return { ...task, name: newName }; } return task; }); setTasks(editedTaskList); } function addTask(name: string) { const id = Math.floor(Math.random() * 1_000_000); const newTask = { id, name, completed: false }; setTasks([...tasks, newTask]); } async function saveTasksToChain() { if (program && program.provider.publicKey) { const [tasksPublicKey, _] = PublicKey.findProgramAddressSync( [program.provider.publicKey.toBuffer()], PROGRAM_ID ); await program.methods .saveTasks(tasks) .accounts({ tasks: tasksPublicKey }) .rpc(); } } const taskList = tasks .filter(FILTER_MAP[filter]) .map(task => ( )); const tasksNoun = taskList.length !== 1 ? 'tasks' : 'task'; const headingText = `${taskList.length} ${tasksNoun} remaining`; const listHeadingRef = useRef(null); const prevTaskLength = usePrevious(tasks.length) ?? 0; useEffect(() => { if (tasks.length - prevTaskLength === -1) { listHeadingRef?.current?.focus(); } }, [tasks.length, prevTaskLength]); return (

      ToDos

      {FILTER_NAMES.map(name => ( ))}

      {headingText}

        {taskList}
      ); } function TodoC({ name, id, editTask, toggleTaskCompleted, deleteTask, completed }: TodoT) { const [isEditing, setEditing] = useState(false); const [newName, setNewName] = useState(''); const editFieldRef = useRef(null); const editButtonRef = useRef(null); const wasEditing = usePrevious(isEditing); function handleChange(e: ChangeEvent) { setNewName(e.target.value); } function handleSubmit(e: FormEvent) { e.preventDefault(); if (!newName.trim()) { return; } editTask(id, newName); setNewName(''); setEditing(false); } const editingTemplate = (
      ); const viewTemplate = (
      toggleTaskCompleted(id)} />
      ); useEffect(() => { if (!wasEditing && isEditing) { editFieldRef?.current?.focus(); } if (wasEditing && !isEditing) { editButtonRef?.current?.focus(); } }, [wasEditing, isEditing]); return
    • {isEditing ? editingTemplate : viewTemplate}
    • ; } function Form({ addTask }: FormT) { const [name, setName] = useState(''); function handleSubmit(e: FormEvent) { e.preventDefault(); if (!name) return; addTask(name); setName(''); } function handleChange(e: ChangeEvent) { setName(e.target.value); } return (

      ); } function FilterButton({ name, isPressed, setFilter }: FilterButtonT) { return ( ); } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/src/index.css ================================================ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/src/main.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import { App } from './app'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( ); ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/src/utils.ts ================================================ import { Wallet } from '@coral-xyz/anchor'; import { useEffect, useRef } from 'react'; export type Task = { id: number; name: string; completed: boolean; }; export type TodoT = { name: string; id: number; completed: boolean; toggleTaskCompleted: (id: number) => void; deleteTask: (id: number) => void; editTask: (id: number, newName: string) => void; }; export type FormT = { addTask: (name: string) => void; }; export type FilterButtonT = { name: Filters; isPressed: boolean; setFilter: (name: Filters) => void; }; export type Filters = keyof typeof FILTER_MAP; export const FILTER_MAP = { All: () => true, Active: (task: Task) => !task.completed, Completed: (task: Task) => task.completed } as const; export const FILTER_NAMES = Object.keys(FILTER_MAP) as Array< keyof typeof FILTER_MAP >; export function usePrevious(value: T) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } export function isWalletConnected(wallet: any): wallet is Wallet { return wallet.publicKey !== null; } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/src/vite-env.d.ts ================================================ /// ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/app/vite.config.ts ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], envDir: '../' }); ================================================ FILE: learn-how-to-deploy-to-devnet/todo/migrations/deploy.ts ================================================ // Migrations are an early feature. Currently, they're nothing more than this // single deploy script that's invoked from the CLI, injecting a provider // configured from the workspace's Anchor.toml. const anchor = require("@coral-xyz/anchor"); module.exports = async function (provider) { // Configure client to use the provider. anchor.setProvider(provider); // Add your deploy script here. }; ================================================ FILE: learn-how-to-deploy-to-devnet/todo/package.json ================================================ { "scripts": { "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" }, "dependencies": { "@coral-xyz/anchor": "0.28.1-beta.1" }, "devDependencies": { "chai": "4.3.10", "mocha": "10.2.0", "ts-mocha": "10.0.0", "@types/bn.js": "5.1.3", "@types/chai": "4.3.9", "@types/mocha": "10.0.1", "typescript": "5.1.6", "prettier": "3.0.1" } } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/programs/todo/Cargo.toml ================================================ [package] name = "todo" version = "0.1.0" description = "A todo list program" edition = "2021" [lib] crate-type = ["cdylib", "lib"] name = "todo" [features] no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] [dependencies] anchor-lang = { version = "0.28.0", features = ["init-if-needed"] } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/programs/todo/Xargo.toml ================================================ [target.bpfel-unknown-unknown.dependencies.std] features = [] ================================================ FILE: learn-how-to-deploy-to-devnet/todo/programs/todo/src/lib.rs ================================================ use anchor_lang::prelude::*; declare_id!("9a43FDYE3S98dfN1rPAeavJT6MzBUEuF3bdX94zihQG2"); const TASK_SIZE: usize = 4 + (4 + 32) + 1; #[program] pub mod todo { use super::*; pub fn save_tasks(ctx: Context, replacing_tasks: Vec) -> Result<()> { let tasks = &mut ctx.accounts.tasks; // Check that the task name is not too long. for task in replacing_tasks.iter() { if task.name.len() > 32 { return Err(ErrorCode::TaskNameTooLong.into()); } } // Check that the task name is not too short. for task in replacing_tasks.iter() { if task.name.len() < 1 { return Err(ErrorCode::TaskNameTooShort.into()); } } // Check that the task id is unique. for (i, task) in replacing_tasks.iter().enumerate() { for (j, other_task) in replacing_tasks.iter().enumerate() { if i != j && task.id == other_task.id { return Err(ErrorCode::TaskIdNotUnique.into()); } } } // If length of tasks is not equal to the length of replacing_tasks, then // reallocate the tasks account. if tasks.tasks.len() < replacing_tasks.len() { let new_space = 8 + TASK_SIZE * replacing_tasks.len(); let new_minimum_balance = Rent::get()?.minimum_balance(new_space); let tasks_account_info = tasks.to_account_info(); let lamports_diff = new_minimum_balance.saturating_sub(tasks_account_info.lamports()); **ctx .accounts .user .to_account_info() .try_borrow_mut_lamports()? -= lamports_diff; **tasks_account_info.try_borrow_mut_lamports()? += lamports_diff; // Allocate the new space for the tasks account. tasks_account_info.realloc(new_space, false)?; } tasks.tasks = replacing_tasks; Ok(()) } } #[derive(Accounts)] #[instruction(replacing_tasks: Vec)] pub struct SaveTasks<'info> { #[account(init_if_needed, space = 8 + replacing_tasks.len() * TASK_SIZE, payer = user, seeds = [user.key().as_ref()], bump)] pub tasks: Account<'info, TasksAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[account] #[derive(Debug)] pub struct TasksAccount { tasks: Vec, } #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] pub struct Task { pub id: u32, /// The name of the task. Max 32 characters, min 1 character. pub name: String, pub completed: bool, } #[error_code] pub enum ErrorCode { #[msg("The task name must be less than 32 characters.")] TaskNameTooLong, #[msg("The task name must be at least 1 character.")] TaskNameTooShort, #[msg("The task id must be unique.")] TaskIdNotUnique, } ================================================ FILE: learn-how-to-deploy-to-devnet/todo/tests/todo.ts ================================================ import * as anchor from '@coral-xyz/anchor'; import { Program } from '@coral-xyz/anchor'; import { Todo } from '../target/types/todo'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; describe('todo', () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.Todo as Program; const connection = new Connection('http://localhost:8899', 'confirmed'); it('saves a new task', async () => { const user = Keypair.generate(); const sig = await connection.requestAirdrop(user.publicKey, 10_000_000_000); await connection.confirmTransaction(sig); const [tasksPublicKey, _] = PublicKey.findProgramAddressSync( [user.publicKey.toBuffer()], program.programId ); const _tx = await program.methods .saveTasks([ { id: 1, name: 'example', completed: false } ]) .accounts({ user: user.publicKey, tasks: tasksPublicKey }) .signers([user]) .rpc({ skipPreflight: true }); const tasks = await program.account.tasksAccount.fetch(tasksPublicKey); console.log('tasks', tasks); }); }); ================================================ FILE: learn-how-to-deploy-to-devnet/todo/tsconfig.json ================================================ { "compilerOptions": { "types": ["mocha", "chai"], "typeRoots": ["./node_modules/@types"], "lib": ["es2015"], "module": "commonjs", "target": "es6", "esModuleInterop": true } } ================================================ FILE: learn-how-to-interact-with-on-chain-programs/cluster-devnet.env ================================================ LIVE=1 CLUSTER=devnet ================================================ FILE: learn-how-to-interact-with-on-chain-programs/cluster-mainnet-beta.env ================================================ LIVE=1 CLUSTER=mainnet-beta ================================================ FILE: learn-how-to-interact-with-on-chain-programs/cluster-testnet.env ================================================ LIVE=1 CLUSTER=testnet ================================================ FILE: learn-how-to-interact-with-on-chain-programs/package.json ================================================ { "name": "learn-how-to-interact-with-on-chain-programs", "version": "0.0.1", "scripts": { "build": "cargo build-sbf --manifest-path=./src/program-rust/Cargo.toml --sbf-out-dir=dist/program", "call:hello-world": "node src/client/main.js", "clean": "npm run clean:program-rust", "clean:program-rust": "cargo clean --manifest-path=./src/program-rust/Cargo.toml && rm -rf ./dist", "deploy": "solana program deploy dist/program/helloworld.so" }, "dependencies": { "@solana/web3.js": "1.87.7", "borsh": "0.7.0" }, "engines": { "node": ">=16.0.0" }, "type": "module" } ================================================ FILE: learn-how-to-interact-with-on-chain-programs/src/_answer/client/hello-world.js ================================================ import { Connection, Keypair, PublicKey, Transaction, SystemProgram, sendAndConfirmTransaction, TransactionInstruction } from '@solana/web3.js'; import { readFile } from 'fs/promises'; import * as borsh from 'borsh'; export function establishConnection() { return new Connection('http://localhost:8899'); } export async function establishPayer() { const secretKeyString = await readFile( '../../../root/.config/solana/id.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } export async function getProgramId() { const secretKeyString = await readFile( 'dist/program/helloworld-keypair.json', 'utf8' ); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); const keypair = Keypair.fromSecretKey(secretKey); return keypair.publicKey; } export async function getAccountPubkey(payer, programId) { return await PublicKey.createWithSeed( payer.publicKey, 'seed-string', programId ); } export async function checkProgram( connection, payer, programId, accountPubkey ) { const programAccountInfo = await connection.getAccountInfo(programId); if (programAccountInfo === null) { throw new Error('Program account info not found'); } if (!programAccountInfo.executable) { throw new Error('Program account is not executable'); } const dataAccountInfo = await connection.getAccountInfo(accountPubkey); if (dataAccountInfo === null) { await createAccount(connection, payer, programId, accountPubkey); } } class HelloWorldAccount { constructor(fields) { if (fields) { this.counter = fields.counter; } } } const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; export async function createAccount( connection, payer, programId, accountPubkey ) { const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction(); const instruction = { basePubkey: payer.publicKey, fromPubkey: payer.publicKey, lamports, newAccountPubkey: accountPubkey, programId, seed: 'seed-string', space: ACCOUNT_SIZE }; const tx = SystemProgram.createAccountWithSeed(instruction); transaction.add(tx); await sendAndConfirmTransaction(connection, transaction, [payer]); } export async function sayHello(connection, payer, programId, accountPubkey) { const transaction = { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0) }; const instruction = new TransactionInstruction(transaction); await sendAndConfirmTransaction( connection, new Transaction().add(instruction), [payer] ); } export async function getHelloCount(connection, accountPubkey) { const accountInfo = await connection.getAccountInfo(accountPubkey); const greeting = borsh.deserialize( HelloWorldSchema, HelloWorldAccount, accountInfo.data ); return greeting.counter; } ================================================ FILE: learn-how-to-interact-with-on-chain-programs/src/_answer/client/main.js ================================================ import { checkProgram, establishConnection, establishPayer, getAccountPubkey, getHelloCount, getProgramId, sayHello } from './hello-world.js'; async function main() { console.log(`Saying 'hello' to a Solana account`); const connection = establishConnection(); const programId = await getProgramId(); const payer = await establishPayer(); const accountPubkey = await getAccountPubkey(payer, programId); await checkProgram(connection, payer, programId, accountPubkey); await sayHello(connection, payer, programId, accountPubkey); const helloCount = await getHelloCount(connection, accountPubkey); console.log(`Hello count: ${helloCount}`); } await main(); ================================================ FILE: learn-how-to-interact-with-on-chain-programs/src/program-rust/.gitignore ================================================ /target/ ================================================ FILE: learn-how-to-interact-with-on-chain-programs/src/program-rust/Cargo.toml ================================================ [package] name = "solana-sbf-helloworld" version = "0.0.1" description = "Hello World program written in Rust" authors = [ "Solana Maintainers ", "Shaun Hamilton ", ] repository = "https://github.com/freeCodeCamp/solana-curriculum" license = "Apache-2.0" homepage = "https://web3.freecodecamp.org/" edition = "2021" [features] no-entrypoint = [] [dependencies] borsh = "0.10.3" borsh-derive = "0.10.3" solana-program = "1.17.3" [dev-dependencies] solana-sdk = "1.17.3" [lib] name = "helloworld" crate-type = ["cdylib", "lib"] ================================================ FILE: learn-how-to-interact-with-on-chain-programs/src/program-rust/src/lib.rs ================================================ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{ account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, program_error::ProgramError, pubkey::Pubkey, }; entrypoint!(process_instruction); pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], _instruction_data: &[u8], ) -> ProgramResult { msg!("Hello World"); let mut accounts_iter = accounts.iter(); if let Some(account) = accounts_iter.next() { if account.owner != program_id { msg!("Account info does not match program id"); return Err(ProgramError::IncorrectProgramId); } let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?; greeting_account.counter += 1; let acc_data = &mut account.data.borrow_mut()[..]; greeting_account.serialize(&mut acc_data.as_mut())?; msg!("Greeted {} time(s)!", greeting_account.counter); Ok(()) } else { msg!("No accounts provided to say hello to"); return Err(ProgramError::NotEnoughAccountKeys); } } /// Define the type of state stored in accounts #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct GreetingAccount { /// number of greetings pub counter: u32, } ================================================ FILE: learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/.gitignore ================================================ /node_modules *.sw[po] /.cargo /dist .env src/client/util/store test-ledger/ .DS_Store ================================================ FILE: learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/package.json ================================================ { "name": "learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract", "version": "0.0.1", "scripts": { "call:hello-world": "ts-node src/client/main.ts", "clean": "npm run clean:program-rust", "clean:program-rust": "cargo clean --manifest-path=./src/program-rust/Cargo.toml && rm -rf ./dist" }, "dependencies": { "@solana/web3.js": "1.87.7", "borsh": "0.7.0", "yaml": "2.3.3" }, "devDependencies": { "@tsconfig/recommended": "1.0.3", "@types/node": "18.18.6", "@types/yaml": "1.9.7", "ts-node": "10.9.1", "typescript": "4.9.5" }, "engines": { "node": ">=16.0.0" } } ================================================ FILE: learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/client/hello_world.ts ================================================ import { Keypair, Connection, PublicKey, LAMPORTS_PER_SOL, SystemProgram, TransactionInstruction, Transaction, sendAndConfirmTransaction } from '@solana/web3.js'; import { existsSync } from 'fs'; import { resolve, join } from 'path'; import * as borsh from 'borsh'; import { getPayer, getRpcUrl, createKeypairFromFile } from './utils'; const GREETING_SEED = 'hello'; /** * Path to program files */ const PROGRAM_PATH = resolve(__dirname, '../../dist/program'); /** * Path to program shared object file which should be deployed on chain. * This file is created when running either: * - `npm run build:program-c` * - `npm run build:program-rust` */ const PROGRAM_SO_PATH = join(PROGRAM_PATH, 'helloworld.so'); /** * Path to the keypair of the deployed program. * This file is created when running `solana program deploy dist/program/helloworld.so` */ const PROGRAM_KEYPAIR_PATH = join(PROGRAM_PATH, 'helloworld-keypair.json'); /** * The state of an account managed by the hello world program */ class HelloWorldAccount { counter = 0; constructor(fields: { counter: number } | undefined = undefined) { if (fields) { this.counter = fields.counter; } } } /** * Borsh schema definition for hello world accounts */ const HelloWorldSchema = new Map([ [HelloWorldAccount, { kind: 'struct', fields: [['counter', 'u32']] }] ]); /** * The expected size of each hello world account. */ const ACCOUNT_SIZE = borsh.serialize( HelloWorldSchema, new HelloWorldAccount() ).length; /** * Establish a connection to the cluster */ export async function establishConnection(): Promise { const rpcUrl = await getRpcUrl(); const connection = new Connection(rpcUrl, 'confirmed'); const version = await connection.getVersion(); console.log('Connection to cluster established:', rpcUrl, version); return connection; } /** * Establish an account to pay for everything */ export async function establishPayer(connection: Connection): Promise { const payer = await getPayer(); let lamports = await connection.getBalance(payer.publicKey); `Using account ${payer.publicKey.toBase58()} containing ${ lamports / LAMPORTS_PER_SOL } SOL to pay for fees.`; return payer; } export async function getProgramId(): Promise { try { const programKeypair = await createKeypairFromFile(PROGRAM_KEYPAIR_PATH); return programKeypair.publicKey; } catch (err) { const errMsg = (err as Error).message; throw new Error( `Failed to read program keypair at '${PROGRAM_KEYPAIR_PATH}' due to error: ${errMsg}. Program may need to be deployed with \`solana program deploy dist/program/helloworld.so\`` ); } } export async function getAccountPubkey( payer: Keypair, programId: PublicKey ): Promise { // Derive the address (public key) of a hello world account from the program so that it's easy to find later. return await PublicKey.createWithSeed( payer.publicKey, GREETING_SEED, programId ); } /** * Check if the hello world BPF program has been deployed */ export async function checkProgram( connection: Connection, payer: Keypair, programId: PublicKey, accountPubkey: PublicKey ): Promise { // Check if the program has been deployed const programInfo = await connection.getAccountInfo(programId); if (programInfo === null) { if (existsSync(PROGRAM_SO_PATH)) { throw new Error( 'Program needs to be deployed with `solana program deploy dist/program/helloworld.so`' ); } else { throw new Error('Program needs to be built and deployed'); } } else if (!programInfo.executable) { throw new Error(`Program is not executable`); } console.log(`Using program ${programId.toBase58()}`); // Check if the hello world account has already been created const greetedAccount = await connection.getAccountInfo(accountPubkey); if (greetedAccount === null) { await createAccount(connection, payer, programId, accountPubkey); } } export async function createAccount( connection: Connection, payer: Keypair, programId: PublicKey, accountPubkey: PublicKey ): Promise { console.log( `Creating account ${accountPubkey.toBase58()} to say hello to...` ); const lamports = await connection.getMinimumBalanceForRentExemption( ACCOUNT_SIZE ); const transaction = new Transaction().add( SystemProgram.createAccountWithSeed({ fromPubkey: payer.publicKey, basePubkey: payer.publicKey, seed: GREETING_SEED, newAccountPubkey: accountPubkey, lamports, space: ACCOUNT_SIZE, programId }) ); await sendAndConfirmTransaction(connection, transaction, [payer]); } /** * Say hello */ export async function sayHello( connection: Connection, payer: Keypair, programId: PublicKey, accountPubkey: PublicKey ): Promise { console.log(`Saying hello to: ${accountPubkey.toBase58()}`); const instruction = new TransactionInstruction({ keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.alloc(0) // All instructions are hellos }); await sendAndConfirmTransaction( connection, new Transaction().add(instruction), [payer] ); } /** * Report the number of times the hello world account has been said hello to */ export async function reportGreetings( connection: Connection, accountPubkey: PublicKey ): Promise { const accountInfo = await connection.getAccountInfo(accountPubkey); if (accountInfo === null) { throw 'Error: cannot find the hello world account'; } const greeting = borsh.deserialize( HelloWorldSchema, HelloWorldAccount, accountInfo.data ); console.log( `${accountPubkey.toBase58()} has been said "hello" to ${ greeting.counter } time(s)` ); } ================================================ FILE: learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/client/main.ts ================================================ import { establishConnection, establishPayer, checkProgram, sayHello, reportGreetings, getProgramId, getAccountPubkey } from './hello_world'; async function main() { console.log("Let's say hello to a Solana account..."); // Establish connection to the cluster const connection = await establishConnection(); // Get the program ID of the hello world program const programId = await getProgramId(); // Determine who pays for the fees const payer = await establishPayer(connection); const accountPubkey = await getAccountPubkey(payer, programId); // Check if the program has been deployed await checkProgram(connection, payer, programId, accountPubkey); // Say hello to an account await sayHello(connection, payer, programId, accountPubkey); // Find out how many times that account has been greeted await reportGreetings(connection, accountPubkey); console.log('Success'); } main().then( () => process.exit(), err => { console.error(err); process.exit(-1); } ); ================================================ FILE: learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/client/utils.ts ================================================ import { homedir } from 'os'; import { readFile } from 'fs/promises'; import { resolve } from 'path'; import { parse } from 'yaml'; import { Keypair } from '@solana/web3.js'; /** * @private */ async function getConfig(): Promise { // Path to Solana CLI config file const CONFIG_FILE_PATH = resolve( homedir(), '.config', 'solana', 'cli', 'config.yml' ); const configYml = await readFile(CONFIG_FILE_PATH, { encoding: 'utf8' }); return parse(configYml); } /** * Load and parse the Solana CLI config file to determine which RPC url to use */ export async function getRpcUrl(): Promise { try { const config = await getConfig(); if (!config.json_rpc_url) throw new Error('Missing RPC URL'); return config.json_rpc_url; } catch (err) { console.warn( 'Failed to read RPC url from CLI config file, falling back to localhost' ); return 'http://127.0.0.1:8899'; } } /** * Load and parse the Solana CLI config file to determine which payer to use */ export async function getPayer(): Promise { try { const config = await getConfig(); if (!config.keypair_path) throw new Error('Missing keypair path'); return await createKeypairFromFile(config.keypair_path); } catch (err) { console.warn( 'Failed to create keypair from CLI config file, falling back to new random keypair' ); return Keypair.generate(); } } /** * Create a Keypair from a secret key stored in file as bytes' array */ export async function createKeypairFromFile( filePath: string ): Promise { const secretKeyString = await readFile(filePath, { encoding: 'utf8' }); const secretKey = Uint8Array.from(JSON.parse(secretKeyString)); return Keypair.fromSecretKey(secretKey); } ================================================ FILE: learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/.gitignore ================================================ /target/ ================================================ FILE: learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/Cargo.toml ================================================ [package] name = "solana-sbf-helloworld" version = "0.0.1" description = "Hello World program written in Rust" authors = [ "Solana Maintainers ", "Shaun Hamilton ", ] repository = "https://github.com/freeCodeCamp/solana-curriculum" license = "Apache-2.0" homepage = "https://web3.freecodecamp.org/" edition = "2021" [features] no-entrypoint = [] [dependencies] borsh = "=1.2.1" borsh-derive = "=1.2.1" solana-program = "=1.17.18" [dev-dependencies] solana-sdk = "=1.17.18" [lib] name = "helloworld" crate-type = ["cdylib", "lib"] ================================================ FILE: learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/src/program-rust/src/lib.rs ================================================ ================================================ FILE: learn-how-to-set-up-solana-by-building-a-hello-world-smart-contract/tsconfig.json ================================================ { "extends": "@tsconfig/recommended/tsconfig.json", "ts-node": { "compilerOptions": { "module": "commonjs" } }, "compilerOptions": { "declaration": true, "moduleResolution": "node", "module": "es2015" }, "include": ["src/client/**/*"], "exclude": ["node_modules"] } ================================================ FILE: learn-solanas-token-program-by-minting-a-fungible-token/.gitkeep ================================================ ================================================ FILE: learn-solanas-token-program-by-minting-a-fungible-token/package.json ================================================ { "name": "learn-solanas-token-program-by-minting-a-fungible-token", "type": "module" } ================================================ FILE: learn-solanas-token-program-by-minting-a-fungible-token/utils.js ================================================ import { Keypair, PublicKey } from '@solana/web3.js'; const secretKey = ( await import('./wallet.json', { assert: { type: 'json' } }) ).default; export const payer = Keypair.fromSecretKey(new Uint8Array(secretKey)); const MINT_ADDRESS_58 = ''; // For simplicity, the mint authority is the payer. const MINT_AUTHORITY_58 = payer.publicKey.toBase58(); const TOKEN_ACCOUNT_58 = ''; // export const mintAddress = new PublicKey(MINT_ADDRESS_58); export const mintAuthority = new PublicKey(MINT_AUTHORITY_58); // export const tokenAccount = new PublicKey(TOKEN_ACCOUNT_58); ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/package.json ================================================ { "name": "learn-the-metaplex-sdk-by-minting-an-nft", "type": "module", "dependencies": { "@solana/spl-token": "0.3.7", "@solana/web3.js": "1.87.7" }, "scripts": { "solana:dump": "solana program dump -u mainnet-beta $(npm pkg get env.METAPLEX_TOKEN_METADATA_PROGRAM_ID | tr -d \\\") ./mlp_token.so", "solana:airdrop": "solana airdrop 10 ./wallet.json", "start:validator": "solana-test-validator --bpf-program $(npm pkg get env.METAPLEX_TOKEN_METADATA_PROGRAM_ID | tr -d \\\") ./mlp_token.so --reset", "start:server": "node server.js" }, "env": { "METAPLEX_TOKEN_METADATA_PROGRAM_ID": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", "MINT_ACCOUNT_ADDRESS": "", "TOKEN_ACCOUNT_ADDRESS": "", "WALLET_ADDRESS": "" }, "devDependencies": { "express": "4.20.0" } } ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/server.js ================================================ import express from 'express'; const app = express(); app.use(express.json()); const metadatas = {}; app.get('/meta/:id', (req, res) => { const metadata = metadatas[req.params.id]; if (!metadata) { return res.status(404).end(); } console.log('GET', req.params.id); return res.send(Buffer.from(metadata)); }); app.put('/meta/:id', (req, res) => { console.log('POST', req.params.id); metadatas[req.params.id] = req.body; res.status(200).end(); }); app.get('/status/ping', (_req, res) => { res.status(200).send('pong'); }); app.listen(3001, () => { console.log('Server started'); }); ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/spl-program/create-mint-account.js ================================================ import { Connection } from '@solana/web3.js'; import { payer } from './utils.js'; import { createMint } from '@solana/spl-token'; const connection = new Connection('http://127.0.0.1:8899'); const mintAuthority = payer.publicKey; const freezeAuthority = payer.publicKey; const mint = await createMint( connection, payer, mintAuthority, freezeAuthority, 9 ); console.log('Token Unique Identifier:', mint.toBase58()); ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/spl-program/create-token-account.js ================================================ import { Connection } from '@solana/web3.js'; import { payer, mintAddress } from './utils.js'; import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token'; const connection = new Connection('http://127.0.0.1:8899'); const tokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, payer.publicKey ); console.log('Token Account Address:', tokenAccount.address.toBase58()); ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/spl-program/get-token-account.js ================================================ import { Connection, PublicKey } from '@solana/web3.js'; import { getAssociatedTokenAddress, getAccount } from '@solana/spl-token'; import { mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const userPublicKey = new PublicKey(process.argv[2]); const tokenAddress = await getAssociatedTokenAddress( mintAddress, userPublicKey ); const tokenAccount = await getAccount(connection, tokenAddress); console.log('Token Account:', tokenAccount); ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/spl-program/get-token-info.js ================================================ import { Connection } from '@solana/web3.js'; import { getMint } from '@solana/spl-token'; import { mintAddress } from './utils.js'; const connection = new Connection('http://127.0.0.1:8899'); const mint = await getMint(connection, mintAddress); console.log(mint); ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/spl-program/mint.js ================================================ import { Connection } from '@solana/web3.js'; import { mintTo } from '@solana/spl-token'; import { payer, mintAddress, tokenAccount, mintAuthority } from './utils.js'; const connection = new Connection('http://127.0.0.1:8899'); await mintTo( connection, payer, mintAddress, tokenAccount, mintAuthority, 1_000_000_000 ); ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/spl-program/package.json ================================================ { "name": "fungi-token", "version": "1.0.0", "description": "A fungible token on Solana", "scripts": { "serve": "npx serve client" }, "author": "", "license": "ISC", "dependencies": { "@solana/spl-token": "0.3.7", "@solana/web3.js": "1.87.7" }, "type": "module" } ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/spl-program/transfer.js ================================================ import { Connection, PublicKey, Keypair } from '@solana/web3.js'; import { getOrCreateAssociatedTokenAccount, getAccount, transfer } from '@solana/spl-token'; import { payer, mintAddress } from './utils.js'; const connection = new Connection('http://localhost:8899'); const fromTokenAccountPublicKey = new PublicKey(process.argv[2]); const toWallet = Keypair.generate(); const toTokenAccount = await getOrCreateAssociatedTokenAccount( connection, payer, mintAddress, toWallet.publicKey ); const fromWallet = await getAccount(connection, fromTokenAccountPublicKey); const owner = fromWallet.owner; const amount = Number(process.argv[3]); await transfer( connection, payer, fromTokenAccountPublicKey, toTokenAccount.address, owner, amount ); console.log( `Transferred ${amount} tokens from ${fromTokenAccountPublicKey.toBase58()} to ${toTokenAccount.address.toBase58()}` ); ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/spl-program/utils.js ================================================ import { PublicKey, Keypair } from '@solana/web3.js'; import { pkg } from '../utils.js'; const secretKey = ( await import('../wallet.json', { assert: { type: 'json' } }) ).default; export const payer = Keypair.fromSecretKey(new Uint8Array(secretKey)); const MINT_ADDRESS_58 = pkg.env.MINT_ACCOUNT_ADDRESS; // For simplicity, the mint authority is the payer. const MINT_AUTHORITY_58 = payer.publicKey.toBase58(); const TOKEN_ACCOUNT_58 = pkg.env.TOKEN_ACCOUNT_ADDRESS; export const mintAddress = new PublicKey(MINT_ADDRESS_58 || '1'.repeat(32)); export const mintAuthority = new PublicKey(MINT_AUTHORITY_58); export const tokenAccount = new PublicKey(TOKEN_ACCOUNT_58 || '1'.repeat(32)); ================================================ FILE: learn-the-metaplex-sdk-by-minting-an-nft/utils.js ================================================ import { Keypair } from '@solana/web3.js'; import { readFile } from 'fs/promises'; const t = ( await import('./wallet.json', { assert: { type: 'json' } }) ).default; // solana address -k wallet.json export const WALLET_KEYPAIR = Keypair.fromSecretKey(new Uint8Array(t)); const file = await readFile('./package.json', 'utf8'); export const pkg = JSON.parse(file); export function localStorage(options) { return { install(metaplex) { metaplex.storage().setDriver(new LocalStorageDriver(options)); } }; } class LocalStorageDriver { constructor(options) { if (!options.baseUrl) { throw new Error('Missing baseUrl option'); } this.baseUrl = options.baseUrl; this.costPerByte = 2; } async getUploadPrice(bytes) { const { amount } = await import('@metaplex-foundation/js'); return amount(this.costPerByte * bytes, { symbol: 'SOL', decimals: 9 }); } async upload(file) { const uri = `${this.baseUrl}meta/${file.uniqueName}`; await fetch(uri, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(file.buffer) }); return uri; } async download(uri) { const { toMetaplexFile } = await import('@metaplex-foundation/js'); const res = await fetch(uri); if (!res) { throw new Error(`URI not found: ${uri}`); } const buffer = await res.arrayBuffer(); const metaplexFile = toMetaplexFile(buffer, uri); return metaplexFile; } } ================================================ FILE: package.json ================================================ { "name": "solana-curriculum", "version": "1.0.0", "description": "Root package.json", "repository": { "type": "git", "url": "https://github.com/freeCodeCamp/solana-curriculum" }, "author": "Shaun Hamilton", "type": "module", "dependencies": { "@babel/parser": "7.24.5", "@freecodecamp/freecodecamp-os": "1.10.0", "@metaplex-foundation/js": "0.20.1", "@solana/spl-token": "0.4.6", "@solana/web3.js": "1.91.8", "babeliser": "0.6.0", "borsh": "2.0.0" } } ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "local>freeCodeCamp/renovate-config" ] } ================================================ FILE: tooling/camper-info.js ================================================ /** * @file Provides command-line output of useful debugging information * @example * * ```bash * node tooling/camper-info.js --history --directory * ``` */ import { getProjectConfig, getConfig, getState } from '../node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/env.js'; import __helpers from '../node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/test-utils.js'; import { Logger } from 'logover'; import { readdir, readFile } from 'fs/promises'; import { join } from 'path'; import os from 'os'; const logover = new Logger({ level: 'debug', timestamp: null }); const FLAGS = process.argv; async function main() { try { const handleFlag = { '--history': printCommandHistory, '--directory': printDirectoryTree }; const projectConfig = await getProjectConfig(); const config = await getConfig(); const state = await getState(); const { currentProject } = state; const { currentLesson } = projectConfig; const { version } = config; const devContainerFile = await readFile( '.devcontainer/devcontainer.json', 'utf-8' ); const devConfig = JSON.parse(devContainerFile); const coursesVersion = devConfig.extensions?.find(e => e.match('freecodecamp-courses') ); const { stdout } = await __helpers.getCommandOutput('git log -1 --oneline'); const osInfo = ` Architecture: ${os.arch()} Platform: ${os.platform()} Release: ${os.release()} Type: ${os.type()} `; logover.info('Project: ', currentProject); logover.info('Lesson Number: ', currentLesson); logover.info('Curriculum Version: ', version); logover.info('freeCodeCamp - Courses: ', coursesVersion); logover.info('Commit: ', stdout); logover.info('OS Info:', osInfo); for (const arg of FLAGS) { await handleFlag[arg]?.(); } async function printDirectoryTree() { const files = await readdir('.', { withFileTypes: true }); let depth = 0; for (const file of files) { if (file.isDirectory() && file.name === currentProject) { await recurseDirectory(file.name, depth); } } } async function printCommandHistory() { const historyCwd = await readFile('.logs/.history_cwd.log', 'utf-8'); logover.info('Command History:\n', historyCwd); } } catch (e) { logover.error(e); } } main(); const IGNORE = [ 'node_modules', 'target', 'test-ledger', 'store', '.cargo', '.DS_Store' ]; async function recurseDirectory(path, depth) { logover.info(`|${' '.repeat(depth * 2)}|-- ${path}`); depth++; const files = await readdir(path, { withFileTypes: true }); for (const file of files) { if (!IGNORE.includes(file.name)) { if (file.isDirectory()) { await recurseDirectory(join(path, file.name), depth); } else { logover.info(`|${' '.repeat(depth * 2)}|-- ${file.name}`); } } } } ================================================ FILE: tooling/helpers.js ================================================ import __helpers from '../node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/test-utils.js'; import { logover } from '../node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/logger.js'; import { ROOT } from '../node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/env.js'; import { writeFileSync } from 'fs'; import { join } from 'path'; import { Babeliser as B } from 'babeliser'; import * as web3 from '@solana/web3.js'; import * as borsh from 'borsh'; export async function rustTest(path, filePath, test, cb) { const PATH_TO_FILE = join(ROOT, filePath); const T_ATTR = '#[test]'; const testString = `${T_ATTR}\n${test}`; const fileContents = await __helpers.getFile(filePath); const fileWithTest = fileContents + '\n\n\n' + testString; let std; try { writeFileSync(PATH_TO_FILE, fileWithTest, 'utf-8'); std = await __helpers.getCommandOutput('cargo test --lib', path); } catch (e) { logover.debug(e); } finally { const ensureFileContents = fileContents.replace(testString, ''); writeFileSync(PATH_TO_FILE, ensureFileContents, 'utf-8'); await cb(std.stdout, std.stderr); } } export const Babeliser = B; // Test wallet: 8rK533RnqBtNPxwCHsPLZe8H89DwZxo3MhhEo4pKCfAw // Program ID: FxcSjVwaWZPkNndA6RS9yZTjomF69AS1JZs6kvuEEp8v // ProgramData Address: 34QiTA3zqEQ72mfmtANBu5M4zguBs9Xa4QCCyXcZQKnG // Program Data Account: 3nyLatY115wbvw1FfafD3Q7Djuz2J2iY6Md6VNsBMFYp export async function getCamperKeypair() { const secretKeyString = await __helpers.getFile(join(__loc, 'wallet.json')); const secretKey = JSON.parse(secretKeyString); const int8secretKey = new Uint8Array(secretKey); return web3.Keypair.fromSecretKey(int8secretKey); } export async function getSOFile() { const dir = await __helpers.getDirectory(join(__loc, 'dist', 'program')); for (const file of dir) { if (file.endsWith('.so')) { return file; } } } export async function getProgramJSONFile() { const dir = await __helpers.getDirectory(join(__loc, 'dist', 'program')); for (const file of dir) { if (file.endsWith('.json')) { return file; } } } export async function getProgramKeypair() { const jsonFile = await getProgramJSONFile(); const json = await __helpers.getFile( join(__loc, 'dist', 'program', jsonFile) ); const keypair = JSON.parse(json); const int8keypair = new Uint8Array(keypair); return web3.Keypair.fromSecretKey(int8keypair); } export async function getDataAccountPublicKey() { const keypair = await getCamperKeypair(); const programKeypair = await getProgramKeypair(); const programId = programKeypair.publicKey; const dataAccount = await web3.PublicKey.createWithSeed( keypair.publicKey, 'fcc-seed', programId ); return dataAccount; } export function establishConnection() { return new web3.Connection('http://localhost:8899'); } class MessageAccount { constructor(fields) { this.message = fields?.message || ' '.repeat(280); } } const MessageSchema = new Map([ [MessageAccount, { kind: 'struct', fields: [['message', 'string']] }] ]); export async function setMessage( connection, payer, programId, accountPubkey, message ) { const transaction = { keys: [{ pubkey: accountPubkey, isSigner: false, isWritable: true }], programId, data: Buffer.from(message) }; const instruction = new web3.TransactionInstruction(transaction); await web3.sendAndConfirmTransaction( connection, new web3.Transaction().add(instruction), [payer] ); } export async function getMessage(connection, accountPubkey) { const accountInfo = await connection.getAccountInfo(accountPubkey); const message = borsh.deserialize( MessageSchema, MessageAccount, accountInfo.data ); return message.message; } ================================================ FILE: tooling/rejig.js ================================================ import { readFile, writeFile, readdir } from 'fs/promises'; import { join } from 'path'; const PATH = process.argv[2]?.trim(); const CURRICULUM_PATH = 'curriculum/locales/english'; /** * Ensures all lessons are incremented by 1 */ async function rejigFile(fileName) { const filePath = join(CURRICULUM_PATH, fileName); const file = await readFile(filePath, 'utf-8'); let lessonNumber = 0; const newFile = file.replace(/## \d+/g, () => { lessonNumber++; return `## ${lessonNumber}`; }); await writeFile(filePath, newFile, 'utf-8'); } try { const rejiggedFiles = []; if (PATH) { await rejigFile(PATH); rejiggedFiles.push(PATH); } else { const files = await readdir(CURRICULUM_PATH); for (const file of files) { console.log(`Rejigging '${file}'`); await rejigFile(file); rejiggedFiles.push(file); } } console.info('Successfully rejigged: ', rejiggedFiles); } catch (e) { console.error(e); console.log('Usage: npm run rejig '); console.log('Curriculum file name MUST include the `.md` extension.'); } ================================================ FILE: tooling/seed.js ================================================ /** * Copies the seed from the previous file match to the given lesson * * @example * node tooling/seed.js */ import clipboardy from 'clipboardy'; import { join } from 'path'; import { getFilesWithSeed, getLessonFromFile, getLessonSeed } from '../node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/parser.js'; const PATH = './curriculum/locales/english/learn-how-to-interact-with-on-chain-programs.md'; const LESSON_NUMBER = Number(process.argv[2]); const FILE = join(process.argv[3]); // Get seed from latest matching file let lessonToCheck = LESSON_NUMBER - 1; let match = null; while (!match) { const lesson = await getLessonFromFile(PATH, lessonToCheck); const seed = getLessonSeed(lesson); const filesWithSeed = getFilesWithSeed(seed); match = filesWithSeed.find(([filePath]) => join(filePath) === FILE); lessonToCheck--; } // Get seed from lesson const [_, seed] = match; // Add seed to clipboard await clipboardy.write(seed);