├── .eslintignore ├── .eslintrc.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── publish-sdk.yml │ └── run-tests.yml ├── .gitignore ├── .mocharc.json ├── .prettierignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── assets └── design.png ├── client ├── sdk │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── idl │ │ │ ├── soar.ts │ │ │ └── tens.ts │ │ ├── index.ts │ │ ├── instructions │ │ │ ├── accountsBuilder.ts │ │ │ ├── index.ts │ │ │ ├── ixBuilder.ts │ │ │ └── rawInstructions.ts │ │ ├── soar.game.ts │ │ ├── soar.program.ts │ │ ├── state │ │ │ ├── achievement.ts │ │ │ ├── game.ts │ │ │ ├── index.ts │ │ │ ├── leaderboard.ts │ │ │ ├── leaderboardTopEntries.ts │ │ │ ├── merged.ts │ │ │ ├── player.ts │ │ │ ├── playerAchievement.ts │ │ │ ├── playerScoresList.ts │ │ │ └── reward.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── typedoc.json └── tests │ ├── fixtures │ ├── metadata │ │ ├── collection.json │ │ ├── collection.png │ │ ├── test.jpeg │ │ └── test.json │ ├── mpl_token_metadata.so │ └── provider.json │ ├── soar.ts │ ├── tens.ts │ └── utils.ts ├── crates └── soar-cpi │ ├── .gitignore │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── build.rs │ ├── idl.json │ └── src │ └── lib.rs ├── examples └── tens │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ └── lib.rs ├── package-lock.json ├── package.json ├── programs └── soar │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ ├── error.rs │ ├── instructions │ ├── add_achievement.rs │ ├── add_leaderboard.rs │ ├── add_reward.rs │ ├── approve_merge.rs │ ├── claim_reward.rs │ ├── create_game.rs │ ├── create_player.rs │ ├── initiate_merge.rs │ ├── mod.rs │ ├── register_player.rs │ ├── submit_score.rs │ ├── unlock_player_achievement.rs │ ├── update_achievement.rs │ ├── update_game.rs │ ├── update_leaderboard.rs │ ├── update_player.rs │ └── verify_reward.rs │ ├── lib.rs │ ├── seeds.rs │ ├── state │ ├── achievement.rs │ ├── check_fields.rs │ ├── game.rs │ ├── game_types.rs │ ├── leaderboard.rs │ ├── merge.rs │ ├── mod.rs │ ├── player.rs │ ├── player_achievement.rs │ ├── player_scores_list.rs │ ├── reward.rs │ └── top_entries.rs │ └── utils.rs ├── readme.md ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | client/sdk/idl/soar.ts 2 | .anchor 3 | target -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | - standard-with-typescript 6 | - plugin:prettier/recommended 7 | plugins: 8 | - prettier 9 | ignorePatterns: 10 | - "**/idl/soar.ts" 11 | parser: "@typescript-eslint/parser" 12 | parserOptions: 13 | ecmaVersion: latest 14 | sourceType: module 15 | project: [./tsconfig.json] 16 | rules: 17 | "@typescript-eslint/strict-boolean-expressions": 18 | - error 19 | - allowNullableObject: true 20 | "@typescript-eslint/return-await": 21 | - error 22 | - in-try-catch 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: tiago18c, murlokito 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | > ⚠️ NOTE: Use notes like this to emphasize something important about the PR. 3 | > 4 | > This could include other PRs this PR is built on top of; API breaking changes; reasons for why the PR is on hold; or anything else you would like to draw attention to. 5 | 6 | | Status | Type | ⚠️ Core Change | Issue | 7 | | :---: | :---: | :---: | :--: | 8 | | Ready/Hold | Feature/Bug/Tooling/Refactor/Hotfix | Yes/No | [Link]() | 9 | 10 | ## Problem 11 | 12 | _What problem are you trying to solve?_ 13 | 14 | 15 | ## Solution 16 | 17 | _How did you solve the problem?_ 18 | 19 | 20 | ## Before & After Screenshots 21 | 22 | _Insert screenshots of example code output_ 23 | 24 | **BEFORE**: 25 | [insert screenshot here] 26 | 27 | **AFTER**: 28 | [insert screenshot here] 29 | 30 | 31 | ## Other changes (e.g. bug fixes, small refactors) 32 | 33 | 34 | ## Deploy Notes 35 | 36 | _Notes regarding deployment of the contained body of work. These should note any 37 | new dependencies, new scripts, etc._ 38 | 39 | **New scripts**: 40 | 41 | - `script` : script details 42 | 43 | **New dependencies**: 44 | 45 | - `dependency` : dependency details -------------------------------------------------------------------------------- /.github/workflows/publish-sdk.yml: -------------------------------------------------------------------------------- 1 | name: Publish Client SDK 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | 7 | env: 8 | solana_version: v1.17.0 9 | anchor_version: 0.29.0 10 | 11 | jobs: 12 | install: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - uses: actions/cache@v3 18 | name: cache solana cli 19 | id: cache-solana 20 | with: 21 | path: | 22 | ~/.cache/solana/ 23 | ~/.local/share/solana/ 24 | key: solana-${{ runner.os }}-v0000-${{ env.solana_version }} 25 | 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 20 29 | 30 | - name: install essentials 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install -y pkg-config build-essential libudev-dev 34 | 35 | - name: Cache node dependencies 36 | uses: actions/cache@v3 37 | with: 38 | path: '**/node_modules' 39 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 40 | 41 | - name: install node_modules 42 | run: | 43 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 44 | yarn --frozen-lockfile --network-concurrency 2 45 | 46 | - uses: dtolnay/rust-toolchain@stable 47 | with: 48 | toolchain: stable 49 | 50 | - name: Cache rust 51 | uses: Swatinem/rust-cache@v2 52 | 53 | - name: install solana 54 | if: steps.cache-solana.outputs.cache-hit != 'true' 55 | run: | 56 | sh -c "$(curl -sSfL https://release.solana.com/${{ env.solana_version }}/install)" 57 | export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" 58 | solana --version 59 | 60 | clippy-lint: 61 | needs: install 62 | runs-on: ubuntu-latest 63 | 64 | steps: 65 | - uses: actions/checkout@v3 66 | - name: Cache rust 67 | uses: Swatinem/rust-cache@v2 68 | - name: Run fmt 69 | run: cargo fmt -- --check 70 | - name: Run clippy 71 | run: cargo clippy -- --deny=warnings 72 | 73 | yarn-lint: 74 | needs: install 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v3 78 | - name: Use Node ${{ matrix.node }} 79 | uses: actions/setup-node@v3 80 | with: 81 | node-version: 20 82 | 83 | - name: Cache node dependencies 84 | uses: actions/cache@v3 85 | with: 86 | path: '**/node_modules' 87 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 88 | 89 | - name: Run lint 90 | run: yarn lint 91 | 92 | test-and-publish: 93 | needs: [clippy-lint, yarn-lint] 94 | runs-on: ubuntu-latest 95 | 96 | steps: 97 | - uses: actions/checkout@v3 98 | 99 | - name: Use Node ${{ matrix.node }} 100 | uses: actions/setup-node@v3 101 | with: 102 | node-version: 20 103 | 104 | - name: Cache node dependencies 105 | uses: actions/cache@v3 106 | with: 107 | path: '**/node_modules' 108 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 109 | - name: install node_modules 110 | run: | 111 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 112 | yarn --frozen-lockfile 113 | 114 | - uses: actions/cache@v3 115 | name: cache solana cli 116 | id: cache-solana 117 | with: 118 | path: | 119 | ~/.cache/solana/ 120 | ~/.local/share/solana/ 121 | key: solana-${{ runner.os }}-v0000-${{ env.solana_version }} 122 | 123 | - name: setup solana 124 | run: | 125 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 126 | solana --version 127 | solana-keygen new --silent --no-bip39-passphrase 128 | 129 | - name: run tests 130 | run: | 131 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 132 | ls node_modules/.bin 133 | npm i -g @coral-xyz/anchor-cli@${{ env.anchor_version }} ts-mocha typescript 134 | npm run build -w client/sdk/ 135 | anchor test 136 | 137 | - run: | 138 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 139 | npm set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }} 140 | yarn lint --fix 141 | npm publish -w client/sdk/ 142 | env: 143 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 144 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | pull_request: 5 | 6 | env: 7 | solana_version: v1.17.0 8 | anchor_version: 0.29.0 9 | 10 | jobs: 11 | install: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - uses: actions/cache@v3 17 | name: cache solana cli 18 | id: cache-solana 19 | with: 20 | path: | 21 | ~/.cache/solana/ 22 | ~/.local/share/solana/ 23 | key: solana-${{ runner.os }}-v0000-${{ env.solana_version }} 24 | 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: 20 28 | 29 | - name: install essentials 30 | run: | 31 | sudo apt-get update 32 | sudo apt-get install -y pkg-config build-essential libudev-dev 33 | 34 | - name: Cache node dependencies 35 | uses: actions/cache@v3 36 | with: 37 | path: '**/node_modules' 38 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 39 | 40 | - name: install node_modules 41 | run: | 42 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 43 | yarn --frozen-lockfile --network-concurrency 2 44 | 45 | - uses: dtolnay/rust-toolchain@stable 46 | with: 47 | toolchain: stable 48 | 49 | - name: Cache rust 50 | uses: Swatinem/rust-cache@v2 51 | 52 | - name: install solana 53 | if: steps.cache-solana.outputs.cache-hit != 'true' 54 | run: | 55 | sh -c "$(curl -sSfL https://release.solana.com/${{ env.solana_version }}/install)" 56 | export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" 57 | solana --version 58 | 59 | clippy-lint: 60 | needs: install 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - uses: actions/checkout@v3 65 | - name: Cache rust 66 | uses: Swatinem/rust-cache@v2 67 | - name: Run fmt 68 | run: cargo fmt -- --check 69 | - name: Run clippy 70 | run: cargo clippy -- --deny=warnings 71 | 72 | yarn-lint: 73 | needs: install 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v3 77 | - name: Use Node ${{ matrix.node }} 78 | uses: actions/setup-node@v3 79 | with: 80 | node-version: 20 81 | 82 | - name: Cache node dependencies 83 | uses: actions/cache@v3 84 | with: 85 | path: '**/node_modules' 86 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 87 | 88 | - name: Run lint 89 | run: yarn lint 90 | 91 | test: 92 | needs: [clippy-lint, yarn-lint] 93 | runs-on: ubuntu-latest 94 | 95 | steps: 96 | - uses: actions/checkout@v3 97 | 98 | - name: Use Node ${{ matrix.node }} 99 | uses: actions/setup-node@v3 100 | with: 101 | node-version: 20 102 | 103 | - name: Cache node dependencies 104 | uses: actions/cache@v3 105 | with: 106 | path: '**/node_modules' 107 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 108 | - name: install node_modules 109 | run: | 110 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 111 | yarn --frozen-lockfile 112 | 113 | - uses: actions/cache@v3 114 | name: cache solana cli 115 | id: cache-solana 116 | with: 117 | path: | 118 | ~/.cache/solana/ 119 | ~/.local/share/solana/ 120 | key: solana-${{ runner.os }}-v0000-${{ env.solana_version }} 121 | 122 | - name: setup solana 123 | run: | 124 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 125 | solana --version 126 | solana-keygen new --silent --no-bip39-passphrase 127 | 128 | - name: run tests 129 | run: | 130 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 131 | ls node_modules/.bin 132 | npm i -g @coral-xyz/anchor-cli@${{ env.anchor_version }} ts-mocha typescript 133 | npm run build -w client/sdk/ 134 | anchor test 135 | 136 | - uses: actions/upload-artifact@v3 137 | if: always() 138 | with: 139 | name: program-logs 140 | path: .anchor/program-logs/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | test-ledger 8 | *.idea/ 9 | *dist/ 10 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec": "client/tests/**/*.ts", 3 | "loader": "tsx", 4 | "timeout": 1000000, 5 | "parallel": false 6 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | node_modules 6 | dist 7 | build 8 | test-ledger 9 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [features] 2 | seeds = false 3 | skip-lint = false 4 | 5 | [programs.localnet] 6 | soar = "SoarNNzwQHMwcfdkdLc6kvbkoMSxcHy89gTHrjhJYkk" 7 | tens = "Tensgwm3DY3UJ8nhF7xnD2Wo65VcnLTXjjoyEvs6Zyk" 8 | 9 | [programs.devnet] 10 | soar = "SoarNNzwQHMwcfdkdLc6kvbkoMSxcHy89gTHrjhJYkk" 11 | tens = "Tensgwm3DY3UJ8nhF7xnD2Wo65VcnLTXjjoyEvs6Zyk" 12 | 13 | [programs.mainnet] 14 | soar = "SoarNNzwQHMwcfdkdLc6kvbkoMSxcHy89gTHrjhJYkk" 15 | 16 | [registry] 17 | url = "https://api.apr.dev" 18 | 19 | [provider] 20 | cluster = "localnet" 21 | wallet = "client/tests/fixtures/provider.json" 22 | 23 | [scripts] 24 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 client/tests/*" 25 | 26 | [workspace] 27 | types = "client/sdk/src/idl" 28 | members = [ 29 | "programs/soar", 30 | "examples/tens" 31 | ] 32 | 33 | [test] 34 | startup_wait = 60000 35 | 36 | [[test.genesis]] 37 | address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" 38 | program = "client/tests/fixtures/mpl_token_metadata.so" 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*", 4 | "crates/*", 5 | "examples/*" 6 | ] 7 | 8 | [profile.release] 9 | overflow-checks = true 10 | lto = "fat" 11 | codegen-units = 1 12 | [profile.release.build-override] 13 | opt-level = 3 14 | incremental = false 15 | codegen-units = 1 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Magicblock Labs Pte. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /assets/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicblock-labs/SOAR/0522ab43fdb5252eaf239783457d7ffc39b4ec97/assets/design.png -------------------------------------------------------------------------------- /client/sdk/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /lib 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | src/idl/tens.ts -------------------------------------------------------------------------------- /client/sdk/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Magicblock Labs Pte. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /client/sdk/README.md: -------------------------------------------------------------------------------- 1 | # SOAR SDK 2 | 3 | This Typescript sdk provides a convenient interface and methods for interacting with the on-chain soar program. 4 | 5 | ## Contents 6 | 7 | - [SOAR SDK](#soar-sdk) 8 | - [Contents](#contents) 9 | - [Getting started](#getting-started) 10 | - [Classes](#classes) 11 | - [SoarProgram](#soarprogram) 12 | - [GameClient](#gameclient) 13 | - [InstructionBuilder](#instructionbuilder) 14 | - [Contributing](#contributing) 15 | - [Tests](#tests) 16 | 17 | ## Getting started 18 | 19 | ### Create a new game 20 | 21 | ```typescript 22 | import { SoarProgram, GameType, Genre } from "@magicblock-labs/soar-sdk"; 23 | 24 | // Create a Soar client using the '@solana/web3.js' active Connection and a defaultPayer 25 | const client = SoarProgram.getFromConnection(connection, defaultPayer); 26 | 27 | let game = Keypair.generate(); 28 | let title = "Game1"; 29 | let description = "Description"; 30 | let genre = Genre.Action; 31 | let gameType = GameType.Web; 32 | let nftMeta = Keypair.generate().publicKey; 33 | let _auths = auths.map((keypair) => keypair.publicKey); 34 | 35 | // Retrieve the bundled transaction. 36 | let { newGame, transaction } = await client.initializeNewGame(game.publicKey, title, description, genre, gameType, nftMeta, _auths); 37 | // Send and confirm the transaction with the game keypair as signer. 38 | await web3.sendAndConfirmTransaction(connection, transaction); 39 | ``` 40 | 41 | ### Create a leaderboard 42 | 43 | ```typescript 44 | const transactionIx = await client.addNewGameLeaderBoard( 45 | newGame, 46 | authWallet.publicKey, 47 | "my leaderboard", // description 48 | leaderboardNft, // nft associated with the leaderboard 49 | 100, 50 | true // isAscending 51 | ); 52 | 53 | await web3.sendAndConfirmTransaction(connection, transactionIx.transaction, [authWallet]); 54 | ```typescript 55 | 56 | ### Submit a score 57 | 58 | ```typescript 59 | const score = 10; 60 | const playerAddress = new web3.PublicKey("..."); // The player publicKey 61 | const authWallet = web3.Keypair.fromSecretKey(bs58.decode("")); // AUTH_WALLET_PRIVATE_KEY 62 | const leaderboardPda = new web3.PublicKey(""); // LEADERBOARD_PDA 63 | 64 | const transactionIx = await client.submitScoreToLeaderBoard( 65 | playerAddress, 66 | authWallet.publicKey, 67 | leaderboardPda, 68 | new BN(score) 69 | ); 70 | 71 | await web3.sendAndConfirmTransaction(connection, transactionIx.transaction, [authWallet]); 72 | ```typescript 73 | 74 | ## Classes 75 | 76 | ### SoarProgram 77 | 78 | The `SoarProgram` class gives client access to every instruction in the on-chain SOAR program. 79 | 80 | It also gives utility functions for deriving PDAs: 81 | 82 | ```typescript 83 | const user = Keypair.generate().publicKey; 84 | const playerAddress = client.utils.derivePlayerAddress(user)[0], 85 | ``` 86 | 87 | fetching an account: 88 | 89 | ```typescript 90 | const account = await client.fetchLeaderBoardAccount(address); 91 | ``` 92 | 93 | and fetching multiple accounts: 94 | 95 | ```typescript 96 | const accounts = await client.fetchAllLeaderboardAccounts([]); 97 | ``` 98 | 99 | ### GameClient 100 | 101 | The `GameClient` provides a more specific set of functions tailored to a single Game account. 102 | 103 | ```typescript 104 | import { GameClient } from "@magicblock-labs/soar-sdk"; 105 | ``` 106 | 107 | Get an instance representing an existing on-chain Game account: 108 | 109 | ```typescript 110 | const soar = SoarProgram.getFromConnection(connection, defaultPayer); 111 | const gameClient = new GameClient(soar, address); 112 | ``` 113 | 114 | Register a new game: 115 | 116 | ```typescript 117 | const soar = SoarProgram.getFromConnection(connection, defaultPayer); 118 | const game = new GameClient.register(soar, ...); 119 | ``` 120 | 121 | ```typescript 122 | // Create a new leaderboard: 123 | await game.addLeaderboard(....); 124 | 125 | // Access the game's state. 126 | await game.init(); 127 | 128 | // Refresh the game's state. 129 | await game.refresh(); 130 | 131 | // Get the most recently-created achievement for a game 132 | const achievement = game.recentAchievementAddress(); 133 | ``` 134 | 135 | ## InstructionBuilder 136 | 137 | ```typescript 138 | import { InstructionBuilder } from "@magicblock-labs/soar-sdk" 139 | ``` 140 | 141 | The InstructionBuilder provides a set of methods for conveniently bundling transactions. 142 | 143 | ```typescript 144 | const transaction = await this.builder 145 | .andInitializePlayer({username, nftMeta}, user) 146 | .andRegisterPlayerEntry(/*...*/) 147 | .andSubmitScoreToLeaderboard(/*...*/) 148 | .and(/*some other transaction*/) 149 | .then((builder) => builder.build()); 150 | ``` 151 | 152 | ### Tests. 153 | 154 | Test files are located in `client/tests/*`. To run the tests, enter `anchor test` from the root directory of the repository. 155 | -------------------------------------------------------------------------------- /client/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@magicblock-labs/soar-sdk", 3 | "version": "0.1.24", 4 | "description": "Sdk bindings for the SOAR smart contract.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:magicblock-labs/SOAR.git", 8 | "directory": "client/sdk" 9 | }, 10 | "main": "lib/index.js", 11 | "scripts": { 12 | "clean": "rimraf lib", 13 | "build": "npm run clean && tsc && npm run lint:fix", 14 | "build:docs": "typedoc && typedoc --plugin typedoc-plugin-markdown --out docs-mk", 15 | "dev": "tsc --watch", 16 | "start": "tsc", 17 | "lint": "eslint -c ../../.eslintrc.yml --ext .ts,.tsx src", 18 | "lint:fix": "eslint -c ../../.eslintrc.yml --ext .ts,.tsx src --fix" 19 | }, 20 | "files": [ 21 | "lib" 22 | ], 23 | "publishConfig": { 24 | "access": "public", 25 | "registry": "https://registry.npmjs.org/" 26 | }, 27 | "dependencies": { 28 | "@coral-xyz/anchor": "^0.27.0", 29 | "@metaplex-foundation/beet-solana": "^0.4.0", 30 | "@metaplex-foundation/js": "^0.20.1", 31 | "@metaplex-foundation/mpl-token-metadata": "^2.13.0", 32 | "@solana/spl-token": "^0.3.8", 33 | "@solana/web3.js": "^1.73.2", 34 | "bn.js": "^5.2.1", 35 | "typescript": "*" 36 | }, 37 | "devDependencies": { 38 | "@types/bn.js": "^5.1.2", 39 | "@types/chai": "^4.3.0", 40 | "@typescript-eslint/eslint-plugin": "^5.50.0", 41 | "@typescript-eslint/parser": "^5.50.0", 42 | "bs58": "^5.0.0", 43 | "chai": "^4.3.4", 44 | "eslint": "^8.33.0", 45 | "eslint-config-prettier": "^8.6.0", 46 | "eslint-plugin-import": "^2.25.3", 47 | "eslint-plugin-n": "^16.5.0", 48 | "eslint-plugin-react": "^7.32.2", 49 | "prettier": "^2.6.2", 50 | "tsx": "^3.12.3", 51 | "typedoc": "^0.25.1", 52 | "typedoc-plugin-markdown": "^3.16.0", 53 | "typescript": "*" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/sdk/src/constants.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | 3 | export const PROGRAM_ADDRESS = "SoarNNzwQHMwcfdkdLc6kvbkoMSxcHy89gTHrjhJYkk"; 4 | export const PROGRAM_ID = new PublicKey(PROGRAM_ADDRESS); 5 | 6 | export const TOKEN_METADATA_PROGRAM_ID = new PublicKey( 7 | "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" 8 | ); 9 | -------------------------------------------------------------------------------- /client/sdk/src/idl/tens.ts: -------------------------------------------------------------------------------- 1 | export interface Tens { 2 | version: "0.1.0"; 3 | name: "tens"; 4 | instructions: [ 5 | { 6 | name: "register"; 7 | accounts: [ 8 | { 9 | name: "signer"; 10 | isMut: true; 11 | isSigner: true; 12 | }, 13 | { 14 | name: "tensState"; 15 | isMut: true; 16 | isSigner: false; 17 | }, 18 | { 19 | name: "systemProgram"; 20 | isMut: false; 21 | isSigner: false; 22 | } 23 | ]; 24 | args: [ 25 | { 26 | name: "soarState"; 27 | type: "publicKey"; 28 | }, 29 | { 30 | name: "soarLeaderboard"; 31 | type: "publicKey"; 32 | }, 33 | { 34 | name: "soarLeaderboardTopEntries"; 35 | type: "publicKey"; 36 | } 37 | ]; 38 | }, 39 | { 40 | name: "makeMove"; 41 | accounts: [ 42 | { 43 | name: "user"; 44 | isMut: true; 45 | isSigner: true; 46 | }, 47 | { 48 | name: "tensState"; 49 | isMut: true; 50 | isSigner: false; 51 | }, 52 | { 53 | name: "soarState"; 54 | isMut: false; 55 | isSigner: false; 56 | }, 57 | { 58 | name: "soarLeaderboard"; 59 | isMut: false; 60 | isSigner: false; 61 | }, 62 | { 63 | name: "soarPlayerAccount"; 64 | isMut: false; 65 | isSigner: false; 66 | }, 67 | { 68 | name: "soarPlayerScores"; 69 | isMut: true; 70 | isSigner: false; 71 | }, 72 | { 73 | name: "soarTopEntries"; 74 | isMut: true; 75 | isSigner: false; 76 | isOptional: true; 77 | }, 78 | { 79 | name: "soarProgram"; 80 | isMut: false; 81 | isSigner: false; 82 | }, 83 | { 84 | name: "systemProgram"; 85 | isMut: false; 86 | isSigner: false; 87 | } 88 | ]; 89 | args: []; 90 | }, 91 | { 92 | name: "claimReward"; 93 | accounts: [ 94 | { 95 | name: "user"; 96 | isMut: true; 97 | isSigner: true; 98 | }, 99 | { 100 | name: "tensState"; 101 | isMut: true; 102 | isSigner: false; 103 | }, 104 | { 105 | name: "playerAccount"; 106 | isMut: true; 107 | isSigner: false; 108 | }, 109 | { 110 | name: "soarPlayerScores"; 111 | isMut: false; 112 | isSigner: false; 113 | }, 114 | { 115 | name: "soarTopEntries"; 116 | isMut: false; 117 | isSigner: false; 118 | }, 119 | { 120 | name: "soarState"; 121 | isMut: false; 122 | isSigner: false; 123 | }, 124 | { 125 | name: "soarAchievement"; 126 | isMut: false; 127 | isSigner: false; 128 | }, 129 | { 130 | name: "soarReward"; 131 | isMut: true; 132 | isSigner: false; 133 | }, 134 | { 135 | name: "soarPlayerAchievement"; 136 | isMut: true; 137 | isSigner: false; 138 | }, 139 | { 140 | name: "sourceTokenAccount"; 141 | isMut: true; 142 | isSigner: false; 143 | }, 144 | { 145 | name: "userTokenAccount"; 146 | isMut: true; 147 | isSigner: false; 148 | }, 149 | { 150 | name: "tokenProgram"; 151 | isMut: false; 152 | isSigner: false; 153 | }, 154 | { 155 | name: "systemProgram"; 156 | isMut: false; 157 | isSigner: false; 158 | }, 159 | { 160 | name: "soarProgram"; 161 | isMut: false; 162 | isSigner: false; 163 | } 164 | ]; 165 | args: []; 166 | } 167 | ]; 168 | accounts: [ 169 | { 170 | name: "tens"; 171 | docs: ["A simple game."]; 172 | type: { 173 | kind: "struct"; 174 | fields: [ 175 | { 176 | name: "counter"; 177 | docs: ["The game counter."]; 178 | type: "u64"; 179 | }, 180 | { 181 | name: "soar"; 182 | docs: ["The SOAR keys for this program."]; 183 | type: { 184 | defined: "SoarKeysStorage"; 185 | }; 186 | } 187 | ]; 188 | }; 189 | } 190 | ]; 191 | types: [ 192 | { 193 | name: "SoarKeysStorage"; 194 | type: { 195 | kind: "struct"; 196 | fields: [ 197 | { 198 | name: "state"; 199 | docs: ["The soar state for this game."]; 200 | type: "publicKey"; 201 | }, 202 | { 203 | name: "leaderboard"; 204 | docs: ["The soar leaderboard for this game."]; 205 | type: "publicKey"; 206 | }, 207 | { 208 | name: "topEntries"; 209 | docs: ["The soar top-entries account for this game."]; 210 | type: "publicKey"; 211 | } 212 | ]; 213 | }; 214 | } 215 | ]; 216 | } 217 | 218 | export const IDL: Tens = { 219 | version: "0.1.0", 220 | name: "tens", 221 | instructions: [ 222 | { 223 | name: "register", 224 | accounts: [ 225 | { 226 | name: "signer", 227 | isMut: true, 228 | isSigner: true, 229 | }, 230 | { 231 | name: "tensState", 232 | isMut: true, 233 | isSigner: false, 234 | }, 235 | { 236 | name: "systemProgram", 237 | isMut: false, 238 | isSigner: false, 239 | }, 240 | ], 241 | args: [ 242 | { 243 | name: "soarState", 244 | type: "publicKey", 245 | }, 246 | { 247 | name: "soarLeaderboard", 248 | type: "publicKey", 249 | }, 250 | { 251 | name: "soarLeaderboardTopEntries", 252 | type: "publicKey", 253 | }, 254 | ], 255 | }, 256 | { 257 | name: "makeMove", 258 | accounts: [ 259 | { 260 | name: "user", 261 | isMut: true, 262 | isSigner: true, 263 | }, 264 | { 265 | name: "tensState", 266 | isMut: true, 267 | isSigner: false, 268 | }, 269 | { 270 | name: "soarState", 271 | isMut: false, 272 | isSigner: false, 273 | }, 274 | { 275 | name: "soarLeaderboard", 276 | isMut: false, 277 | isSigner: false, 278 | }, 279 | { 280 | name: "soarPlayerAccount", 281 | isMut: false, 282 | isSigner: false, 283 | }, 284 | { 285 | name: "soarPlayerScores", 286 | isMut: true, 287 | isSigner: false, 288 | }, 289 | { 290 | name: "soarTopEntries", 291 | isMut: true, 292 | isSigner: false, 293 | isOptional: true, 294 | }, 295 | { 296 | name: "soarProgram", 297 | isMut: false, 298 | isSigner: false, 299 | }, 300 | { 301 | name: "systemProgram", 302 | isMut: false, 303 | isSigner: false, 304 | }, 305 | ], 306 | args: [], 307 | }, 308 | { 309 | name: "claimReward", 310 | accounts: [ 311 | { 312 | name: "user", 313 | isMut: true, 314 | isSigner: true, 315 | }, 316 | { 317 | name: "tensState", 318 | isMut: true, 319 | isSigner: false, 320 | }, 321 | { 322 | name: "playerAccount", 323 | isMut: true, 324 | isSigner: false, 325 | }, 326 | { 327 | name: "soarPlayerScores", 328 | isMut: false, 329 | isSigner: false, 330 | }, 331 | { 332 | name: "soarTopEntries", 333 | isMut: false, 334 | isSigner: false, 335 | }, 336 | { 337 | name: "soarState", 338 | isMut: false, 339 | isSigner: false, 340 | }, 341 | { 342 | name: "soarAchievement", 343 | isMut: false, 344 | isSigner: false, 345 | }, 346 | { 347 | name: "soarReward", 348 | isMut: true, 349 | isSigner: false, 350 | }, 351 | { 352 | name: "soarPlayerAchievement", 353 | isMut: true, 354 | isSigner: false, 355 | }, 356 | { 357 | name: "sourceTokenAccount", 358 | isMut: true, 359 | isSigner: false, 360 | }, 361 | { 362 | name: "userTokenAccount", 363 | isMut: true, 364 | isSigner: false, 365 | }, 366 | { 367 | name: "tokenProgram", 368 | isMut: false, 369 | isSigner: false, 370 | }, 371 | { 372 | name: "systemProgram", 373 | isMut: false, 374 | isSigner: false, 375 | }, 376 | { 377 | name: "soarProgram", 378 | isMut: false, 379 | isSigner: false, 380 | }, 381 | ], 382 | args: [], 383 | }, 384 | ], 385 | accounts: [ 386 | { 387 | name: "tens", 388 | docs: ["A simple game."], 389 | type: { 390 | kind: "struct", 391 | fields: [ 392 | { 393 | name: "counter", 394 | docs: ["The game counter."], 395 | type: "u64", 396 | }, 397 | { 398 | name: "soar", 399 | docs: ["The SOAR keys for this program."], 400 | type: { 401 | defined: "SoarKeysStorage", 402 | }, 403 | }, 404 | ], 405 | }, 406 | }, 407 | ], 408 | types: [ 409 | { 410 | name: "SoarKeysStorage", 411 | type: { 412 | kind: "struct", 413 | fields: [ 414 | { 415 | name: "state", 416 | docs: ["The soar state for this game."], 417 | type: "publicKey", 418 | }, 419 | { 420 | name: "leaderboard", 421 | docs: ["The soar leaderboard for this game."], 422 | type: "publicKey", 423 | }, 424 | { 425 | name: "topEntries", 426 | docs: ["The soar top-entries account for this game."], 427 | type: "publicKey", 428 | }, 429 | ], 430 | }, 431 | }, 432 | ], 433 | }; 434 | -------------------------------------------------------------------------------- /client/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./idl/soar"; 2 | export * from "./instructions"; 3 | export { Genre, GameType } from "./state"; 4 | export * from "./soar.game"; 5 | export * from "./soar.program"; 6 | -------------------------------------------------------------------------------- /client/sdk/src/instructions/accountsBuilder.ts: -------------------------------------------------------------------------------- 1 | import { IDL, type Soar } from "../idl/soar"; 2 | import { type AnchorProvider, Program } from "@coral-xyz/anchor"; 3 | import { 4 | type PublicKey, 5 | SystemProgram, 6 | SYSVAR_RENT_PUBKEY, 7 | } from "@solana/web3.js"; 8 | import { TOKEN_METADATA_PROGRAM_ID } from "../constants"; 9 | import { Utils } from "../utils"; 10 | import { 11 | ASSOCIATED_TOKEN_PROGRAM_ID, 12 | TOKEN_PROGRAM_ID, 13 | } from "@solana/spl-token"; 14 | import { AchievementAccount, RewardAccount } from "../state"; 15 | 16 | export class AccountsBuilder { 17 | readonly program: Program; 18 | readonly utils: Utils; 19 | 20 | constructor(private readonly provider: AnchorProvider, programId: PublicKey) { 21 | this.program = new Program(IDL, programId, provider); 22 | this.utils = new Utils(this.program.programId); 23 | } 24 | 25 | initializeGameAccounts = async ( 26 | game: PublicKey, 27 | creator?: PublicKey 28 | ): Promise<{ 29 | creator: PublicKey; 30 | game: PublicKey; 31 | systemProgram: PublicKey; 32 | }> => { 33 | return { 34 | creator: creator != null ? creator : this.provider.publicKey, 35 | game, 36 | systemProgram: SystemProgram.programId, 37 | }; 38 | }; 39 | 40 | initializePlayerAccounts = async ( 41 | user: PublicKey, 42 | payer?: PublicKey 43 | ): Promise<{ 44 | playerAccount: PublicKey; 45 | user: PublicKey; 46 | payer: PublicKey; 47 | systemProgram: PublicKey; 48 | }> => { 49 | return { 50 | playerAccount: this.utils.derivePlayerAddress(user)[0], 51 | user, 52 | payer: payer != null ? payer : this.provider.publicKey, 53 | systemProgram: SystemProgram.programId, 54 | }; 55 | }; 56 | 57 | initiateMergeAccounts = async ( 58 | user: PublicKey, 59 | mergeAccount: PublicKey, 60 | payer?: PublicKey 61 | ): Promise<{ 62 | user: PublicKey; 63 | payer: PublicKey; 64 | playerAccount: PublicKey; 65 | mergeAccount: PublicKey; 66 | systemProgram: PublicKey; 67 | }> => { 68 | return { 69 | user, 70 | payer: payer != null ? payer : this.provider.publicKey, 71 | playerAccount: this.utils.derivePlayerAddress(user)[0], 72 | mergeAccount, 73 | systemProgram: SystemProgram.programId, 74 | }; 75 | }; 76 | 77 | addAchievementAccounts = async ( 78 | game: PublicKey, 79 | authority: PublicKey, 80 | nextAchievement?: PublicKey, 81 | payer?: PublicKey 82 | ): Promise<{ 83 | newAchievement: PublicKey; 84 | game: PublicKey; 85 | payer: PublicKey; 86 | authority: PublicKey; 87 | systemProgram: PublicKey; 88 | }> => { 89 | let newAchievement = nextAchievement; 90 | if (newAchievement === undefined) { 91 | const gameAccount = await this.program.account.game.fetch(game); 92 | const id = gameAccount.achievementCount.addn(1); 93 | newAchievement = this.utils.deriveAchievementAddress(id, game)[0]; 94 | } 95 | 96 | return { 97 | newAchievement, 98 | game, 99 | payer: payer != null ? payer : this.provider.publicKey, 100 | authority, 101 | systemProgram: SystemProgram.programId, 102 | }; 103 | }; 104 | 105 | addLeaderboardAccounts = async ( 106 | game: PublicKey, 107 | authority: PublicKey, 108 | nextLeaderboard?: PublicKey, 109 | nullTopEntries?: boolean, 110 | payer?: PublicKey 111 | ): Promise<{ 112 | authority: PublicKey; 113 | game: PublicKey; 114 | payer: PublicKey; 115 | leaderboard: PublicKey; 116 | topEntries: PublicKey | null; 117 | systemProgram: PublicKey; 118 | }> => { 119 | let newLeaderBoard = nextLeaderboard; 120 | if (nextLeaderboard !== undefined) { 121 | newLeaderBoard = nextLeaderboard; 122 | } else { 123 | const gameAccount = await this.program.account.game.fetch(game); 124 | const id = gameAccount.leaderboardCount.addn(1); 125 | newLeaderBoard = this.utils.deriveLeaderBoardAddress(id, game)[0]; 126 | } 127 | 128 | let topEntries: PublicKey | null; 129 | if (nullTopEntries !== undefined && nullTopEntries) { 130 | topEntries = null; 131 | } else { 132 | topEntries = this.utils.deriveLeaderTopEntriesAddress(newLeaderBoard)[0]; 133 | } 134 | 135 | return { 136 | authority, 137 | game, 138 | payer: payer != null ? payer : this.provider.publicKey, 139 | leaderboard: newLeaderBoard, 140 | topEntries, 141 | systemProgram: SystemProgram.programId, 142 | }; 143 | }; 144 | 145 | addFtRewardAccounts = async ( 146 | authority: PublicKey, 147 | newReward: PublicKey, 148 | achievement: PublicKey, 149 | sourceTokenAccount: PublicKey, 150 | tokenAccountOwner: PublicKey, 151 | mint: PublicKey, 152 | game?: PublicKey, 153 | payer?: PublicKey 154 | ): Promise<{ 155 | authority: PublicKey; 156 | payer: PublicKey; 157 | game: PublicKey; 158 | achievement: PublicKey; 159 | newReward: PublicKey; 160 | rewardTokenMint: PublicKey; 161 | delegateFromTokenAccount: PublicKey; 162 | tokenAccountOwner: PublicKey; 163 | tokenProgram: PublicKey; 164 | systemProgram: PublicKey; 165 | }> => { 166 | const gameAddress = 167 | game ?? (await this.program.account.achievement.fetch(achievement)).game; 168 | 169 | return { 170 | authority, 171 | payer: payer != null ? payer : this.provider.publicKey, 172 | game: gameAddress, 173 | achievement, 174 | newReward, 175 | rewardTokenMint: mint, 176 | delegateFromTokenAccount: sourceTokenAccount, 177 | tokenAccountOwner, 178 | tokenProgram: TOKEN_PROGRAM_ID, 179 | systemProgram: SystemProgram.programId, 180 | }; 181 | }; 182 | 183 | addNftRewardAccounts = async ( 184 | authority: PublicKey, 185 | newReward: PublicKey, 186 | achievement: PublicKey, 187 | collectionMint?: PublicKey, 188 | collectionUpdateAuthority?: PublicKey, 189 | game?: PublicKey, 190 | payer?: PublicKey 191 | ): Promise<{ 192 | authority: PublicKey; 193 | payer: PublicKey; 194 | game: PublicKey; 195 | achievement: PublicKey; 196 | newReward: PublicKey; 197 | systemProgram: PublicKey; 198 | rewardCollectionMint: PublicKey | null; 199 | collectionUpdateAuth: PublicKey | null; 200 | collectionMetadata: PublicKey | null; 201 | tokenMetadataProgram: PublicKey | null; 202 | }> => { 203 | const gameAddress = 204 | game ?? (await this.program.account.achievement.fetch(achievement)).game; 205 | 206 | let collectionMetadata: PublicKey | null = null; 207 | let metadataProgram: PublicKey | null = null; 208 | 209 | if (collectionMint !== undefined) { 210 | if (collectionUpdateAuthority === undefined) { 211 | throw new Error("Collection update authority should be defined"); 212 | } 213 | 214 | collectionMetadata = this.utils.deriveMetadataAddress(collectionMint)[0]; 215 | metadataProgram = TOKEN_METADATA_PROGRAM_ID; 216 | } 217 | 218 | return { 219 | authority, 220 | payer: payer != null ? payer : this.provider.publicKey, 221 | game: gameAddress, 222 | achievement, 223 | newReward, 224 | systemProgram: SystemProgram.programId, 225 | rewardCollectionMint: collectionMint ?? null, 226 | collectionUpdateAuth: collectionUpdateAuthority ?? null, 227 | collectionMetadata, 228 | tokenMetadataProgram: metadataProgram ?? null, 229 | }; 230 | }; 231 | 232 | registerMergeApprovalAccounts = async ( 233 | user: PublicKey, 234 | mergeAccount: PublicKey 235 | ): Promise<{ 236 | user: PublicKey; 237 | playerAccount: PublicKey; 238 | mergeAccount: PublicKey; 239 | }> => { 240 | return { 241 | user, 242 | playerAccount: this.utils.derivePlayerAddress(user)[0], 243 | mergeAccount, 244 | }; 245 | }; 246 | 247 | claimFtRewardAccounts = async ( 248 | authority: PublicKey, 249 | achievement: PublicKey, 250 | user: PublicKey, 251 | reward?: PublicKey, 252 | game?: PublicKey 253 | ): Promise<{ 254 | user: PublicKey; 255 | authority: PublicKey; 256 | playerAccount: PublicKey; 257 | game: PublicKey; 258 | achievement: PublicKey; 259 | reward: PublicKey; 260 | playerAchievement: PublicKey; 261 | sourceTokenAccount: PublicKey; 262 | userTokenAccount: PublicKey; 263 | tokenProgram: PublicKey; 264 | mint: PublicKey; 265 | }> => { 266 | let rewardAddress: PublicKey; 267 | let gameAddress: PublicKey; 268 | 269 | if (reward === undefined || game === undefined) { 270 | const account = await this.program.account.achievement.fetch(achievement); 271 | const achievementAccount = AchievementAccount.fromIdlAccount( 272 | account, 273 | achievement 274 | ); 275 | if (achievementAccount.reward === null) { 276 | throw new Error("No reward for achievement"); 277 | } 278 | 279 | rewardAddress = achievementAccount.reward; 280 | gameAddress = achievementAccount.game; 281 | } else { 282 | rewardAddress = reward; 283 | gameAddress = game; 284 | } 285 | 286 | const playerAccount = this.utils.derivePlayerAddress(user)[0]; 287 | const playerAchievement = this.utils.derivePlayerAchievementAddress( 288 | user, 289 | achievement 290 | )[0]; 291 | 292 | const idlAccount = await this.program.account.reward.fetch(rewardAddress); 293 | const rewardAccount = RewardAccount.fromIdlAccount( 294 | idlAccount, 295 | rewardAddress 296 | ); 297 | 298 | if (rewardAccount.FungibleToken === undefined) { 299 | throw new Error("Not a fungible-token reward"); 300 | } 301 | const mint = rewardAccount.FungibleToken.mint; 302 | 303 | return { 304 | user, 305 | authority, 306 | playerAccount, 307 | game: gameAddress, 308 | achievement, 309 | reward: rewardAddress, 310 | playerAchievement, 311 | sourceTokenAccount: rewardAccount.FungibleToken.account, 312 | userTokenAccount: this.utils.deriveAssociatedTokenAddress(mint, user), 313 | tokenProgram: TOKEN_PROGRAM_ID, 314 | mint, 315 | }; 316 | }; 317 | 318 | claimNftRewardAccounts = async ( 319 | authority: PublicKey, 320 | achievement: PublicKey, 321 | mint: PublicKey, 322 | user: PublicKey, 323 | reward?: PublicKey, 324 | game?: PublicKey, 325 | payer?: PublicKey 326 | ): Promise<{ 327 | user: PublicKey; 328 | authority: PublicKey; 329 | playerAccount: PublicKey; 330 | game: PublicKey; 331 | achievement: PublicKey; 332 | reward: PublicKey; 333 | playerAchievement: PublicKey; 334 | payer: PublicKey; 335 | claim: PublicKey; 336 | newMint: PublicKey; 337 | newMetadata: PublicKey; 338 | newMasterEdition: PublicKey; 339 | mintTo: PublicKey; 340 | tokenMetadataProgram: PublicKey; 341 | associatedTokenProgram: PublicKey; 342 | systemProgram: PublicKey; 343 | tokenProgram: PublicKey; 344 | rent: PublicKey; 345 | }> => { 346 | let rewardAddress: PublicKey; 347 | let gameAddress: PublicKey; 348 | 349 | if (reward === undefined || game === undefined) { 350 | const account = await this.program.account.achievement.fetch(achievement); 351 | const achievementAccount = AchievementAccount.fromIdlAccount( 352 | account, 353 | achievement 354 | ); 355 | if (achievementAccount.reward === null) { 356 | throw new Error("No reward for achievement"); 357 | } 358 | 359 | rewardAddress = achievementAccount.reward; 360 | gameAddress = achievementAccount.game; 361 | } else { 362 | rewardAddress = reward; 363 | gameAddress = game; 364 | } 365 | 366 | const playerAccount = this.utils.derivePlayerAddress(user)[0]; 367 | const playerAchievement = this.utils.derivePlayerAchievementAddress( 368 | user, 369 | achievement 370 | )[0]; 371 | 372 | const metadata = this.utils.deriveMetadataAddress(mint)[0]; 373 | const masterEdition = this.utils.deriveEditionAddress(mint)[0]; 374 | const userAta = this.utils.deriveAssociatedTokenAddress(mint, user); 375 | 376 | const claim = this.utils.deriveNftClaimAddress(rewardAddress, mint)[0]; 377 | 378 | return { 379 | user, 380 | authority, 381 | playerAccount, 382 | game: gameAddress, 383 | achievement, 384 | reward: rewardAddress, 385 | playerAchievement, 386 | payer: payer != null ? payer : this.provider.publicKey, 387 | claim, 388 | newMint: mint, 389 | newMetadata: metadata, 390 | newMasterEdition: masterEdition, 391 | mintTo: userAta, 392 | tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, 393 | associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, 394 | systemProgram: SystemProgram.programId, 395 | tokenProgram: TOKEN_PROGRAM_ID, 396 | rent: SYSVAR_RENT_PUBKEY, 397 | }; 398 | }; 399 | 400 | registerPlayerEntryAccounts = async ( 401 | user: PublicKey, 402 | leaderboard: PublicKey, 403 | game?: PublicKey, 404 | payer?: PublicKey 405 | ): Promise<{ 406 | user: PublicKey; 407 | payer: PublicKey; 408 | playerAccount: PublicKey; 409 | newList: PublicKey; 410 | game: PublicKey; 411 | leaderboard: PublicKey; 412 | systemProgram: PublicKey; 413 | }> => { 414 | const gameAddress = 415 | game ?? (await this.program.account.leaderBoard.fetch(leaderboard)).game; 416 | const playerAccount = this.utils.derivePlayerAddress(user)[0]; 417 | const newList = this.utils.derivePlayerScoresListAddress( 418 | user, 419 | leaderboard 420 | )[0]; 421 | 422 | return { 423 | user, 424 | payer: payer != null ? payer : this.provider.publicKey, 425 | playerAccount, 426 | newList, 427 | game: gameAddress, 428 | leaderboard, 429 | systemProgram: SystemProgram.programId, 430 | }; 431 | }; 432 | 433 | submitScoreAccounts = async ( 434 | user: PublicKey, 435 | authority: PublicKey, 436 | leaderboard: PublicKey, 437 | game?: PublicKey, 438 | payer?: PublicKey 439 | ): Promise<{ 440 | payer: PublicKey; 441 | playerAccount: PublicKey; 442 | authority: PublicKey; 443 | game: PublicKey; 444 | leaderboard: PublicKey; 445 | playerScores: PublicKey; 446 | topEntries: PublicKey | null; 447 | systemProgram: PublicKey; 448 | }> => { 449 | const leaderboardAccount = await this.program.account.leaderBoard.fetch( 450 | leaderboard 451 | ); 452 | const gameAddress = game ?? leaderboardAccount.game; 453 | const playerAccount = this.utils.derivePlayerAddress(user)[0]; 454 | const playerScores = this.utils.derivePlayerScoresListAddress( 455 | user, 456 | leaderboard 457 | )[0]; 458 | const topEntries = leaderboardAccount.topEntries; 459 | 460 | return { 461 | payer: payer != null ? payer : this.provider.publicKey, 462 | playerAccount, 463 | authority, 464 | game: gameAddress, 465 | leaderboard, 466 | playerScores, 467 | topEntries, 468 | systemProgram: SystemProgram.programId, 469 | }; 470 | }; 471 | 472 | updateAchievementAccounts = async ( 473 | authority: PublicKey, 474 | achievement: PublicKey, 475 | game?: PublicKey 476 | ): Promise<{ 477 | authority: PublicKey; 478 | game: PublicKey; 479 | achievement: PublicKey; 480 | }> => { 481 | const gameAddress = 482 | game ?? (await this.program.account.achievement.fetch(achievement)).game; 483 | 484 | return { 485 | authority, 486 | game: gameAddress, 487 | achievement, 488 | }; 489 | }; 490 | 491 | unlockPlayerAchievementAccounts = async ( 492 | user: PublicKey, 493 | authority: PublicKey, 494 | achievement: PublicKey, 495 | leaderboard: PublicKey, 496 | game?: PublicKey, 497 | payer?: PublicKey 498 | ): Promise<{ 499 | payer: PublicKey; 500 | playerAccount: PublicKey; 501 | playerScores: PublicKey; 502 | game: PublicKey; 503 | achievement: PublicKey; 504 | authority: PublicKey; 505 | playerAchievement: PublicKey; 506 | }> => { 507 | const playerAccount = this.utils.derivePlayerAddress(user)[0]; 508 | 509 | const gameAddress = 510 | game ?? (await this.program.account.achievement.fetch(achievement)).game; 511 | const playerEntryList = this.utils.derivePlayerScoresListAddress( 512 | user, 513 | leaderboard 514 | )[0]; 515 | const newPlayerAchievement = this.utils.derivePlayerAchievementAddress( 516 | user, 517 | achievement 518 | )[0]; 519 | 520 | return { 521 | payer: payer != null ? payer : this.provider.publicKey, 522 | playerAccount, 523 | playerScores: playerEntryList, 524 | game: gameAddress, 525 | achievement, 526 | authority, 527 | playerAchievement: newPlayerAchievement, 528 | }; 529 | }; 530 | 531 | updateGameAccounts = async ( 532 | game: PublicKey, 533 | authority: PublicKey, 534 | payer?: PublicKey 535 | ): Promise<{ 536 | payer: PublicKey; 537 | game: PublicKey; 538 | authority: PublicKey; 539 | systemProgram: PublicKey; 540 | }> => { 541 | return { 542 | payer: payer != null ? payer : this.provider.publicKey, 543 | game, 544 | authority, 545 | systemProgram: SystemProgram.programId, 546 | }; 547 | }; 548 | 549 | updateLeaderboardAccounts = async ( 550 | authority: PublicKey, 551 | leaderboard: PublicKey, 552 | game?: PublicKey, 553 | topEntries?: PublicKey 554 | ): Promise<{ 555 | authority: PublicKey; 556 | game: PublicKey; 557 | leaderboard: PublicKey; 558 | topEntries: PublicKey | null; 559 | }> => { 560 | const gameAddress = 561 | game ?? (await this.program.account.leaderBoard.fetch(leaderboard)).game; 562 | 563 | return { 564 | authority, 565 | game: gameAddress, 566 | leaderboard, 567 | topEntries: topEntries ?? null, 568 | }; 569 | }; 570 | 571 | updatePlayerAccounts = async ( 572 | user: PublicKey 573 | ): Promise<{ 574 | user: PublicKey; 575 | playerAccount: PublicKey; 576 | }> => { 577 | return { 578 | user, 579 | playerAccount: this.utils.derivePlayerAddress(user)[0], 580 | }; 581 | }; 582 | 583 | verifyNftRewardAccounts = async ( 584 | user: PublicKey, 585 | achievement: PublicKey, 586 | mint: PublicKey, 587 | reward?: PublicKey, 588 | game?: PublicKey, 589 | payer?: PublicKey 590 | ): Promise<{ 591 | payer: PublicKey; 592 | user: PublicKey; 593 | playerAccount: PublicKey; 594 | achievement: PublicKey; 595 | game: PublicKey; 596 | reward: PublicKey; 597 | playerAchievement: PublicKey; 598 | mint: PublicKey; 599 | claim: PublicKey; 600 | metadataToVerify: PublicKey; 601 | collectionMint: PublicKey; 602 | collectionMetadata: PublicKey; 603 | collectionEdition: PublicKey; 604 | tokenMetadataProgram: PublicKey; 605 | }> => { 606 | let rewardAddress: PublicKey; 607 | let gameAddress: PublicKey; 608 | 609 | if (reward === undefined || game === undefined) { 610 | const account = await this.program.account.achievement.fetch(achievement); 611 | const achievementAccount = AchievementAccount.fromIdlAccount( 612 | account, 613 | achievement 614 | ); 615 | if (achievementAccount.reward === null) { 616 | throw new Error("No reward for achievement"); 617 | } 618 | 619 | rewardAddress = achievementAccount.reward; 620 | gameAddress = achievementAccount.game; 621 | } else { 622 | rewardAddress = reward; 623 | gameAddress = game; 624 | } 625 | 626 | const playerAccount = this.utils.derivePlayerAddress(user)[0]; 627 | const playerAchievement = this.utils.derivePlayerAchievementAddress( 628 | user, 629 | achievement 630 | )[0]; 631 | 632 | const claim = this.utils.deriveNftClaimAddress(rewardAddress, mint)[0]; 633 | const metadata = this.utils.deriveMetadataAddress(mint)[0]; 634 | 635 | const rewardAccount = RewardAccount.fromIdlAccount( 636 | await this.program.account.reward.fetch(rewardAddress), 637 | rewardAddress 638 | ); 639 | if ( 640 | rewardAccount.NonFungibleToken === undefined || 641 | rewardAccount.NonFungibleToken?.collection === null 642 | ) { 643 | throw new Error("No collection to verify rewards for."); 644 | } 645 | 646 | const collectionMint = rewardAccount.NonFungibleToken.collection; 647 | const collectionMetadata = 648 | this.utils.deriveMetadataAddress(collectionMint)[0]; 649 | const collectionEdition = 650 | this.utils.deriveEditionAddress(collectionMint)[0]; 651 | 652 | return { 653 | payer: payer != null ? payer : this.provider.publicKey, 654 | user, 655 | playerAccount, 656 | achievement, 657 | game: gameAddress, 658 | reward: rewardAddress, 659 | playerAchievement, 660 | mint, 661 | claim, 662 | metadataToVerify: metadata, 663 | collectionMint, 664 | collectionEdition, 665 | collectionMetadata, 666 | tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, 667 | }; 668 | }; 669 | } 670 | -------------------------------------------------------------------------------- /client/sdk/src/instructions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ixBuilder"; 2 | export * from "./accountsBuilder"; 3 | -------------------------------------------------------------------------------- /client/sdk/src/instructions/ixBuilder.ts: -------------------------------------------------------------------------------- 1 | import { IDL, type Soar } from "../idl/soar"; 2 | import { type AnchorProvider, Program } from "@coral-xyz/anchor"; 3 | import { 4 | Transaction, 5 | type PublicKey, 6 | type TransactionInstruction, 7 | type Signer, 8 | type ConfirmOptions, 9 | } from "@solana/web3.js"; 10 | import { 11 | type InitializeGameArgs, 12 | type AddAchievementArgs, 13 | type AddNftRewardArgs, 14 | type AddFtRewardArgs, 15 | type InitializePlayerArgs, 16 | type InitMergeArgs, 17 | type SubmitScoreArgs, 18 | type UpdateAchievementArgs, 19 | type UpdateGameArgs, 20 | type UpdateLeaderboardArgs, 21 | type UpdatePlayerArgs, 22 | type AddLeaderBoardArgs, 23 | } from "../types"; 24 | import { 25 | addLeaderBoardInstruction, 26 | addAchievementInstruction, 27 | initPlayerInstruction, 28 | initializeGameInstruction, 29 | registerPlayerEntryInstruction, 30 | updateAchievementInstruction, 31 | updateGameInstruction, 32 | updatePlayerInstruction, 33 | updateLeaderBoardInstruction, 34 | submitScoreInstruction, 35 | unlockPlayerAchievementInstruction, 36 | initiateMergeInstruction, 37 | registerMergeApprovalInstruction, 38 | addFtRewardInstruction, 39 | addNftRewardInstruction, 40 | claimFtRewardInstruction, 41 | claimNftRewardInstruction, 42 | verifyNftRewardInstruction, 43 | } from "./rawInstructions"; 44 | import { Utils } from "../utils"; 45 | import { AccountsBuilder } from "./accountsBuilder"; 46 | 47 | /** A class for constructing more-tailored and specific instructions. */ 48 | export class InstructionBuilder { 49 | instructions: TransactionInstruction[]; 50 | signers: Signer[]; 51 | 52 | readonly program: Program; 53 | readonly utils: Utils; 54 | readonly accounts: AccountsBuilder; 55 | 56 | constructor(private readonly provider: AnchorProvider, programId: PublicKey) { 57 | this.instructions = []; 58 | this.signers = []; 59 | this.program = new Program(IDL, programId, provider); 60 | this.utils = new Utils(this.program.programId); 61 | this.accounts = new AccountsBuilder(provider, programId); 62 | } 63 | 64 | append = (instructions: TransactionInstruction[]): InstructionBuilder => { 65 | this.instructions = this.instructions.concat(instructions); 66 | return this; 67 | }; 68 | 69 | public async initGameStep( 70 | args: InitializeGameArgs, 71 | newGame: PublicKey, 72 | creator?: PublicKey 73 | ): Promise< 74 | [ 75 | InstructionBuilder, 76 | Awaited< 77 | ReturnType 78 | > 79 | ] 80 | > { 81 | const accounts = await this.accounts.initializeGameAccounts( 82 | newGame, 83 | creator 84 | ); 85 | const instruction = await initializeGameInstruction( 86 | this.program, 87 | args, 88 | accounts 89 | ); 90 | 91 | return [this.append([instruction]), accounts]; 92 | } 93 | 94 | public async initPlayerStep( 95 | args: InitializePlayerArgs, 96 | user: PublicKey, 97 | payer?: PublicKey 98 | ): Promise< 99 | [ 100 | InstructionBuilder, 101 | Awaited< 102 | ReturnType 103 | > 104 | ] 105 | > { 106 | const accounts = await this.accounts.initializePlayerAccounts(user, payer); 107 | const instruction = await initPlayerInstruction( 108 | this.program, 109 | args, 110 | accounts 111 | ); 112 | 113 | return [this.append([instruction]), accounts]; 114 | } 115 | 116 | public async updateGameStep( 117 | args: UpdateGameArgs, 118 | game: PublicKey, 119 | authority: PublicKey, 120 | payer?: PublicKey 121 | ): Promise< 122 | [ 123 | InstructionBuilder, 124 | Awaited> 125 | ] 126 | > { 127 | const accounts = await this.accounts.updateGameAccounts( 128 | game, 129 | authority, 130 | payer 131 | ); 132 | const instruction = await updateGameInstruction( 133 | this.program, 134 | args, 135 | accounts 136 | ); 137 | 138 | return [this.append([instruction]), accounts]; 139 | } 140 | 141 | public async updatePlayerStep( 142 | args: UpdatePlayerArgs, 143 | user: PublicKey 144 | ): Promise< 145 | [ 146 | InstructionBuilder, 147 | Awaited> 148 | ] 149 | > { 150 | const accounts = await this.accounts.updatePlayerAccounts(user); 151 | const instruction = await updatePlayerInstruction( 152 | this.program, 153 | args, 154 | accounts 155 | ); 156 | 157 | return [this.append([instruction]), accounts]; 158 | } 159 | 160 | public async initMergeStep( 161 | args: InitMergeArgs, 162 | user: PublicKey, 163 | newMergeAccount: PublicKey, 164 | payer?: PublicKey 165 | ): Promise< 166 | [ 167 | InstructionBuilder, 168 | Awaited< 169 | ReturnType 170 | > 171 | ] 172 | > { 173 | const accounts = await this.accounts.initiateMergeAccounts( 174 | user, 175 | newMergeAccount, 176 | payer 177 | ); 178 | const instruction = await initiateMergeInstruction( 179 | this.program, 180 | args, 181 | accounts 182 | ); 183 | 184 | return [this.append([instruction]), accounts]; 185 | } 186 | 187 | public async registerMergeApprovalStep( 188 | user: PublicKey, 189 | mergeAccount: PublicKey 190 | ): Promise< 191 | [ 192 | InstructionBuilder, 193 | Awaited< 194 | ReturnType< 195 | typeof AccountsBuilder.prototype.registerMergeApprovalAccounts 196 | > 197 | > 198 | ] 199 | > { 200 | const accounts = await this.accounts.registerMergeApprovalAccounts( 201 | user, 202 | mergeAccount 203 | ); 204 | const instruction = await registerMergeApprovalInstruction( 205 | this.program, 206 | accounts 207 | ); 208 | 209 | return [this.append([instruction]), accounts]; 210 | } 211 | 212 | public async addAchievementStep( 213 | args: AddAchievementArgs, 214 | game: PublicKey, 215 | authority: PublicKey, 216 | nextAchievement?: PublicKey, 217 | payer?: PublicKey 218 | ): Promise< 219 | [ 220 | InstructionBuilder, 221 | Awaited< 222 | ReturnType 223 | > 224 | ] 225 | > { 226 | const accounts = await this.accounts.addAchievementAccounts( 227 | game, 228 | authority, 229 | nextAchievement, 230 | payer 231 | ); 232 | const instruction = await addAchievementInstruction( 233 | this.program, 234 | args, 235 | accounts 236 | ); 237 | 238 | return [this.append([instruction]), accounts]; 239 | } 240 | 241 | public async addLeaderBoardStep( 242 | args: AddLeaderBoardArgs, 243 | gameAddress: PublicKey, 244 | authority: PublicKey, 245 | nextLeaderboard?: PublicKey, 246 | payer?: PublicKey 247 | ): Promise< 248 | [ 249 | InstructionBuilder, 250 | Awaited< 251 | ReturnType 252 | > 253 | ] 254 | > { 255 | let nullTopEntries; 256 | if (args.scoresToRetain !== null && args.scoresToRetain > 0) { 257 | nullTopEntries = false; 258 | } else { 259 | nullTopEntries = true; 260 | } 261 | 262 | const accounts = await this.accounts.addLeaderboardAccounts( 263 | gameAddress, 264 | authority, 265 | nextLeaderboard, 266 | nullTopEntries, 267 | payer 268 | ); 269 | const instruction = await addLeaderBoardInstruction( 270 | this.program, 271 | args, 272 | accounts 273 | ); 274 | 275 | return [this.append([instruction]), accounts]; 276 | } 277 | 278 | public async updateLeaderboardStep( 279 | args: UpdateLeaderboardArgs, 280 | authority: PublicKey, 281 | leaderboard: PublicKey, 282 | game?: PublicKey, 283 | topEntries?: PublicKey 284 | ): Promise< 285 | [ 286 | InstructionBuilder, 287 | Awaited< 288 | ReturnType 289 | > 290 | ] 291 | > { 292 | const accounts = await this.accounts.updateLeaderboardAccounts( 293 | authority, 294 | leaderboard, 295 | game, 296 | topEntries 297 | ); 298 | const instruction = await updateLeaderBoardInstruction( 299 | this.program, 300 | args, 301 | accounts 302 | ); 303 | 304 | return [this.append([instruction]), accounts]; 305 | } 306 | 307 | public async registerPlayerEntryStep( 308 | user: PublicKey, 309 | leaderboard: PublicKey, 310 | game?: PublicKey, 311 | payer?: PublicKey 312 | ): Promise< 313 | [ 314 | InstructionBuilder, 315 | Awaited< 316 | ReturnType 317 | > 318 | ] 319 | > { 320 | const accounts = await this.accounts.registerPlayerEntryAccounts( 321 | user, 322 | leaderboard, 323 | game, 324 | payer 325 | ); 326 | const instruction = await registerPlayerEntryInstruction( 327 | this.program, 328 | accounts 329 | ); 330 | 331 | return [this.append([instruction]), accounts]; 332 | } 333 | 334 | public async submitScoreStep( 335 | args: SubmitScoreArgs, 336 | user: PublicKey, 337 | authority: PublicKey, 338 | leaderboard: PublicKey, 339 | game?: PublicKey, 340 | payer?: PublicKey 341 | ): Promise< 342 | [ 343 | InstructionBuilder, 344 | Awaited> 345 | ] 346 | > { 347 | const accounts = await this.accounts.submitScoreAccounts( 348 | user, 349 | authority, 350 | leaderboard, 351 | game, 352 | payer 353 | ); 354 | const instruction = await submitScoreInstruction( 355 | this.program, 356 | args, 357 | accounts 358 | ); 359 | 360 | return [this.append([instruction]), accounts]; 361 | } 362 | 363 | public async updateAchievementStep( 364 | args: UpdateAchievementArgs, 365 | authority: PublicKey, 366 | achievement: PublicKey, 367 | game?: PublicKey 368 | ): Promise< 369 | [ 370 | InstructionBuilder, 371 | Awaited< 372 | ReturnType 373 | > 374 | ] 375 | > { 376 | const accounts = await this.accounts.updateAchievementAccounts( 377 | authority, 378 | achievement, 379 | game 380 | ); 381 | const instruction = await updateAchievementInstruction( 382 | this.program, 383 | args, 384 | accounts 385 | ); 386 | 387 | return [this.append([instruction]), accounts]; 388 | } 389 | 390 | public async unlockPlayerAchievementStep( 391 | user: PublicKey, 392 | authority: PublicKey, 393 | achievement: PublicKey, 394 | leaderboard: PublicKey, 395 | game?: PublicKey, 396 | payer?: PublicKey 397 | ): Promise< 398 | [ 399 | InstructionBuilder, 400 | Awaited< 401 | ReturnType< 402 | typeof AccountsBuilder.prototype.unlockPlayerAchievementAccounts 403 | > 404 | > 405 | ] 406 | > { 407 | const accounts = await this.accounts.unlockPlayerAchievementAccounts( 408 | user, 409 | authority, 410 | achievement, 411 | leaderboard, 412 | game, 413 | payer 414 | ); 415 | const instruction = await unlockPlayerAchievementInstruction( 416 | this.program, 417 | accounts 418 | ); 419 | 420 | return [this.append([instruction]), accounts]; 421 | } 422 | 423 | public async addFungibleRewardStep( 424 | args: AddFtRewardArgs, 425 | authority: PublicKey, 426 | newReward: PublicKey, 427 | achievement: PublicKey, 428 | sourceTokenAccount: PublicKey, 429 | tokenAccountOwner: PublicKey, 430 | mint: PublicKey, 431 | game?: PublicKey, 432 | payer?: PublicKey 433 | ): Promise< 434 | [ 435 | InstructionBuilder, 436 | Awaited> 437 | ] 438 | > { 439 | const accounts = await this.accounts.addFtRewardAccounts( 440 | authority, 441 | newReward, 442 | achievement, 443 | sourceTokenAccount, 444 | tokenAccountOwner, 445 | mint, 446 | game, 447 | payer 448 | ); 449 | const instruction = await addFtRewardInstruction( 450 | this.program, 451 | args, 452 | accounts 453 | ); 454 | 455 | return [this.append([instruction]), accounts]; 456 | } 457 | 458 | public async addNonFungibleRewardStep( 459 | args: AddNftRewardArgs, 460 | authority: PublicKey, 461 | newReward: PublicKey, 462 | achievement: PublicKey, 463 | collectionMint?: PublicKey, 464 | collectionUpdateAuthority?: PublicKey, 465 | game?: PublicKey, 466 | payer?: PublicKey 467 | ): Promise< 468 | [ 469 | InstructionBuilder, 470 | Awaited> 471 | ] 472 | > { 473 | const accounts = await this.accounts.addNftRewardAccounts( 474 | authority, 475 | newReward, 476 | achievement, 477 | collectionMint, 478 | collectionUpdateAuthority, 479 | game, 480 | payer 481 | ); 482 | const instruction = await addNftRewardInstruction( 483 | this.program, 484 | args, 485 | accounts 486 | ); 487 | 488 | return [this.append([instruction]), accounts]; 489 | } 490 | 491 | public async claimNftRewardStep( 492 | authority: PublicKey, 493 | achievement: PublicKey, 494 | mint: PublicKey, 495 | user: PublicKey, 496 | reward?: PublicKey, 497 | game?: PublicKey, 498 | payer?: PublicKey 499 | ): Promise< 500 | [ 501 | InstructionBuilder, 502 | Awaited< 503 | ReturnType 504 | > 505 | ] 506 | > { 507 | const accounts = await this.accounts.claimNftRewardAccounts( 508 | authority, 509 | achievement, 510 | mint, 511 | user, 512 | reward, 513 | game, 514 | payer 515 | ); 516 | const instruction = await claimNftRewardInstruction(this.program, accounts); 517 | 518 | return [this.append([instruction]), accounts]; 519 | } 520 | 521 | public async claimFtRewardStep( 522 | authority: PublicKey, 523 | achievement: PublicKey, 524 | user: PublicKey, 525 | reward?: PublicKey, 526 | game?: PublicKey 527 | ): Promise< 528 | [ 529 | InstructionBuilder, 530 | Awaited< 531 | ReturnType 532 | > 533 | ] 534 | > { 535 | const accounts = await this.accounts.claimFtRewardAccounts( 536 | authority, 537 | achievement, 538 | user, 539 | reward, 540 | game 541 | ); 542 | const omitted: Omit< 543 | Awaited< 544 | ReturnType 545 | >, 546 | "mint" 547 | > = { ...accounts }; 548 | const instruction = await claimFtRewardInstruction(this.program, omitted); 549 | 550 | return [this.append([instruction]), accounts]; 551 | } 552 | 553 | public async verifyPlayerNftRewardStep( 554 | user: PublicKey, 555 | achievement: PublicKey, 556 | mint: PublicKey, 557 | reward?: PublicKey, 558 | game?: PublicKey, 559 | payer?: PublicKey 560 | ): Promise< 561 | [ 562 | InstructionBuilder, 563 | Awaited< 564 | ReturnType 565 | > 566 | ] 567 | > { 568 | const accounts = await this.accounts.verifyNftRewardAccounts( 569 | user, 570 | achievement, 571 | mint, 572 | reward, 573 | game, 574 | payer 575 | ); 576 | const instruction = await verifyNftRewardInstruction( 577 | this.program, 578 | accounts 579 | ); 580 | 581 | return [this.append([instruction]), accounts]; 582 | } 583 | 584 | sign(signers: Signer[]): void { 585 | this.signers = this.signers.concat(signers); 586 | } 587 | 588 | /** Bundle instructions into a single transaction. */ 589 | build(): Transaction { 590 | const transaction = new Transaction(); 591 | this.instructions.forEach((ix) => transaction.add(ix)); 592 | this.clean(); 593 | return transaction; 594 | } 595 | 596 | /** Internally reset the instruction list in this instance. */ 597 | clean(): void { 598 | this.instructions = []; 599 | } 600 | 601 | /** Send and confirm the bundled transaction. */ 602 | public async complete(opts?: ConfirmOptions): Promise { 603 | return this.provider 604 | .sendAndConfirm(this.build(), this.signers, opts) 605 | .catch((e) => { 606 | throw e; 607 | }); 608 | } 609 | } 610 | -------------------------------------------------------------------------------- /client/sdk/src/instructions/rawInstructions.ts: -------------------------------------------------------------------------------- 1 | import { type Soar } from "../idl/soar"; 2 | import { 3 | type AddAchievementArgs, 4 | type AddLeaderBoardArgs, 5 | type AddFtRewardArgs, 6 | type AddNftRewardArgs, 7 | type InitializeGameArgs, 8 | type InitializePlayerArgs, 9 | type SubmitScoreArgs, 10 | type UpdateAchievementArgs, 11 | type UpdatePlayerArgs, 12 | type UpdateLeaderboardArgs, 13 | type UpdateGameArgs, 14 | type InitMergeArgs, 15 | } from "../types"; 16 | import { type PublicKey, type TransactionInstruction } from "@solana/web3.js"; 17 | import { type Program } from "@coral-xyz/anchor"; 18 | 19 | export const initializeGameInstruction = async ( 20 | program: Program, 21 | args: InitializeGameArgs, 22 | accounts: { 23 | creator: PublicKey; 24 | game: PublicKey; 25 | systemProgram: PublicKey; 26 | }, 27 | pre?: TransactionInstruction[] 28 | ): Promise => 29 | program.methods 30 | .initializeGame(args.gameMeta, args.authorities) 31 | .accounts(accounts) 32 | .preInstructions(pre ?? []) 33 | .instruction(); 34 | 35 | export const initPlayerInstruction = async ( 36 | program: Program, 37 | args: InitializePlayerArgs, 38 | accounts: { 39 | playerAccount: PublicKey; 40 | user: PublicKey; 41 | payer: PublicKey; 42 | systemProgram: PublicKey; 43 | }, 44 | pre?: TransactionInstruction[] 45 | ): Promise => 46 | program.methods 47 | .initializePlayer(args.username, args.nftMeta) 48 | .accounts(accounts) 49 | .preInstructions(pre ?? []) 50 | .instruction(); 51 | 52 | export const initiateMergeInstruction = async ( 53 | program: Program, 54 | args: InitMergeArgs, 55 | accounts: { 56 | user: PublicKey; 57 | payer: PublicKey; 58 | playerAccount: PublicKey; 59 | mergeAccount: PublicKey; 60 | systemProgram: PublicKey; 61 | }, 62 | pre?: TransactionInstruction[] 63 | ): Promise => 64 | program.methods 65 | .initiateMerge(args.keys) 66 | .accounts(accounts) 67 | .preInstructions(pre ?? []) 68 | .instruction(); 69 | 70 | export const addAchievementInstruction = async ( 71 | program: Program, 72 | args: AddAchievementArgs, 73 | accounts: { 74 | newAchievement: PublicKey; 75 | game: PublicKey; 76 | payer: PublicKey; 77 | authority: PublicKey; 78 | systemProgram: PublicKey; 79 | }, 80 | pre?: TransactionInstruction[] 81 | ): Promise => 82 | program.methods 83 | .addAchievement(args.title, args.description, args.nftMeta) 84 | .accounts(accounts) 85 | .preInstructions(pre ?? []) 86 | .instruction(); 87 | 88 | export const addLeaderBoardInstruction = async ( 89 | program: Program, 90 | args: AddLeaderBoardArgs, 91 | accounts: { 92 | authority: PublicKey; 93 | game: PublicKey; 94 | payer: PublicKey; 95 | leaderboard: PublicKey; 96 | topEntries: PublicKey | null; 97 | systemProgram: PublicKey; 98 | }, 99 | pre?: TransactionInstruction[] 100 | ): Promise => 101 | program.methods 102 | .addLeaderboard(args) 103 | .accounts(accounts) 104 | .preInstructions(pre ?? []) 105 | .instruction(); 106 | 107 | export const addFtRewardInstruction = async ( 108 | program: Program, 109 | args: AddFtRewardArgs, 110 | accounts: { 111 | authority: PublicKey; 112 | payer: PublicKey; 113 | game: PublicKey; 114 | achievement: PublicKey; 115 | newReward: PublicKey; 116 | rewardTokenMint: PublicKey; 117 | delegateFromTokenAccount: PublicKey; 118 | tokenAccountOwner: PublicKey; 119 | tokenProgram: PublicKey; 120 | systemProgram: PublicKey; 121 | }, 122 | pre?: TransactionInstruction[] 123 | ): Promise => 124 | program.methods 125 | .addFtReward({ 126 | availableSpots: args.availableRewards, 127 | kind: { 128 | ft: args.kind, 129 | }, 130 | }) 131 | .accounts(accounts) 132 | .preInstructions(pre ?? []) 133 | .instruction(); 134 | 135 | export const addNftRewardInstruction = async ( 136 | program: Program, 137 | args: AddNftRewardArgs, 138 | accounts: { 139 | authority: PublicKey; 140 | payer: PublicKey; 141 | game: PublicKey; 142 | achievement: PublicKey; 143 | newReward: PublicKey; 144 | systemProgram: PublicKey; 145 | rewardCollectionMint: PublicKey | null; 146 | collectionUpdateAuth: PublicKey | null; 147 | collectionMetadata: PublicKey | null; 148 | tokenMetadataProgram: PublicKey | null; 149 | }, 150 | pre?: TransactionInstruction[] 151 | ): Promise => 152 | program.methods 153 | .addNftReward({ 154 | availableSpots: args.availableRewards, 155 | kind: { 156 | nft: args.kind, 157 | }, 158 | }) 159 | .accounts(accounts) 160 | .preInstructions(pre ?? []) 161 | .instruction(); 162 | 163 | export const registerMergeApprovalInstruction = async ( 164 | program: Program, 165 | accounts: { 166 | user: PublicKey; 167 | playerAccount: PublicKey; 168 | mergeAccount: PublicKey; 169 | }, 170 | pre?: TransactionInstruction[] 171 | ): Promise => 172 | program.methods 173 | .approveMerge() 174 | .accounts(accounts) 175 | .preInstructions(pre ?? []) 176 | .instruction(); 177 | 178 | export const claimFtRewardInstruction = async ( 179 | program: Program, 180 | accounts: { 181 | user: PublicKey; 182 | authority: PublicKey; 183 | playerAccount: PublicKey; 184 | game: PublicKey; 185 | achievement: PublicKey; 186 | reward: PublicKey; 187 | playerAchievement: PublicKey; 188 | sourceTokenAccount: PublicKey; 189 | userTokenAccount: PublicKey; 190 | tokenProgram: PublicKey; 191 | } 192 | ): Promise => 193 | program.methods.claimFtReward().accounts(accounts).instruction(); 194 | 195 | export const claimNftRewardInstruction = async ( 196 | program: Program, 197 | accounts: { 198 | user: PublicKey; 199 | authority: PublicKey; 200 | playerAccount: PublicKey; 201 | game: PublicKey; 202 | achievement: PublicKey; 203 | reward: PublicKey; 204 | playerAchievement: PublicKey; 205 | payer: PublicKey; 206 | claim: PublicKey; 207 | newMint: PublicKey; 208 | newMetadata: PublicKey; 209 | newMasterEdition: PublicKey; 210 | mintTo: PublicKey; 211 | tokenMetadataProgram: PublicKey; 212 | associatedTokenProgram: PublicKey; 213 | systemProgram: PublicKey; 214 | tokenProgram: PublicKey; 215 | rent: PublicKey; 216 | }, 217 | pre?: TransactionInstruction[] 218 | ): Promise => 219 | program.methods 220 | .claimNftReward() 221 | .accounts(accounts) 222 | .preInstructions(pre ?? []) 223 | .instruction(); 224 | 225 | export const registerPlayerEntryInstruction = async ( 226 | program: Program, 227 | accounts: { 228 | user: PublicKey; 229 | payer: PublicKey; 230 | playerAccount: PublicKey; 231 | newList: PublicKey; 232 | game: PublicKey; 233 | leaderboard: PublicKey; 234 | systemProgram: PublicKey; 235 | }, 236 | pre?: TransactionInstruction[] 237 | ): Promise => 238 | program.methods 239 | .registerPlayer() 240 | .accounts(accounts) 241 | .preInstructions(pre ?? []) 242 | .instruction(); 243 | 244 | export const submitScoreInstruction = async ( 245 | program: Program, 246 | args: SubmitScoreArgs, 247 | accounts: { 248 | payer: PublicKey; 249 | playerAccount: PublicKey; 250 | authority: PublicKey; 251 | game: PublicKey; 252 | leaderboard: PublicKey; 253 | playerScores: PublicKey; 254 | topEntries: PublicKey | null; 255 | systemProgram: PublicKey; 256 | }, 257 | pre?: TransactionInstruction[] 258 | ): Promise => 259 | program.methods 260 | .submitScore(args.score) 261 | .accounts(accounts) 262 | .preInstructions(pre ?? []) 263 | .instruction(); 264 | 265 | export const updateAchievementInstruction = async ( 266 | program: Program, 267 | args: UpdateAchievementArgs, 268 | accounts: { 269 | authority: PublicKey; 270 | game: PublicKey; 271 | achievement: PublicKey; 272 | }, 273 | pre?: TransactionInstruction[] 274 | ): Promise => 275 | program.methods 276 | .updateAchievement(args.newTitle, args.newDescription, args.newNftMeta) 277 | .accounts(accounts) 278 | .preInstructions(pre ?? []) 279 | .instruction(); 280 | 281 | export const unlockPlayerAchievementInstruction = async ( 282 | program: Program, 283 | accounts: { 284 | payer: PublicKey; 285 | playerAccount: PublicKey; 286 | playerScores: PublicKey; 287 | game: PublicKey; 288 | achievement: PublicKey; 289 | authority: PublicKey; 290 | playerAchievement: PublicKey; 291 | }, 292 | pre?: TransactionInstruction[] 293 | ): Promise => 294 | program.methods 295 | .unlockPlayerAchievement() 296 | .accounts(accounts) 297 | .preInstructions(pre ?? []) 298 | .instruction(); 299 | 300 | export const updateGameInstruction = async ( 301 | program: Program, 302 | args: UpdateGameArgs, 303 | accounts: { 304 | payer: PublicKey; 305 | game: PublicKey; 306 | authority: PublicKey; 307 | systemProgram: PublicKey; 308 | }, 309 | pre?: TransactionInstruction[] 310 | ): Promise => 311 | program.methods 312 | .updateGame(args.newMeta, args.newAuths) 313 | .accounts(accounts) 314 | .preInstructions(pre ?? []) 315 | .instruction(); 316 | 317 | export const updateLeaderBoardInstruction = async ( 318 | program: Program, 319 | args: UpdateLeaderboardArgs, 320 | accounts: { 321 | authority: PublicKey; 322 | game: PublicKey; 323 | leaderboard: PublicKey; 324 | topEntries: PublicKey | null; 325 | }, 326 | pre?: TransactionInstruction[] 327 | ): Promise => { 328 | return program.methods 329 | .updateLeaderboard( 330 | args.newDescription, 331 | args.newNftMeta, 332 | args.newMinScore, 333 | args.newMaxScore, 334 | args.newIsAscending, 335 | args.newAllowMultipleScores 336 | ) 337 | .accounts(accounts) 338 | .preInstructions(pre ?? []) 339 | .instruction(); 340 | }; 341 | 342 | export const updatePlayerInstruction = async ( 343 | program: Program, 344 | args: UpdatePlayerArgs, 345 | accounts: { 346 | user: PublicKey; 347 | playerAccount: PublicKey; 348 | }, 349 | pre?: TransactionInstruction[] 350 | ): Promise => 351 | program.methods 352 | .updatePlayer(args.newUsername, args.newNftMeta) 353 | .accounts(accounts) 354 | .preInstructions(pre ?? []) 355 | .instruction(); 356 | 357 | export const verifyNftRewardInstruction = async ( 358 | program: Program, 359 | accounts: { 360 | payer: PublicKey; 361 | user: PublicKey; 362 | playerAccount: PublicKey; 363 | achievement: PublicKey; 364 | game: PublicKey; 365 | reward: PublicKey; 366 | playerAchievement: PublicKey; 367 | mint: PublicKey; 368 | claim: PublicKey; 369 | metadataToVerify: PublicKey; 370 | collectionMint: PublicKey; 371 | collectionMetadata: PublicKey; 372 | collectionEdition: PublicKey; 373 | tokenMetadataProgram: PublicKey; 374 | }, 375 | pre?: TransactionInstruction[] 376 | ): Promise => 377 | program.methods 378 | .verifyNftReward() 379 | .accounts(accounts) 380 | .preInstructions(pre ?? []) 381 | .instruction(); 382 | -------------------------------------------------------------------------------- /client/sdk/src/soar.game.ts: -------------------------------------------------------------------------------- 1 | import { Keypair, type PublicKey } from "@solana/web3.js"; 2 | import type BN from "bn.js"; 3 | import { type SoarProgram } from "./soar.program"; 4 | import { type InstructionResult } from "./types"; 5 | import { 6 | GameAccount, 7 | type Genre, 8 | type GameType, 9 | type AchievementAccount, 10 | type LeaderBoardAccount, 11 | type GameAttributes, 12 | } from "./state"; 13 | 14 | /** Class representing actions on a single Game. */ 15 | export class GameClient { 16 | program: SoarProgram; 17 | address: PublicKey; 18 | account: GameAccount | undefined; 19 | 20 | constructor(program: SoarProgram, address: PublicKey, account?: GameAccount) { 21 | this.address = address; 22 | this.program = program; 23 | this.account = account; 24 | } 25 | 26 | public static async register( 27 | program: SoarProgram, 28 | title: string, 29 | description: string, 30 | genre: Genre, 31 | gameType: GameType, 32 | nftMeta: PublicKey, 33 | auths: PublicKey[] 34 | ): Promise { 35 | const game = Keypair.generate(); 36 | 37 | const { newGame, transaction } = await program.initializeNewGame( 38 | game.publicKey, 39 | title, 40 | description, 41 | genre, 42 | gameType, 43 | nftMeta, 44 | auths 45 | ); 46 | 47 | await program.sendAndConfirmTransaction(transaction, [game]); 48 | const client = new GameClient(program, newGame); 49 | 50 | await client.init(); 51 | return client; 52 | } 53 | 54 | public async init(): Promise { 55 | const account = await this.program.fetchGameAccount(this.address); 56 | this.account = GameAccount.fromIdlAccount(account, this.address); 57 | } 58 | 59 | public async refresh(): Promise { 60 | await this.init(); 61 | } 62 | 63 | recentLeaderBoardAddress = (): PublicKey => { 64 | if (this.account === undefined) { 65 | throw new Error("init not called"); 66 | } 67 | return this.program.utils.deriveLeaderBoardAddress( 68 | this.account.leaderboardCount, 69 | this.address 70 | )[0]; 71 | }; 72 | 73 | nextLeaderBoardAddress = (): PublicKey => { 74 | if (this.account === undefined) { 75 | throw new Error("init not called"); 76 | } 77 | const nextId = this.account.leaderboardCount.addn(1); 78 | return this.program.utils.deriveLeaderBoardAddress(nextId, this.address)[0]; 79 | }; 80 | 81 | recentAchievementAddress = (): PublicKey => { 82 | if (this.account === undefined) { 83 | throw new Error("init not called"); 84 | } 85 | return this.program.utils.deriveAchievementAddress( 86 | this.account.achievementCount, 87 | this.address 88 | )[0]; 89 | }; 90 | 91 | nextAchievementAddress = (): PublicKey => { 92 | if (this.account === undefined) { 93 | throw new Error("init not called"); 94 | } 95 | const nextId = this.account.achievementCount.addn(1); 96 | return this.program.utils.deriveAchievementAddress(nextId, this.address)[0]; 97 | }; 98 | 99 | public async update( 100 | authority: PublicKey, 101 | newMeta?: GameAttributes, 102 | newAuths?: PublicKey[] 103 | ): Promise { 104 | return this.program.updateGameAccount( 105 | this.address, 106 | authority, 107 | newMeta, 108 | newAuths 109 | ); 110 | } 111 | 112 | public async addLeaderBoard( 113 | authority: PublicKey, 114 | description: string, 115 | nftMeta: PublicKey, 116 | scoresToRetain: number, 117 | scoresOrder: boolean, 118 | decimals?: number, 119 | minScore?: BN, 120 | maxScore?: BN, 121 | allowMultipleScores?: boolean 122 | ): Promise { 123 | return this.program.addNewGameLeaderBoard( 124 | this.address, 125 | authority, 126 | description, 127 | nftMeta, 128 | scoresToRetain, 129 | scoresOrder, 130 | decimals, 131 | minScore, 132 | maxScore, 133 | allowMultipleScores 134 | ); 135 | } 136 | 137 | public async addAchievement( 138 | authority: PublicKey, 139 | title: string, 140 | description: string, 141 | nftMeta: PublicKey 142 | ): Promise { 143 | return this.program.addNewGameAchievement( 144 | this.address, 145 | authority, 146 | title, 147 | description, 148 | nftMeta 149 | ); 150 | } 151 | 152 | public async registerPlayer( 153 | user: PublicKey, 154 | leaderBoard?: PublicKey 155 | ): Promise { 156 | const leaderboard = leaderBoard ?? this.recentLeaderBoardAddress(); 157 | return this.program.registerPlayerEntryForLeaderBoard(user, leaderboard); 158 | } 159 | 160 | public async submitScore( 161 | user: PublicKey, 162 | authority: PublicKey, 163 | score: BN, 164 | leaderBoard?: PublicKey 165 | ): Promise { 166 | const leaderboard = leaderBoard ?? this.recentLeaderBoardAddress(); 167 | return this.program.submitScoreToLeaderBoard( 168 | user, 169 | authority, 170 | leaderboard, 171 | score 172 | ); 173 | } 174 | 175 | public async updateAchievement( 176 | authority: PublicKey, 177 | achievement: PublicKey, 178 | newTitle?: string, 179 | newDescription?: string, 180 | newNftMeta?: PublicKey 181 | ): Promise { 182 | return this.program.updateGameAchievement( 183 | authority, 184 | achievement, 185 | newTitle, 186 | newDescription, 187 | newNftMeta 188 | ); 189 | } 190 | 191 | public async fetchLeaderBoardAccounts(): Promise { 192 | return this.program.fetchAllLeaderboardAccounts([ 193 | { 194 | memcmp: { 195 | offset: 8 + 8, 196 | bytes: this.address.toBase58(), 197 | }, 198 | }, 199 | ]); 200 | } 201 | 202 | public async fetchAchievementAccounts(): Promise { 203 | return this.program.fetchAllAchievementAccounts([ 204 | { 205 | memcmp: { 206 | offset: 8, 207 | bytes: this.address.toBase58(), 208 | }, 209 | }, 210 | ]); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /client/sdk/src/state/achievement.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey } from "@solana/web3.js"; 2 | import { type IdlAccounts } from "@coral-xyz/anchor"; 3 | import { type Soar } from "../idl/soar"; 4 | import type BN from "bn.js"; 5 | 6 | /** Class representing a deserialized on-chain `Achievement` account. */ 7 | export class AchievementAccount { 8 | private constructor( 9 | public readonly address: PublicKey, 10 | public readonly game: PublicKey, 11 | public readonly id: BN, 12 | public readonly title: string, 13 | public readonly description: string, 14 | public readonly nftMeta: PublicKey, 15 | public readonly reward: PublicKey | null 16 | ) {} 17 | 18 | /** Create a new instance from an anchor-deserialized account. */ 19 | public static fromIdlAccount( 20 | account: IdlAccounts["achievement"], 21 | address: PublicKey 22 | ): AchievementAccount { 23 | return new AchievementAccount( 24 | address, 25 | account.game, 26 | account.id, 27 | account.title, 28 | account.description, 29 | account.nftMeta, 30 | account.reward 31 | ); 32 | } 33 | 34 | /** Pretty print. */ 35 | public pretty(): { 36 | address: string; 37 | game: string; 38 | id: string; 39 | title: string; 40 | description: string; 41 | nftMeta: string; 42 | reward: string | PublicKey | null; 43 | } { 44 | return { 45 | address: this.address.toBase58(), 46 | game: this.game.toBase58(), 47 | id: this.id.toString(), 48 | title: this.title, 49 | description: this.description, 50 | nftMeta: this.nftMeta.toBase58(), 51 | reward: this.reward !== null ? this.reward.toBase58() : this.reward, 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/sdk/src/state/game.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey } from "@solana/web3.js"; 2 | import type BN from "bn.js"; 3 | import { type IdlAccounts } from "@coral-xyz/anchor"; 4 | import { type Soar } from "../idl/soar"; 5 | import { type SoarProgram } from "../soar.program"; 6 | import { GameClient } from "../soar.game"; 7 | 8 | /** Class representing a deserialized on-chain `Game` account. */ 9 | export class GameAccount { 10 | public readonly address: PublicKey; 11 | public readonly meta: GameAttributes; 12 | 13 | public readonly achievementCount: BN; 14 | public readonly leaderboardCount: BN; 15 | public readonly auth: PublicKey[]; 16 | 17 | protected constructor( 18 | _address: PublicKey, 19 | account: IdlAccounts["game"] 20 | ) { 21 | this.address = _address; 22 | this.meta = account.meta; 23 | this.achievementCount = account.achievementCount; 24 | this.leaderboardCount = account.leaderboardCount; 25 | this.auth = account.auth; 26 | } 27 | 28 | /** Create a new instance from an anchor-deserialized account. */ 29 | public static fromIdlAccount( 30 | account: IdlAccounts["game"], 31 | address: PublicKey 32 | ): GameAccount { 33 | return new GameAccount(address, account); 34 | } 35 | 36 | public async client(soar: SoarProgram): Promise { 37 | return new GameClient(soar, this.address, this); 38 | } 39 | 40 | /** Pretty print. */ 41 | public pretty(): { 42 | address: string; 43 | meta: { 44 | title: string; 45 | description: string; 46 | genre: Genre; 47 | gameType: GameType; 48 | nftMeta: string; 49 | }; 50 | achievementCount: string; 51 | leaderboardCount: string; 52 | auth: string[]; 53 | } { 54 | return { 55 | address: this.address.toBase58(), 56 | meta: { 57 | ...this.meta, 58 | nftMeta: this.meta.nftMeta.toBase58(), 59 | }, 60 | achievementCount: this.achievementCount.toString(), 61 | leaderboardCount: this.leaderboardCount.toString(), 62 | auth: this.auth.map((auth) => auth.toBase58()), 63 | }; 64 | } 65 | } 66 | 67 | export interface GameAttributes { 68 | title: string; 69 | description: string; 70 | genre: Genre; 71 | gameType: GameType; 72 | nftMeta: PublicKey; 73 | } 74 | 75 | export const enum GameType { 76 | Mobile = 0, 77 | Desktop = 1, 78 | Web = 2, 79 | Unspecified = 255, 80 | } 81 | 82 | export const enum Genre { 83 | RPG = 0, 84 | MMO = 1, 85 | Action = 2, 86 | Adventure = 3, 87 | Puzzle = 4, 88 | Casual = 5, 89 | Unspecified = 255, 90 | } 91 | -------------------------------------------------------------------------------- /client/sdk/src/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./achievement"; 2 | export * from "./game"; 3 | export * from "./leaderboard"; 4 | export * from "./leaderboardTopEntries"; 5 | export * from "./merged"; 6 | export * from "./playerAchievement"; 7 | export * from "./playerScoresList"; 8 | export * from "./player"; 9 | export * from "./reward"; 10 | -------------------------------------------------------------------------------- /client/sdk/src/state/leaderboard.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey } from "@solana/web3.js"; 2 | import type BN from "bn.js"; 3 | import { type IdlAccounts } from "@coral-xyz/anchor"; 4 | import { type Soar } from "../idl/soar"; 5 | 6 | /** Class representing a deserialized on-chain `Leaderboard` account. */ 7 | export class LeaderBoardAccount { 8 | private constructor( 9 | public readonly address: PublicKey, 10 | public readonly id: BN, 11 | public readonly game: PublicKey, 12 | public readonly description: string, 13 | public readonly nftMeta: PublicKey, 14 | public readonly decimals: number, 15 | public readonly minScore: BN, 16 | public readonly maxScore: BN, 17 | public readonly allowMultipleScores: boolean, 18 | public readonly topEntries: PublicKey | null 19 | ) {} 20 | 21 | /** Create a new instance from an anchor-deserialized account. */ 22 | public static fromIdlAccount( 23 | account: IdlAccounts["leaderBoard"], 24 | address: PublicKey 25 | ): LeaderBoardAccount { 26 | return new LeaderBoardAccount( 27 | address, 28 | account.id, 29 | account.game, 30 | account.description, 31 | account.nftMeta, 32 | account.decimals, 33 | account.minScore, 34 | account.maxScore, 35 | account.allowMultipleScores, 36 | account.topEntries 37 | ); 38 | } 39 | 40 | /** Pretty print. */ 41 | public pretty(): { 42 | address: string; 43 | id: string; 44 | game: string; 45 | description: string; 46 | nftMeta: string; 47 | decimals: number; 48 | minScore: string; 49 | maxScore: string; 50 | allowMultipleScores: boolean; 51 | topEntries: string | null; 52 | } { 53 | return { 54 | address: this.address.toBase58(), 55 | id: this.id.toString(), 56 | game: this.game.toBase58(), 57 | description: this.description, 58 | nftMeta: this.nftMeta.toBase58(), 59 | decimals: this.decimals, 60 | minScore: this.minScore.toString(), 61 | maxScore: this.maxScore.toString(), 62 | allowMultipleScores: this.allowMultipleScores, 63 | topEntries: this.topEntries ? this.topEntries.toBase58() : null, 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/sdk/src/state/leaderboardTopEntries.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey } from "@solana/web3.js"; 2 | import type BN from "bn.js"; 3 | import { type IdlAccounts } from "@coral-xyz/anchor"; 4 | import { type Soar } from "../idl/soar"; 5 | 6 | /** Class representing a deserialized on-chain `LeaderTopScores` account. */ 7 | export class TopEntriesAccount { 8 | constructor( 9 | public readonly address: PublicKey, 10 | public readonly isAscending: boolean, 11 | public readonly topScores: LeaderboardScore[] 12 | ) {} 13 | 14 | /** Create a new instance from an anchor-deserialized account. */ 15 | public static fromIdlAccount( 16 | account: IdlAccounts["leaderTopEntries"], 17 | address: PublicKey 18 | ): TopEntriesAccount { 19 | return new TopEntriesAccount( 20 | address, 21 | account.isAscending, 22 | account.topScores 23 | ); 24 | } 25 | 26 | /** Pretty print. */ 27 | public pretty(): { 28 | address: string; 29 | isAscending: boolean; 30 | topScores: Array<{ 31 | player: string; 32 | entry: { 33 | score: string; 34 | timestamp: string; 35 | }; 36 | }>; 37 | } { 38 | return { 39 | address: this.address.toBase58(), 40 | isAscending: this.isAscending, 41 | topScores: this.topScores.map((score) => printLeaderboardScore(score)), 42 | }; 43 | } 44 | } 45 | 46 | interface LeaderboardScore { 47 | player: PublicKey; 48 | entry: { 49 | score: BN; 50 | timestamp: BN; 51 | }; 52 | } 53 | 54 | const printLeaderboardScore = ( 55 | raw: LeaderboardScore 56 | ): { 57 | player: string; 58 | entry: { 59 | score: string; 60 | timestamp: string; 61 | }; 62 | } => { 63 | return { 64 | player: raw.player.toBase58(), 65 | entry: { 66 | score: raw.entry.score.toString(), 67 | timestamp: raw.entry.score.toString(), 68 | }, 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /client/sdk/src/state/merged.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey } from "@solana/web3.js"; 2 | import { type IdlAccounts } from "@coral-xyz/anchor"; 3 | import { type Soar } from "../idl/soar"; 4 | 5 | /** Class representing a deserialized on-chain `Merged` account. */ 6 | export class MergedAccount { 7 | private constructor( 8 | public readonly address: PublicKey, 9 | public readonly initiator: PublicKey, 10 | public readonly approvals: MergeApproval[], 11 | public readonly mergeComplete: boolean 12 | ) {} 13 | 14 | /** Create a new instance from an anchor-deserialized account. */ 15 | public static fromIdlAccount( 16 | account: IdlAccounts["merged"], 17 | address: PublicKey 18 | ): MergedAccount { 19 | return new MergedAccount( 20 | address, 21 | account.initiator, 22 | account.approvals, 23 | account.mergeComplete 24 | ); 25 | } 26 | 27 | /** Pretty print. */ 28 | public pretty(): { 29 | address: string; 30 | initiator: string; 31 | others: Array<{ 32 | key: string; 33 | approved: boolean; 34 | }>; 35 | mergeComplete: boolean; 36 | } { 37 | return { 38 | address: this.address.toBase58(), 39 | initiator: this.initiator.toBase58(), 40 | others: this.approvals.map((approval) => printMergeApproval(approval)), 41 | mergeComplete: this.mergeComplete, 42 | }; 43 | } 44 | } 45 | 46 | interface MergeApproval { 47 | key: PublicKey; 48 | approved: boolean; 49 | } 50 | 51 | const printMergeApproval = ( 52 | raw: MergeApproval 53 | ): { 54 | key: string; 55 | approved: boolean; 56 | } => { 57 | return { 58 | key: raw.key.toBase58(), 59 | approved: raw.approved, 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /client/sdk/src/state/player.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey } from "@solana/web3.js"; 2 | import { type IdlAccounts } from "@coral-xyz/anchor"; 3 | import { type Soar } from "../idl/soar"; 4 | 5 | /** Class representing a deserialized on-chain `Player` account. */ 6 | export class PlayerAccount { 7 | private constructor( 8 | public readonly address: PublicKey, 9 | public readonly user: PublicKey, 10 | public readonly username: string, 11 | public readonly nftMeta: PublicKey 12 | ) {} 13 | 14 | /** Create a new instance from an anchor-deserialized account. */ 15 | public static fromIdlAccount( 16 | account: IdlAccounts["player"], 17 | address: PublicKey 18 | ): PlayerAccount { 19 | return new PlayerAccount( 20 | address, 21 | account.user, 22 | account.username, 23 | account.nftMeta 24 | ); 25 | } 26 | 27 | /** Pretty print. */ 28 | public pretty(): { 29 | address: string; 30 | user: string; 31 | username: string; 32 | nftMeta: string; 33 | } { 34 | return { 35 | address: this.address.toBase58(), 36 | user: this.user.toBase58(), 37 | username: this.username, 38 | nftMeta: this.nftMeta.toBase58(), 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/sdk/src/state/playerAchievement.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey } from "@solana/web3.js"; 2 | import type BN from "bn.js"; 3 | import { type IdlAccounts } from "@coral-xyz/anchor"; 4 | import { type Soar } from "../idl/soar"; 5 | 6 | /** Class representing a deserialized on-chain `PlayerAchievement` account. */ 7 | export class PlayerAchievementAccount { 8 | private constructor( 9 | public readonly address: PublicKey, 10 | public readonly player: PublicKey, 11 | public readonly achievement: PublicKey, 12 | public readonly timestamp: BN, 13 | public readonly unlocked: boolean, 14 | public readonly claimed: boolean 15 | ) {} 16 | 17 | /** Create a new instance from an anchor-deserialized account. */ 18 | public static fromIdlAccount( 19 | account: IdlAccounts["playerAchievement"], 20 | address: PublicKey 21 | ): PlayerAchievementAccount { 22 | return new PlayerAchievementAccount( 23 | address, 24 | account.playerAccount, 25 | account.achievement, 26 | account.timestamp, 27 | account.unlocked, 28 | account.claimed 29 | ); 30 | } 31 | 32 | /** Pretty print. */ 33 | public pretty(): { 34 | address: string; 35 | player: string; 36 | achievement: string; 37 | timestamp: string; 38 | unlocked: boolean; 39 | claimed: boolean; 40 | } { 41 | return { 42 | address: this.address.toBase58(), 43 | player: this.player.toBase58(), 44 | achievement: this.achievement.toBase58(), 45 | timestamp: this.timestamp.toString(), 46 | unlocked: this.unlocked, 47 | claimed: this.claimed, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/sdk/src/state/playerScoresList.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey } from "@solana/web3.js"; 2 | import type BN from "bn.js"; 3 | import { type IdlAccounts } from "@coral-xyz/anchor"; 4 | import { type Soar } from "../idl/soar"; 5 | 6 | /** Class representing a deserialized on-chain `PlayerScoresList` account. */ 7 | export class PlayerScoresListAccount { 8 | private constructor( 9 | public readonly address: PublicKey, 10 | public readonly playerAccount: PublicKey, 11 | public readonly leaderboard: PublicKey, 12 | public readonly allocCount: number, 13 | public readonly scores: ScoreEntry[] 14 | ) {} 15 | 16 | /** Create a new instance from an anchor-deserialized account. */ 17 | public static fromIdlAccount( 18 | account: IdlAccounts["playerScoresList"], 19 | address: PublicKey 20 | ): PlayerScoresListAccount { 21 | return new PlayerScoresListAccount( 22 | address, 23 | account.playerAccount, 24 | account.leaderboard, 25 | account.allocCount, 26 | account.scores 27 | ); 28 | } 29 | 30 | /** Pretty print. */ 31 | public pretty(): { 32 | address: string; 33 | playerInfo: string; 34 | leaderboard: string; 35 | allocCount: number; 36 | scores: Array<{ 37 | score: string; 38 | timestamp: string; 39 | }>; 40 | } { 41 | return { 42 | address: this.address.toBase58(), 43 | playerInfo: this.playerAccount.toBase58(), 44 | leaderboard: this.leaderboard.toBase58(), 45 | allocCount: this.allocCount, 46 | scores: this.scores.map((score) => printScoreEntry(score)), 47 | }; 48 | } 49 | } 50 | 51 | interface ScoreEntry { 52 | score: BN; 53 | timestamp: BN; 54 | } 55 | 56 | const printScoreEntry = ( 57 | entry: ScoreEntry 58 | ): { 59 | score: string; 60 | timestamp: string; 61 | } => { 62 | return { 63 | score: entry.score.toString(), 64 | timestamp: entry.timestamp.toString(), 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /client/sdk/src/state/reward.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey } from "@solana/web3.js"; 2 | import type BN from "bn.js"; 3 | import { type IdlAccounts } from "@coral-xyz/anchor"; 4 | import { type Soar } from "../idl/soar"; 5 | 6 | /** Class representing a deserialized on-chain `Reward` account. */ 7 | export class RewardAccount { 8 | private constructor( 9 | public readonly address: PublicKey, 10 | public readonly achievement: PublicKey, 11 | public readonly availableSpots: BN, 12 | public readonly FungibleToken: 13 | | { 14 | mint: PublicKey; 15 | account: PublicKey; 16 | amount: BN; 17 | } 18 | | undefined, 19 | public readonly NonFungibleToken: 20 | | { 21 | uri: string; 22 | name: string; 23 | symbol: string; 24 | minted: BN; 25 | collection: PublicKey | null; 26 | } 27 | | undefined 28 | ) {} 29 | 30 | /** Create a new instance from an anchor-deserialized account. */ 31 | public static fromIdlAccount( 32 | account: IdlAccounts["reward"], 33 | address: PublicKey 34 | ): RewardAccount { 35 | return new RewardAccount( 36 | address, 37 | account.achievement, 38 | account.availableSpots, 39 | account.reward.fungibleToken, 40 | account.reward.nonFungibleToken 41 | ); 42 | } 43 | 44 | /** Pretty print. */ 45 | public pretty(): { 46 | address: string; 47 | achievement: string; 48 | availableSpots: string; 49 | FungibleToken: 50 | | { 51 | mint: string; 52 | account: string; 53 | amount: string; 54 | } 55 | | undefined; 56 | NonFungibleToken: 57 | | { 58 | uri: string; 59 | name: string; 60 | symbol: string; 61 | minted: string; 62 | collection: string | null; 63 | } 64 | | undefined; 65 | } { 66 | return { 67 | address: this.address.toBase58(), 68 | achievement: this.achievement.toBase58(), 69 | availableSpots: this.availableSpots.toString(), 70 | FungibleToken: 71 | this.FungibleToken !== undefined 72 | ? { 73 | mint: this.FungibleToken.mint.toBase58(), 74 | account: this.FungibleToken.account.toBase58(), 75 | amount: this.FungibleToken.amount.toString(), 76 | } 77 | : undefined, 78 | NonFungibleToken: 79 | this.NonFungibleToken !== undefined 80 | ? { 81 | uri: this.NonFungibleToken.uri, 82 | name: this.NonFungibleToken.name, 83 | symbol: this.NonFungibleToken.symbol, 84 | minted: this.NonFungibleToken.minted.toString(), 85 | collection: 86 | this.NonFungibleToken.collection !== null 87 | ? this.NonFungibleToken.collection.toBase58() 88 | : null, 89 | } 90 | : undefined, 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/sdk/src/types.ts: -------------------------------------------------------------------------------- 1 | import { type PublicKey, type Transaction } from "@solana/web3.js"; 2 | import { type GameType, type Genre } from "./state/game"; 3 | import type BN from "bn.js"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-namespace 6 | export module InstructionResult { 7 | export interface InitializeGame { 8 | newGame: PublicKey; 9 | transaction: Transaction; 10 | } 11 | export interface UpdateGame { 12 | transaction: Transaction; 13 | } 14 | export interface InitializePlayer { 15 | newPlayer: PublicKey; 16 | transaction: Transaction; 17 | } 18 | export interface UpdatePlayer { 19 | transaction: Transaction; 20 | } 21 | export interface AddGameAchievement { 22 | newAchievement: PublicKey; 23 | transaction: Transaction; 24 | } 25 | export interface AddLeaderBoard { 26 | newLeaderBoard: PublicKey; 27 | topEntries: PublicKey | null; 28 | transaction: Transaction; 29 | } 30 | export interface RegisterPlayerEntry { 31 | newList: PublicKey; 32 | transaction: Transaction; 33 | } 34 | export interface SubmitScore { 35 | transaction: Transaction; 36 | } 37 | export interface UpdateAchievement { 38 | transaction: Transaction; 39 | } 40 | export interface UnlockPlayerAchievement { 41 | newPlayerAchievement: PublicKey; 42 | transaction: Transaction; 43 | } 44 | export interface AddReward { 45 | oldReward: PublicKey | null; 46 | newReward: PublicKey; 47 | transaction: Transaction; 48 | } 49 | export interface ClaimNftReward { 50 | newMint: PublicKey; 51 | transaction: Transaction; 52 | } 53 | export interface ClaimFtReward { 54 | transaction: Transaction; 55 | } 56 | export interface VerifyReward { 57 | transaction: Transaction; 58 | } 59 | export interface InitiateMerge { 60 | newMerge: PublicKey; 61 | transaction: Transaction; 62 | } 63 | export interface RegisterMergeApproval { 64 | transaction: Transaction; 65 | } 66 | export interface UpdateLeaderboard { 67 | transaction: Transaction; 68 | } 69 | } 70 | 71 | export interface InitializeGameArgs { 72 | gameMeta: { 73 | title: string; 74 | description: string; 75 | genre: Genre; 76 | gameType: GameType; 77 | nftMeta: PublicKey; 78 | }; 79 | authorities: PublicKey[]; 80 | } 81 | export interface AddAchievementArgs { 82 | title: string; 83 | description: string; 84 | nftMeta: PublicKey; 85 | } 86 | export interface AddLeaderBoardArgs { 87 | description: string; 88 | nftMeta: PublicKey; 89 | decimals: number | null; 90 | minScore: BN | null; 91 | maxScore: BN | null; 92 | scoresToRetain: number; 93 | isAscending: boolean; 94 | allowMultipleScores: boolean; 95 | } 96 | export interface AddNftRewardArgs { 97 | availableRewards: BN; 98 | kind: { 99 | uri: string; 100 | name: string; 101 | symbol: string; 102 | }; 103 | } 104 | export interface AddFtRewardArgs { 105 | availableRewards: BN; 106 | kind: { 107 | deposit: BN; 108 | amount: BN; 109 | }; 110 | } 111 | export interface InitializePlayerArgs { 112 | username: string; 113 | nftMeta: PublicKey; 114 | } 115 | export interface InitMergeArgs { 116 | keys: PublicKey[]; 117 | } 118 | export interface SubmitScoreArgs { 119 | score: BN; 120 | } 121 | export interface UpdateAchievementArgs { 122 | newTitle: string | null; 123 | newDescription: string | null; 124 | newNftMeta: PublicKey | null; 125 | } 126 | export interface UpdateGameArgs { 127 | newMeta: { 128 | title: string; 129 | description: string; 130 | genre: Genre; 131 | gameType: GameType; 132 | nftMeta: PublicKey; 133 | } | null; 134 | newAuths: PublicKey[] | null; 135 | } 136 | export interface UpdateLeaderboardArgs { 137 | newDescription: string | null; 138 | newNftMeta: PublicKey | null; 139 | newMinScore: BN | null; 140 | newMaxScore: BN | null; 141 | newIsAscending: boolean | null; 142 | newAllowMultipleScores: boolean | null; 143 | } 144 | export interface UpdatePlayerArgs { 145 | newUsername: string | null; 146 | newNftMeta: PublicKey | null; 147 | } 148 | -------------------------------------------------------------------------------- /client/sdk/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { TOKEN_METADATA_PROGRAM_ID } from "./constants"; 3 | import { getAssociatedTokenAddressSync } from "@solana/spl-token"; 4 | import type BN from "bn.js"; 5 | 6 | export const enum Seeds { 7 | GAME = "game", 8 | LEADER = "leaderboard", 9 | ACHIEVEMENT = "achievement", 10 | PLAYER = "player", 11 | PLAYER_SCORES = "player-scores-list", 12 | PLAYER_ACHIEVEMENT = "player-achievement", 13 | LEADER_TOP_ENTRIES = "top-scores", 14 | NFT_CLAIM = "nft-claim", 15 | } 16 | 17 | export class Utils { 18 | constructor(readonly programId: PublicKey) {} 19 | 20 | public deriveLeaderBoardAddress( 21 | id: BN, 22 | game: PublicKey 23 | ): [PublicKey, number] { 24 | return PublicKey.findProgramAddressSync( 25 | [Buffer.from(Seeds.LEADER), game.toBuffer(), id.toBuffer("le", 8)], 26 | this.programId 27 | ); 28 | } 29 | 30 | public deriveLeaderTopEntriesAddress( 31 | leaderboard: PublicKey 32 | ): [PublicKey, number] { 33 | return PublicKey.findProgramAddressSync( 34 | [Buffer.from(Seeds.LEADER_TOP_ENTRIES), leaderboard.toBuffer()], 35 | this.programId 36 | ); 37 | } 38 | 39 | public deriveAchievementAddress( 40 | id: BN, 41 | game: PublicKey 42 | ): [PublicKey, number] { 43 | return PublicKey.findProgramAddressSync( 44 | [Buffer.from(Seeds.ACHIEVEMENT), game.toBuffer(), id.toBuffer("le", 8)], 45 | this.programId 46 | ); 47 | } 48 | 49 | public derivePlayerAddress(user: PublicKey): [PublicKey, number] { 50 | return PublicKey.findProgramAddressSync( 51 | [Buffer.from(Seeds.PLAYER), user.toBuffer()], 52 | this.programId 53 | ); 54 | } 55 | 56 | public derivePlayerScoresListAddress( 57 | user: PublicKey, 58 | leaderboard: PublicKey 59 | ): [PublicKey, number] { 60 | const player = this.derivePlayerAddress(user)[0]; 61 | return PublicKey.findProgramAddressSync( 62 | [ 63 | Buffer.from(Seeds.PLAYER_SCORES), 64 | player.toBuffer(), 65 | leaderboard.toBuffer(), 66 | ], 67 | this.programId 68 | ); 69 | } 70 | 71 | public derivePlayerAchievementAddress( 72 | user: PublicKey, 73 | achievement: PublicKey 74 | ): [PublicKey, number] { 75 | const player = this.derivePlayerAddress(user)[0]; 76 | return PublicKey.findProgramAddressSync( 77 | [ 78 | Buffer.from(Seeds.PLAYER_ACHIEVEMENT), 79 | player.toBuffer(), 80 | achievement.toBuffer(), 81 | ], 82 | this.programId 83 | ); 84 | } 85 | 86 | public deriveNftClaimAddress( 87 | reward: PublicKey, 88 | mint: PublicKey 89 | ): [PublicKey, number] { 90 | return PublicKey.findProgramAddressSync( 91 | [Buffer.from(Seeds.NFT_CLAIM), reward.toBuffer(), mint.toBuffer()], 92 | this.programId 93 | ); 94 | } 95 | 96 | deriveMetadataAddress = (mint: PublicKey): [PublicKey, number] => { 97 | return PublicKey.findProgramAddressSync( 98 | [ 99 | Buffer.from("metadata"), 100 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 101 | mint.toBuffer(), 102 | ], 103 | TOKEN_METADATA_PROGRAM_ID 104 | ); 105 | }; 106 | 107 | deriveEditionAddress = (mint: PublicKey): [PublicKey, number] => { 108 | return PublicKey.findProgramAddressSync( 109 | [ 110 | Buffer.from("metadata"), 111 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 112 | mint.toBuffer(), 113 | Buffer.from("edition"), 114 | ], 115 | TOKEN_METADATA_PROGRAM_ID 116 | ); 117 | }; 118 | 119 | deriveAssociatedTokenAddress = ( 120 | mint: PublicKey, 121 | user: PublicKey 122 | ): PublicKey => { 123 | return getAssociatedTokenAddressSync(mint, user); 124 | }; 125 | 126 | zip = (a: T[], b: U[], defaultB: U): Array<[T, U]> => 127 | a.map((k, i) => { 128 | if (b.length <= i) return [k, defaultB]; 129 | return [k, b[i]]; 130 | }); 131 | } 132 | -------------------------------------------------------------------------------- /client/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "baseUrl": "src", 5 | "declaration": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "declarationMap": true, 9 | "removeComments": true, 10 | "preserveConstEnums": true, 11 | "module": "commonjs", 12 | "noEmit": false, 13 | "resolveJsonModule": true, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "sourceMap": true, 17 | "strictNullChecks": true 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "lib" 25 | ] 26 | } -------------------------------------------------------------------------------- /client/sdk/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "out": "docs", 4 | "exclude": ["**/*+(test|spec).ts"], 5 | "excludeExternals": true, 6 | "excludePrivate": true, 7 | "externalPattern": ["**/node_modules/**"], 8 | "excludeNotDocumented": false 9 | } 10 | -------------------------------------------------------------------------------- /client/tests/fixtures/metadata/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collection", 3 | "symbol": "coll", 4 | "description": "test collection for achievements", 5 | "seller_fee_basis_points": 1, 6 | "external_url": "", 7 | "edition": "", 8 | "background_color": "000000", 9 | "image": "https://raw.githubusercontent.com/magicblock-labs/SOAR/client/tests/fixtures/metadata/collection.png" 10 | } -------------------------------------------------------------------------------- /client/tests/fixtures/metadata/collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicblock-labs/SOAR/0522ab43fdb5252eaf239783457d7ffc39b4ec97/client/tests/fixtures/metadata/collection.png -------------------------------------------------------------------------------- /client/tests/fixtures/metadata/test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicblock-labs/SOAR/0522ab43fdb5252eaf239783457d7ffc39b4ec97/client/tests/fixtures/metadata/test.jpeg -------------------------------------------------------------------------------- /client/tests/fixtures/metadata/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "symbol": "t1", 4 | "description": "test_description_1", 5 | "seller_fee_basis_points": 1, 6 | "external_url": "", 7 | "edition": "", 8 | "background_color": "000000", 9 | "image": "https://raw.githubusercontent.com/magicblock-labs/SOAR/client/tests/fixtures/metadata/test.jpeg" 10 | } -------------------------------------------------------------------------------- /client/tests/fixtures/mpl_token_metadata.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicblock-labs/SOAR/0522ab43fdb5252eaf239783457d7ffc39b4ec97/client/tests/fixtures/mpl_token_metadata.so -------------------------------------------------------------------------------- /client/tests/fixtures/provider.json: -------------------------------------------------------------------------------- 1 | [72,197,161,15,165,213,238,227,177,189,88,12,150,27,37,143,22,254,72,121,165,119,93,162,36,25,79,150,160,47,145,117,157,105,215,206,34,105,220,200,15,174,37,155,196,197,123,103,204,247,220,118,218,119,237,1,206,135,220,128,196,211,192,223] -------------------------------------------------------------------------------- /client/tests/tens.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { IDL } from "../../target/types/tens"; 4 | import { PublicKey, Keypair, SystemProgram } from "@solana/web3.js"; 5 | import { SoarProgram, GameType, Genre, AccountsBuilder } from "../sdk/lib"; 6 | import BN from "bn.js"; 7 | import * as utils from "./utils"; 8 | 9 | describe("tens", () => { 10 | const provider = anchor.AnchorProvider.env(); 11 | anchor.setProvider(provider); 12 | 13 | const tensProgramId = new PublicKey( 14 | "Tensgwm3DY3UJ8nhF7xnD2Wo65VcnLTXjjoyEvs6Zyk" 15 | ); 16 | const tensProgram = new Program(IDL, tensProgramId, provider); 17 | 18 | const soar = SoarProgram.get(provider); 19 | 20 | const gameKp = Keypair.generate(); 21 | // The SOAR state for our tens program. 22 | const soarGame = gameKp.publicKey; 23 | 24 | // The `offChainAuthority` is an authority of the SOAR state representing our `tens` game that 25 | // exists off-chain. The SOAR state can also have an on-chain PDA of the `tens` program, giving 26 | // it both a permissioned(signed by a real-user), and permissionless(signed by an on-chain PDA) 27 | // authority. 28 | // 29 | // Either OR both authority kinds can exist for a SOAR state. Here we use the `offChainAuthority` 30 | // so we can keep admin actions that don't rely on our `tens` program's on-chain logic off-chain. 31 | // (i.e like updating a game and leaderboard). 32 | // 33 | // For admin actions that we want to be permissionless(i.e in this case we want our `tens` game to 34 | // automatically submit a score for a user), we can set a `tens` PDA as an additional authority and 35 | // write CPI functionality to call and sign the submitScore instruction from our on-chain program. 36 | const offChainAuthority = Keypair.generate(); 37 | let auths = [offChainAuthority.publicKey]; 38 | 39 | const user = Keypair.generate(); 40 | 41 | it("Can play the tens game and submit scores directly on-chain by CPI!", async () => { 42 | const title = "Tens"; 43 | const description = "Increase and get a ten to win!"; 44 | const genre = Genre.Casual; 45 | const gameType = GameType.Web; 46 | const nftMeta = Keypair.generate().publicKey; 47 | 48 | // Initialize the `SOAR` state representing the `tens` game. 49 | const { transaction: init } = await soar.initializeNewGame( 50 | gameKp.publicKey, 51 | title, 52 | description, 53 | genre, 54 | gameType, 55 | nftMeta, 56 | auths 57 | ); 58 | await soar.sendAndConfirmTransaction(init, [gameKp], { 59 | skipPreflight: true, 60 | }); 61 | 62 | const leaderboardDescription = "LeaderBoard1"; 63 | const leaderboardMeta = Keypair.generate().publicKey; 64 | 65 | // Initialize a leaderboard for it. 66 | const { newLeaderBoard, topEntries, transaction } = 67 | await soar.addNewGameLeaderBoard( 68 | soarGame, 69 | offChainAuthority.publicKey, 70 | leaderboardDescription, 71 | leaderboardMeta, 72 | 5, 73 | false 74 | ); 75 | await soar.sendAndConfirmTransaction(transaction, [offChainAuthority]); 76 | 77 | // Derive the tensStatePDA of the `tens` program. 78 | const tensPDA = PublicKey.findProgramAddressSync( 79 | [Buffer.from("tens")], 80 | tensProgram.programId 81 | )[0]; 82 | // Initialize the internal state and register its `SOAR` game and leaderboard in our tens program 83 | // so it can validate that the correct accounts are passed in for subsequent instructions. 84 | await tensProgram.methods 85 | .register(soarGame, newLeaderBoard, topEntries) 86 | .accounts({ 87 | signer: provider.publicKey, 88 | tensState: tensPDA, 89 | systemProgram: SystemProgram.programId, 90 | }) 91 | .signers([]) 92 | .rpc(); 93 | 94 | // Make the tensState PDA of our `tens` program an authority of the game so it can permissionlessly 95 | // sign CPI requests to SOAR that require the authority's signature. 96 | const newAuths = auths.concat([tensPDA]); 97 | auths = newAuths; 98 | 99 | const { transaction: update } = await soar.updateGameAccount( 100 | soarGame, 101 | offChainAuthority.publicKey, 102 | undefined, 103 | newAuths 104 | ); 105 | await soar.sendAndConfirmTransaction(update, [offChainAuthority]); 106 | 107 | // Initialize a SOAR player account, required for interacting with the `tens` game. 108 | const { transaction: initPlayer } = await soar.initializePlayerAccount( 109 | user.publicKey, 110 | "player1", 111 | PublicKey.default 112 | ); 113 | await soar.sendAndConfirmTransaction(initPlayer, [user]); 114 | 115 | // Register the player to the leaderboard. 116 | const { newList: playerScoresList, transaction: regPlayer } = 117 | await soar.registerPlayerEntryForLeaderBoard( 118 | user.publicKey, 119 | newLeaderBoard 120 | ); 121 | await soar.sendAndConfirmTransaction(regPlayer, [user]); 122 | console.log(`Registered to leaderboard ${newLeaderBoard.toBase58()}!\n`); 123 | 124 | // Repetitively make moves in the tens game. Scores will be submitted automatically by CPI. 125 | for (let i = 0; i < 20; ++i) { 126 | // Use the helper function from the SOAR sdk to get the accounts required for a submit score CPI. 127 | // This conveniently derive accounts required for CPI. 128 | // 129 | // The authority for this submitScore instruction is the tensState PDA of the `tens` program. 130 | // 131 | // Since this has been made an authority for the SOAR state, it is now authorized to sign 132 | // for the submitScore instruction and will do so by CPI. 133 | // 134 | const accounts = await new AccountsBuilder( 135 | provider, 136 | soar.program.programId 137 | ).submitScoreAccounts(user.publicKey, tensPDA, newLeaderBoard); 138 | 139 | await tensProgram.methods 140 | .makeMove() 141 | .accounts({ 142 | user: user.publicKey, 143 | tensState: tensPDA, 144 | soarState: accounts.game, // `soarGame` can also be used here. 145 | soarLeaderboard: accounts.leaderboard, 146 | soarPlayerAccount: accounts.playerAccount, 147 | soarPlayerScores: accounts.playerScores, 148 | soarTopEntries: accounts.topEntries, // created when player registered to leaderboard 149 | soarProgram: soar.program.programId, 150 | systemProgram: accounts.systemProgram, 151 | }) 152 | .signers([user]) 153 | .rpc(); 154 | 155 | const tens = await tensProgram.account.tens.fetch(tensPDA); 156 | const counter = tens.counter.toNumber(); 157 | 158 | if (counter % 10 !== 0) { 159 | console.log(`..Moved ${counter}. Keep going!`); 160 | } else { 161 | console.log(`> Brilliant!. You moved ${counter} and won this round!`); 162 | const playerScores = await soar.fetchPlayerScoresListAccount( 163 | accounts.playerScores 164 | ); 165 | console.log( 166 | `> Added ${counter} to your scores: ${JSON.stringify( 167 | playerScores.pretty().scores 168 | )}\n` 169 | ); 170 | } 171 | } 172 | 173 | // ---------------------------------------------------------------------------------------------// 174 | // Setup mint and token accounts for testing. // 175 | // ---------------------------------------------------------------------------------------------// 176 | const { mint, authority } = await utils.initializeTestMint(soar); 177 | 178 | const tokenAccountOwner = offChainAuthority; 179 | const tokenAccount = await utils.createTokenAccount( 180 | soar, 181 | tokenAccountOwner.publicKey, 182 | mint 183 | ); 184 | await utils.mintToAccount(soar, mint, authority, tokenAccount, 5); 185 | 186 | const userATA = await utils.createTokenAccount(soar, user.publicKey, mint); 187 | // ---------------------------------------------------------------------------------------------// 188 | // ---------------------------------------------------------------------------------------------// 189 | 190 | // Now we add an achievement for this game and a ft reward for that achievement. 191 | const { newAchievement, transaction: addAchievement } = 192 | await soar.addNewGameAchievement( 193 | soarGame, 194 | offChainAuthority.publicKey, 195 | "title", 196 | "desc", 197 | PublicKey.default 198 | ); 199 | await soar.sendAndConfirmTransaction(addAchievement, [offChainAuthority]); 200 | 201 | const reward = Keypair.generate(); 202 | const { newReward, transaction: addReward } = await soar.addFungibleReward( 203 | offChainAuthority.publicKey, 204 | reward.publicKey, 205 | newAchievement, 206 | new BN(4), 207 | new BN(5), 208 | new BN(100), 209 | mint, 210 | tokenAccount, 211 | tokenAccountOwner.publicKey 212 | ); 213 | await soar.sendAndConfirmTransaction(addReward, [ 214 | reward, 215 | tokenAccountOwner, 216 | ]); 217 | 218 | // Now the user can try to claim a reward by sending a transaction to the `tens` smart-contract. 219 | // 220 | // The `tens` smart-contract decides what invariant it wants to establish for a reward to be 221 | // claimed for a user. In this particular case, our on-chain program chooses to only claim rewards 222 | // for users that have a score in the `top-entries` list for our leaderboard. Since this is handled 223 | // on-chain, our on-chain authority(the tens PDA) is used. 224 | /// 225 | const accounts = await new AccountsBuilder( 226 | provider, 227 | soar.program.programId 228 | ).claimFtRewardAccounts( 229 | tensPDA, 230 | newAchievement, 231 | user.publicKey, 232 | newReward, 233 | soarGame 234 | ); 235 | await utils.airdropTo(soar, user.publicKey, 1); 236 | 237 | let balance = await soar.provider.connection.getTokenAccountBalance( 238 | userATA 239 | ); 240 | console.log(`> Balance before claim: ${balance.value.uiAmount}`); 241 | 242 | console.log("..claiming..."); 243 | await tensProgram.methods 244 | .claimReward() 245 | .accounts({ 246 | user: user.publicKey, 247 | tensState: tensPDA, 248 | playerAccount: accounts.playerAccount, 249 | soarPlayerScores: playerScoresList, 250 | soarTopEntries: topEntries, 251 | soarState: accounts.game, 252 | soarAchievement: accounts.achievement, 253 | soarReward: accounts.reward, 254 | soarPlayerAchievement: accounts.playerAchievement, 255 | sourceTokenAccount: accounts.sourceTokenAccount, 256 | userTokenAccount: accounts.userTokenAccount, 257 | tokenProgram: accounts.tokenProgram, 258 | systemProgram: SystemProgram.programId, 259 | soarProgram: soar.program.programId, 260 | }) 261 | .signers([user]) 262 | .rpc(); 263 | 264 | balance = await soar.provider.connection.getTokenAccountBalance(userATA); 265 | console.log(`> Claim successful. New balance: ${balance.value.uiAmount}`); 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /client/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | SystemProgram, 6 | Transaction, 7 | } from "@solana/web3.js"; 8 | import { 9 | createMint, 10 | createAssociatedTokenAccountIdempotentInstruction, 11 | getAssociatedTokenAddressSync, 12 | mintToChecked, 13 | } from "@solana/spl-token"; 14 | import { Metaplex, keypairIdentity } from "@metaplex-foundation/js"; 15 | import { Metadata } from "@metaplex-foundation/mpl-token-metadata"; 16 | import fs from "fs"; 17 | import { SoarProgram } from "../sdk/lib"; 18 | 19 | const KEYPAIR_PATH = process.cwd() + "/client/tests/fixtures/provider.json"; 20 | export const COLLECTION_URI = 21 | "https://raw.githubusercontent.com/magicblock-labs/SOAR/client/tests/fixtures/metadata/collection.json"; 22 | export const TEST1_URI = 23 | "https://raw.githubusercontent.com/magicblock-labs/SOAR/client/tests/fixtures/metadata/image_test1"; 24 | 25 | export const initializeTestMint = async ( 26 | client: SoarProgram 27 | ): Promise<{ 28 | mint: PublicKey; 29 | authority: Keypair; 30 | }> => { 31 | let mint = Keypair.generate(); 32 | let mintAuthority = Keypair.generate(); 33 | await airdropTo(client, mintAuthority.publicKey, 1); 34 | const mintAddress = await createMint( 35 | client.provider.connection, 36 | mintAuthority, 37 | mintAuthority.publicKey, 38 | mintAuthority.publicKey, 39 | 0, 40 | mint 41 | ); 42 | return { 43 | mint: mintAddress, 44 | authority: mintAuthority, 45 | }; 46 | }; 47 | 48 | export const airdropTo = async ( 49 | client: SoarProgram, 50 | account: PublicKey, 51 | amount: number 52 | ): Promise => { 53 | const transaction = new Transaction().add( 54 | SystemProgram.transfer({ 55 | fromPubkey: client.provider.publicKey, 56 | toPubkey: account, 57 | lamports: amount * 1_000_000_000, 58 | }) 59 | ); 60 | return await client.sendAndConfirmTransaction(transaction); 61 | }; 62 | 63 | export const mintToAccount = async ( 64 | client: SoarProgram, 65 | mint: PublicKey, 66 | authority: Keypair, 67 | to: PublicKey, 68 | amount: number 69 | ) => { 70 | await mintToChecked( 71 | client.provider.connection, 72 | authority, 73 | mint, 74 | to, 75 | authority, 76 | amount * 1, 77 | 0 78 | ); 79 | }; 80 | 81 | export const createTokenAccount = async ( 82 | client: SoarProgram, 83 | owner: PublicKey, 84 | mint: PublicKey 85 | ): Promise => { 86 | const tokenAccount = getAssociatedTokenAddressSync(mint, owner); 87 | const tx = new Transaction().add( 88 | createAssociatedTokenAccountIdempotentInstruction( 89 | client.provider.publicKey, 90 | tokenAccount, 91 | owner, 92 | mint 93 | ) 94 | ); 95 | await client.sendAndConfirmTransaction(tx); 96 | 97 | return tokenAccount; 98 | }; 99 | 100 | export const initMetaplex = (connection: Connection): Metaplex => { 101 | const walletString = fs.readFileSync(KEYPAIR_PATH, { encoding: "utf8" }); 102 | const secretKey = Buffer.from(JSON.parse(walletString)); 103 | const keypair = Keypair.fromSecretKey(secretKey); 104 | 105 | return Metaplex.make(connection).use(keypairIdentity(keypair)); 106 | }; 107 | 108 | export const initTestCollectionNft = async ( 109 | metaplex: Metaplex, 110 | uri: string, 111 | name: string 112 | ): Promise => { 113 | const mint = Keypair.generate(); 114 | 115 | await metaplex.nfts().create({ 116 | uri, 117 | name, 118 | sellerFeeBasisPoints: 100, 119 | useNewMint: mint, 120 | isCollection: true, 121 | }); 122 | 123 | return mint; 124 | }; 125 | 126 | export const fetchMetadataAccount = async ( 127 | soar: SoarProgram, 128 | mint: PublicKey 129 | ): Promise => { 130 | const metadataPDA = soar.utils.deriveMetadataAddress(mint)[0]; 131 | const account = await soar.provider.connection.getAccountInfo(metadataPDA); 132 | 133 | return Metadata.fromAccountInfo(account)[0]; 134 | }; 135 | -------------------------------------------------------------------------------- /crates/soar-cpi/.gitignore: -------------------------------------------------------------------------------- 1 | idl.json -------------------------------------------------------------------------------- /crates/soar-cpi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "soar-cpi" 3 | version = "0.1.4" 4 | edition = "2021" 5 | include = ["src/**/*", "idl.json"] 6 | description = "CPI helpers for the SOAR program" 7 | authors = ["Magicblock "] 8 | repository = "https://github.com/magicblock-labs/SOAR/tree/main/crates/soar-cpi" 9 | license = "MIT" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [features] 14 | default = ["cpi"] 15 | no-entrypoint = [] 16 | no-idl = [] 17 | no-log-ix-name = [] 18 | cpi = ["no-entrypoint"] 19 | 20 | [dependencies] 21 | clockwork-anchor-gen = "0.3.2" 22 | anchor-lang = "0.29.0" 23 | -------------------------------------------------------------------------------- /crates/soar-cpi/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 magicblock-labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /crates/soar-cpi/README.md: -------------------------------------------------------------------------------- 1 | #### SOAR-CPI 2 | 3 | CPI helpers for the [SOAR](https://github.com/magicblock-labs/SOAR) program, that provides an interface for on-chain achievements and rewards for games build on Solana. 4 | 5 | This crate was automatically generated using [anchor-gen](https://github.com/saber-hq/anchor-gen), a crate for generating Anchor CPI helpers from JSON IDLs. 6 | -------------------------------------------------------------------------------- /crates/soar-cpi/build.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | // Copy the IDL from the target directory before building or publishing the cpi library 3 | fn main() -> std::io::Result<()> { 4 | // ignore error if build has not created idl yet. 5 | fs::copy("../target/idl/soar.json", "idl.json").ok(); 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /crates/soar-cpi/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::declare_id; 2 | use clockwork_anchor_gen::generate_cpi_interface; 3 | 4 | generate_cpi_interface!(idl_path = "idl.json"); 5 | declare_id!("SoarNNzwQHMwcfdkdLc6kvbkoMSxcHy89gTHrjhJYkk"); 6 | -------------------------------------------------------------------------------- /examples/tens/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tens" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "tens" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | 18 | [dependencies] 19 | anchor-lang = "0.29.0" 20 | soar = { path = "../../programs/soar", features = ["cpi"] } 21 | -------------------------------------------------------------------------------- /examples/tens/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /examples/tens/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::result_large_err)] 2 | 3 | use anchor_lang::prelude::*; 4 | use soar::cpi::accounts::{ClaimFtReward, SubmitScore}; 5 | use soar::cpi::{self}; 6 | use soar::{LeaderTopEntries, PlayerScoresList}; 7 | 8 | declare_id!("Tensgwm3DY3UJ8nhF7xnD2Wo65VcnLTXjjoyEvs6Zyk"); 9 | 10 | #[program] 11 | mod tens { 12 | use super::*; 13 | 14 | pub fn register( 15 | ctx: Context, 16 | soar_state: Pubkey, 17 | soar_leaderboard: Pubkey, 18 | soar_leaderboard_top_entries: Pubkey, 19 | ) -> Result<()> { 20 | let state = &mut ctx.accounts.tens_state; 21 | 22 | // Prelude: We initialize the soar_game and soar_leaderboard off-chain and set 23 | // this program's `internal_state` as an authority so it can sign for CPIs to 24 | // the SOAR program. 25 | 26 | state.soar.state = soar_state; 27 | state.soar.leaderboard = soar_leaderboard; 28 | state.soar.top_entries = soar_leaderboard_top_entries; 29 | state.counter = 0; 30 | 31 | Ok(()) 32 | } 33 | 34 | pub fn make_move(ctx: Context) -> Result<()> { 35 | let tens = &mut ctx.accounts.tens_state; 36 | tens.counter = tens.counter.checked_add(1).unwrap(); 37 | 38 | if tens.counter % 10 == 0 { 39 | msg!(" You won this round! "); 40 | 41 | let accounts = SubmitScore { 42 | payer: ctx.accounts.user.to_account_info(), 43 | authority: tens.to_account_info(), 44 | player_account: ctx.accounts.soar_player_account.to_account_info(), 45 | game: ctx.accounts.soar_state.to_account_info(), 46 | leaderboard: ctx.accounts.soar_leaderboard.to_account_info(), 47 | player_scores: ctx.accounts.soar_player_scores.to_account_info(), 48 | top_entries: ctx 49 | .accounts 50 | .soar_top_entries 51 | .as_ref() 52 | .map(|a| a.to_account_info()), 53 | system_program: ctx.accounts.system_program.to_account_info(), 54 | }; 55 | 56 | let state_bump = ctx.bumps.tens_state; 57 | let seeds = &[b"tens".as_ref(), &[state_bump]]; 58 | let signer = &[&seeds[..]]; 59 | 60 | let cpi_ctx = CpiContext::new(ctx.accounts.soar_program.to_account_info(), accounts) 61 | .with_signer(signer); 62 | msg!("Submitting score {} for user.", tens.counter); 63 | cpi::submit_score(cpi_ctx, tens.counter)?; 64 | } 65 | 66 | Ok(()) 67 | } 68 | 69 | pub fn claim_reward(ctx: Context) -> Result<()> { 70 | // We claim a reward if the user's score is present in the top-entries account. 71 | let player = &ctx.accounts.player_account; 72 | let top_entries = &ctx.accounts.soar_top_entries; 73 | if top_entries 74 | .top_scores 75 | .iter() 76 | .any(|score| score.player == player.key()) 77 | .eq(&true) 78 | { 79 | msg!("Player has a top score!..Claiming reward: "); 80 | let accounts = ClaimFtReward { 81 | user: ctx.accounts.user.to_account_info(), 82 | authority: ctx.accounts.tens_state.to_account_info(), 83 | payer: ctx.accounts.user.to_account_info(), 84 | game: ctx.accounts.soar_state.to_account_info(), 85 | achievement: ctx.accounts.soar_achievement.to_account_info(), 86 | reward: ctx.accounts.soar_reward.to_account_info(), 87 | player_account: ctx.accounts.player_account.to_account_info(), 88 | player_achievement: ctx.accounts.soar_player_achievement.to_account_info(), 89 | source_token_account: ctx.accounts.source_token_account.to_account_info(), 90 | user_token_account: ctx.accounts.user_token_account.to_account_info(), 91 | token_program: ctx.accounts.token_program.to_account_info(), 92 | system_program: ctx.accounts.system_program.to_account_info(), 93 | }; 94 | 95 | let state_bump = ctx.bumps.tens_state; 96 | let seeds = &[b"tens".as_ref(), &[state_bump]]; 97 | let signer = &[&seeds[..]]; 98 | 99 | let cpi_ctx = CpiContext::new(ctx.accounts.soar_program.to_account_info(), accounts) 100 | .with_signer(signer); 101 | 102 | cpi::claim_ft_reward(cpi_ctx)?; 103 | } else { 104 | msg!("This user isn't eligible for a reward!"); 105 | } 106 | 107 | Ok(()) 108 | } 109 | } 110 | 111 | #[derive(Accounts)] 112 | pub struct Initialize<'info> { 113 | #[account(mut)] 114 | pub signer: Signer<'info>, 115 | #[account( 116 | init, 117 | seeds = [b"tens"], 118 | bump, 119 | space = 112, 120 | payer = signer, 121 | )] 122 | pub tens_state: Account<'info, Tens>, 123 | pub system_program: Program<'info, System>, 124 | } 125 | 126 | #[derive(Accounts)] 127 | pub struct MakeMove<'info> { 128 | #[account(mut)] 129 | pub user: Signer<'info>, 130 | #[account( 131 | mut, 132 | seeds = [b"tens"], bump, 133 | constraint = tens_state.soar.leaderboard == soar_leaderboard.key(), 134 | constraint = tens_state.soar.state == soar_state.key(), 135 | )] 136 | pub tens_state: Account<'info, Tens>, 137 | /// CHECK: The SOAR game account for this program. 138 | pub soar_state: UncheckedAccount<'info>, 139 | /// CHECK: The SOAR leaderboard for this program. 140 | pub soar_leaderboard: UncheckedAccount<'info>, 141 | /// CHECK: The SOAR player account for this user. 142 | pub soar_player_account: UncheckedAccount<'info>, 143 | #[account(mut)] 144 | /// CHECK: The SOAR player scores account for this user. 145 | pub soar_player_scores: UncheckedAccount<'info>, 146 | #[account(mut)] 147 | /// CHECK: The SOAR top entries account for this leaderboard. 148 | pub soar_top_entries: Option>, 149 | /// CHECK: The SOAR program ID. 150 | #[account(address = soar::ID)] 151 | pub soar_program: UncheckedAccount<'info>, 152 | pub system_program: Program<'info, System>, 153 | } 154 | 155 | #[derive(Accounts)] 156 | pub struct Claim<'info> { 157 | #[account(mut)] 158 | pub user: Signer<'info>, 159 | #[account( 160 | mut, 161 | seeds = [b"tens"], bump, 162 | constraint = tens_state.soar.state == soar_state.key(), 163 | )] 164 | pub tens_state: Account<'info, Tens>, 165 | /// CHECK: The SOAR player account for this user. 166 | #[account(mut)] 167 | pub player_account: UncheckedAccount<'info>, 168 | #[account( 169 | has_one = player_account, 170 | constraint = soar_player_scores.leaderboard == tens_state.soar.leaderboard 171 | )] 172 | pub soar_player_scores: Account<'info, PlayerScoresList>, 173 | #[account(constraint = tens_state.soar.top_entries == soar_top_entries.key())] 174 | pub soar_top_entries: Account<'info, LeaderTopEntries>, 175 | /// CHECK: The SOAR game for this tens program. 176 | pub soar_state: UncheckedAccount<'info>, 177 | /// CHECK: The SOAR achievement. 178 | pub soar_achievement: UncheckedAccount<'info>, 179 | /// CHECK: The SOAR reward account. 180 | #[account(mut)] 181 | pub soar_reward: UncheckedAccount<'info>, 182 | /// CHECK: The player-achievement account to be initialized. 183 | #[account(mut)] 184 | pub soar_player_achievement: UncheckedAccount<'info>, 185 | /// CHECK: The specified source account for the reward. 186 | #[account(mut)] 187 | pub source_token_account: UncheckedAccount<'info>, 188 | /// CHECK: The user's token account. 189 | #[account(mut)] 190 | pub user_token_account: UncheckedAccount<'info>, 191 | /// CHECK: The token program. 192 | pub token_program: UncheckedAccount<'info>, 193 | /// CHECK: The system program. 194 | pub system_program: UncheckedAccount<'info>, 195 | /// CHECK: The SOAR program. 196 | #[account(address = soar::ID)] 197 | pub soar_program: UncheckedAccount<'info>, 198 | } 199 | 200 | #[account] 201 | /// A simple game. 202 | pub struct Tens { 203 | /// The game counter. 204 | pub counter: u64, 205 | /// The SOAR keys for this program. 206 | pub soar: SoarKeysStorage, 207 | } 208 | 209 | #[derive(AnchorSerialize, AnchorDeserialize, Clone)] 210 | pub struct SoarKeysStorage { 211 | /// The soar state for this game. 212 | state: Pubkey, 213 | /// The soar leaderboard for this game. 214 | leaderboard: Pubkey, 215 | /// The soar top-entries account for this game. 216 | top_entries: Pubkey, 217 | } 218 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaces": [ 3 | "client/sdk", 4 | "client/tests" 5 | ], 6 | "private": true, 7 | "scripts": { 8 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 9 | "lint": "yarn workspaces run lint" 10 | }, 11 | "dependencies": { 12 | "@coral-xyz/anchor": "^0.27.0", 13 | "@metaplex-foundation/js": "^0.20.1", 14 | "@metaplex-foundation/mpl-token-metadata": "^2.13.0", 15 | "@solana/spl-token": "^0.3.8" 16 | }, 17 | "devDependencies": { 18 | "@types/bn.js": "^5.1.0", 19 | "@types/chai": "^4.3.0", 20 | "@types/mocha": "^9.0.0", 21 | "@typescript-eslint/eslint-plugin": "^5.50.0", 22 | "@typescript-eslint/parser": "^5.50.0", 23 | "chai": "^4.3.4", 24 | "cross-env": "^7.0.3", 25 | "eslint": "^8.33.0", 26 | "eslint-config-prettier": "^8.6.0", 27 | "eslint-config-standard-with-typescript": "^34.0.0", 28 | "eslint-plugin-import": "^2.25.3", 29 | "eslint-plugin-n": "^15.6.1", 30 | "eslint-plugin-prettier": "^4.2.1", 31 | "eslint-plugin-promise": "6.1.1", 32 | "eslint-plugin-react": "^7.32.2", 33 | "lerna": "^6.4.1", 34 | "mocha": "^9.0.3", 35 | "prettier": "^2.6.2", 36 | "ts-mocha": "^10.0.0", 37 | "tsx": "^3.12.3", 38 | "typescript": "*" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /programs/soar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "soar" 3 | version = "0.1.1" 4 | description = "Soar by Magicblock Labs" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "soar" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | 18 | [dependencies] 19 | anchor-lang = { version = "0.29.0", features = ["init-if-needed"] } 20 | anchor-spl = "0.29.0" 21 | mpl-token-metadata = { version="1.13.2", features = ["no-entrypoint"] } 22 | winnow = "=0.5.15" 23 | solana-security-txt = "1.1.1" 24 | -------------------------------------------------------------------------------- /programs/soar/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/soar/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum SoarError { 5 | /// Returned if the length of a parameter exceeds its allowed limits. 6 | #[msg("Exceeded max length for field.")] 7 | InvalidFieldLength, 8 | 9 | /// Returned if the wrong authority attempts to sign for an instruction 10 | #[msg("Invalid authority for instruction")] 11 | InvalidAuthority, 12 | 13 | /// Returned if an account that's expected to sign doesn't. 14 | #[msg("An expected signature isn't present")] 15 | MissingSignature, 16 | 17 | #[msg("Reward not specified for this achievement")] 18 | NoRewardForAchievement, 19 | 20 | #[msg("The merge account does not include this player account")] 21 | AccountNotPartOfMerge, 22 | 23 | #[msg("Tried to input score that is below the minimum or above the maximum")] 24 | ScoreNotWithinBounds, 25 | 26 | #[msg("An optional but expected account is missing")] 27 | MissingExpectedAccount, 28 | 29 | #[msg("Invalid reward kind for this instruction")] 30 | InvalidRewardKind, 31 | 32 | #[msg("No more rewards are being given out for this game")] 33 | NoAvailableRewards, 34 | } 35 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/add_achievement.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | state::{Achievement, FieldsCheck}, 3 | AddAchievement, 4 | }; 5 | use anchor_lang::prelude::*; 6 | 7 | pub fn handler( 8 | ctx: Context, 9 | title: String, 10 | description: String, 11 | nft_meta: Pubkey, 12 | ) -> Result<()> { 13 | let game = &mut ctx.accounts.game; 14 | game.achievement_count = game.next_achievement(); 15 | let obj = Achievement::new( 16 | game.key(), 17 | title, 18 | description, 19 | nft_meta, 20 | game.achievement_count, 21 | ); 22 | 23 | obj.check()?; 24 | ctx.accounts.new_achievement.set_inner(obj); 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/add_leaderboard.rs: -------------------------------------------------------------------------------- 1 | use crate::error::SoarError; 2 | use crate::state::{FieldsCheck, LeaderBoardScore, RegisterLeaderBoardInput}; 3 | use crate::AddLeaderBoard; 4 | use anchor_lang::prelude::*; 5 | 6 | pub fn handler(ctx: Context, input: RegisterLeaderBoardInput) -> Result<()> { 7 | input.check()?; 8 | 9 | let game = &ctx.accounts.game; 10 | let new_count = game.next_leaderboard(); 11 | 12 | let retain_count = input.scores_to_retain; 13 | let order = input.is_ascending; 14 | 15 | let leaderboard = input.into(); 16 | ctx.accounts.leaderboard.set_inner(leaderboard); 17 | ctx.accounts.leaderboard.id = new_count; 18 | ctx.accounts.leaderboard.game = game.key(); 19 | ctx.accounts.game.leaderboard_count = new_count; 20 | 21 | if retain_count > 0 { 22 | require!( 23 | ctx.accounts.top_entries.is_some(), 24 | SoarError::MissingExpectedAccount 25 | ); 26 | } 27 | 28 | if retain_count > 0 && ctx.accounts.top_entries.is_some() { 29 | let top_entries = &mut ctx.accounts.top_entries.as_mut().unwrap(); 30 | 31 | top_entries.is_ascending = order; 32 | top_entries.top_scores = vec![LeaderBoardScore::default(); retain_count as usize]; 33 | 34 | if top_entries.is_ascending { 35 | top_entries.top_scores.iter_mut().for_each(|s| { 36 | s.entry.score = ctx.accounts.leaderboard.max_score; 37 | }); 38 | } 39 | ctx.accounts.leaderboard.top_entries = Some(top_entries.key()); 40 | } 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/add_reward.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::SoarError, 3 | state::{AddNewRewardInput, RewardKind, RewardKindInput}, 4 | utils, FieldsCheck, 5 | }; 6 | use anchor_lang::prelude::*; 7 | use anchor_spl::token::{self, Approve}; 8 | 9 | pub mod ft { 10 | use super::*; 11 | use crate::AddFtReward; 12 | 13 | pub fn handler(ctx: Context, input: AddNewRewardInput) -> Result<()> { 14 | let new_reward = &mut ctx.accounts.new_reward; 15 | new_reward.achievement = ctx.accounts.achievement.key(); 16 | new_reward.available_spots = input.available_spots; 17 | 18 | let achievement = &ctx.accounts.achievement; 19 | 20 | match input.kind { 21 | RewardKindInput::Ft { deposit, amount } => { 22 | let mint = &ctx.accounts.reward_token_mint; 23 | let token_account = &ctx.accounts.delegate_from_token_account; 24 | let token_account_owner = &ctx.accounts.token_account_owner; 25 | let token_program = &ctx.accounts.token_program; 26 | 27 | let reward = RewardKind::FungibleToken { 28 | mint: mint.key(), 29 | account: token_account.key(), 30 | amount, 31 | }; 32 | new_reward.reward = reward; 33 | new_reward.check()?; 34 | 35 | // Delegate authority to spend some amount of tokens to the `achievement` PDA. 36 | let cpi_ctx = CpiContext::new( 37 | token_program.to_account_info(), 38 | Approve { 39 | to: token_account.to_account_info(), 40 | delegate: achievement.to_account_info(), 41 | authority: token_account_owner.to_account_info(), 42 | }, 43 | ); 44 | token::approve(cpi_ctx, deposit)?; 45 | 46 | let achievement = &mut ctx.accounts.achievement; 47 | achievement.reward = Some(new_reward.key()); 48 | 49 | Ok(()) 50 | } 51 | RewardKindInput::Nft { 52 | uri: _, 53 | name: _, 54 | symbol: _, 55 | } => Err(SoarError::InvalidRewardKind.into()), 56 | } 57 | } 58 | } 59 | 60 | pub mod nft { 61 | use super::*; 62 | use crate::AddNftReward; 63 | 64 | pub fn handler(ctx: Context, input: AddNewRewardInput) -> Result<()> { 65 | let new_reward = &mut ctx.accounts.new_reward; 66 | new_reward.achievement = ctx.accounts.achievement.key(); 67 | new_reward.available_spots = input.available_spots; 68 | 69 | let achievement = &ctx.accounts.achievement; 70 | 71 | match input.kind { 72 | RewardKindInput::Nft { uri, name, symbol } => { 73 | let mut nft_reward_collection_mint: Option = None; 74 | 75 | if let Some(collection_mint) = ctx.accounts.reward_collection_mint.as_ref() { 76 | let update_auth = ctx 77 | .accounts 78 | .collection_update_auth 79 | .as_ref() 80 | .ok_or(SoarError::MissingExpectedAccount)?; 81 | let collection_metadata = ctx 82 | .accounts 83 | .collection_metadata 84 | .as_ref() 85 | .ok_or(SoarError::MissingExpectedAccount)?; 86 | let token_metadata_program = ctx 87 | .accounts 88 | .token_metadata_program 89 | .as_ref() 90 | .ok_or(SoarError::MissingExpectedAccount)?; 91 | 92 | let decoded = utils::decode_mpl_metadata_account(collection_metadata)?; 93 | let mint_key = collection_mint.key(); 94 | require_keys_eq!(decoded.mint, mint_key); 95 | 96 | // Make the newly created `reward` account the update_authority of the collection 97 | // metadata so that it can sign verification. 98 | utils::update_metadata_account( 99 | None, 100 | None, 101 | None, 102 | None, 103 | Some(achievement.key()), 104 | collection_metadata, 105 | update_auth, 106 | token_metadata_program, 107 | None, 108 | )?; 109 | nft_reward_collection_mint = Some(collection_mint.key()); 110 | } 111 | 112 | let reward = RewardKind::NonFungibleToken { 113 | uri, 114 | name, 115 | symbol, 116 | minted: 0, 117 | collection: nft_reward_collection_mint, 118 | }; 119 | 120 | new_reward.reward = reward; 121 | new_reward.check()?; 122 | 123 | let achievement = &mut ctx.accounts.achievement; 124 | achievement.reward = Some(new_reward.key()); 125 | 126 | Ok(()) 127 | } 128 | RewardKindInput::Ft { 129 | deposit: _, 130 | amount: _, 131 | } => Err(SoarError::InvalidRewardKind.into()), 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/approve_merge.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::SoarError, ApproveMerge}; 2 | use anchor_lang::prelude::*; 3 | 4 | pub fn handler(ctx: Context) -> Result<()> { 5 | let merge_account = &mut ctx.accounts.merge_account; 6 | let player_account = &ctx.accounts.player_account; 7 | let approvals = &mut merge_account.approvals; 8 | 9 | let found = approvals 10 | .iter_mut() 11 | .find(|one| one.key == player_account.key()); 12 | 13 | if found.is_none() { 14 | return Err(SoarError::AccountNotPartOfMerge.into()); 15 | } 16 | 17 | let merge = found.unwrap(); 18 | merge.approved = true; 19 | 20 | if !approvals.iter().any(|info| !info.approved) { 21 | // Failed to find any unapproved key hence merge complete! 22 | merge_account.merge_complete = true; 23 | } 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/claim_reward.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::SoarError, 3 | state::{PlayerAchievement, RewardKind}, 4 | utils, 5 | }; 6 | use anchor_lang::prelude::*; 7 | use anchor_spl::token::{self, Transfer}; 8 | 9 | pub mod ft { 10 | use super::*; 11 | use crate::ClaimFtReward; 12 | 13 | pub fn handler(ctx: Context) -> Result<()> { 14 | let token_program = &ctx.accounts.token_program; 15 | let player_achievement = &mut ctx.accounts.player_achievement; 16 | 17 | let reward_account = &mut ctx.accounts.reward; 18 | let achievement_account = &ctx.accounts.achievement; 19 | 20 | let game_key = &ctx.accounts.game.key(); 21 | let achievement_bump = ctx.bumps.achievement; 22 | let id = ctx.accounts.achievement.id; 23 | let achievement_seeds = &[ 24 | crate::seeds::ACHIEVEMENT, 25 | game_key.as_ref(), 26 | &id.to_le_bytes(), 27 | &[achievement_bump], 28 | ]; 29 | let signer = &[&achievement_seeds[..]]; 30 | 31 | let reward = &mut reward_account.reward; 32 | match reward { 33 | RewardKind::FungibleToken { 34 | mint: _, 35 | account, 36 | amount, 37 | } => { 38 | let user_token_account = &ctx.accounts.user_token_account; 39 | let source_token_account = &ctx.accounts.source_token_account; 40 | require_keys_eq!(source_token_account.key(), *account); 41 | 42 | let cpi_ctx = CpiContext::new_with_signer( 43 | token_program.to_account_info(), 44 | Transfer { 45 | from: source_token_account.to_account_info(), 46 | to: user_token_account.to_account_info(), 47 | authority: achievement_account.to_account_info(), 48 | }, 49 | signer, 50 | ); 51 | 52 | token::transfer(cpi_ctx, *amount)?; 53 | 54 | player_achievement.set_inner(PlayerAchievement::new( 55 | ctx.accounts.player_account.key(), 56 | ctx.accounts.achievement.key(), 57 | Clock::get().unwrap().unix_timestamp, 58 | )); 59 | player_achievement.claimed = true; 60 | 61 | reward_account.available_spots = 62 | reward_account.available_spots.checked_sub(1).unwrap(); 63 | Ok(()) 64 | } 65 | RewardKind::NonFungibleToken { 66 | uri: _, 67 | name: _, 68 | symbol: _, 69 | minted: _, 70 | collection: _, 71 | } => Err(SoarError::InvalidRewardKind.into()), 72 | } 73 | } 74 | } 75 | 76 | pub mod nft { 77 | use super::*; 78 | use crate::ClaimNftReward; 79 | 80 | pub fn handler(ctx: Context) -> Result<()> { 81 | let user = &ctx.accounts.user; 82 | let token_program = &ctx.accounts.token_program; 83 | let player_achievement = &mut ctx.accounts.player_achievement; 84 | 85 | let reward_account = &mut ctx.accounts.reward; 86 | let achievement_account = &ctx.accounts.achievement; 87 | 88 | let game_key = &ctx.accounts.game.key(); 89 | let achievement_bump = ctx.bumps.achievement; 90 | let id = ctx.accounts.achievement.id; 91 | let achievement_seeds = &[ 92 | crate::seeds::ACHIEVEMENT, 93 | game_key.as_ref(), 94 | &id.to_le_bytes(), 95 | &[achievement_bump], 96 | ]; 97 | let signer = &[&achievement_seeds[..]]; 98 | 99 | let reward = &mut reward_account.reward; 100 | match reward { 101 | RewardKind::NonFungibleToken { 102 | uri, 103 | name, 104 | symbol, 105 | minted, 106 | collection, 107 | } => { 108 | let payer = &ctx.accounts.payer; 109 | let mint = &ctx.accounts.new_mint; 110 | let metadata = &ctx.accounts.new_metadata; 111 | let master_edition = &ctx.accounts.new_master_edition; 112 | let mint_to = &ctx.accounts.mint_to; 113 | let token_metadata_program = &ctx.accounts.token_metadata_program; 114 | let associated_token_program = &ctx.accounts.associated_token_program; 115 | let system_program = &ctx.accounts.system_program; 116 | let rent = &ctx.accounts.rent; 117 | 118 | // Mint authority ends up being transferred to the master edition account. 119 | let temp_mint_authority = &payer.to_account_info(); 120 | 121 | utils::create_mint( 122 | payer, 123 | mint, 124 | temp_mint_authority, 125 | system_program, 126 | token_program, 127 | &rent.to_account_info(), 128 | )?; 129 | 130 | let user_token_account = mint_to; 131 | utils::create_token_account( 132 | payer, 133 | user_token_account, 134 | user, 135 | mint, 136 | system_program, 137 | token_program, 138 | associated_token_program, 139 | )?; 140 | 141 | utils::mint_token( 142 | mint, 143 | user_token_account, 144 | temp_mint_authority, 145 | token_program, 146 | None, 147 | )?; 148 | 149 | let update_authority = achievement_account.to_account_info(); 150 | let creator = Some(achievement_account.key()); 151 | 152 | utils::create_metadata_account( 153 | name, 154 | symbol, 155 | uri, 156 | metadata, 157 | mint, 158 | temp_mint_authority, 159 | payer, 160 | &update_authority, 161 | &creator, 162 | collection, 163 | token_metadata_program, 164 | system_program, 165 | &rent.to_account_info(), 166 | Some(signer), 167 | )?; 168 | 169 | utils::create_master_edition_account( 170 | master_edition, 171 | mint, 172 | payer, 173 | metadata, 174 | temp_mint_authority, 175 | &update_authority, 176 | token_metadata_program, 177 | system_program, 178 | &rent.to_account_info(), 179 | Some(signer), 180 | )?; 181 | 182 | *minted = minted.checked_add(1).unwrap(); 183 | 184 | player_achievement.set_inner(PlayerAchievement::new( 185 | ctx.accounts.player_account.key(), 186 | ctx.accounts.achievement.key(), 187 | Clock::get().unwrap().unix_timestamp, 188 | )); 189 | player_achievement.claimed = true; 190 | 191 | reward_account.available_spots = 192 | reward_account.available_spots.checked_sub(1).unwrap(); 193 | Ok(()) 194 | } 195 | RewardKind::FungibleToken { 196 | mint: _, 197 | account: _, 198 | amount: _, 199 | } => Err(SoarError::InvalidRewardKind.into()), 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/create_game.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | state::{FieldsCheck, Game, GameAttributes}, 3 | InitializeGame, 4 | }; 5 | use anchor_lang::prelude::*; 6 | 7 | pub fn handler( 8 | ctx: Context, 9 | game_meta_input: GameAttributes, 10 | game_auth_input: Vec, 11 | ) -> Result<()> { 12 | game_meta_input.check()?; 13 | 14 | let game_account = &mut ctx.accounts.game; 15 | let mut game_object = Game::default(); 16 | 17 | game_object.set_attributes(game_meta_input); 18 | game_object.leaderboard_count = 0; 19 | game_object.achievement_count = 0; 20 | game_object.auth = game_auth_input; 21 | 22 | game_account.set_inner(game_object); 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/create_player.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | state::{FieldsCheck, Player}, 3 | InitializePlayer, 4 | }; 5 | use anchor_lang::prelude::*; 6 | 7 | pub fn handler(ctx: Context, username: String, nft_meta: Pubkey) -> Result<()> { 8 | let user = &ctx.accounts.user.key(); 9 | let player = Player::new(username, nft_meta, *user); 10 | player.check()?; 11 | 12 | ctx.accounts.player_account.set_inner(player); 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/initiate_merge.rs: -------------------------------------------------------------------------------- 1 | use crate::{state::MergeApproval, InitiateMerge}; 2 | use anchor_lang::prelude::*; 3 | 4 | pub fn handler(ctx: Context, keys: Vec) -> Result<()> { 5 | let merge_account = &mut ctx.accounts.merge_account; 6 | let player_key = &ctx.accounts.player_account.key(); 7 | 8 | merge_account.initiator = ctx.accounts.user.key(); 9 | merge_account.approvals = crate::dedup_input(player_key, keys) 10 | .0 11 | .into_iter() 12 | .map(MergeApproval::new) 13 | .collect(); 14 | merge_account.merge_complete = merge_account.approvals.is_empty(); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | 3 | pub mod add_achievement; 4 | pub mod add_leaderboard; 5 | pub mod add_reward; 6 | pub mod approve_merge; 7 | pub mod claim_reward; 8 | pub mod create_game; 9 | pub mod create_player; 10 | pub mod initiate_merge; 11 | pub mod register_player; 12 | pub mod submit_score; 13 | pub mod unlock_player_achievement; 14 | pub mod update_achievement; 15 | pub mod update_game; 16 | pub mod update_leaderboard; 17 | pub mod update_player; 18 | pub mod verify_reward; 19 | 20 | pub use add_achievement::*; 21 | pub use add_leaderboard::*; 22 | pub use add_reward::*; 23 | pub use approve_merge::*; 24 | pub use claim_reward::*; 25 | pub use create_game::*; 26 | pub use create_player::*; 27 | pub use register_player::*; 28 | pub use submit_score::*; 29 | pub use unlock_player_achievement::*; 30 | pub use update_achievement::*; 31 | pub use update_game::*; 32 | pub use update_leaderboard::*; 33 | pub use update_player::*; 34 | pub use verify_reward::*; 35 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/register_player.rs: -------------------------------------------------------------------------------- 1 | use crate::{state::PlayerScoresList, RegisterPlayer}; 2 | use anchor_lang::prelude::*; 3 | 4 | pub fn handler(ctx: Context) -> Result<()> { 5 | let player_info = ctx.accounts.player_account.key(); 6 | let leaderboard = ctx.accounts.leaderboard.key(); 7 | 8 | let new_list = &mut ctx.accounts.new_list; 9 | let obj = PlayerScoresList::new(player_info, leaderboard); 10 | 11 | new_list.set_inner(obj); 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/submit_score.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::SoarError, 3 | state::{LeaderBoardScore, PlayerScoresList, ScoreEntry}, 4 | utils, SubmitScore, 5 | }; 6 | use anchor_lang::prelude::*; 7 | 8 | pub fn handler(ctx: Context, score: u64) -> Result<()> { 9 | let player_scores = &mut ctx.accounts.player_scores; 10 | let leaderboard = &ctx.accounts.leaderboard; 11 | 12 | if score < leaderboard.min_score || score > leaderboard.max_score { 13 | return Err(SoarError::ScoreNotWithinBounds.into()); 14 | } 15 | 16 | let clock = Clock::get().unwrap(); 17 | let entry = ScoreEntry::new(score, clock.unix_timestamp); 18 | 19 | let count = player_scores.scores.len(); 20 | if count == player_scores.alloc_count as usize { 21 | let window = PlayerScoresList::REALLOC_WINDOW; 22 | msg!( 23 | "count: {}. Reallocating space for {} entries", 24 | count, 25 | window 26 | ); 27 | 28 | let size = player_scores.current_size(); 29 | let to_add = window.checked_mul(ScoreEntry::SIZE).unwrap(); 30 | let new_size = size.checked_add(to_add).unwrap(); 31 | 32 | utils::resize_account( 33 | &player_scores.to_account_info(), 34 | &ctx.accounts.payer.to_account_info(), 35 | &ctx.accounts.system_program.to_account_info(), 36 | new_size, 37 | )?; 38 | player_scores.alloc_count = player_scores 39 | .alloc_count 40 | .checked_add(PlayerScoresList::REALLOC_WINDOW as u16) 41 | .unwrap(); 42 | 43 | let new_space = player_scores.to_account_info().data_len(); 44 | msg!( 45 | "Resized account with initial space {}. New space: {}.", 46 | size, 47 | new_space 48 | ); 49 | } 50 | 51 | player_scores.scores.push(entry); 52 | let player_key = ctx.accounts.player_account.key(); 53 | 54 | if let Some(top_entries) = &mut ctx.accounts.top_entries { 55 | require_keys_eq!(leaderboard.top_entries.unwrap(), top_entries.key()); 56 | let mut score_entry = LeaderBoardScore::new(player_key, entry); 57 | let is_ascending = top_entries.is_ascending; 58 | 59 | let last_index = top_entries.top_scores.len() - 1; 60 | let mut index = last_index; 61 | 62 | let scores = &mut top_entries.top_scores; 63 | if is_ascending && entry.score < scores[last_index].entry.score { 64 | if !ctx.accounts.leaderboard.allow_multiple_scores { 65 | if let Some(idx) = scores.iter().position(|s| s.player == player_key) { 66 | index = idx; 67 | score_entry = if score_entry.entry.score < scores[idx].entry.score { 68 | score_entry 69 | } else { 70 | scores[idx].clone() 71 | }; 72 | } 73 | } 74 | scores[index] = score_entry; 75 | scores.sort_by(|a, b| a.entry.score.cmp(&b.entry.score)); 76 | } else if !is_ascending && entry.score > scores[last_index].entry.score { 77 | if !ctx.accounts.leaderboard.allow_multiple_scores { 78 | if let Some(idx) = scores.iter().position(|s| s.player == player_key) { 79 | index = idx; 80 | score_entry = if score_entry.entry.score > scores[idx].entry.score { 81 | score_entry 82 | } else { 83 | scores[idx].clone() 84 | }; 85 | } 86 | } 87 | scores[index] = score_entry; 88 | scores.sort_by(|a, b| b.entry.score.cmp(&a.entry.score)); 89 | } 90 | } 91 | 92 | Ok(()) 93 | } 94 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/unlock_player_achievement.rs: -------------------------------------------------------------------------------- 1 | use crate::{state::PlayerAchievement, UnlockPlayerAchievement}; 2 | use anchor_lang::prelude::*; 3 | 4 | pub fn handler(ctx: Context) -> Result<()> { 5 | let pa_account = &mut ctx.accounts.player_achievement; 6 | let clock = Clock::get().unwrap(); 7 | 8 | let obj = PlayerAchievement::new( 9 | ctx.accounts.player_account.key(), 10 | ctx.accounts.achievement.key(), 11 | clock.unix_timestamp, 12 | ); 13 | 14 | pa_account.set_inner(obj); 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/update_achievement.rs: -------------------------------------------------------------------------------- 1 | use crate::state::FieldsCheck; 2 | use crate::UpdateAchievement; 3 | use anchor_lang::prelude::*; 4 | 5 | pub fn handler( 6 | ctx: Context, 7 | new_title: Option, 8 | new_description: Option, 9 | new_meta: Option, 10 | ) -> Result<()> { 11 | let achievement = &mut ctx.accounts.achievement; 12 | 13 | if let Some(title) = new_title { 14 | achievement.title = title; 15 | } 16 | if let Some(description) = new_description { 17 | achievement.description = description; 18 | } 19 | if let Some(meta) = new_meta { 20 | achievement.nft_meta = meta; 21 | } 22 | 23 | achievement.check()?; 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/update_game.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | state::{FieldsCheck, Game, GameAttributes}, 3 | utils, UpdateGame, 4 | }; 5 | use anchor_lang::prelude::*; 6 | 7 | pub fn handler( 8 | ctx: Context, 9 | new_attributes: Option, 10 | new_auth: Option>, 11 | ) -> Result<()> { 12 | let game_account = &mut ctx.accounts.game; 13 | 14 | if let Some(attr) = new_attributes { 15 | attr.check()?; 16 | game_account.set_attributes(attr); 17 | } 18 | 19 | if let Some(new_auth) = new_auth { 20 | let initial_auth_len = game_account.auth.len(); 21 | let prev_size = Game::size(initial_auth_len); 22 | 23 | let new_size = prev_size 24 | .checked_sub(initial_auth_len * 32) 25 | .unwrap() 26 | .checked_add(new_auth.len() * 32) 27 | .unwrap(); 28 | 29 | utils::resize_account( 30 | &game_account.to_account_info(), 31 | &ctx.accounts.payer.to_account_info(), 32 | &ctx.accounts.system_program.to_account_info(), 33 | new_size, 34 | )?; 35 | 36 | game_account.auth = new_auth; 37 | }; 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/update_leaderboard.rs: -------------------------------------------------------------------------------- 1 | use crate::state::FieldsCheck; 2 | use crate::UpdateLeaderBoard; 3 | use anchor_lang::prelude::*; 4 | 5 | pub fn handler( 6 | ctx: Context, 7 | new_description: Option, 8 | new_nft_meta: Option, 9 | new_min_score: Option, 10 | new_max_score: Option, 11 | new_is_ascending: Option, 12 | new_allow_multiple_scores: Option, 13 | ) -> Result<()> { 14 | let leaderboard = &mut ctx.accounts.leaderboard; 15 | 16 | if let Some(description) = new_description { 17 | leaderboard.description = description; 18 | } 19 | if let Some(nft_meta) = new_nft_meta { 20 | leaderboard.nft_meta = nft_meta; 21 | } 22 | if let Some(max_score) = new_max_score { 23 | leaderboard.max_score = max_score; 24 | } 25 | if let Some(min_score) = new_min_score { 26 | leaderboard.min_score = min_score; 27 | } 28 | if let Some(is_ascending) = new_is_ascending { 29 | let top_entries = &mut ctx.accounts.top_entries; 30 | if let Some(top_entries) = top_entries { 31 | top_entries.is_ascending = is_ascending; 32 | } 33 | } 34 | if let Some(allow_multiple_scores) = new_allow_multiple_scores { 35 | leaderboard.allow_multiple_scores = allow_multiple_scores; 36 | } 37 | leaderboard.check()?; 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/update_player.rs: -------------------------------------------------------------------------------- 1 | use crate::state::FieldsCheck; 2 | use crate::UpdatePlayer; 3 | use anchor_lang::prelude::*; 4 | 5 | pub fn handler( 6 | ctx: Context, 7 | username: Option, 8 | nft_metadata: Option, 9 | ) -> Result<()> { 10 | let player_account = &mut ctx.accounts.player_account; 11 | 12 | if let Some(name) = username { 13 | player_account.username = name; 14 | } 15 | if let Some(meta) = nft_metadata { 16 | player_account.nft_meta = meta; 17 | } 18 | 19 | player_account.check()?; 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /programs/soar/src/instructions/verify_reward.rs: -------------------------------------------------------------------------------- 1 | use crate::{seeds, utils, VerifyNftReward}; 2 | use anchor_lang::prelude::*; 3 | 4 | pub fn handler(ctx: Context) -> Result<()> { 5 | let achievement_account = &ctx.accounts.achievement; 6 | let metadata_account = &ctx.accounts.metadata_to_verify; 7 | let mint = &ctx.accounts.mint; 8 | 9 | let decoded = utils::decode_mpl_metadata_account(metadata_account)?; 10 | require_keys_eq!(decoded.mint, mint.key()); 11 | 12 | let game_key = &ctx.accounts.game.key(); 13 | let achievement_bump = ctx.bumps.achievement; 14 | let id = ctx.accounts.achievement.id; 15 | let achievement_seeds = &[ 16 | seeds::ACHIEVEMENT, 17 | game_key.as_ref(), 18 | &id.to_le_bytes(), 19 | &[achievement_bump], 20 | ]; 21 | 22 | utils::verify_nft( 23 | &ctx.accounts.metadata_to_verify, 24 | &ctx.accounts.payer, 25 | &ctx.accounts.collection_mint, 26 | &ctx.accounts.collection_metadata, 27 | &ctx.accounts.collection_edition, 28 | &achievement_account.to_account_info(), 29 | &ctx.accounts.token_metadata_program, 30 | Some(&[&achievement_seeds[..]]), 31 | )?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /programs/soar/src/seeds.rs: -------------------------------------------------------------------------------- 1 | pub const LEADER: &[u8] = b"leaderboard"; 2 | pub const ACHIEVEMENT: &[u8] = b"achievement"; 3 | pub const PLAYER: &[u8] = b"player"; 4 | pub const PLAYER_SCORES: &[u8] = b"player-scores-list"; 5 | pub const PLAYER_ACHIEVEMENT: &[u8] = b"player-achievement"; 6 | pub const LEADER_TOP_ENTRIES: &[u8] = b"top-scores"; 7 | pub const NFT_CLAIM: &[u8] = b"nft-claim"; 8 | -------------------------------------------------------------------------------- /programs/soar/src/state/achievement.rs: -------------------------------------------------------------------------------- 1 | use super::{MAX_DESCRIPTION_LEN, MAX_TITLE_LEN}; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | #[derive(Debug)] 6 | /// Represents an achievement(with optional rewards) for this game 7 | /// that can be attained by players. 8 | /// 9 | /// PDA with seeds = `[b"achievement", game.key().as_ref(), &id.to_le_bytes()]` 10 | /// 11 | /// `id` is an incrementing index stored in the game account. 12 | pub struct Achievement { 13 | /// Public key of the game account this achievement is derived from. 14 | pub game: Pubkey, 15 | 16 | /// The achievement_count of the game account when this account was 17 | /// created, also used as a seed for its PDA. 18 | pub id: u64, 19 | 20 | /// Achievement title. 21 | pub title: String, 22 | 23 | /// Achievement description. 24 | pub description: String, 25 | 26 | /// Public key of a nft metadata account describing this achievement. 27 | pub nft_meta: Pubkey, 28 | 29 | /// Optional: Specify a reward to players for unlocking this achievement. 30 | pub reward: Option, 31 | } 32 | 33 | impl Achievement { 34 | /// Size of a borsh-serialized achievement account. 35 | pub const SIZE: usize = 8 + // discriminator 36 | 32 + // game 37 | 8 + // id 38 | 4 + MAX_TITLE_LEN + // title 39 | 4 + MAX_DESCRIPTION_LEN + // description 40 | 32 + // nft_meta 41 | 1 + 32; // reward 42 | 43 | /// Create a new [Achievement] instance. 44 | pub fn new( 45 | game: Pubkey, 46 | title: String, 47 | description: String, 48 | nft_meta: Pubkey, 49 | id: u64, 50 | ) -> Self { 51 | Achievement { 52 | game, 53 | id, 54 | title, 55 | description, 56 | nft_meta, 57 | reward: None, 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /programs/soar/src/state/check_fields.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::SoarError; 3 | use anchor_lang::prelude::*; 4 | 5 | #[constant] 6 | pub const MAX_TITLE_LEN: usize = 30; 7 | #[constant] 8 | pub const MAX_DESCRIPTION_LEN: usize = 200; 9 | 10 | /// Check that field lengths don't exceed the maximum space. 11 | /// 12 | /// Returns an error if any field has an invalid length. 13 | pub trait FieldsCheck { 14 | fn check(&self) -> Result<()>; 15 | } 16 | 17 | impl FieldsCheck for Achievement { 18 | fn check(&self) -> Result<()> { 19 | if self.title.len() > MAX_TITLE_LEN || self.description.len() > MAX_DESCRIPTION_LEN { 20 | return Err(SoarError::InvalidFieldLength.into()); 21 | } 22 | 23 | Ok(()) 24 | } 25 | } 26 | 27 | impl FieldsCheck for GameAttributes { 28 | fn check(&self) -> Result<()> { 29 | if self.title.len() > MAX_TITLE_LEN || self.description.len() > MAX_DESCRIPTION_LEN { 30 | return Err(SoarError::InvalidFieldLength.into()); 31 | } 32 | 33 | Ok(()) 34 | } 35 | } 36 | 37 | impl FieldsCheck for Game { 38 | fn check(&self) -> Result<()> { 39 | self.meta.check() 40 | } 41 | } 42 | 43 | impl FieldsCheck for RegisterLeaderBoardInput { 44 | fn check(&self) -> Result<()> { 45 | if self.description.len() > MAX_DESCRIPTION_LEN { 46 | return Err(SoarError::InvalidFieldLength.into()); 47 | } 48 | 49 | Ok(()) 50 | } 51 | } 52 | 53 | impl FieldsCheck for LeaderBoard { 54 | fn check(&self) -> Result<()> { 55 | if self.description.len() > MAX_DESCRIPTION_LEN { 56 | return Err(SoarError::InvalidFieldLength.into()); 57 | } 58 | 59 | Ok(()) 60 | } 61 | } 62 | 63 | impl FieldsCheck for Player { 64 | fn check(&self) -> Result<()> { 65 | if self.username.len() > Self::MAX_USERNAME_LEN { 66 | return Err(SoarError::InvalidFieldLength.into()); 67 | } 68 | 69 | Ok(()) 70 | } 71 | } 72 | 73 | impl FieldsCheck for Reward { 74 | fn check(&self) -> Result<()> { 75 | match &self.reward { 76 | RewardKind::NonFungibleToken { 77 | uri, 78 | name, 79 | symbol, 80 | minted: _, 81 | collection: _, 82 | } => { 83 | if uri.len() > Reward::MAX_URI_LENGTH 84 | || name.len() > Reward::MAX_NAME_LENGTH 85 | || symbol.len() > Reward::MAX_SYMBOL_LENGTH 86 | { 87 | Err(SoarError::InvalidFieldLength.into()) 88 | } else { 89 | Ok(()) 90 | } 91 | } 92 | _ => Ok(()), 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /programs/soar/src/state/game.rs: -------------------------------------------------------------------------------- 1 | use super::{MAX_DESCRIPTION_LEN, MAX_TITLE_LEN}; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | #[derive(Debug, Default)] 6 | /// An account representing a single game. 7 | pub struct Game { 8 | /// Game meta-information. 9 | pub meta: GameAttributes, 10 | 11 | /// Number of leaderboards this game has created. Used both 12 | /// in determining the most recent leaderboard address, and 13 | /// as a seed for the next leaderboard. 14 | pub leaderboard_count: u64, 15 | 16 | /// Number of achievements that exist for this game. Also 17 | /// used to determine the u64 seed for the next achievement. 18 | pub achievement_count: u64, 19 | 20 | /// A collection of pubkeys which each represent a valid 21 | /// authority for this game. 22 | pub auth: Vec, 23 | } 24 | 25 | impl Game { 26 | /// Base size of a game account, not counting the auth vec. 27 | pub const SIZE_NO_AUTHS: usize = 8 + // discriminator 28 | GameAttributes::SIZE + // GameMeta fixed size 29 | 8 + // leaderboard_count 30 | 8; // achievement_count 31 | 32 | /// The size of a game account, considering a auth vec of 33 | /// size `auths_len`. 34 | pub fn size(auth_len: usize) -> usize { 35 | Self::SIZE_NO_AUTHS + // Base size 36 | 4 + (auth_len * 32) // Auths vector. 37 | } 38 | 39 | /// Check that a given pubkey is one of the Game's authorities. 40 | pub fn check_signer(&self, key: &Pubkey) -> bool { 41 | self.auth.contains(key) 42 | } 43 | 44 | /// Set a game's attributes. 45 | pub fn set_attributes(&mut self, new_meta: GameAttributes) { 46 | self.meta = new_meta; 47 | } 48 | 49 | /// Get next achievement id. 50 | pub fn next_achievement(&self) -> u64 { 51 | self.achievement_count.checked_add(1).unwrap() 52 | } 53 | 54 | /// Get next leaderboard id. 55 | pub fn next_leaderboard(&self) -> u64 { 56 | self.leaderboard_count.checked_add(1).unwrap() 57 | } 58 | } 59 | 60 | /// A type that represents game-specific information. 61 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default)] 62 | pub struct GameAttributes { 63 | /// The title of the game, max length = 30 bytes. 64 | pub title: String, 65 | 66 | /// The game description, max length = 200 bytes. 67 | pub description: String, 68 | 69 | /// The game's [genre](super::Genre), as a u8. 70 | pub genre: u8, 71 | 72 | /// The game's [type](super::GameType), as a u8. 73 | pub game_type: u8, 74 | 75 | /// An nft metadata account describing the game. 76 | pub nft_meta: Pubkey, 77 | } 78 | 79 | impl GameAttributes { 80 | /// The constant borsh-serialized size of a [GameAttributes] account. 81 | pub const SIZE: usize = 4 + MAX_TITLE_LEN + // title 82 | 4 + MAX_DESCRIPTION_LEN + // description 83 | 1 + // genre as u8 84 | 1 + // game_type as u8 85 | 32; // nft_meta 86 | 87 | /// Create a new [GameAttributes] instance. 88 | pub fn new( 89 | title: String, 90 | description: String, 91 | genre: u8, 92 | game_type: u8, 93 | nft_meta: Pubkey, 94 | ) -> Self { 95 | GameAttributes { 96 | title, 97 | description, 98 | genre, 99 | game_type, 100 | nft_meta, 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /programs/soar/src/state/game_types.rs: -------------------------------------------------------------------------------- 1 | pub enum GameType { 2 | Mobile, 3 | Desktop, 4 | Web, 5 | Unspecified, 6 | } 7 | 8 | impl From for GameType { 9 | fn from(val: u8) -> Self { 10 | match val { 11 | 0 => Self::Mobile, 12 | 1 => Self::Desktop, 13 | 2 => Self::Web, 14 | _ => Self::Unspecified, 15 | } 16 | } 17 | } 18 | 19 | #[allow(non_snake_case)] 20 | pub enum Genre { 21 | Rpg, 22 | Mmo, 23 | Action, 24 | Adventure, 25 | Puzzle, 26 | Casual, 27 | Unspecified, 28 | } 29 | 30 | impl From for Genre { 31 | fn from(val: u8) -> Self { 32 | match val { 33 | 0 => Self::Rpg, 34 | 1 => Self::Mmo, 35 | 2 => Self::Action, 36 | 3 => Self::Adventure, 37 | 4 => Self::Puzzle, 38 | 5 => Self::Casual, 39 | _ => Self::Unspecified, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /programs/soar/src/state/leaderboard.rs: -------------------------------------------------------------------------------- 1 | use super::{RegisterLeaderBoardInput, MAX_DESCRIPTION_LEN}; 2 | use anchor_lang::prelude::*; 3 | 4 | /// A single score entry for a player. 5 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, Debug, Default)] 6 | pub struct ScoreEntry { 7 | /// The player's score. 8 | pub score: u64, 9 | 10 | /// When this entry was made. 11 | pub timestamp: i64, 12 | } 13 | 14 | #[account] 15 | #[derive(Debug, Default)] 16 | /// Represents a [Game][super::Game]'s leaderboard. 17 | /// 18 | /// Seeds: `[b"leaderboard", game.key().as_ref(), &id.to_le_bytes()]` 19 | pub struct LeaderBoard { 20 | /// The leaderboard's id, used in deriving its address from the game. 21 | pub id: u64, 22 | 23 | /// The game this leaderboard belongs to and is derived from. 24 | pub game: Pubkey, 25 | 26 | /// Leaderboard description. 27 | pub description: String, 28 | 29 | /// Pubkey of an nft metadata account that describes this leaderboard. 30 | pub nft_meta: Pubkey, 31 | 32 | /// Used to contextualize scores for this leaderboard. 33 | pub decimals: u8, 34 | 35 | /// Minimum possible score for this leaderboard. 36 | pub min_score: u64, 37 | 38 | /// Maximum possible score for this leaderboard. 39 | pub max_score: u64, 40 | 41 | /// Top [entries](ScoreEntry) for a leaderboard. 42 | pub top_entries: Option, 43 | 44 | /// Whether or not multiple scores are allowed for a single player. 45 | pub allow_multiple_scores: bool, 46 | } 47 | 48 | impl LeaderBoard { 49 | /// Size of a borsh-serialized leaderboard account. 50 | pub const SIZE: usize = 8 + // discriminator 51 | 8 + // id 52 | 32 + // game 53 | 4 + MAX_DESCRIPTION_LEN + // description 54 | 32 + // nft_meta 55 | 1 + // decimals 56 | 8 + // min_score 57 | 8 + // max_score 58 | 1 + // allow_multiple_scores 59 | 1 + 32; // top_entries 60 | 61 | /// Create a new [LeaderBoard] instance. 62 | pub fn new( 63 | id: u64, 64 | game: Pubkey, 65 | description: String, 66 | nft_meta: Pubkey, 67 | decimals: Option, 68 | min_score: Option, 69 | max_score: Option, 70 | ) -> Self { 71 | LeaderBoard { 72 | id, 73 | game, 74 | description, 75 | nft_meta, 76 | decimals: decimals.unwrap_or(0), 77 | min_score: min_score.unwrap_or(u64::MIN), 78 | max_score: max_score.unwrap_or(u64::MAX), 79 | allow_multiple_scores: false, 80 | top_entries: None, 81 | } 82 | } 83 | } 84 | 85 | impl From for LeaderBoard { 86 | fn from(input: RegisterLeaderBoardInput) -> Self { 87 | LeaderBoard { 88 | description: input.description, 89 | nft_meta: input.nft_meta, 90 | decimals: input.decimals.unwrap_or(0), 91 | min_score: input.min_score.unwrap_or(u64::MIN), 92 | max_score: input.max_score.unwrap_or(u64::MAX), 93 | allow_multiple_scores: input.allow_multiple_scores, 94 | ..Default::default() 95 | } 96 | } 97 | } 98 | 99 | impl ScoreEntry { 100 | /// Size of a [ScoreEntry]. 101 | pub const SIZE: usize = 8 + 8; 102 | 103 | /// Create a new instance of self. 104 | pub fn new(score: u64, timestamp: i64) -> Self { 105 | ScoreEntry { score, timestamp } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /programs/soar/src/state/merge.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | /// An account that represents a single user's ownership of 4 | /// multiple [Player][super::Player] accounts. 5 | #[account] 6 | #[derive(Debug)] 7 | pub struct Merged { 8 | /// The user that initialized this merge. 9 | pub initiator: Pubkey, 10 | 11 | /// Details of all the player accounts to be merged with the main_user's. 12 | pub approvals: Vec, 13 | 14 | /// Set to true when every user in `others` has registered their approval. 15 | pub merge_complete: bool, 16 | } 17 | 18 | /// Represents a [Player][super::Player] account involved in a merge 19 | /// and if that account's user/authority has granted approval. 20 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] 21 | pub struct MergeApproval { 22 | /// The player_account pubkey. 23 | pub key: Pubkey, 24 | 25 | /// User's approval status. 26 | pub approved: bool, 27 | } 28 | 29 | impl Merged { 30 | /// Calculate the size of a [Merged] account given a `count` 31 | /// of the [MergeApproval] accounts it intends to hold. 32 | pub fn size(count: usize) -> usize { 33 | 8 + // discriminator 34 | 32 + // initiator 35 | 4 + (count * MergeApproval::SIZE) // approvals vec 36 | + 1 // merge_complete 37 | } 38 | } 39 | 40 | impl MergeApproval { 41 | /// Size of a borsh-serialized instance of Self. 42 | pub const SIZE: usize = 32 + 1; 43 | 44 | /// Create a new instance of Self. 45 | pub fn new(key: Pubkey) -> Self { 46 | MergeApproval { 47 | key, 48 | approved: false, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /programs/soar/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | mod achievement; 2 | mod check_fields; 3 | mod game; 4 | mod game_types; 5 | mod leaderboard; 6 | mod merge; 7 | mod player; 8 | mod player_achievement; 9 | mod player_scores_list; 10 | mod reward; 11 | mod top_entries; 12 | 13 | pub use achievement::*; 14 | pub use check_fields::*; 15 | pub use game::*; 16 | pub use game_types::*; 17 | pub use leaderboard::*; 18 | pub use merge::*; 19 | pub use player::*; 20 | pub use player_achievement::*; 21 | pub use player_scores_list::*; 22 | pub use reward::*; 23 | pub use top_entries::*; 24 | 25 | use anchor_lang::prelude::*; 26 | 27 | /// Parameters needed when registering a leaderboard. 28 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default)] 29 | pub struct RegisterLeaderBoardInput { 30 | /// Leaderboard description. 31 | pub description: String, 32 | 33 | /// Nft metadata representing the leaderboard. 34 | pub nft_meta: Pubkey, 35 | 36 | /// Specify the decimals score values are represented in. Defaults to `0` if [None]. 37 | pub decimals: Option, 38 | 39 | /// Specifies minimum allowed score. Defaults to `u64::MIN` if [None]. 40 | pub min_score: Option, 41 | 42 | /// Specifies maximum allowed score. Defaults to `u64::MAX` if [None]. 43 | pub max_score: Option, 44 | 45 | /// Number of top scores to store on-chain. 46 | pub scores_to_retain: u8, 47 | 48 | /// Order by which scores are stored. `true` for ascending, `false` for descending. 49 | pub is_ascending: bool, 50 | 51 | /// Whether or not multiple scores are kept in the leaderboard for a single player. 52 | pub allow_multiple_scores: bool, 53 | } 54 | 55 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] 56 | /// Input to add a new reward for an achievement. 57 | pub struct AddNewRewardInput { 58 | /// Number of rewards to be given out. 59 | pub available_spots: u64, 60 | /// Specific reward kind. 61 | pub kind: RewardKindInput, 62 | } 63 | 64 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] 65 | /// Specific variant of [AddNewRewardInput]. 66 | pub enum RewardKindInput { 67 | Ft { 68 | /// Amount to be delegated to this program's PDA 69 | /// so it can spend for reward claims. 70 | deposit: u64, 71 | 72 | /// Amount given to a single user. 73 | amount: u64, 74 | }, 75 | Nft { 76 | /// Uri of the minted nft. 77 | uri: String, 78 | /// Name of the minted nft. 79 | name: String, 80 | /// Symbol of the minted nft. 81 | symbol: String, 82 | }, 83 | } 84 | -------------------------------------------------------------------------------- /programs/soar/src/state/player.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[account] 4 | #[derive(Debug, Default)] 5 | /// An account representing a player. 6 | /// 7 | /// Seeds: `[b"player", user.key().as_ref()]` 8 | pub struct Player { 9 | /// The wallet that owns this player-info account 10 | pub user: Pubkey, 11 | 12 | /// The player's username. 13 | pub username: String, 14 | 15 | /// Metadata to represent this player. 16 | pub nft_meta: Pubkey, 17 | } 18 | 19 | impl Player { 20 | pub const MAX_USERNAME_LEN: usize = 100; 21 | 22 | pub const SIZE: usize = 8 + // discriminator 23 | 32 + Self::MAX_USERNAME_LEN + 32; 24 | 25 | pub fn new(username: String, nft_meta: Pubkey, user: Pubkey) -> Self { 26 | Player { 27 | user, 28 | username, 29 | nft_meta, 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /programs/soar/src/state/player_achievement.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[account] 4 | #[derive(Debug, Default)] 5 | /// Represents a player's status for a particular [Achievement](super::Achievement). 6 | /// 7 | /// Seeds = `[b"player-achievement", player.key().as_ref(), achievement.key().as_ref()]`. 8 | pub struct PlayerAchievement { 9 | /// The user's [player][super::Player] account. 10 | pub player_account: Pubkey, 11 | 12 | /// The key of the achievement unlocked for this player. 13 | pub achievement: Pubkey, 14 | 15 | /// Timestamp showing when this achievement was unlocked. 16 | pub timestamp: i64, 17 | 18 | /// A player's unlock status for this achievement. 19 | pub unlocked: bool, 20 | 21 | /// Whether or not this player has claimed their reward. 22 | pub claimed: bool, 23 | } 24 | 25 | impl PlayerAchievement { 26 | /// Size of a serialized playerAchievement account. 27 | pub const SIZE: usize = 8 + // discriminator 28 | 32 + // player 29 | 32 + // achievement 30 | 8 + // timestamp 31 | 1 + // unlocked 32 | 1; // claimed 33 | 34 | pub fn new(player_account: Pubkey, achievement: Pubkey, timestamp: i64) -> Self { 35 | Self { 36 | player_account, 37 | achievement, 38 | timestamp, 39 | unlocked: true, 40 | claimed: false, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /programs/soar/src/state/player_scores_list.rs: -------------------------------------------------------------------------------- 1 | use super::ScoreEntry; 2 | use anchor_lang::prelude::*; 3 | 4 | #[account] 5 | #[derive(Debug, Default)] 6 | /// Holds a list of a [player][super::Player]'s [scores][ScoreEntry]) 7 | /// for a particular [LeaderBoard]. 8 | /// 9 | /// Seeds: `[b"player-scores-list", player_account.key().as_ref(), leaderboard.key().as_ref()]` 10 | pub struct PlayerScoresList { 11 | /// The player[super::Player] account this entry is derived from 12 | pub player_account: Pubkey, 13 | 14 | /// The id of the specific leaderboard. 15 | pub leaderboard: Pubkey, 16 | 17 | /// Max number of [scores][ScoreEntry] the current space allocation supports. 18 | pub alloc_count: u16, 19 | 20 | /// Collection of [scores][ScoreEntry]. 21 | pub scores: Vec, 22 | } 23 | 24 | impl PlayerScoresList { 25 | /// Base size of this account without counting the scores list. 26 | pub const SIZE_WITHOUT_VEC: usize = 8 + // discriminator 27 | 32 + // player_account 28 | 32 + // leaderboard 29 | 2; // alloc_count 30 | 31 | /// Initial number of scores[ScoreEntry] space is allocated for. 32 | pub const INITIAL_SCORES_LENGTH: usize = 10; 33 | 34 | /// Increase space to accommodate this number more during a resize. 35 | pub const REALLOC_WINDOW: usize = 10; 36 | 37 | /// Calculate the space required during account initialization 38 | pub fn initial_size() -> usize { 39 | Self::SIZE_WITHOUT_VEC + // base size. 40 | 4 + (Self::INITIAL_SCORES_LENGTH * ScoreEntry::SIZE) // size of scores vec. 41 | } 42 | 43 | /// Gets the current size of a [PlayerScoresList] account. 44 | pub fn current_size(&self) -> usize { 45 | Self::SIZE_WITHOUT_VEC + // base size. 46 | 4 + (self.alloc_count as usize * ScoreEntry::SIZE) // size of scores vec. 47 | } 48 | 49 | /// Create a new instance of Self. 50 | pub fn new(player_account: Pubkey, leaderboard: Pubkey) -> Self { 51 | PlayerScoresList { 52 | player_account, 53 | leaderboard, 54 | alloc_count: Self::INITIAL_SCORES_LENGTH as u16, 55 | scores: Vec::with_capacity(Self::INITIAL_SCORES_LENGTH), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /programs/soar/src/state/reward.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | // Existence serves as proof of a valid claimed nft and 4 | // is checked during collection verification. 5 | #[account] 6 | pub struct NftClaim {} 7 | impl NftClaim { 8 | pub const SIZE: usize = 8; 9 | } 10 | 11 | #[account] 12 | #[derive(Debug)] 13 | /// An account representing a reward for a given achievement. 14 | pub struct Reward { 15 | /// The achievement this reward is given for. 16 | pub achievement: Pubkey, 17 | 18 | /// Number of available reward spots. 19 | pub available_spots: u64, 20 | 21 | /// The reward kind. Current supports Nft and Ft rewards only. 22 | pub reward: RewardKind, 23 | } 24 | 25 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] 26 | /// The kind of reward to be given out. 27 | pub enum RewardKind { 28 | /// Fungible token rewards. 29 | FungibleToken { 30 | /// The mint of the token to be given out. 31 | mint: Pubkey, 32 | 33 | /// The token account to withdraw from. 34 | account: Pubkey, 35 | 36 | /// Reward amount per user. 37 | amount: u64, 38 | }, 39 | /// NFT rewards. 40 | NonFungibleToken { 41 | /// URI of the NFT to be minted. 42 | uri: String, 43 | 44 | /// Name of the NFT to be minted. 45 | name: String, 46 | 47 | /// Symbol of the NFT to be minted. 48 | symbol: String, 49 | 50 | /// Total NFTs minted so far. 51 | minted: u64, 52 | 53 | /// Optional field for a collection mint used for 54 | /// verifying minted rewards. 55 | collection: Option, 56 | }, 57 | } 58 | 59 | impl Reward { 60 | pub const MAX_URI_LENGTH: usize = 200; 61 | pub const MAX_NAME_LENGTH: usize = 32; 62 | pub const MAX_SYMBOL_LENGTH: usize = 10; 63 | 64 | /// Size of a borsh-serialized [Reward]. 65 | pub const SIZE: usize = 8 + // discriminator 66 | 32 + // achievement 67 | 8 + // available 68 | RewardKind::MAX_SIZE; // reward_kind 69 | } 70 | 71 | impl RewardKind { 72 | /// Size of an [nft][RewardKind::NonFungibleToken] reward type. 73 | const MAX_SIZE: usize = (200 + 4) + // uri 74 | (32 + 4) + // name 75 | (10 + 4) + // symbol 76 | 8 + // minted 77 | 1 + 32; // collection 78 | } 79 | -------------------------------------------------------------------------------- /programs/soar/src/state/top_entries.rs: -------------------------------------------------------------------------------- 1 | use super::ScoreEntry; 2 | use anchor_lang::prelude::*; 3 | 4 | /// Keeps track of a sorted list of top scores for a leaderboard. 5 | /// 6 | /// Seeds = [b"top-scores", leaderboard.key().as_ref()] 7 | #[account] 8 | #[derive(Debug)] 9 | pub struct LeaderTopEntries { 10 | /// Arrangement order. 11 | pub is_ascending: bool, 12 | 13 | /// Top scores. 14 | pub top_scores: Vec, 15 | } 16 | 17 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Debug)] 18 | /// An single entry to a [LeaderTopEntries]. 19 | pub struct LeaderBoardScore { 20 | /// The player 21 | pub player: Pubkey, 22 | 23 | /// The user's [score][super::ScoreEntry]. 24 | pub entry: ScoreEntry, 25 | } 26 | 27 | impl LeaderTopEntries { 28 | /// Calculate the size for a given `top_scores` vector length. 29 | pub fn size(scores_to_retain: usize) -> usize { 30 | 8 + // discriminator 31 | 1 + // is_ascending 32 | 4 + (scores_to_retain * LeaderBoardScore::SIZE) // top_scores vec 33 | } 34 | } 35 | 36 | impl LeaderBoardScore { 37 | /// Size of a borsh-serialized [LeaderBoardScore] account. 38 | pub const SIZE: usize = 32 + // player key 39 | ScoreEntry::SIZE; // entry 40 | 41 | /// Create a new instance of Self. 42 | pub fn new(player: Pubkey, entry: ScoreEntry) -> Self { 43 | LeaderBoardScore { player, entry } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /programs/soar/src/utils.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_lang::solana_program::{ 3 | program::{invoke, invoke_signed}, 4 | system_instruction, 5 | sysvar::rent::Rent, 6 | }; 7 | use anchor_spl::token; 8 | use mpl_token_metadata::instruction::{create_master_edition_v3, create_metadata_accounts_v3}; 9 | use mpl_token_metadata::state::{DataV2, Metadata, TokenMetadataAccount}; 10 | 11 | // https://solanacookbook.com/references/programs.html#how-to-change-account-size 12 | pub fn resize_account<'a>( 13 | target_account: &AccountInfo<'a>, 14 | funding_account: &AccountInfo<'a>, 15 | system_program: &AccountInfo<'a>, 16 | new_size: usize, 17 | ) -> Result<()> { 18 | let rent = Rent::get()?; 19 | let new_minimum_balance = rent.minimum_balance(new_size); 20 | 21 | let lamports_diff = new_minimum_balance.saturating_sub(target_account.lamports()); 22 | invoke( 23 | &system_instruction::transfer(funding_account.key, target_account.key, lamports_diff), 24 | &[ 25 | funding_account.clone(), 26 | target_account.clone(), 27 | system_program.clone(), 28 | ], 29 | )?; 30 | 31 | target_account.realloc(new_size, false)?; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub fn decode_mpl_metadata_account(account: &AccountInfo<'_>) -> Result { 37 | if account.owner != &mpl_token_metadata::ID { 38 | return Err(ProgramError::IllegalOwner.into()); 39 | } 40 | Ok(TokenMetadataAccount::from_account_info(&account.to_account_info()).unwrap()) 41 | } 42 | 43 | pub fn create_mint<'a>( 44 | payer: &AccountInfo<'a>, 45 | mint: &AccountInfo<'a>, 46 | mint_authority: &AccountInfo<'a>, 47 | system_program: &AccountInfo<'a>, 48 | token_program: &AccountInfo<'a>, 49 | rent_sysvar: &AccountInfo<'a>, 50 | ) -> Result<()> { 51 | let rent = Rent::get()?; 52 | let lamports = rent.minimum_balance(token::Mint::LEN); 53 | invoke( 54 | &system_instruction::create_account( 55 | payer.key, 56 | mint.key, 57 | lamports, 58 | token::Mint::LEN as u64, 59 | token_program.key, 60 | ), 61 | &[ 62 | payer.clone(), 63 | mint.clone(), 64 | system_program.to_account_info().clone(), 65 | ], 66 | )?; 67 | 68 | let accounts = token::InitializeMint { 69 | mint: mint.clone(), 70 | rent: rent_sysvar.clone(), 71 | }; 72 | let cpi_ctx = CpiContext::new(token_program.to_account_info(), accounts); 73 | token::initialize_mint(cpi_ctx, 0, mint_authority.key, Some(mint_authority.key)) 74 | } 75 | 76 | pub fn mint_token<'a>( 77 | mint: &AccountInfo<'a>, 78 | token_account: &AccountInfo<'a>, 79 | mint_authority: &AccountInfo<'a>, 80 | token_program: &AccountInfo<'a>, 81 | signer_seeds: Option<&[&[&[u8]]]>, 82 | ) -> Result<()> { 83 | let mint_to = token::MintTo { 84 | mint: mint.to_account_info(), 85 | to: token_account.to_account_info(), 86 | authority: mint_authority.to_account_info(), 87 | }; 88 | 89 | let cpi_ctx = if let Some(signature) = signer_seeds { 90 | CpiContext::new_with_signer(token_program.to_account_info(), mint_to, signature) 91 | } else { 92 | CpiContext::new(token_program.to_account_info(), mint_to) 93 | }; 94 | 95 | token::mint_to(cpi_ctx, 1)?; 96 | Ok(()) 97 | } 98 | 99 | pub fn create_token_account<'a>( 100 | payer: &AccountInfo<'a>, 101 | token_account: &AccountInfo<'a>, 102 | token_account_owner: &AccountInfo<'a>, 103 | mint: &AccountInfo<'a>, 104 | system_program: &AccountInfo<'a>, 105 | token_program: &AccountInfo<'a>, 106 | associated_token_program: &AccountInfo<'a>, 107 | ) -> Result<()> { 108 | anchor_spl::associated_token::create(CpiContext::new( 109 | associated_token_program.to_account_info(), 110 | anchor_spl::associated_token::Create { 111 | payer: payer.to_account_info(), 112 | associated_token: token_account.to_account_info(), 113 | authority: token_account_owner.to_account_info(), 114 | mint: mint.to_account_info(), 115 | system_program: system_program.to_account_info(), 116 | token_program: token_program.to_account_info(), 117 | }, 118 | ))?; 119 | 120 | Ok(()) 121 | } 122 | 123 | #[allow(clippy::too_many_arguments)] 124 | pub fn create_metadata_account<'a>( 125 | name: &str, 126 | symbol: &str, 127 | uri: &str, 128 | metadata_account: &AccountInfo<'a>, 129 | mint: &AccountInfo<'a>, 130 | mint_authority: &AccountInfo<'a>, 131 | payer: &AccountInfo<'a>, 132 | update_authority: &AccountInfo<'a>, 133 | creator: &Option, 134 | collection_mint: &Option, 135 | token_metadata_program: &AccountInfo<'a>, 136 | system_program: &AccountInfo<'a>, 137 | rent: &AccountInfo<'a>, 138 | signer: Option<&[&[&[u8]]]>, 139 | ) -> Result<()> { 140 | let creators = creator.map(|key| { 141 | vec![mpl_token_metadata::state::Creator { 142 | address: key, 143 | verified: true, 144 | share: 100, 145 | }] 146 | }); 147 | let collection = collection_mint.map(|key| mpl_token_metadata::state::Collection { 148 | verified: false, 149 | key, 150 | }); 151 | 152 | let instruction = create_metadata_accounts_v3( 153 | token_metadata_program.key(), 154 | metadata_account.key(), 155 | mint.key(), 156 | mint_authority.key(), 157 | payer.key(), 158 | update_authority.key(), 159 | name.into(), 160 | symbol.into(), 161 | uri.into(), 162 | creators, 163 | 1, 164 | true, 165 | true, 166 | collection, 167 | None, 168 | None, 169 | ); 170 | let accounts = [ 171 | metadata_account.clone(), 172 | mint.clone(), 173 | mint_authority.clone(), 174 | payer.clone(), 175 | update_authority.clone(), 176 | system_program.to_account_info().clone(), 177 | rent.clone(), 178 | ]; 179 | 180 | if let Some(signature) = signer { 181 | invoke_signed(&instruction, &accounts, signature)?; 182 | } else { 183 | invoke(&instruction, &accounts)?; 184 | } 185 | 186 | Ok(()) 187 | } 188 | 189 | #[allow(clippy::too_many_arguments)] 190 | pub fn create_master_edition_account<'a>( 191 | master_edition: &AccountInfo<'a>, 192 | mint: &AccountInfo<'a>, 193 | payer: &AccountInfo<'a>, 194 | metadata: &AccountInfo<'a>, 195 | mint_authority: &AccountInfo<'a>, 196 | metadata_update_authority: &AccountInfo<'a>, 197 | token_metadata_program: &AccountInfo<'a>, 198 | system_program: &AccountInfo<'a>, 199 | rent: &AccountInfo<'a>, 200 | signer: Option<&[&[&[u8]]]>, 201 | ) -> Result<()> { 202 | let instruction = create_master_edition_v3( 203 | token_metadata_program.key(), 204 | master_edition.key(), 205 | mint.key(), 206 | metadata_update_authority.key(), 207 | mint_authority.key(), 208 | metadata.key(), 209 | payer.key(), 210 | Some(0), 211 | ); 212 | let accounts = [ 213 | metadata_update_authority.clone(), 214 | master_edition.clone(), 215 | mint.clone(), 216 | payer.clone(), 217 | metadata.clone(), 218 | system_program.to_account_info(), 219 | rent.clone(), 220 | ]; 221 | 222 | if let Some(signature) = signer { 223 | invoke_signed(&instruction, &accounts, signature)?; 224 | } else { 225 | invoke(&instruction, &accounts)?; 226 | } 227 | 228 | Ok(()) 229 | } 230 | 231 | #[allow(clippy::too_many_arguments)] 232 | pub fn update_metadata_account<'a>( 233 | new_name: Option, 234 | new_symbol: Option, 235 | new_uri: Option, 236 | new_collection: Option, 237 | new_update_authority: Option, 238 | metadata: &AccountInfo<'a>, 239 | update_authority: &AccountInfo<'a>, 240 | token_metadata_program: &AccountInfo<'a>, 241 | signer_seeds: Option<&[&[&[u8]]]>, 242 | ) -> Result<()> { 243 | let state: Metadata = TokenMetadataAccount::from_account_info(&metadata.to_account_info())?; 244 | let initial = state.data; 245 | 246 | let new_collection = new_collection.map(|key| mpl_token_metadata::state::Collection { 247 | verified: false, 248 | key, 249 | }); 250 | 251 | let collection = if new_collection.is_some() { 252 | new_collection 253 | } else { 254 | state.collection 255 | }; 256 | 257 | let new_data = DataV2 { 258 | name: new_name.unwrap_or(initial.name), 259 | symbol: new_symbol.unwrap_or(initial.symbol), 260 | uri: new_uri.unwrap_or(initial.uri), 261 | seller_fee_basis_points: initial.seller_fee_basis_points, 262 | creators: initial.creators, 263 | collection, 264 | uses: None, 265 | }; 266 | 267 | let instruction = mpl_token_metadata::instruction::update_metadata_accounts_v2( 268 | mpl_token_metadata::ID, 269 | *metadata.key, 270 | *update_authority.key, 271 | new_update_authority, 272 | Some(new_data), 273 | None, 274 | None, 275 | ); 276 | 277 | let accounts = [ 278 | token_metadata_program.clone(), 279 | metadata.clone(), 280 | update_authority.clone(), 281 | ]; 282 | 283 | if let Some(signature) = signer_seeds { 284 | invoke_signed(&instruction, &accounts, signature)?; 285 | } else { 286 | invoke(&instruction, &accounts)?; 287 | } 288 | 289 | Ok(()) 290 | } 291 | 292 | #[allow(clippy::too_many_arguments)] 293 | pub fn verify_nft<'a>( 294 | unverified_metadata: &AccountInfo<'a>, 295 | payer: &AccountInfo<'a>, 296 | collection_mint: &AccountInfo<'a>, 297 | collection_metadata: &AccountInfo<'a>, 298 | collection_master_edition: &AccountInfo<'a>, 299 | collection_update_authority: &AccountInfo<'a>, 300 | token_metadata_program: &AccountInfo<'a>, 301 | signer: Option<&[&[&[u8]]]>, 302 | ) -> Result<()> { 303 | let instruction = mpl_token_metadata::instruction::verify_sized_collection_item( 304 | mpl_token_metadata::ID, 305 | *unverified_metadata.key, 306 | *collection_update_authority.key, 307 | *payer.key, 308 | *collection_mint.key, 309 | *collection_metadata.key, 310 | *collection_master_edition.key, 311 | None, 312 | ); 313 | let accounts = [ 314 | token_metadata_program.clone(), 315 | unverified_metadata.clone(), 316 | collection_update_authority.clone(), 317 | payer.clone(), 318 | collection_mint.clone(), 319 | collection_metadata.clone(), 320 | collection_master_edition.clone(), 321 | ]; 322 | 323 | if let Some(signature) = signer { 324 | invoke_signed(&instruction, &accounts, signature)?; 325 | } else { 326 | invoke(&instruction, &accounts)?; 327 | } 328 | 329 | Ok(()) 330 | } 331 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |

SOAR

6 | 7 |

8 | 👑 Solana On-chain Achievements & Rankings 👑 9 |

10 | 11 |

12 | Discord Chat 13 | License 14 |

15 |
16 | 17 | SOAR is a program that provides a seamless solution for managing leaderboards, achievements, players' profiles and automatic rewards distribution on the Solana blockchain. Currently supporting invocation from a TypeScript client and with the [Solana.Unity-SDK](https://github.com/magicblock-labs/Solana.Unity-SDK). The program IDL is public and other clients can be easily auto-generated. 18 | 19 | ## ⚙ Features 20 | 21 | - Leaderboard Creation: Easily create leaderboards to track and display user rankings based on specific criteria or metrics. 22 | - Achievement Management: Define and manage achievements that can be unlocked by users based on their on-chain activities. 23 | - Typescript Client Support: Invoke SOAR functionalities using a TypeScript client, allowing for seamless integration with your Solana application. 24 | - CPI Invocation: Interact with SOAR through CPI (Cross-Program Invocation) calls, enabling secure and efficient communication between programs. 25 | - Solana.Unity-SDK Integration (Coming Soon): Integration with the Solana.Unity-SDK is currently underway, providing an additional layer of functionality for Unity-based applications. 26 | 27 | ## 📁 Repository Structure 28 | The SOAR repository is structured as follows: 29 | 30 |
31 | SOAR/
32 | ├── program/
33 | │   ├── soar_program.rs
34 | ├── client/
35 | │   ├── sdk
36 | │   ├── tests
37 | ├── examples/
38 | │   ├── tens
39 | ├── .....
40 | ├── README.md
41 | 
42 | 43 | program/: Contains the SOAR program source code for deployment on the Solana blockchain. 44 | client/: Includes the TypeScript sdk and the tests. 45 | examples/: Provides example scripts demonstrating how to use the SOAR program trough CPI. 46 | README.md: The document you are currently reading, providing an overview of the SOAR program repository. 47 | 48 | ## 🚀 Getting Started 49 | 50 | - For the Typescript SDK see the detailed [documentation](https://magicblock-labs.github.io/SOAR/). 51 | - For using SOAR from your solana program trough CPI, see the [tens](https://github.com/magicblock-labs/SOAR/tree/main/examples/tens) example. 52 | - Solana.Unity-SDK integration (Coming Soon) 53 | 54 | ## 📚 Documentation 55 | For detailed usage instructions, API reference, and examples, refer to the SOAR Documentation. 56 | 57 | ## :floppy_disk: Program 58 | 59 | The program is available at [SoarNNzwQHMwcfdkdLc6kvbkoMSxcHy89gTHrjhJYkk](https://solscan.io/account/SoarNNzwQHMwcfdkdLc6kvbkoMSxcHy89gTHrjhJYkk#anchorProgramIDL) 60 | 61 | ## 🤝 Contributing 62 | Contributions to SOAR are welcome! To contribute, please follow the guidelines outlined in [CONTRIBUTING.md](https://github.com/magicblock-labs/Solana.Unity-SDK/blob/main/CONTRIBUTING.md). 63 | 64 | ## 📃 License 65 | SOAR is licensed under the MIT License. 66 | 67 | ## 📧 Contact 68 | For any inquiries or support requests, please email us at dev@magicblock.gg. 69 | 70 | We appreciate your interest in SOAR! Let's unlock achievements and rise through the ranks together on the Solana blockchain! 🚀✨ 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------