├── .gitattributes ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── dependabot-auto-merge.yml │ ├── main.yml │ ├── publish-js-client.yml │ └── publish-rust.yml ├── .gitignore ├── .prettierrc ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── clients ├── js │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierrc.json │ ├── README.md │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── generated │ │ │ ├── accounts │ │ │ │ ├── index.ts │ │ │ │ ├── mint.ts │ │ │ │ ├── multisig.ts │ │ │ │ └── token.ts │ │ │ ├── errors │ │ │ │ ├── associatedToken.ts │ │ │ │ ├── index.ts │ │ │ │ └── token.ts │ │ │ ├── index.ts │ │ │ ├── instructions │ │ │ │ ├── amountToUiAmount.ts │ │ │ │ ├── approve.ts │ │ │ │ ├── approveChecked.ts │ │ │ │ ├── burn.ts │ │ │ │ ├── burnChecked.ts │ │ │ │ ├── closeAccount.ts │ │ │ │ ├── createAssociatedToken.ts │ │ │ │ ├── createAssociatedTokenIdempotent.ts │ │ │ │ ├── freezeAccount.ts │ │ │ │ ├── getAccountDataSize.ts │ │ │ │ ├── index.ts │ │ │ │ ├── initializeAccount.ts │ │ │ │ ├── initializeAccount2.ts │ │ │ │ ├── initializeAccount3.ts │ │ │ │ ├── initializeImmutableOwner.ts │ │ │ │ ├── initializeMint.ts │ │ │ │ ├── initializeMint2.ts │ │ │ │ ├── initializeMultisig.ts │ │ │ │ ├── initializeMultisig2.ts │ │ │ │ ├── mintTo.ts │ │ │ │ ├── mintToChecked.ts │ │ │ │ ├── recoverNestedAssociatedToken.ts │ │ │ │ ├── revoke.ts │ │ │ │ ├── setAuthority.ts │ │ │ │ ├── syncNative.ts │ │ │ │ ├── thawAccount.ts │ │ │ │ ├── transfer.ts │ │ │ │ ├── transferChecked.ts │ │ │ │ └── uiAmountToAmount.ts │ │ │ ├── pdas │ │ │ │ ├── associatedToken.ts │ │ │ │ └── index.ts │ │ │ ├── programs │ │ │ │ ├── associatedToken.ts │ │ │ │ ├── index.ts │ │ │ │ └── token.ts │ │ │ ├── shared │ │ │ │ └── index.ts │ │ │ └── types │ │ │ │ ├── accountState.ts │ │ │ │ ├── authorityType.ts │ │ │ │ └── index.ts │ │ └── index.ts │ ├── test │ │ ├── _setup.ts │ │ ├── createAssociatedToken.test.ts │ │ ├── createAssociatedTokenIdempotent.test.ts │ │ ├── initializeAccount.test.ts │ │ ├── initializeMint.test.ts │ │ ├── mintTo.test.ts │ │ └── transfer.test.ts │ ├── tsconfig.declarations.json │ ├── tsconfig.json │ ├── tsup.config.ts │ └── typedoc.json └── rust │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── interface ├── Cargo.toml ├── README.md └── src │ ├── error.rs │ ├── instruction.rs │ ├── lib.rs │ ├── native_mint.rs │ └── state │ ├── account.rs │ ├── account_state.rs │ ├── mint.rs │ ├── mod.rs │ └── multisig.rs ├── p-token ├── Cargo.toml ├── README.md ├── src │ ├── entrypoint.rs │ ├── lib.rs │ └── processor │ │ ├── amount_to_ui_amount.rs │ │ ├── approve.rs │ │ ├── approve_checked.rs │ │ ├── batch.rs │ │ ├── burn.rs │ │ ├── burn_checked.rs │ │ ├── close_account.rs │ │ ├── freeze_account.rs │ │ ├── get_account_data_size.rs │ │ ├── initialize_account.rs │ │ ├── initialize_account2.rs │ │ ├── initialize_account3.rs │ │ ├── initialize_immutable_owner.rs │ │ ├── initialize_mint.rs │ │ ├── initialize_mint2.rs │ │ ├── initialize_multisig.rs │ │ ├── initialize_multisig2.rs │ │ ├── mint_to.rs │ │ ├── mint_to_checked.rs │ │ ├── mod.rs │ │ ├── revoke.rs │ │ ├── set_authority.rs │ │ ├── shared │ │ ├── approve.rs │ │ ├── burn.rs │ │ ├── initialize_account.rs │ │ ├── initialize_mint.rs │ │ ├── initialize_multisig.rs │ │ ├── mint_to.rs │ │ ├── mod.rs │ │ ├── toggle_account_state.rs │ │ └── transfer.rs │ │ ├── sync_native.rs │ │ ├── thaw_account.rs │ │ ├── transfer.rs │ │ ├── transfer_checked.rs │ │ ├── ui_amount_to_amount.rs │ │ └── withdraw_excess_lamports.rs └── tests │ ├── amount_to_ui_amount.rs │ ├── approve.rs │ ├── approve_checked.rs │ ├── batch.rs │ ├── burn.rs │ ├── burn_checked.rs │ ├── close_account.rs │ ├── freeze_account.rs │ ├── initialize_account.rs │ ├── initialize_account2.rs │ ├── initialize_account3.rs │ ├── initialize_mint.rs │ ├── initialize_mint2.rs │ ├── initialize_multisig.rs │ ├── initialize_multisig2.rs │ ├── mint_to.rs │ ├── mint_to_checked.rs │ ├── revoke.rs │ ├── set_authority.rs │ ├── setup │ ├── account.rs │ ├── mint.rs │ └── mod.rs │ ├── thaw_account.rs │ ├── transfer.rs │ ├── transfer_checked.rs │ ├── ui_amount_to_amount.rs │ └── withdraw_excess_lamports.rs ├── package.json ├── pnpm-lock.yaml ├── program ├── Cargo.toml ├── README.md ├── idl.json ├── src │ ├── entrypoint.rs │ ├── error.rs │ ├── instruction.rs │ ├── lib.rs │ ├── native_mint.rs │ ├── processor.rs │ └── state.rs └── tests │ ├── assert_instruction_count.rs │ ├── close_account.rs │ ├── processor.rs │ └── setup.rs ├── rust-toolchain.toml ├── rustfmt.toml └── scripts ├── check-solana-version.mjs ├── ci └── set-env.mjs ├── generate-clients.mjs ├── js ├── format.mjs ├── lint.mjs ├── publish.mjs └── test.mjs ├── link-solana-version.mjs ├── rust ├── audit.mjs ├── build-sbf.mjs ├── fixtures.mjs ├── format.mjs ├── lint.mjs ├── publish.mjs └── test.mjs ├── solana.dic ├── spellcheck.toml ├── start-validator.mjs ├── stop-validator.mjs ├── upgrade-template.mjs └── utils.mjs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup environment 2 | 3 | inputs: 4 | cargo-cache-key: 5 | description: The key to cache cargo dependencies. Skips cargo caching if not provided. 6 | required: false 7 | cargo-cache-fallback-key: 8 | description: The fallback key to use when caching cargo dependencies. Default to not using a fallback key. 9 | required: false 10 | cargo-cache-local-key: 11 | description: The key to cache local cargo dependencies. Skips local cargo caching if not provided. 12 | required: false 13 | clippy: 14 | description: Install Clippy if `true`. Defaults to `false`. 15 | required: false 16 | rustfmt: 17 | description: Install Rustfmt if `true`. Defaults to `false`. 18 | required: false 19 | solana: 20 | description: Install Solana if `true`. Defaults to `false`. 21 | required: false 22 | 23 | runs: 24 | using: 'composite' 25 | steps: 26 | - name: Setup pnpm 27 | uses: pnpm/action-setup@v3 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 20 33 | cache: 'pnpm' 34 | 35 | - name: Install Dependencies 36 | run: pnpm install --frozen-lockfile 37 | shell: bash 38 | 39 | - name: Set Environment Variables 40 | shell: bash 41 | run: pnpm zx ./scripts/ci/set-env.mjs 42 | 43 | - name: Install Rustfmt 44 | if: ${{ inputs.rustfmt == 'true' }} 45 | uses: dtolnay/rust-toolchain@master 46 | with: 47 | toolchain: ${{ env.TOOLCHAIN_FORMAT }} 48 | components: rustfmt 49 | 50 | - name: Install Clippy 51 | if: ${{ inputs.clippy == 'true' }} 52 | uses: dtolnay/rust-toolchain@master 53 | with: 54 | toolchain: ${{ env.TOOLCHAIN_LINT }} 55 | components: clippy 56 | 57 | - name: Install Solana 58 | if: ${{ inputs.solana == 'true' }} 59 | uses: solana-program/actions/install-solana@v1 60 | with: 61 | version: ${{ env.SOLANA_VERSION }} 62 | cache: true 63 | 64 | - name: Cache Cargo Dependencies 65 | if: ${{ inputs.cargo-cache-key && !inputs.cargo-cache-fallback-key }} 66 | uses: actions/cache@v4 67 | with: 68 | path: | 69 | ~/.cargo/bin/ 70 | ~/.cargo/registry/index/ 71 | ~/.cargo/registry/cache/ 72 | ~/.cargo/git/db/ 73 | target/ 74 | key: ${{ runner.os }}-${{ inputs.cargo-cache-key }}-${{ hashFiles('**/Cargo.lock') }} 75 | restore-keys: ${{ runner.os }}-${{ inputs.cargo-cache-key }} 76 | 77 | - name: Cache Cargo Dependencies With Fallback 78 | if: ${{ inputs.cargo-cache-key && inputs.cargo-cache-fallback-key }} 79 | uses: actions/cache@v4 80 | with: 81 | path: | 82 | ~/.cargo/bin/ 83 | ~/.cargo/registry/index/ 84 | ~/.cargo/registry/cache/ 85 | ~/.cargo/git/db/ 86 | target/ 87 | key: ${{ runner.os }}-${{ inputs.cargo-cache-key }}-${{ hashFiles('**/Cargo.lock') }} 88 | restore-keys: | 89 | ${{ runner.os }}-${{ inputs.cargo-cache-key }} 90 | ${{ runner.os }}-${{ inputs.cargo-cache-fallback-key }}-${{ hashFiles('**/Cargo.lock') }} 91 | ${{ runner.os }}-${{ inputs.cargo-cache-fallback-key }} 92 | 93 | - name: Cache Local Cargo Dependencies 94 | if: ${{ inputs.cargo-cache-local-key }} 95 | uses: actions/cache@v4 96 | with: 97 | path: | 98 | .cargo/bin/ 99 | .cargo/registry/index/ 100 | .cargo/registry/cache/ 101 | .cargo/git/db/ 102 | key: ${{ runner.os }}-${{ inputs.cargo-cache-local-key }}-${{ hashFiles('**/Cargo.lock') }} 103 | restore-keys: ${{ runner.os }}-${{ inputs.cargo-cache-local-key }} 104 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository_owner == 'solana-program' 12 | steps: 13 | - name: Enable auto-merge 14 | run: gh pr merge --auto --squash "$PR_URL" 15 | env: 16 | PR_URL: ${{ github.event.pull_request.html_url }} 17 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | - name: Approve 19 | run: gh pr review --approve "$PR_URL" 20 | env: 21 | PR_URL: ${{ github.event.pull_request.html_url }} 22 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/publish-js-client.yml: -------------------------------------------------------------------------------- 1 | name: Publish JS Client 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | level: 7 | description: Version level 8 | required: true 9 | default: patch 10 | type: choice 11 | options: 12 | - patch 13 | - minor 14 | - major 15 | - prerelease 16 | - prepatch 17 | - preminor 18 | - premajor 19 | tag: 20 | description: NPM Tag (and preid for pre-releases) 21 | required: true 22 | type: string 23 | default: latest 24 | create_release: 25 | description: Create a GitHub release 26 | required: true 27 | type: boolean 28 | default: true 29 | 30 | jobs: 31 | test_js: 32 | name: Test JS client 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Git Checkout 36 | uses: actions/checkout@v4 37 | 38 | - name: Setup Environment 39 | uses: ./.github/actions/setup 40 | with: 41 | cargo-cache-key: cargo-programs 42 | solana: true 43 | 44 | - name: Format JS Client 45 | run: pnpm clients:js:format 46 | 47 | - name: Lint JS Client 48 | run: pnpm clients:js:lint 49 | 50 | - name: Build Programs 51 | run: pnpm programs:build 52 | 53 | - name: Test JS Client 54 | run: pnpm clients:js:test 55 | 56 | publish_js: 57 | name: Publish JS client 58 | runs-on: ubuntu-latest 59 | needs: test_js 60 | permissions: 61 | contents: write 62 | steps: 63 | - name: Git Checkout 64 | uses: actions/checkout@v4 65 | with: 66 | token: ${{ secrets.ANZA_TEAM_PAT }} 67 | 68 | - name: Setup Environment 69 | uses: ./.github/actions/setup 70 | 71 | - name: Ensure NPM_TOKEN variable is set 72 | env: 73 | token: ${{ secrets.NPM_TOKEN }} 74 | if: ${{ env.token == '' }} 75 | run: | 76 | echo "The NPM_TOKEN secret variable is not set" 77 | echo "Go to \"Settings\" -> \"Secrets and variables\" -> \"Actions\" -> \"New repository secret\"." 78 | exit 1 79 | 80 | - name: NPM Authentication 81 | run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" 82 | env: 83 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 84 | 85 | - name: Set Git Author 86 | run: | 87 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 88 | git config --global user.name "github-actions[bot]" 89 | 90 | - name: Publish JS Client 91 | id: publish 92 | run: pnpm clients:js:publish ${{ inputs.level }} ${{ inputs.tag }} 93 | 94 | - name: Push Commit and Tag 95 | run: git push origin --follow-tags 96 | 97 | - name: Create GitHub release 98 | if: github.event.inputs.create_release == 'true' 99 | uses: ncipollo/release-action@v1 100 | with: 101 | tag: js@v${{ steps.publish.outputs.new_version }} 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .anchor 2 | .cargo 3 | .DS_Store 4 | **/.DS_Store 5 | **/target 6 | **/*.rs.bk 7 | node_modules 8 | test-ledger 9 | dist 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "arrowParens": "always", 8 | "printWidth": 80 9 | } 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["interface", "p-token", "program"] 4 | 5 | [workspace.package] 6 | authors = ["Anza Maintainers "] 7 | repository = "https://github.com/solana-program/token" 8 | license = "Apache-2.0" 9 | edition = "2021" 10 | 11 | [workspace.lints.rust.unexpected_cfgs] 12 | level = "warn" 13 | check-cfg = [ 14 | 'cfg(target_os, values("solana"))', 15 | 'cfg(feature, values("custom-alloc", "custom-panic", "frozen-abi", "no-entrypoint"))', 16 | ] 17 | 18 | [workspace.metadata.cli] 19 | solana = "2.2.0" 20 | 21 | # Specify Rust toolchains for rustfmt, clippy, and build. 22 | # Any unprovided toolchains default to stable. 23 | [workspace.metadata.toolchains] 24 | format = "nightly-2024-11-22" 25 | lint = "nightly-2024-11-22" 26 | 27 | [workspace.metadata.spellcheck] 28 | config = "scripts/spellcheck.toml" 29 | 30 | [workspace.metadata.release] 31 | pre-release-commit-message = "Publish {{crate_name}} v{{version}}" 32 | tag-message = "Publish {{crate_name}} v{{version}}" 33 | consolidate-commits = false 34 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting security problems 4 | 5 | **DO NOT CREATE A GITHUB ISSUE** to report a security problem. 6 | 7 | Instead please use this [Report a Vulnerability](https://github.com/solana-program/token/security/advisories/new) link. 8 | Provide a helpful title and detailed description of the problem. 9 | 10 | If you haven't done so already, please **enable two-factor auth** in your GitHub account. 11 | 12 | Expect a response as fast as possible in the advisory, typically within 72 hours. 13 | 14 | -- 15 | 16 | If you do not receive a response in the advisory, send an email to 17 | with the full URL of the advisory you have created. DO NOT 18 | include attachments or provide detail sufficient for exploitation regarding the 19 | security issue in this email. **Only provide such details in the advisory**. 20 | 21 | If you do not receive a response from please followup with 22 | the team directly. You can do this in one of the `#Dev Tooling` channels of the 23 | [Solana Tech discord server](https://solana.com/discord), by pinging the admins 24 | in the channel and referencing the fact that you submitted a security problem. 25 | 26 | ## Security Bug Bounties 27 | 28 | The Solana Foundation offer bounties for critical security issues. Please 29 | see the [Agave Security Bug 30 | Bounties](https://github.com/anza-xyz/agave/security/policy#security-bug-bounties) 31 | for details on classes of bugs and payment amounts. 32 | 33 | ## Scope 34 | 35 | Only the `spl-token` program is included in the bounty scope, at 36 | [program](https://github.com/solana-program/token/tree/master/program). 37 | 38 | If you discover a critical security issue in an out-of-scope component, your finding 39 | may still be valuable. 40 | -------------------------------------------------------------------------------- /clients/js/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@solana/eslint-config-solana'], 3 | ignorePatterns: ['.eslintrc.cjs', 'tsup.config.ts', 'env-shim.ts'], 4 | parserOptions: { 5 | project: 'tsconfig.json', 6 | tsconfigRootDir: __dirname, 7 | sourceType: 'module', 8 | }, 9 | rules: { 10 | '@typescript-eslint/ban-types': 'off', 11 | '@typescript-eslint/sort-type-constituents': 'off', 12 | 'prefer-destructuring': 'off', 13 | 'simple-import-sort/imports': 'off', 14 | 'sort-keys-fix/sort-keys-fix': 'off', 15 | 'typescript-sort-keys/interface': 'off', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /clients/js/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | docs 3 | -------------------------------------------------------------------------------- /clients/js/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "arrowParens": "always", 8 | "printWidth": 80 9 | } 10 | -------------------------------------------------------------------------------- /clients/js/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript client 2 | 3 | A generated JavaScript library for the Token program. 4 | 5 | ## Getting started 6 | 7 | To build and test your JavaScript client from the root of the repository, you may use the following command. 8 | 9 | ```sh 10 | pnpm clients:js:test 11 | ``` 12 | 13 | This will start a new local validator, if one is not already running, and run the tests for your JavaScript client. 14 | 15 | ## Available client scripts. 16 | 17 | Alternatively, you can go into the client directory and run the tests directly. 18 | 19 | ```sh 20 | # Build your programs and start the validator. 21 | pnpm programs:build 22 | pnpm validator:restart 23 | 24 | # Go into the client directory and run the tests. 25 | cd clients/js 26 | pnpm install 27 | pnpm build 28 | pnpm test 29 | ``` 30 | 31 | You may also use the following scripts to lint and/or format your JavaScript client. 32 | 33 | ```sh 34 | pnpm lint 35 | pnpm lint:fix 36 | pnpm format 37 | pnpm format:fix 38 | ``` 39 | -------------------------------------------------------------------------------- /clients/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@solana-program/token", 3 | "version": "0.5.1", 4 | "description": "JavaScript client for the Token program", 5 | "sideEffects": false, 6 | "module": "./dist/src/index.mjs", 7 | "main": "./dist/src/index.js", 8 | "types": "./dist/types/index.d.ts", 9 | "type": "commonjs", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/types/index.d.ts", 13 | "import": "./dist/src/index.mjs", 14 | "require": "./dist/src/index.js" 15 | } 16 | }, 17 | "files": [ 18 | "./dist/src", 19 | "./dist/types" 20 | ], 21 | "scripts": { 22 | "build": "rimraf dist && tsup && tsc -p ./tsconfig.declarations.json", 23 | "build:docs": "typedoc", 24 | "test": "ava", 25 | "lint": "eslint --ext js,ts,tsx src", 26 | "lint:fix": "eslint --fix --ext js,ts,tsx src", 27 | "format": "prettier --check src test", 28 | "format:fix": "prettier --write src test", 29 | "prepublishOnly": "pnpm build" 30 | }, 31 | "publishConfig": { 32 | "access": "public", 33 | "registry": "https://registry.npmjs.org" 34 | }, 35 | "license": "Apache-2.0", 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/solana-program/token.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/solana-program/token/issues" 42 | }, 43 | "homepage": "https://github.com/solana-program/token#readme", 44 | "peerDependencies": { 45 | "@solana/kit": "^2.1.0" 46 | }, 47 | "devDependencies": { 48 | "@ava/typescript": "^4.1.0", 49 | "@solana-program/system": "^0.6.1", 50 | "@solana/eslint-config-solana": "^3.0.3", 51 | "@solana/kit": "^2.1.0", 52 | "@types/node": "^20", 53 | "@typescript-eslint/eslint-plugin": "^7.16.1", 54 | "@typescript-eslint/parser": "^7.16.1", 55 | "ava": "^6.1.3", 56 | "eslint": "^8.57.0", 57 | "prettier": "^3.3.3", 58 | "rimraf": "^5.0.5", 59 | "tsup": "^8.1.2", 60 | "typedoc": "^0.25.12", 61 | "typescript": "^5.5.3" 62 | }, 63 | "ava": { 64 | "nodeArguments": [ 65 | "--no-warnings" 66 | ], 67 | "typescript": { 68 | "compile": false, 69 | "rewritePaths": { 70 | "test/": "dist/test/" 71 | } 72 | } 73 | }, 74 | "packageManager": "pnpm@9.1.0" 75 | } 76 | -------------------------------------------------------------------------------- /clients/js/src/generated/accounts/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | export * from './mint'; 10 | export * from './multisig'; 11 | export * from './token'; 12 | -------------------------------------------------------------------------------- /clients/js/src/generated/accounts/multisig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | import { 10 | assertAccountExists, 11 | assertAccountsExist, 12 | combineCodec, 13 | decodeAccount, 14 | fetchEncodedAccount, 15 | fetchEncodedAccounts, 16 | getAddressDecoder, 17 | getAddressEncoder, 18 | getArrayDecoder, 19 | getArrayEncoder, 20 | getBooleanDecoder, 21 | getBooleanEncoder, 22 | getStructDecoder, 23 | getStructEncoder, 24 | getU8Decoder, 25 | getU8Encoder, 26 | type Account, 27 | type Address, 28 | type Codec, 29 | type Decoder, 30 | type EncodedAccount, 31 | type Encoder, 32 | type FetchAccountConfig, 33 | type FetchAccountsConfig, 34 | type MaybeAccount, 35 | type MaybeEncodedAccount, 36 | } from '@solana/kit'; 37 | 38 | export type Multisig = { 39 | /** Number of signers required. */ 40 | m: number; 41 | /** Number of valid signers. */ 42 | n: number; 43 | /** Is `true` if this structure has been initialized. */ 44 | isInitialized: boolean; 45 | /** Signer public keys. */ 46 | signers: Array
; 47 | }; 48 | 49 | export type MultisigArgs = Multisig; 50 | 51 | export function getMultisigEncoder(): Encoder { 52 | return getStructEncoder([ 53 | ['m', getU8Encoder()], 54 | ['n', getU8Encoder()], 55 | ['isInitialized', getBooleanEncoder()], 56 | ['signers', getArrayEncoder(getAddressEncoder(), { size: 11 })], 57 | ]); 58 | } 59 | 60 | export function getMultisigDecoder(): Decoder { 61 | return getStructDecoder([ 62 | ['m', getU8Decoder()], 63 | ['n', getU8Decoder()], 64 | ['isInitialized', getBooleanDecoder()], 65 | ['signers', getArrayDecoder(getAddressDecoder(), { size: 11 })], 66 | ]); 67 | } 68 | 69 | export function getMultisigCodec(): Codec { 70 | return combineCodec(getMultisigEncoder(), getMultisigDecoder()); 71 | } 72 | 73 | export function decodeMultisig( 74 | encodedAccount: EncodedAccount 75 | ): Account; 76 | export function decodeMultisig( 77 | encodedAccount: MaybeEncodedAccount 78 | ): MaybeAccount; 79 | export function decodeMultisig( 80 | encodedAccount: EncodedAccount | MaybeEncodedAccount 81 | ): Account | MaybeAccount { 82 | return decodeAccount( 83 | encodedAccount as MaybeEncodedAccount, 84 | getMultisigDecoder() 85 | ); 86 | } 87 | 88 | export async function fetchMultisig( 89 | rpc: Parameters[0], 90 | address: Address, 91 | config?: FetchAccountConfig 92 | ): Promise> { 93 | const maybeAccount = await fetchMaybeMultisig(rpc, address, config); 94 | assertAccountExists(maybeAccount); 95 | return maybeAccount; 96 | } 97 | 98 | export async function fetchMaybeMultisig( 99 | rpc: Parameters[0], 100 | address: Address, 101 | config?: FetchAccountConfig 102 | ): Promise> { 103 | const maybeAccount = await fetchEncodedAccount(rpc, address, config); 104 | return decodeMultisig(maybeAccount); 105 | } 106 | 107 | export async function fetchAllMultisig( 108 | rpc: Parameters[0], 109 | addresses: Array
, 110 | config?: FetchAccountsConfig 111 | ): Promise[]> { 112 | const maybeAccounts = await fetchAllMaybeMultisig(rpc, addresses, config); 113 | assertAccountsExist(maybeAccounts); 114 | return maybeAccounts; 115 | } 116 | 117 | export async function fetchAllMaybeMultisig( 118 | rpc: Parameters[0], 119 | addresses: Array
, 120 | config?: FetchAccountsConfig 121 | ): Promise[]> { 122 | const maybeAccounts = await fetchEncodedAccounts(rpc, addresses, config); 123 | return maybeAccounts.map((maybeAccount) => decodeMultisig(maybeAccount)); 124 | } 125 | 126 | export function getMultisigSize(): number { 127 | return 355; 128 | } 129 | -------------------------------------------------------------------------------- /clients/js/src/generated/errors/associatedToken.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | import { 10 | isProgramError, 11 | type Address, 12 | type SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, 13 | type SolanaError, 14 | } from '@solana/kit'; 15 | import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS } from '../programs'; 16 | 17 | /** InvalidOwner: Associated token account owner does not match address derivation */ 18 | export const ASSOCIATED_TOKEN_ERROR__INVALID_OWNER = 0x0; // 0 19 | 20 | export type AssociatedTokenError = typeof ASSOCIATED_TOKEN_ERROR__INVALID_OWNER; 21 | 22 | let associatedTokenErrorMessages: 23 | | Record 24 | | undefined; 25 | if (process.env.NODE_ENV !== 'production') { 26 | associatedTokenErrorMessages = { 27 | [ASSOCIATED_TOKEN_ERROR__INVALID_OWNER]: `Associated token account owner does not match address derivation`, 28 | }; 29 | } 30 | 31 | export function getAssociatedTokenErrorMessage( 32 | code: AssociatedTokenError 33 | ): string { 34 | if (process.env.NODE_ENV !== 'production') { 35 | return ( 36 | associatedTokenErrorMessages as Record 37 | )[code]; 38 | } 39 | 40 | return 'Error message not available in production bundles.'; 41 | } 42 | 43 | export function isAssociatedTokenError< 44 | TProgramErrorCode extends AssociatedTokenError, 45 | >( 46 | error: unknown, 47 | transactionMessage: { 48 | instructions: Record; 49 | }, 50 | code?: TProgramErrorCode 51 | ): error is SolanaError & 52 | Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> { 53 | return isProgramError( 54 | error, 55 | transactionMessage, 56 | ASSOCIATED_TOKEN_PROGRAM_ADDRESS, 57 | code 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /clients/js/src/generated/errors/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | export * from './associatedToken'; 10 | export * from './token'; 11 | -------------------------------------------------------------------------------- /clients/js/src/generated/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | export * from './accounts'; 10 | export * from './errors'; 11 | export * from './instructions'; 12 | export * from './pdas'; 13 | export * from './programs'; 14 | export * from './types'; 15 | -------------------------------------------------------------------------------- /clients/js/src/generated/instructions/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | export * from './amountToUiAmount'; 10 | export * from './approve'; 11 | export * from './approveChecked'; 12 | export * from './burn'; 13 | export * from './burnChecked'; 14 | export * from './closeAccount'; 15 | export * from './createAssociatedToken'; 16 | export * from './createAssociatedTokenIdempotent'; 17 | export * from './freezeAccount'; 18 | export * from './getAccountDataSize'; 19 | export * from './initializeAccount'; 20 | export * from './initializeAccount2'; 21 | export * from './initializeAccount3'; 22 | export * from './initializeImmutableOwner'; 23 | export * from './initializeMint'; 24 | export * from './initializeMint2'; 25 | export * from './initializeMultisig'; 26 | export * from './initializeMultisig2'; 27 | export * from './mintTo'; 28 | export * from './mintToChecked'; 29 | export * from './recoverNestedAssociatedToken'; 30 | export * from './revoke'; 31 | export * from './setAuthority'; 32 | export * from './syncNative'; 33 | export * from './thawAccount'; 34 | export * from './transfer'; 35 | export * from './transferChecked'; 36 | export * from './uiAmountToAmount'; 37 | -------------------------------------------------------------------------------- /clients/js/src/generated/instructions/syncNative.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | import { 10 | combineCodec, 11 | getStructDecoder, 12 | getStructEncoder, 13 | getU8Decoder, 14 | getU8Encoder, 15 | transformEncoder, 16 | type Address, 17 | type Codec, 18 | type Decoder, 19 | type Encoder, 20 | type IAccountMeta, 21 | type IInstruction, 22 | type IInstructionWithAccounts, 23 | type IInstructionWithData, 24 | type WritableAccount, 25 | } from '@solana/kit'; 26 | import { TOKEN_PROGRAM_ADDRESS } from '../programs'; 27 | import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; 28 | 29 | export const SYNC_NATIVE_DISCRIMINATOR = 17; 30 | 31 | export function getSyncNativeDiscriminatorBytes() { 32 | return getU8Encoder().encode(SYNC_NATIVE_DISCRIMINATOR); 33 | } 34 | 35 | export type SyncNativeInstruction< 36 | TProgram extends string = typeof TOKEN_PROGRAM_ADDRESS, 37 | TAccountAccount extends string | IAccountMeta = string, 38 | TRemainingAccounts extends readonly IAccountMeta[] = [], 39 | > = IInstruction & 40 | IInstructionWithData & 41 | IInstructionWithAccounts< 42 | [ 43 | TAccountAccount extends string 44 | ? WritableAccount 45 | : TAccountAccount, 46 | ...TRemainingAccounts, 47 | ] 48 | >; 49 | 50 | export type SyncNativeInstructionData = { discriminator: number }; 51 | 52 | export type SyncNativeInstructionDataArgs = {}; 53 | 54 | export function getSyncNativeInstructionDataEncoder(): Encoder { 55 | return transformEncoder( 56 | getStructEncoder([['discriminator', getU8Encoder()]]), 57 | (value) => ({ ...value, discriminator: SYNC_NATIVE_DISCRIMINATOR }) 58 | ); 59 | } 60 | 61 | export function getSyncNativeInstructionDataDecoder(): Decoder { 62 | return getStructDecoder([['discriminator', getU8Decoder()]]); 63 | } 64 | 65 | export function getSyncNativeInstructionDataCodec(): Codec< 66 | SyncNativeInstructionDataArgs, 67 | SyncNativeInstructionData 68 | > { 69 | return combineCodec( 70 | getSyncNativeInstructionDataEncoder(), 71 | getSyncNativeInstructionDataDecoder() 72 | ); 73 | } 74 | 75 | export type SyncNativeInput = { 76 | /** The native token account to sync with its underlying lamports. */ 77 | account: Address; 78 | }; 79 | 80 | export function getSyncNativeInstruction< 81 | TAccountAccount extends string, 82 | TProgramAddress extends Address = typeof TOKEN_PROGRAM_ADDRESS, 83 | >( 84 | input: SyncNativeInput, 85 | config?: { programAddress?: TProgramAddress } 86 | ): SyncNativeInstruction { 87 | // Program address. 88 | const programAddress = config?.programAddress ?? TOKEN_PROGRAM_ADDRESS; 89 | 90 | // Original accounts. 91 | const originalAccounts = { 92 | account: { value: input.account ?? null, isWritable: true }, 93 | }; 94 | const accounts = originalAccounts as Record< 95 | keyof typeof originalAccounts, 96 | ResolvedAccount 97 | >; 98 | 99 | const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); 100 | const instruction = { 101 | accounts: [getAccountMeta(accounts.account)], 102 | programAddress, 103 | data: getSyncNativeInstructionDataEncoder().encode({}), 104 | } as SyncNativeInstruction; 105 | 106 | return instruction; 107 | } 108 | 109 | export type ParsedSyncNativeInstruction< 110 | TProgram extends string = typeof TOKEN_PROGRAM_ADDRESS, 111 | TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[], 112 | > = { 113 | programAddress: Address; 114 | accounts: { 115 | /** The native token account to sync with its underlying lamports. */ 116 | account: TAccountMetas[0]; 117 | }; 118 | data: SyncNativeInstructionData; 119 | }; 120 | 121 | export function parseSyncNativeInstruction< 122 | TProgram extends string, 123 | TAccountMetas extends readonly IAccountMeta[], 124 | >( 125 | instruction: IInstruction & 126 | IInstructionWithAccounts & 127 | IInstructionWithData 128 | ): ParsedSyncNativeInstruction { 129 | if (instruction.accounts.length < 1) { 130 | // TODO: Coded error. 131 | throw new Error('Not enough accounts'); 132 | } 133 | let accountIndex = 0; 134 | const getNextAccount = () => { 135 | const accountMeta = instruction.accounts![accountIndex]!; 136 | accountIndex += 1; 137 | return accountMeta; 138 | }; 139 | return { 140 | programAddress: instruction.programAddress, 141 | accounts: { 142 | account: getNextAccount(), 143 | }, 144 | data: getSyncNativeInstructionDataDecoder().decode(instruction.data), 145 | }; 146 | } 147 | -------------------------------------------------------------------------------- /clients/js/src/generated/pdas/associatedToken.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | import { 10 | getAddressEncoder, 11 | getProgramDerivedAddress, 12 | type Address, 13 | type ProgramDerivedAddress, 14 | } from '@solana/kit'; 15 | 16 | export type AssociatedTokenSeeds = { 17 | /** The wallet address of the associated token account. */ 18 | owner: Address; 19 | /** The address of the token program to use. */ 20 | tokenProgram: Address; 21 | /** The mint address of the associated token account. */ 22 | mint: Address; 23 | }; 24 | 25 | export async function findAssociatedTokenPda( 26 | seeds: AssociatedTokenSeeds, 27 | config: { programAddress?: Address | undefined } = {} 28 | ): Promise { 29 | const { 30 | programAddress = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' as Address<'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'>, 31 | } = config; 32 | return await getProgramDerivedAddress({ 33 | programAddress, 34 | seeds: [ 35 | getAddressEncoder().encode(seeds.owner), 36 | getAddressEncoder().encode(seeds.tokenProgram), 37 | getAddressEncoder().encode(seeds.mint), 38 | ], 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /clients/js/src/generated/pdas/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | export * from './associatedToken'; 10 | -------------------------------------------------------------------------------- /clients/js/src/generated/programs/associatedToken.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | import { 10 | containsBytes, 11 | getU8Encoder, 12 | type Address, 13 | type ReadonlyUint8Array, 14 | } from '@solana/kit'; 15 | import { 16 | type ParsedCreateAssociatedTokenIdempotentInstruction, 17 | type ParsedCreateAssociatedTokenInstruction, 18 | type ParsedRecoverNestedAssociatedTokenInstruction, 19 | } from '../instructions'; 20 | 21 | export const ASSOCIATED_TOKEN_PROGRAM_ADDRESS = 22 | 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' as Address<'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'>; 23 | 24 | export enum AssociatedTokenInstruction { 25 | CreateAssociatedToken, 26 | CreateAssociatedTokenIdempotent, 27 | RecoverNestedAssociatedToken, 28 | } 29 | 30 | export function identifyAssociatedTokenInstruction( 31 | instruction: { data: ReadonlyUint8Array } | ReadonlyUint8Array 32 | ): AssociatedTokenInstruction { 33 | const data = 'data' in instruction ? instruction.data : instruction; 34 | if (containsBytes(data, getU8Encoder().encode(0), 0)) { 35 | return AssociatedTokenInstruction.CreateAssociatedToken; 36 | } 37 | if (containsBytes(data, getU8Encoder().encode(1), 0)) { 38 | return AssociatedTokenInstruction.CreateAssociatedTokenIdempotent; 39 | } 40 | if (containsBytes(data, getU8Encoder().encode(2), 0)) { 41 | return AssociatedTokenInstruction.RecoverNestedAssociatedToken; 42 | } 43 | throw new Error( 44 | 'The provided instruction could not be identified as a associatedToken instruction.' 45 | ); 46 | } 47 | 48 | export type ParsedAssociatedTokenInstruction< 49 | TProgram extends string = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', 50 | > = 51 | | ({ 52 | instructionType: AssociatedTokenInstruction.CreateAssociatedToken; 53 | } & ParsedCreateAssociatedTokenInstruction) 54 | | ({ 55 | instructionType: AssociatedTokenInstruction.CreateAssociatedTokenIdempotent; 56 | } & ParsedCreateAssociatedTokenIdempotentInstruction) 57 | | ({ 58 | instructionType: AssociatedTokenInstruction.RecoverNestedAssociatedToken; 59 | } & ParsedRecoverNestedAssociatedTokenInstruction); 60 | -------------------------------------------------------------------------------- /clients/js/src/generated/programs/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | export * from './associatedToken'; 10 | export * from './token'; 11 | -------------------------------------------------------------------------------- /clients/js/src/generated/shared/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | import { 10 | AccountRole, 11 | isProgramDerivedAddress, 12 | isTransactionSigner as kitIsTransactionSigner, 13 | type Address, 14 | type IAccountMeta, 15 | type IAccountSignerMeta, 16 | type ProgramDerivedAddress, 17 | type TransactionSigner, 18 | upgradeRoleToSigner, 19 | } from '@solana/kit'; 20 | 21 | /** 22 | * Asserts that the given value is not null or undefined. 23 | * @internal 24 | */ 25 | export function expectSome(value: T | null | undefined): T { 26 | if (value == null) { 27 | throw new Error('Expected a value but received null or undefined.'); 28 | } 29 | return value; 30 | } 31 | 32 | /** 33 | * Asserts that the given value is a PublicKey. 34 | * @internal 35 | */ 36 | export function expectAddress( 37 | value: 38 | | Address 39 | | ProgramDerivedAddress 40 | | TransactionSigner 41 | | null 42 | | undefined 43 | ): Address { 44 | if (!value) { 45 | throw new Error('Expected a Address.'); 46 | } 47 | if (typeof value === 'object' && 'address' in value) { 48 | return value.address; 49 | } 50 | if (Array.isArray(value)) { 51 | return value[0]; 52 | } 53 | return value as Address; 54 | } 55 | 56 | /** 57 | * Asserts that the given value is a PDA. 58 | * @internal 59 | */ 60 | export function expectProgramDerivedAddress( 61 | value: 62 | | Address 63 | | ProgramDerivedAddress 64 | | TransactionSigner 65 | | null 66 | | undefined 67 | ): ProgramDerivedAddress { 68 | if (!value || !Array.isArray(value) || !isProgramDerivedAddress(value)) { 69 | throw new Error('Expected a ProgramDerivedAddress.'); 70 | } 71 | return value; 72 | } 73 | 74 | /** 75 | * Asserts that the given value is a TransactionSigner. 76 | * @internal 77 | */ 78 | export function expectTransactionSigner( 79 | value: 80 | | Address 81 | | ProgramDerivedAddress 82 | | TransactionSigner 83 | | null 84 | | undefined 85 | ): TransactionSigner { 86 | if (!value || !isTransactionSigner(value)) { 87 | throw new Error('Expected a TransactionSigner.'); 88 | } 89 | return value; 90 | } 91 | 92 | /** 93 | * Defines an instruction account to resolve. 94 | * @internal 95 | */ 96 | export type ResolvedAccount< 97 | T extends string = string, 98 | U extends 99 | | Address 100 | | ProgramDerivedAddress 101 | | TransactionSigner 102 | | null = 103 | | Address 104 | | ProgramDerivedAddress 105 | | TransactionSigner 106 | | null, 107 | > = { 108 | isWritable: boolean; 109 | value: U; 110 | }; 111 | 112 | /** 113 | * Defines an instruction that stores additional bytes on-chain. 114 | * @internal 115 | */ 116 | export type IInstructionWithByteDelta = { 117 | byteDelta: number; 118 | }; 119 | 120 | /** 121 | * Get account metas and signers from resolved accounts. 122 | * @internal 123 | */ 124 | export function getAccountMetaFactory( 125 | programAddress: Address, 126 | optionalAccountStrategy: 'omitted' | 'programId' 127 | ) { 128 | return ( 129 | account: ResolvedAccount 130 | ): IAccountMeta | IAccountSignerMeta | undefined => { 131 | if (!account.value) { 132 | if (optionalAccountStrategy === 'omitted') return; 133 | return Object.freeze({ 134 | address: programAddress, 135 | role: AccountRole.READONLY, 136 | }); 137 | } 138 | 139 | const writableRole = account.isWritable 140 | ? AccountRole.WRITABLE 141 | : AccountRole.READONLY; 142 | return Object.freeze({ 143 | address: expectAddress(account.value), 144 | role: isTransactionSigner(account.value) 145 | ? upgradeRoleToSigner(writableRole) 146 | : writableRole, 147 | ...(isTransactionSigner(account.value) ? { signer: account.value } : {}), 148 | }); 149 | }; 150 | } 151 | 152 | export function isTransactionSigner( 153 | value: 154 | | Address 155 | | ProgramDerivedAddress 156 | | TransactionSigner 157 | ): value is TransactionSigner { 158 | return ( 159 | !!value && 160 | typeof value === 'object' && 161 | 'address' in value && 162 | kitIsTransactionSigner(value) 163 | ); 164 | } 165 | -------------------------------------------------------------------------------- /clients/js/src/generated/types/accountState.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | import { 10 | combineCodec, 11 | getEnumDecoder, 12 | getEnumEncoder, 13 | type Codec, 14 | type Decoder, 15 | type Encoder, 16 | } from '@solana/kit'; 17 | 18 | export enum AccountState { 19 | Uninitialized, 20 | Initialized, 21 | Frozen, 22 | } 23 | 24 | export type AccountStateArgs = AccountState; 25 | 26 | export function getAccountStateEncoder(): Encoder { 27 | return getEnumEncoder(AccountState); 28 | } 29 | 30 | export function getAccountStateDecoder(): Decoder { 31 | return getEnumDecoder(AccountState); 32 | } 33 | 34 | export function getAccountStateCodec(): Codec { 35 | return combineCodec(getAccountStateEncoder(), getAccountStateDecoder()); 36 | } 37 | -------------------------------------------------------------------------------- /clients/js/src/generated/types/authorityType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | import { 10 | combineCodec, 11 | getEnumDecoder, 12 | getEnumEncoder, 13 | type Codec, 14 | type Decoder, 15 | type Encoder, 16 | } from '@solana/kit'; 17 | 18 | export enum AuthorityType { 19 | MintTokens, 20 | FreezeAccount, 21 | AccountOwner, 22 | CloseAccount, 23 | } 24 | 25 | export type AuthorityTypeArgs = AuthorityType; 26 | 27 | export function getAuthorityTypeEncoder(): Encoder { 28 | return getEnumEncoder(AuthorityType); 29 | } 30 | 31 | export function getAuthorityTypeDecoder(): Decoder { 32 | return getEnumDecoder(AuthorityType); 33 | } 34 | 35 | export function getAuthorityTypeCodec(): Codec< 36 | AuthorityTypeArgs, 37 | AuthorityType 38 | > { 39 | return combineCodec(getAuthorityTypeEncoder(), getAuthorityTypeDecoder()); 40 | } 41 | -------------------------------------------------------------------------------- /clients/js/src/generated/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This code was AUTOGENERATED using the codama library. 3 | * Please DO NOT EDIT THIS FILE, instead use visitors 4 | * to add features, then rerun codama to update it. 5 | * 6 | * @see https://github.com/codama-idl/codama 7 | */ 8 | 9 | export * from './accountState'; 10 | export * from './authorityType'; 11 | -------------------------------------------------------------------------------- /clients/js/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generated'; 2 | -------------------------------------------------------------------------------- /clients/js/test/createAssociatedToken.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Account, 3 | appendTransactionMessageInstruction, 4 | generateKeyPairSigner, 5 | none, 6 | pipe, 7 | } from '@solana/kit'; 8 | import test from 'ava'; 9 | import { 10 | AccountState, 11 | TOKEN_PROGRAM_ADDRESS, 12 | Token, 13 | fetchToken, 14 | findAssociatedTokenPda, 15 | getCreateAssociatedTokenInstructionAsync, 16 | } from '../src'; 17 | import { 18 | createDefaultSolanaClient, 19 | createDefaultTransaction, 20 | createMint, 21 | generateKeyPairSignerWithSol, 22 | signAndSendTransaction, 23 | } from './_setup'; 24 | 25 | test('it creates a new associated token account', async (t) => { 26 | // Given a mint account, its mint authority and a token owner. 27 | const client = createDefaultSolanaClient(); 28 | const [payer, mintAuthority, owner] = await Promise.all([ 29 | generateKeyPairSignerWithSol(client), 30 | generateKeyPairSigner(), 31 | generateKeyPairSigner(), 32 | ]); 33 | const mint = await createMint(client, payer, mintAuthority.address); 34 | 35 | // When we create and initialize a token account at this address. 36 | const createAta = await getCreateAssociatedTokenInstructionAsync({ 37 | payer, 38 | mint, 39 | owner: owner.address, 40 | }); 41 | 42 | await pipe( 43 | await createDefaultTransaction(client, payer), 44 | (tx) => appendTransactionMessageInstruction(createAta, tx), 45 | (tx) => signAndSendTransaction(client, tx) 46 | ); 47 | 48 | // Then we expect the token account to exist and have the following data. 49 | const [ata] = await findAssociatedTokenPda({ 50 | mint, 51 | owner: owner.address, 52 | tokenProgram: TOKEN_PROGRAM_ADDRESS, 53 | }); 54 | t.like(await fetchToken(client.rpc, ata), >{ 55 | address: ata, 56 | data: { 57 | mint, 58 | owner: owner.address, 59 | amount: 0n, 60 | delegate: none(), 61 | state: AccountState.Initialized, 62 | isNative: none(), 63 | delegatedAmount: 0n, 64 | closeAuthority: none(), 65 | }, 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /clients/js/test/createAssociatedTokenIdempotent.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Account, 3 | appendTransactionMessageInstruction, 4 | generateKeyPairSigner, 5 | none, 6 | pipe, 7 | } from '@solana/kit'; 8 | import test from 'ava'; 9 | import { 10 | AccountState, 11 | TOKEN_PROGRAM_ADDRESS, 12 | Token, 13 | fetchToken, 14 | findAssociatedTokenPda, 15 | getCreateAssociatedTokenIdempotentInstructionAsync, 16 | } from '../src'; 17 | import { 18 | createDefaultSolanaClient, 19 | createDefaultTransaction, 20 | createMint, 21 | generateKeyPairSignerWithSol, 22 | signAndSendTransaction, 23 | } from './_setup'; 24 | 25 | test('it creates a new associated token account', async (t) => { 26 | // Given a mint account, its mint authority and a token owner. 27 | const client = createDefaultSolanaClient(); 28 | const [payer, mintAuthority, owner] = await Promise.all([ 29 | generateKeyPairSignerWithSol(client), 30 | generateKeyPairSigner(), 31 | generateKeyPairSigner(), 32 | ]); 33 | const mint = await createMint(client, payer, mintAuthority.address); 34 | 35 | // When we create and initialize a token account at this address. 36 | const createAta = await getCreateAssociatedTokenIdempotentInstructionAsync({ 37 | payer, 38 | mint, 39 | owner: owner.address, 40 | }); 41 | 42 | await pipe( 43 | await createDefaultTransaction(client, payer), 44 | (tx) => appendTransactionMessageInstruction(createAta, tx), 45 | (tx) => signAndSendTransaction(client, tx) 46 | ); 47 | 48 | // Then we expect the token account to exist and have the following data. 49 | const [ata] = await findAssociatedTokenPda({ 50 | mint, 51 | owner: owner.address, 52 | tokenProgram: TOKEN_PROGRAM_ADDRESS, 53 | }); 54 | t.like(await fetchToken(client.rpc, ata), >{ 55 | address: ata, 56 | data: { 57 | mint, 58 | owner: owner.address, 59 | amount: 0n, 60 | delegate: none(), 61 | state: AccountState.Initialized, 62 | isNative: none(), 63 | delegatedAmount: 0n, 64 | closeAuthority: none(), 65 | }, 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /clients/js/test/initializeAccount.test.ts: -------------------------------------------------------------------------------- 1 | import { getCreateAccountInstruction } from '@solana-program/system'; 2 | import { 3 | Account, 4 | appendTransactionMessageInstructions, 5 | generateKeyPairSigner, 6 | none, 7 | pipe, 8 | } from '@solana/kit'; 9 | import test from 'ava'; 10 | import { 11 | AccountState, 12 | TOKEN_PROGRAM_ADDRESS, 13 | Token, 14 | fetchToken, 15 | getInitializeAccountInstruction, 16 | getTokenSize, 17 | } from '../src'; 18 | import { 19 | createDefaultSolanaClient, 20 | createDefaultTransaction, 21 | createMint, 22 | generateKeyPairSignerWithSol, 23 | signAndSendTransaction, 24 | } from './_setup'; 25 | 26 | test('it creates and initializes a new token account', async (t) => { 27 | // Given a mint account, its mint authority and two generated keypairs 28 | // for the token to be created and its owner. 29 | const client = createDefaultSolanaClient(); 30 | const [payer, mintAuthority, token, owner] = await Promise.all([ 31 | generateKeyPairSignerWithSol(client), 32 | generateKeyPairSigner(), 33 | generateKeyPairSigner(), 34 | generateKeyPairSigner(), 35 | ]); 36 | const mint = await createMint(client, payer, mintAuthority.address); 37 | 38 | // When we create and initialize a token account at this address. 39 | const space = BigInt(getTokenSize()); 40 | const rent = await client.rpc.getMinimumBalanceForRentExemption(space).send(); 41 | const instructions = [ 42 | getCreateAccountInstruction({ 43 | payer, 44 | newAccount: token, 45 | lamports: rent, 46 | space, 47 | programAddress: TOKEN_PROGRAM_ADDRESS, 48 | }), 49 | getInitializeAccountInstruction({ 50 | account: token.address, 51 | mint, 52 | owner: owner.address, 53 | }), 54 | ]; 55 | await pipe( 56 | await createDefaultTransaction(client, payer), 57 | (tx) => appendTransactionMessageInstructions(instructions, tx), 58 | (tx) => signAndSendTransaction(client, tx) 59 | ); 60 | 61 | // Then we expect the token account to exist and have the following data. 62 | const tokenAccount = await fetchToken(client.rpc, token.address); 63 | t.like(tokenAccount, >{ 64 | address: token.address, 65 | data: { 66 | mint, 67 | owner: owner.address, 68 | amount: 0n, 69 | delegate: none(), 70 | state: AccountState.Initialized, 71 | isNative: none(), 72 | delegatedAmount: 0n, 73 | closeAuthority: none(), 74 | }, 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /clients/js/test/initializeMint.test.ts: -------------------------------------------------------------------------------- 1 | import { getCreateAccountInstruction } from '@solana-program/system'; 2 | import { 3 | Account, 4 | appendTransactionMessageInstructions, 5 | generateKeyPairSigner, 6 | none, 7 | pipe, 8 | some, 9 | } from '@solana/kit'; 10 | import test from 'ava'; 11 | import { 12 | Mint, 13 | TOKEN_PROGRAM_ADDRESS, 14 | fetchMint, 15 | getInitializeMintInstruction, 16 | getMintSize, 17 | } from '../src'; 18 | import { 19 | createDefaultSolanaClient, 20 | createDefaultTransaction, 21 | generateKeyPairSignerWithSol, 22 | signAndSendTransaction, 23 | } from './_setup'; 24 | 25 | test('it creates and initializes a new mint account', async (t) => { 26 | // Given an authority and a mint account. 27 | const client = createDefaultSolanaClient(); 28 | const authority = await generateKeyPairSignerWithSol(client); 29 | const mint = await generateKeyPairSigner(); 30 | 31 | // When we create and initialize a mint account at this address. 32 | const space = BigInt(getMintSize()); 33 | const rent = await client.rpc.getMinimumBalanceForRentExemption(space).send(); 34 | const instructions = [ 35 | getCreateAccountInstruction({ 36 | payer: authority, 37 | newAccount: mint, 38 | lamports: rent, 39 | space, 40 | programAddress: TOKEN_PROGRAM_ADDRESS, 41 | }), 42 | getInitializeMintInstruction({ 43 | mint: mint.address, 44 | decimals: 2, 45 | mintAuthority: authority.address, 46 | }), 47 | ]; 48 | await pipe( 49 | await createDefaultTransaction(client, authority), 50 | (tx) => appendTransactionMessageInstructions(instructions, tx), 51 | (tx) => signAndSendTransaction(client, tx) 52 | ); 53 | 54 | // Then we expect the mint account to exist and have the following data. 55 | const mintAccount = await fetchMint(client.rpc, mint.address); 56 | t.like(mintAccount, >{ 57 | address: mint.address, 58 | data: { 59 | mintAuthority: some(authority.address), 60 | supply: 0n, 61 | decimals: 2, 62 | isInitialized: true, 63 | freezeAuthority: none(), 64 | }, 65 | }); 66 | }); 67 | 68 | test('it creates a new mint account with a freeze authority', async (t) => { 69 | // Given an authority and a mint account. 70 | const client = createDefaultSolanaClient(); 71 | const [payer, mintAuthority, freezeAuthority, mint] = await Promise.all([ 72 | generateKeyPairSignerWithSol(client), 73 | generateKeyPairSigner(), 74 | generateKeyPairSigner(), 75 | generateKeyPairSigner(), 76 | ]); 77 | 78 | // When we create and initialize a mint account at this address. 79 | const space = BigInt(getMintSize()); 80 | const rent = await client.rpc.getMinimumBalanceForRentExemption(space).send(); 81 | const instructions = [ 82 | getCreateAccountInstruction({ 83 | payer, 84 | newAccount: mint, 85 | lamports: rent, 86 | space, 87 | programAddress: TOKEN_PROGRAM_ADDRESS, 88 | }), 89 | getInitializeMintInstruction({ 90 | mint: mint.address, 91 | decimals: 0, 92 | mintAuthority: mintAuthority.address, 93 | freezeAuthority: freezeAuthority.address, 94 | }), 95 | ]; 96 | await pipe( 97 | await createDefaultTransaction(client, payer), 98 | (tx) => appendTransactionMessageInstructions(instructions, tx), 99 | (tx) => signAndSendTransaction(client, tx) 100 | ); 101 | 102 | // Then we expect the mint account to exist and have the following data. 103 | const mintAccount = await fetchMint(client.rpc, mint.address); 104 | t.like(mintAccount, >{ 105 | address: mint.address, 106 | data: { 107 | mintAuthority: some(mintAuthority.address), 108 | freezeAuthority: some(freezeAuthority.address), 109 | }, 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /clients/js/test/mintTo.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | appendTransactionMessageInstruction, 3 | generateKeyPairSigner, 4 | pipe, 5 | } from '@solana/kit'; 6 | import test from 'ava'; 7 | import { 8 | Mint, 9 | Token, 10 | fetchMint, 11 | fetchToken, 12 | getMintToInstruction, 13 | } from '../src'; 14 | import { 15 | createDefaultSolanaClient, 16 | createDefaultTransaction, 17 | createMint, 18 | createToken, 19 | generateKeyPairSignerWithSol, 20 | signAndSendTransaction, 21 | } from './_setup'; 22 | 23 | test('it mints tokens to a token account', async (t) => { 24 | // Given a mint account and a token account. 25 | const client = createDefaultSolanaClient(); 26 | const [payer, mintAuthority, owner] = await Promise.all([ 27 | generateKeyPairSignerWithSol(client), 28 | generateKeyPairSigner(), 29 | generateKeyPairSigner(), 30 | ]); 31 | const mint = await createMint(client, payer, mintAuthority.address); 32 | const token = await createToken(client, payer, mint, owner.address); 33 | 34 | // When the mint authority mints tokens to the token account. 35 | const mintTo = getMintToInstruction({ 36 | mint, 37 | token, 38 | mintAuthority, 39 | amount: 100n, 40 | }); 41 | await pipe( 42 | await createDefaultTransaction(client, payer), 43 | (tx) => appendTransactionMessageInstruction(mintTo, tx), 44 | (tx) => signAndSendTransaction(client, tx) 45 | ); 46 | 47 | // Then we expect the mint and token accounts to have the following updated data. 48 | const [{ data: mintData }, { data: tokenData }] = await Promise.all([ 49 | fetchMint(client.rpc, mint), 50 | fetchToken(client.rpc, token), 51 | ]); 52 | t.like(mintData, { supply: 100n }); 53 | t.like(tokenData, { amount: 100n }); 54 | }); 55 | -------------------------------------------------------------------------------- /clients/js/test/transfer.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | appendTransactionMessageInstruction, 3 | generateKeyPairSigner, 4 | pipe, 5 | } from '@solana/kit'; 6 | import test from 'ava'; 7 | import { 8 | Mint, 9 | Token, 10 | fetchMint, 11 | fetchToken, 12 | getTransferInstruction, 13 | } from '../src'; 14 | import { 15 | createDefaultSolanaClient, 16 | createDefaultTransaction, 17 | createMint, 18 | createToken, 19 | createTokenWithAmount, 20 | generateKeyPairSignerWithSol, 21 | signAndSendTransaction, 22 | } from './_setup'; 23 | 24 | test('it transfers tokens from one account to another', async (t) => { 25 | // Given a mint account and two token accounts. 26 | // One with 100 tokens and the other with 0 tokens. 27 | const client = createDefaultSolanaClient(); 28 | const [payer, mintAuthority, ownerA, ownerB] = await Promise.all([ 29 | generateKeyPairSignerWithSol(client), 30 | generateKeyPairSigner(), 31 | generateKeyPairSigner(), 32 | generateKeyPairSigner(), 33 | ]); 34 | const mint = await createMint(client, payer, mintAuthority.address); 35 | const [tokenA, tokenB] = await Promise.all([ 36 | createTokenWithAmount( 37 | client, 38 | payer, 39 | mintAuthority, 40 | mint, 41 | ownerA.address, 42 | 100n 43 | ), 44 | createToken(client, payer, mint, ownerB.address), 45 | ]); 46 | 47 | // When owner A transfers 50 tokens to owner B. 48 | const transfer = getTransferInstruction({ 49 | source: tokenA, 50 | destination: tokenB, 51 | authority: ownerA, 52 | amount: 50n, 53 | }); 54 | await pipe( 55 | await createDefaultTransaction(client, payer), 56 | (tx) => appendTransactionMessageInstruction(transfer, tx), 57 | (tx) => signAndSendTransaction(client, tx) 58 | ); 59 | 60 | // Then we expect the mint and token accounts to have the following updated data. 61 | const [{ data: mintData }, { data: tokenDataA }, { data: tokenDataB }] = 62 | await Promise.all([ 63 | fetchMint(client.rpc, mint), 64 | fetchToken(client.rpc, tokenA), 65 | fetchToken(client.rpc, tokenB), 66 | ]); 67 | t.like(mintData, { supply: 100n }); 68 | t.like(tokenDataA, { amount: 50n }); 69 | t.like(tokenDataB, { amount: 50n }); 70 | }); 71 | -------------------------------------------------------------------------------- /clients/js/tsconfig.declarations.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "./dist/types", 7 | }, 8 | "extends": "./tsconfig.json", 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /clients/js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "composite": false, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "inlineSources": false, 10 | "isolatedModules": true, 11 | "module": "ESNext", 12 | "moduleResolution": "node", 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "outDir": "./dist", 17 | "preserveWatchOutput": true, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "target": "ESNext" 21 | }, 22 | "exclude": ["node_modules"], 23 | "include": ["src", "test"] 24 | } 25 | -------------------------------------------------------------------------------- /clients/js/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'node:process'; 2 | import { defineConfig, Options } from 'tsup'; 3 | 4 | const SHARED_OPTIONS: Options = { 5 | define: { __VERSION__: `"${env.npm_package_version}"` }, 6 | entry: ['./src/index.ts'], 7 | outDir: './dist/src', 8 | outExtension: ({ format }) => ({ js: format === 'cjs' ? '.js' : '.mjs' }), 9 | sourcemap: true, 10 | treeshake: true, 11 | }; 12 | 13 | export default defineConfig(() => [ 14 | // Source. 15 | { ...SHARED_OPTIONS, format: 'cjs' }, 16 | { ...SHARED_OPTIONS, format: 'esm' }, 17 | 18 | // Tests. 19 | { 20 | ...SHARED_OPTIONS, 21 | bundle: false, 22 | entry: ['./test/**/*.ts'], 23 | format: 'cjs', 24 | outDir: './dist/test', 25 | }, 26 | ]); 27 | -------------------------------------------------------------------------------- /clients/js/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "includeVersion": true, 4 | "readme": "none", 5 | "out": "docs" 6 | } 7 | -------------------------------------------------------------------------------- /clients/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spl-token-client" 3 | version = "0.0.0" 4 | description = "A generated Rust library for the Token program" 5 | repository = "https://github.com/solana-program/token" 6 | edition = "2021" 7 | readme = "README.md" 8 | license-file = "../../LICENSE" 9 | 10 | [features] 11 | test-sbf = [] 12 | serde = ["dep:serde", "dep:serde_with"] 13 | 14 | [dependencies] 15 | borsh = "^0.10" 16 | num-derive = "^0.3" 17 | num-traits = "^0.2" 18 | serde = { version = "^1.0", features = ["derive"], optional = true } 19 | serde_with = { version = "^3.0", optional = true } 20 | solana-program = "2.1" 21 | thiserror = "^1.0" 22 | -------------------------------------------------------------------------------- /clients/rust/README.md: -------------------------------------------------------------------------------- 1 | # Rust client 2 | 3 | A generated Rust library for the Token program. 4 | 5 | ## Getting started 6 | 7 | To build and test your Rust client from the root of the repository, you may use the following command. 8 | 9 | ```sh 10 | pnpm clients:js:test 11 | ``` 12 | 13 | This will start a new local validator, if one is not already running, and run the tests for your Rust client. 14 | -------------------------------------------------------------------------------- /clients/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | mod generated; 3 | 4 | pub use generated::programs::TOKEN_ID as ID; 5 | pub use generated::*; 6 | */ 7 | -------------------------------------------------------------------------------- /interface/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spl-token-interface" 3 | version = "0.0.0" 4 | description = "Instructions and types for interacting with SPL Token program" 5 | authors = { workspace = true} 6 | repository = { workspace = true} 7 | license = { workspace = true} 8 | edition = { workspace = true} 9 | readme = "./README.md" 10 | 11 | [lib] 12 | crate-type = ["rlib"] 13 | 14 | [dependencies] 15 | pinocchio = "0.8.4" 16 | pinocchio-pubkey = "0.2" 17 | 18 | [dev-dependencies] 19 | strum = "0.27" 20 | strum_macros = "0.27" 21 | -------------------------------------------------------------------------------- /interface/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Solana 4 | 5 |

6 | 7 | # SPL Token Interface 8 | 9 | This crate contains instructions and constructors for interacting with the [SPL Token program](https://spl.solana.com/token). 10 | 11 | The Token program defines a common implementation for Fungible and Non Fungible tokens. 12 | 13 | ## Getting Started 14 | 15 | From your project folder: 16 | 17 | ```bash 18 | cargo add spl-token-interface 19 | ``` 20 | 21 | This will add the `spl-token-interface` dependency to your `Cargo.toml` file. 22 | 23 | ## Documentation 24 | 25 | Read more about the SPL Token interface on the crate [documentation](https://docs.rs/spl-token-interface). 26 | -------------------------------------------------------------------------------- /interface/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | pub mod error; 4 | pub mod instruction; 5 | pub mod native_mint; 6 | pub mod state; 7 | 8 | pub mod program { 9 | pinocchio_pubkey::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); 10 | } 11 | -------------------------------------------------------------------------------- /interface/src/native_mint.rs: -------------------------------------------------------------------------------- 1 | //! The Mint that represents the native token. 2 | 3 | use pinocchio::pubkey::Pubkey; 4 | 5 | /// There are `10^9` lamports in one SOL 6 | pub const DECIMALS: u8 = 9; 7 | 8 | // The Mint for native SOL Token accounts 9 | pub const ID: Pubkey = pinocchio_pubkey::pubkey!("So11111111111111111111111111111111111111112"); 10 | 11 | #[inline(always)] 12 | pub fn is_native_mint(mint: &Pubkey) -> bool { 13 | mint == &ID 14 | } 15 | -------------------------------------------------------------------------------- /interface/src/state/account_state.rs: -------------------------------------------------------------------------------- 1 | use pinocchio::program_error::ProgramError; 2 | 3 | #[repr(u8)] 4 | #[derive(Clone, Copy, Debug, PartialEq)] 5 | pub enum AccountState { 6 | /// Account is not yet initialized 7 | Uninitialized, 8 | 9 | /// Account is initialized; the account owner and/or delegate may perform 10 | /// permitted operations on this account 11 | Initialized, 12 | 13 | /// Account has been frozen by the mint freeze authority. Neither the 14 | /// account owner nor the delegate are able to perform operations on 15 | /// this account. 16 | Frozen, 17 | } 18 | 19 | impl TryFrom for AccountState { 20 | type Error = ProgramError; 21 | 22 | #[inline(always)] 23 | fn try_from(value: u8) -> Result { 24 | match value { 25 | // SAFETY: `value` is guaranteed to be in the range of the enum variants. 26 | 0..=2 => Ok(unsafe { core::mem::transmute::(value) }), 27 | _ => Err(ProgramError::InvalidAccountData), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /interface/src/state/mint.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{COption, Initializable, Transmutable}, 3 | pinocchio::{program_error::ProgramError, pubkey::Pubkey}, 4 | }; 5 | 6 | /// Internal representation of a mint data. 7 | #[repr(C)] 8 | pub struct Mint { 9 | /// Optional authority used to mint new tokens. The mint authority may only 10 | /// be provided during mint creation. If no mint authority is present 11 | /// then the mint has a fixed supply and no further tokens may be 12 | /// minted. 13 | mint_authority: COption, 14 | 15 | /// Total supply of tokens. 16 | supply: [u8; 8], 17 | 18 | /// Number of base 10 digits to the right of the decimal place. 19 | pub decimals: u8, 20 | 21 | /// Is `true` if this structure has been initialized. 22 | is_initialized: u8, 23 | 24 | // Indicates whether the freeze authority is present or not. 25 | //freeze_authority_option: [u8; 4], 26 | /// Optional authority to freeze token accounts. 27 | freeze_authority: COption, 28 | } 29 | 30 | impl Mint { 31 | #[inline(always)] 32 | pub fn set_supply(&mut self, supply: u64) { 33 | self.supply = supply.to_le_bytes(); 34 | } 35 | 36 | #[inline(always)] 37 | pub fn supply(&self) -> u64 { 38 | u64::from_le_bytes(self.supply) 39 | } 40 | 41 | #[inline(always)] 42 | pub fn set_initialized(&mut self) { 43 | self.is_initialized = 1; 44 | } 45 | 46 | #[inline(always)] 47 | pub fn clear_mint_authority(&mut self) { 48 | self.mint_authority.0[0] = 0; 49 | } 50 | 51 | #[inline(always)] 52 | pub fn set_mint_authority(&mut self, mint_authority: &Pubkey) { 53 | self.mint_authority.0[0] = 1; 54 | self.mint_authority.1 = *mint_authority; 55 | } 56 | 57 | #[inline(always)] 58 | pub fn mint_authority(&self) -> Option<&Pubkey> { 59 | if self.mint_authority.0[0] == 1 { 60 | Some(&self.mint_authority.1) 61 | } else { 62 | None 63 | } 64 | } 65 | 66 | #[inline(always)] 67 | pub fn clear_freeze_authority(&mut self) { 68 | self.freeze_authority.0[0] = 0; 69 | } 70 | 71 | #[inline(always)] 72 | pub fn set_freeze_authority(&mut self, freeze_authority: &Pubkey) { 73 | self.freeze_authority.0[0] = 1; 74 | self.freeze_authority.1 = *freeze_authority; 75 | } 76 | 77 | #[inline(always)] 78 | pub fn freeze_authority(&self) -> Option<&Pubkey> { 79 | if self.freeze_authority.0[0] == 1 { 80 | Some(&self.freeze_authority.1) 81 | } else { 82 | None 83 | } 84 | } 85 | } 86 | 87 | impl Transmutable for Mint { 88 | /// The length of the `Mint` account data. 89 | const LEN: usize = core::mem::size_of::(); 90 | } 91 | 92 | impl Initializable for Mint { 93 | #[inline(always)] 94 | fn is_initialized(&self) -> Result { 95 | match self.is_initialized { 96 | 0 => Ok(false), 97 | 1 => Ok(true), 98 | _ => Err(ProgramError::InvalidAccountData), 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /interface/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | use pinocchio::program_error::ProgramError; 2 | 3 | pub mod account; 4 | pub mod account_state; 5 | pub mod mint; 6 | pub mod multisig; 7 | 8 | /// Type alias for fields represented as `COption`. 9 | pub type COption = ([u8; 4], T); 10 | 11 | /// Marker trait for types that can be cast from a raw pointer. 12 | /// 13 | /// It is up to the type implementing this trait to guarantee that the cast is 14 | /// safe, i.e., the fields of the type are well aligned and there are no padding 15 | /// bytes. 16 | pub trait Transmutable { 17 | /// The length of the type. 18 | /// 19 | /// This must be equal to the size of each individual field in the type. 20 | const LEN: usize; 21 | } 22 | 23 | /// Trait to represent a type that can be initialized. 24 | pub trait Initializable { 25 | /// Return `true` if the object is initialized. 26 | fn is_initialized(&self) -> Result; 27 | } 28 | 29 | /// Return a reference for an initialized `T` from the given bytes. 30 | /// 31 | /// # Safety 32 | /// 33 | /// The caller must ensure that `bytes` contains a valid representation of `T`. 34 | #[inline(always)] 35 | pub unsafe fn load(bytes: &[u8]) -> Result<&T, ProgramError> { 36 | load_unchecked(bytes).and_then(|t: &T| { 37 | // checks if the data is initialized 38 | if t.is_initialized()? { 39 | Ok(t) 40 | } else { 41 | Err(ProgramError::UninitializedAccount) 42 | } 43 | }) 44 | } 45 | 46 | /// Return a `T` reference from the given bytes. 47 | /// 48 | /// This function does not check if the data is initialized. 49 | /// 50 | /// # Safety 51 | /// 52 | /// The caller must ensure that `bytes` contains a valid representation of `T`. 53 | #[inline(always)] 54 | pub unsafe fn load_unchecked(bytes: &[u8]) -> Result<&T, ProgramError> { 55 | if bytes.len() != T::LEN { 56 | return Err(ProgramError::InvalidAccountData); 57 | } 58 | Ok(&*(bytes.as_ptr() as *const T)) 59 | } 60 | 61 | /// Return a mutable reference for an initialized `T` from the given bytes. 62 | /// 63 | /// # Safety 64 | /// 65 | /// The caller must ensure that `bytes` contains a valid representation of `T`. 66 | #[inline(always)] 67 | pub unsafe fn load_mut( 68 | bytes: &mut [u8], 69 | ) -> Result<&mut T, ProgramError> { 70 | load_mut_unchecked(bytes).and_then(|t: &mut T| { 71 | // checks if the data is initialized 72 | if t.is_initialized()? { 73 | Ok(t) 74 | } else { 75 | Err(ProgramError::UninitializedAccount) 76 | } 77 | }) 78 | } 79 | 80 | /// Return a mutable `T` reference from the given bytes. 81 | /// 82 | /// This function does not check if the data is initialized. 83 | /// 84 | /// # Safety 85 | /// 86 | /// The caller must ensure that `bytes` contains a valid representation of `T`. 87 | #[inline(always)] 88 | pub unsafe fn load_mut_unchecked( 89 | bytes: &mut [u8], 90 | ) -> Result<&mut T, ProgramError> { 91 | if bytes.len() != T::LEN { 92 | return Err(ProgramError::InvalidAccountData); 93 | } 94 | Ok(&mut *(bytes.as_mut_ptr() as *mut T)) 95 | } 96 | -------------------------------------------------------------------------------- /interface/src/state/multisig.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{Initializable, Transmutable}, 3 | pinocchio::{program_error::ProgramError, pubkey::Pubkey}, 4 | }; 5 | 6 | /// Minimum number of multisignature signers (min N) 7 | pub const MIN_SIGNERS: u8 = 1; 8 | 9 | /// Maximum number of multisignature signers (max N) 10 | pub const MAX_SIGNERS: u8 = 11; 11 | 12 | /// Multisignature data. 13 | #[repr(C)] 14 | pub struct Multisig { 15 | /// Number of signers required. 16 | pub m: u8, 17 | 18 | /// Number of valid signers. 19 | pub n: u8, 20 | 21 | /// Is `true` if this structure has been initialized. 22 | is_initialized: u8, 23 | 24 | /// Signer public keys. 25 | pub signers: [Pubkey; MAX_SIGNERS as usize], 26 | } 27 | 28 | impl Multisig { 29 | /// Utility function that checks index is between [`MIN_SIGNERS`] and 30 | /// [`MAX_SIGNERS`]. 31 | pub fn is_valid_signer_index(index: u8) -> bool { 32 | (MIN_SIGNERS..=MAX_SIGNERS).contains(&index) 33 | } 34 | 35 | #[inline] 36 | pub fn set_initialized(&mut self, value: bool) { 37 | self.is_initialized = value as u8; 38 | } 39 | } 40 | 41 | impl Transmutable for Multisig { 42 | /// The length of the `Multisig` account data. 43 | const LEN: usize = core::mem::size_of::(); 44 | } 45 | 46 | impl Initializable for Multisig { 47 | #[inline(always)] 48 | fn is_initialized(&self) -> Result { 49 | match self.is_initialized { 50 | 0 => Ok(false), 51 | 1 => Ok(true), 52 | _ => Err(ProgramError::InvalidAccountData), 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /p-token/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pinocchio-token-program" 3 | version = "0.0.0" 4 | description = "A pinocchio-based Token (aka 'p-token') program" 5 | authors = { workspace = true} 6 | repository = { workspace = true} 7 | license = { workspace = true} 8 | edition = { workspace = true} 9 | readme = "./README.md" 10 | 11 | [lib] 12 | crate-type = ["cdylib"] 13 | 14 | [features] 15 | logging = [] 16 | 17 | [dependencies] 18 | pinocchio = "0.8.4" 19 | pinocchio-log = { version = "0.4", default-features = false } 20 | spl-token-interface = { version = "^0", path = "../interface" } 21 | 22 | [dev-dependencies] 23 | assert_matches = "1.5.0" 24 | num-traits = "0.2" 25 | solana-program-test = "2.1" 26 | solana-sdk = "2.1" 27 | spl-token = { version="^4", features=["no-entrypoint"] } 28 | spl-token-2022 = { version="^7", features=["no-entrypoint"] } 29 | 30 | [lints] 31 | workspace = true 32 | -------------------------------------------------------------------------------- /p-token/README.md: -------------------------------------------------------------------------------- 1 | # `p-token` 2 | 3 | A `pinocchio`-based Token program. 4 | 5 | ## Overview 6 | 7 | `p-token` is a reimplementation of the SPL Token program, one of the most popular programs on Solana, using [`pinocchio`](https://github.com/anza-xyz/pinocchio). The purpose is to have an implementation that optimizes the compute units, while being fully compatible with the original implementation — i.e., support the exact same instruction and account layouts as SPL Token, byte for byte. 8 | 9 | ## Features 10 | 11 | - `no_std` crate 12 | - Same instruction and account layout as SPL Token 13 | - Minimal CU usage 14 | 15 | 16 | ## License 17 | 18 | The code is licensed under the [Apache License Version 2.0](LICENSE) 19 | -------------------------------------------------------------------------------- /p-token/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Another ERC20-like Token program for the Solana blockchain. 2 | 3 | #![no_std] 4 | 5 | mod entrypoint; 6 | mod processor; 7 | -------------------------------------------------------------------------------- /p-token/src/processor/amount_to_ui_amount.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{check_account_owner, unpack_amount, MAX_FORMATTED_DIGITS}, 3 | core::str::from_utf8_unchecked, 4 | pinocchio::{ 5 | account_info::AccountInfo, program::set_return_data, program_error::ProgramError, 6 | ProgramResult, 7 | }, 8 | pinocchio_log::logger::{Argument, Logger}, 9 | spl_token_interface::{ 10 | error::TokenError, 11 | state::{load, mint::Mint}, 12 | }, 13 | }; 14 | 15 | pub fn process_amount_to_ui_amount( 16 | accounts: &[AccountInfo], 17 | instruction_data: &[u8], 18 | ) -> ProgramResult { 19 | let amount = unpack_amount(instruction_data)?; 20 | 21 | let mint_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; 22 | check_account_owner(mint_info)?; 23 | // SAFETY: single immutable borrow to `mint_info` account data and 24 | // `load` validates that the mint is initialized. 25 | let mint = unsafe { 26 | load::(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)? 27 | }; 28 | 29 | let mut logger = Logger::::default(); 30 | logger.append_with_args(amount, &[Argument::Precision(mint.decimals)]); 31 | // "Extract" the formatted string from the logger. 32 | // 33 | // SAFETY: the logger is guaranteed to be a valid UTF-8 string. 34 | let mut s = unsafe { from_utf8_unchecked(&logger) }; 35 | 36 | if mint.decimals > 0 && s.contains('.') { 37 | let zeros_trimmed = s.trim_end_matches('0'); 38 | s = zeros_trimmed.trim_end_matches('.'); 39 | } 40 | 41 | set_return_data(s.as_bytes()); 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /p-token/src/processor/approve.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{shared, unpack_amount}, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_approve(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { 8 | let amount = unpack_amount(instruction_data)?; 9 | 10 | shared::approve::process_approve(accounts, amount, None) 11 | } 12 | -------------------------------------------------------------------------------- /p-token/src/processor/approve_checked.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{shared, unpack_amount_and_decimals}, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_approve_checked(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { 8 | let (amount, decimals) = unpack_amount_and_decimals(instruction_data)?; 9 | 10 | shared::approve::process_approve(accounts, amount, Some(decimals)) 11 | } 12 | -------------------------------------------------------------------------------- /p-token/src/processor/batch.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::entrypoint::inner_process_instruction, 3 | pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, 4 | spl_token_interface::error::TokenError, 5 | }; 6 | 7 | /// The size of the batch instruction header. 8 | /// 9 | /// The header of each instruction consists of two `u8` values: 10 | /// * number of the accounts 11 | /// * length of the instruction data 12 | const IX_HEADER_SIZE: usize = 2; 13 | 14 | #[allow(clippy::arithmetic_side_effects)] 15 | pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) -> ProgramResult { 16 | loop { 17 | // Validates the instruction data and accounts offset. 18 | 19 | if instruction_data.len() < IX_HEADER_SIZE { 20 | // The instruction data must have at least two bytes. 21 | return Err(TokenError::InvalidInstruction.into()); 22 | } 23 | 24 | // SAFETY: The instruction data is guaranteed to have at least two bytes 25 | // (header) + one byte (discriminator) and the values are within the bounds 26 | // of an `usize`. 27 | let expected_accounts = unsafe { *instruction_data.get_unchecked(0) as usize }; 28 | let data_offset = IX_HEADER_SIZE + unsafe { *instruction_data.get_unchecked(1) as usize }; 29 | 30 | if instruction_data.len() < data_offset || data_offset == IX_HEADER_SIZE { 31 | return Err(TokenError::InvalidInstruction.into()); 32 | } 33 | 34 | if accounts.len() < expected_accounts { 35 | return Err(ProgramError::NotEnoughAccountKeys); 36 | } 37 | 38 | // Process the instruction. 39 | 40 | // SAFETY: The instruction data and accounts lengths are already validated so 41 | // all slices are guaranteed to be valid. 42 | let (ix_accounts, ix_data) = unsafe { 43 | ( 44 | accounts.get_unchecked(..expected_accounts), 45 | instruction_data.get_unchecked(IX_HEADER_SIZE..data_offset), 46 | ) 47 | }; 48 | 49 | inner_process_instruction(ix_accounts, ix_data)?; 50 | 51 | if data_offset == instruction_data.len() { 52 | // The batch is complete. 53 | break; 54 | } 55 | 56 | accounts = &accounts[expected_accounts..]; 57 | instruction_data = &instruction_data[data_offset..]; 58 | } 59 | 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /p-token/src/processor/burn.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{shared, unpack_amount}, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_burn(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { 8 | let amount = unpack_amount(instruction_data)?; 9 | 10 | shared::burn::process_burn(accounts, amount, None) 11 | } 12 | -------------------------------------------------------------------------------- /p-token/src/processor/burn_checked.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{shared, unpack_amount_and_decimals}, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_burn_checked(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { 8 | let (amount, decimals) = unpack_amount_and_decimals(instruction_data)?; 9 | 10 | shared::burn::process_burn(accounts, amount, Some(decimals)) 11 | } 12 | -------------------------------------------------------------------------------- /p-token/src/processor/close_account.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::validate_owner, 3 | pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, 4 | spl_token_interface::{ 5 | error::TokenError, 6 | state::{ 7 | account::{Account, INCINERATOR_ID}, 8 | load, 9 | }, 10 | }, 11 | }; 12 | 13 | #[inline(always)] 14 | pub fn process_close_account(accounts: &[AccountInfo]) -> ProgramResult { 15 | let [source_account_info, destination_account_info, authority_info, remaining @ ..] = accounts 16 | else { 17 | return Err(ProgramError::NotEnoughAccountKeys); 18 | }; 19 | 20 | // Comparing whether the AccountInfo's "point" to the same account or 21 | // not - this is a faster comparison since it just checks the internal 22 | // raw pointer. 23 | if source_account_info == destination_account_info { 24 | return Err(ProgramError::InvalidAccountData); 25 | } else { 26 | // SAFETY: scoped immutable borrow to `source_account_info` account data and 27 | // `load` validates that the account is initialized. 28 | let source_account = 29 | unsafe { load::(source_account_info.borrow_data_unchecked())? }; 30 | 31 | if !source_account.is_native() && source_account.amount() != 0 { 32 | return Err(TokenError::NonNativeHasBalance.into()); 33 | } 34 | 35 | let authority = source_account 36 | .close_authority() 37 | .unwrap_or(&source_account.owner); 38 | 39 | if !source_account.is_owned_by_system_program_or_incinerator() { 40 | validate_owner(authority, authority_info, remaining)?; 41 | } else if destination_account_info.key() != &INCINERATOR_ID { 42 | return Err(ProgramError::InvalidAccountData); 43 | } 44 | } 45 | 46 | let destination_starting_lamports = destination_account_info.lamports(); 47 | // SAFETY: single mutable borrow to `destination_account_info` lamports and 48 | // there are no "active" borrows of `source_account_info` account data. 49 | unsafe { 50 | // Moves the lamports to the destination account. 51 | *destination_account_info.borrow_mut_lamports_unchecked() = destination_starting_lamports 52 | .checked_add(source_account_info.lamports()) 53 | .ok_or(TokenError::Overflow)?; 54 | // Closes the source account. 55 | source_account_info.close_unchecked(); 56 | } 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /p-token/src/processor/freeze_account.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::shared::toggle_account_state::process_toggle_account_state, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_freeze_account(accounts: &[AccountInfo]) -> ProgramResult { 8 | process_toggle_account_state(accounts, true) 9 | } 10 | -------------------------------------------------------------------------------- /p-token/src/processor/get_account_data_size.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::check_account_owner, 3 | pinocchio::{ 4 | account_info::AccountInfo, program::set_return_data, program_error::ProgramError, 5 | ProgramResult, 6 | }, 7 | spl_token_interface::{ 8 | error::TokenError, 9 | state::{account::Account, load, mint::Mint, Transmutable}, 10 | }, 11 | }; 12 | 13 | #[inline(always)] 14 | pub fn process_get_account_data_size(accounts: &[AccountInfo]) -> ProgramResult { 15 | let [mint_info, _remaining @ ..] = accounts else { 16 | return Err(ProgramError::NotEnoughAccountKeys); 17 | }; 18 | 19 | // Make sure the mint is valid. 20 | check_account_owner(mint_info)?; 21 | 22 | // SAFETY: single immutable borrow to `mint_info` account data and 23 | // `load` validates that the mint is initialized. 24 | let _ = unsafe { 25 | load::(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)? 26 | }; 27 | 28 | set_return_data(&Account::LEN.to_le_bytes()); 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /p-token/src/processor/initialize_account.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::shared, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_initialize_account(accounts: &[AccountInfo]) -> ProgramResult { 8 | shared::initialize_account::process_initialize_account(accounts, None, true) 9 | } 10 | -------------------------------------------------------------------------------- /p-token/src/processor/initialize_account2.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::shared, 3 | pinocchio::{ 4 | account_info::AccountInfo, 5 | pubkey::{Pubkey, PUBKEY_BYTES}, 6 | ProgramResult, 7 | }, 8 | spl_token_interface::error::TokenError, 9 | }; 10 | 11 | #[inline(always)] 12 | pub fn process_initialize_account2( 13 | accounts: &[AccountInfo], 14 | instruction_data: &[u8], 15 | ) -> ProgramResult { 16 | let owner = if instruction_data.len() >= PUBKEY_BYTES { 17 | // SAFETY: The minimum size of the instruction data is `PUBKEY_BYTES` bytes. 18 | unsafe { &*(instruction_data.as_ptr() as *const Pubkey) } 19 | } else { 20 | return Err(TokenError::InvalidInstruction.into()); 21 | }; 22 | 23 | shared::initialize_account::process_initialize_account(accounts, Some(owner), true) 24 | } 25 | -------------------------------------------------------------------------------- /p-token/src/processor/initialize_account3.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::shared, 3 | pinocchio::{ 4 | account_info::AccountInfo, 5 | pubkey::{Pubkey, PUBKEY_BYTES}, 6 | ProgramResult, 7 | }, 8 | spl_token_interface::error::TokenError, 9 | }; 10 | 11 | #[inline(always)] 12 | pub fn process_initialize_account3( 13 | accounts: &[AccountInfo], 14 | instruction_data: &[u8], 15 | ) -> ProgramResult { 16 | let owner = if instruction_data.len() >= PUBKEY_BYTES { 17 | // SAFETY: The minimum size of the instruction data is `PUBKEY_BYTES` bytes. 18 | unsafe { &*(instruction_data.as_ptr() as *const Pubkey) } 19 | } else { 20 | return Err(TokenError::InvalidInstruction.into()); 21 | }; 22 | 23 | shared::initialize_account::process_initialize_account(accounts, Some(owner), false) 24 | } 25 | -------------------------------------------------------------------------------- /p-token/src/processor/initialize_immutable_owner.rs: -------------------------------------------------------------------------------- 1 | use { 2 | pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, 3 | spl_token_interface::{ 4 | error::TokenError, 5 | state::{account::Account, load_unchecked, Initializable}, 6 | }, 7 | }; 8 | 9 | #[inline(always)] 10 | pub fn process_initialize_immutable_owner(accounts: &[AccountInfo]) -> ProgramResult { 11 | let token_account_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; 12 | 13 | // SAFETY: single immutable borrow to `token_account_info` account data. 14 | let account = unsafe { load_unchecked::(token_account_info.borrow_data_unchecked())? }; 15 | 16 | if account.is_initialized()? { 17 | return Err(TokenError::AlreadyInUse.into()); 18 | } 19 | // Please upgrade to SPL Token 2022 for immutable owner support. 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /p-token/src/processor/initialize_mint.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::shared, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_initialize_mint(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { 8 | shared::initialize_mint::process_initialize_mint(accounts, instruction_data, true) 9 | } 10 | -------------------------------------------------------------------------------- /p-token/src/processor/initialize_mint2.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::shared::initialize_mint::process_initialize_mint, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_initialize_mint2( 8 | accounts: &[AccountInfo], 9 | instruction_data: &[u8], 10 | ) -> ProgramResult { 11 | process_initialize_mint(accounts, instruction_data, false) 12 | } 13 | -------------------------------------------------------------------------------- /p-token/src/processor/initialize_multisig.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::shared, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | spl_token_interface::error::TokenError, 5 | }; 6 | 7 | #[inline(always)] 8 | pub fn process_initialize_multisig( 9 | accounts: &[AccountInfo], 10 | instruction_data: &[u8], 11 | ) -> ProgramResult { 12 | let m = instruction_data 13 | .first() 14 | .ok_or(TokenError::InvalidInstruction)?; 15 | 16 | shared::initialize_multisig::process_initialize_multisig(accounts, *m, true) 17 | } 18 | -------------------------------------------------------------------------------- /p-token/src/processor/initialize_multisig2.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::shared, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | spl_token_interface::error::TokenError, 5 | }; 6 | 7 | pub fn process_initialize_multisig2( 8 | accounts: &[AccountInfo], 9 | instruction_data: &[u8], 10 | ) -> ProgramResult { 11 | let m = instruction_data 12 | .first() 13 | .ok_or(TokenError::InvalidInstruction)?; 14 | 15 | shared::initialize_multisig::process_initialize_multisig(accounts, *m, false) 16 | } 17 | -------------------------------------------------------------------------------- /p-token/src/processor/mint_to.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{shared, unpack_amount}, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_mint_to(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { 8 | let amount = unpack_amount(instruction_data)?; 9 | 10 | shared::mint_to::process_mint_to(accounts, amount, None) 11 | } 12 | -------------------------------------------------------------------------------- /p-token/src/processor/mint_to_checked.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{shared, unpack_amount_and_decimals}, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_mint_to_checked(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { 8 | let (amount, decimals) = unpack_amount_and_decimals(instruction_data)?; 9 | 10 | shared::mint_to::process_mint_to(accounts, amount, Some(decimals)) 11 | } 12 | -------------------------------------------------------------------------------- /p-token/src/processor/revoke.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::validate_owner, 3 | pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, 4 | spl_token_interface::{ 5 | error::TokenError, 6 | state::{account::Account, load_mut}, 7 | }, 8 | }; 9 | 10 | #[inline(always)] 11 | pub fn process_revoke(accounts: &[AccountInfo]) -> ProgramResult { 12 | let [source_account_info, remaining @ ..] = accounts else { 13 | return Err(ProgramError::NotEnoughAccountKeys); 14 | }; 15 | 16 | // SAFETY: single mutable borrow to `source_account_info` account data and 17 | // `load_mut` validates that the account is initialized. 18 | let source_account = 19 | unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; 20 | 21 | // Unpacking the remaining accounts to get the owner account at this point 22 | // to maintain the same order as SPL Token. 23 | let [owner_info, remaining @ ..] = remaining else { 24 | return Err(ProgramError::NotEnoughAccountKeys); 25 | }; 26 | 27 | if source_account.is_frozen()? { 28 | return Err(TokenError::AccountFrozen.into()); 29 | } 30 | 31 | validate_owner(&source_account.owner, owner_info, remaining)?; 32 | 33 | source_account.clear_delegate(); 34 | source_account.set_delegated_amount(0); 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /p-token/src/processor/shared/approve.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::processor::validate_owner, 3 | pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, 4 | spl_token_interface::{ 5 | error::TokenError, 6 | state::{account::Account, load, load_mut, mint::Mint}, 7 | }, 8 | }; 9 | 10 | #[inline(always)] 11 | pub fn process_approve( 12 | accounts: &[AccountInfo], 13 | amount: u64, 14 | expected_decimals: Option, 15 | ) -> ProgramResult { 16 | // Accounts expected depend on whether we have the mint `decimals` or not; when 17 | // we have the mint `decimals`, we expect the mint account to be present. 18 | 19 | let (source_account_info, expected_mint_info, delegate_info, owner_info, remaining) = 20 | if let Some(expected_decimals) = expected_decimals { 21 | let [source_account_info, expected_mint_info, delegate_info, owner_info, remaining @ ..] = 22 | accounts 23 | else { 24 | return Err(ProgramError::NotEnoughAccountKeys); 25 | }; 26 | 27 | ( 28 | source_account_info, 29 | Some((expected_mint_info, expected_decimals)), 30 | delegate_info, 31 | owner_info, 32 | remaining, 33 | ) 34 | } else { 35 | let [source_account_info, delegate_info, owner_info, remaining @ ..] = accounts else { 36 | return Err(ProgramError::NotEnoughAccountKeys); 37 | }; 38 | ( 39 | source_account_info, 40 | None, 41 | delegate_info, 42 | owner_info, 43 | remaining, 44 | ) 45 | }; 46 | 47 | // Validates source account. 48 | 49 | // SAFETY: single mutable borrow to `source_account_info` account data and 50 | // `load_mut` validates that the account is initialized. 51 | let source_account = 52 | unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; 53 | 54 | if source_account.is_frozen()? { 55 | return Err(TokenError::AccountFrozen.into()); 56 | } 57 | 58 | if let Some((mint_info, expected_decimals)) = expected_mint_info { 59 | if mint_info.key() != &source_account.mint { 60 | return Err(TokenError::MintMismatch.into()); 61 | } 62 | 63 | // SAFETY: single immutable borrow of `mint_info` account data and 64 | // `load` validates that the mint is initialized. 65 | let mint = unsafe { load::(mint_info.borrow_data_unchecked())? }; 66 | 67 | if expected_decimals != mint.decimals { 68 | return Err(TokenError::MintDecimalsMismatch.into()); 69 | } 70 | } 71 | 72 | validate_owner(&source_account.owner, owner_info, remaining)?; 73 | 74 | // Sets the delegate and delegated amount. 75 | 76 | source_account.set_delegate(delegate_info.key()); 77 | source_account.set_delegated_amount(amount); 78 | 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /p-token/src/processor/shared/burn.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::processor::{check_account_owner, validate_owner}, 3 | pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, 4 | spl_token_interface::{ 5 | error::TokenError, 6 | state::{account::Account, load_mut, mint::Mint}, 7 | }, 8 | }; 9 | 10 | #[inline(always)] 11 | pub fn process_burn( 12 | accounts: &[AccountInfo], 13 | amount: u64, 14 | expected_decimals: Option, 15 | ) -> ProgramResult { 16 | let [source_account_info, mint_info, authority_info, remaining @ ..] = accounts else { 17 | return Err(ProgramError::NotEnoughAccountKeys); 18 | }; 19 | 20 | // SAFETY: single mutable borrow to `source_account_info` account data and 21 | // `load_mut` validates that the account is initialized. 22 | let source_account = 23 | unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; 24 | // SAFETY: single mutable borrow to `mint_info` account data and 25 | // `load_mut` validates that the mint is initialized; additionally, an 26 | // account cannot be both a token account and a mint, so if duplicates are 27 | // passed in, one of them will fail the `load_mut` check. 28 | let mint = unsafe { load_mut::(mint_info.borrow_mut_data_unchecked())? }; 29 | 30 | if source_account.is_frozen()? { 31 | return Err(TokenError::AccountFrozen.into()); 32 | } 33 | if source_account.is_native() { 34 | return Err(TokenError::NativeNotSupported.into()); 35 | } 36 | 37 | // Ensure the source account has the sufficient amount. This is done before 38 | // the value is updated on the account. 39 | let updated_source_amount = source_account 40 | .amount() 41 | .checked_sub(amount) 42 | .ok_or(TokenError::InsufficientFunds)?; 43 | 44 | if mint_info.key() != &source_account.mint { 45 | return Err(TokenError::MintMismatch.into()); 46 | } 47 | 48 | if let Some(expected_decimals) = expected_decimals { 49 | if expected_decimals != mint.decimals { 50 | return Err(TokenError::MintDecimalsMismatch.into()); 51 | } 52 | } 53 | 54 | if !source_account.is_owned_by_system_program_or_incinerator() { 55 | match source_account.delegate() { 56 | Some(delegate) if authority_info.key() == delegate => { 57 | validate_owner(delegate, authority_info, remaining)?; 58 | 59 | let delegated_amount = source_account 60 | .delegated_amount() 61 | .checked_sub(amount) 62 | .ok_or(TokenError::InsufficientFunds)?; 63 | source_account.set_delegated_amount(delegated_amount); 64 | 65 | if delegated_amount == 0 { 66 | source_account.clear_delegate(); 67 | } 68 | } 69 | _ => { 70 | validate_owner(&source_account.owner, authority_info, remaining)?; 71 | } 72 | } 73 | } 74 | 75 | // Updates the source account and mint supply. 76 | 77 | if amount == 0 { 78 | check_account_owner(source_account_info)?; 79 | check_account_owner(mint_info)?; 80 | } else { 81 | source_account.set_amount(updated_source_amount); 82 | // Note: The amount of a token account is always within the range of the 83 | // mint supply (`u64`). 84 | let mint_supply = mint.supply().checked_sub(amount).unwrap(); 85 | mint.set_supply(mint_supply); 86 | } 87 | 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /p-token/src/processor/shared/initialize_account.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::processor::check_account_owner, 3 | pinocchio::{ 4 | account_info::AccountInfo, 5 | program_error::ProgramError, 6 | pubkey::Pubkey, 7 | sysvars::{rent::Rent, Sysvar}, 8 | ProgramResult, 9 | }, 10 | spl_token_interface::{ 11 | error::TokenError, 12 | native_mint::is_native_mint, 13 | state::{ 14 | account::Account, account_state::AccountState, load, load_mut_unchecked, mint::Mint, 15 | Initializable, 16 | }, 17 | }, 18 | }; 19 | 20 | #[inline(always)] 21 | #[allow(clippy::arithmetic_side_effects)] 22 | pub fn process_initialize_account( 23 | accounts: &[AccountInfo], 24 | owner: Option<&Pubkey>, 25 | rent_sysvar_account_provided: bool, 26 | ) -> ProgramResult { 27 | // Accounts expected depend on whether we have the `rent_sysvar` account or not. 28 | 29 | let (new_account_info, mint_info, owner, remaining) = if let Some(owner) = owner { 30 | let [new_account_info, mint_info, remaining @ ..] = accounts else { 31 | return Err(ProgramError::NotEnoughAccountKeys); 32 | }; 33 | (new_account_info, mint_info, owner, remaining) 34 | } else { 35 | let [new_account_info, mint_info, owner_info, remaining @ ..] = accounts else { 36 | return Err(ProgramError::NotEnoughAccountKeys); 37 | }; 38 | (new_account_info, mint_info, owner_info.key(), remaining) 39 | }; 40 | 41 | // Check rent-exempt status of the token account. 42 | 43 | let new_account_info_data_len = new_account_info.data_len(); 44 | 45 | let minimum_balance = if rent_sysvar_account_provided { 46 | let rent_sysvar_info = remaining 47 | .first() 48 | .ok_or(ProgramError::NotEnoughAccountKeys)?; 49 | // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length 50 | // are checked by `from_account_info_unchecked`. 51 | let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? }; 52 | rent.minimum_balance(new_account_info_data_len) 53 | } else { 54 | Rent::get()?.minimum_balance(new_account_info_data_len) 55 | }; 56 | 57 | let is_native_mint = is_native_mint(mint_info.key()); 58 | 59 | // Initialize the account. 60 | 61 | // SAFETY: single mutable borrow of the 'new_account_info' account data. 62 | let account = 63 | unsafe { load_mut_unchecked::(new_account_info.borrow_mut_data_unchecked())? }; 64 | 65 | if account.is_initialized()? { 66 | return Err(TokenError::AlreadyInUse.into()); 67 | } 68 | 69 | if new_account_info.lamports() < minimum_balance { 70 | return Err(TokenError::NotRentExempt.into()); 71 | } 72 | 73 | if !is_native_mint { 74 | check_account_owner(mint_info)?; 75 | 76 | // SAFETY: single immutable borrow of `mint_info` account data and 77 | // `load` validates that the mint is initialized. 78 | let _ = unsafe { 79 | load::(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)? 80 | }; 81 | } 82 | 83 | account.set_account_state(AccountState::Initialized); 84 | account.mint = *mint_info.key(); 85 | account.owner = *owner; 86 | 87 | if is_native_mint { 88 | account.set_native(true); 89 | account.set_native_amount(minimum_balance); 90 | // `new_account_info` lamports are already checked to be greater than or equal 91 | // to the minimum balance. 92 | account.set_amount(new_account_info.lamports() - minimum_balance); 93 | } 94 | 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /p-token/src/processor/shared/initialize_mint.rs: -------------------------------------------------------------------------------- 1 | use { 2 | pinocchio::{ 3 | account_info::AccountInfo, 4 | program_error::ProgramError, 5 | pubkey::Pubkey, 6 | sysvars::{rent::Rent, Sysvar}, 7 | ProgramResult, 8 | }, 9 | spl_token_interface::{ 10 | error::TokenError, 11 | state::{load_mut_unchecked, mint::Mint, Initializable}, 12 | }, 13 | }; 14 | 15 | #[inline(always)] 16 | pub fn process_initialize_mint( 17 | accounts: &[AccountInfo], 18 | instruction_data: &[u8], 19 | rent_sysvar_account_provided: bool, 20 | ) -> ProgramResult { 21 | // Validates the instruction data. 22 | 23 | let (decimals, mint_authority, freeze_authority) = if instruction_data.len() >= 34 { 24 | // SAFETY: The minimum size of the instruction data is either 34 or 66 bytes: 25 | // - decimals (1 byte) 26 | // - mint_authority (32 bytes) 27 | // - option + freeze_authority (1 byte + 32 bytes) 28 | unsafe { 29 | let decimals = *instruction_data.get_unchecked(0); 30 | let mint_authority = &*(instruction_data.as_ptr().add(1) as *const Pubkey); 31 | let freeze_authority = if *instruction_data.get_unchecked(33) == 0 { 32 | None 33 | } else if *instruction_data.get_unchecked(33) == 1 && instruction_data.len() >= 66 { 34 | Some(&*(instruction_data.as_ptr().add(34) as *const Pubkey)) 35 | } else { 36 | return Err(TokenError::InvalidInstruction.into()); 37 | }; 38 | 39 | (decimals, mint_authority, freeze_authority) 40 | } 41 | } else { 42 | return Err(TokenError::InvalidInstruction.into()); 43 | }; 44 | 45 | // Validates the accounts. 46 | 47 | let (mint_info, rent_sysvar_info) = if rent_sysvar_account_provided { 48 | let [mint_info, rent_sysvar_info, _remaining @ ..] = accounts else { 49 | return Err(ProgramError::NotEnoughAccountKeys); 50 | }; 51 | (mint_info, Some(rent_sysvar_info)) 52 | } else { 53 | let [mint_info, _remaining @ ..] = accounts else { 54 | return Err(ProgramError::NotEnoughAccountKeys); 55 | }; 56 | (mint_info, None) 57 | }; 58 | 59 | let mint_data_len = mint_info.data_len(); 60 | 61 | let is_exempt = if let Some(rent_sysvar_info) = rent_sysvar_info { 62 | // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length 63 | // are checked by `from_account_info_unchecked`. 64 | let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? }; 65 | rent.is_exempt(mint_info.lamports(), mint_data_len) 66 | } else { 67 | Rent::get()?.is_exempt(mint_info.lamports(), mint_data_len) 68 | }; 69 | 70 | // SAFETY: single mutable borrow to `mint_info` account data. 71 | let mint = unsafe { load_mut_unchecked::(mint_info.borrow_mut_data_unchecked())? }; 72 | 73 | if mint.is_initialized()? { 74 | return Err(TokenError::AlreadyInUse.into()); 75 | } 76 | 77 | if !is_exempt { 78 | return Err(TokenError::NotRentExempt.into()); 79 | } 80 | 81 | // Initialize the mint. 82 | 83 | mint.set_initialized(); 84 | mint.set_mint_authority(mint_authority); 85 | mint.decimals = decimals; 86 | 87 | if let Some(freeze_authority) = freeze_authority { 88 | mint.set_freeze_authority(freeze_authority); 89 | } 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /p-token/src/processor/shared/initialize_multisig.rs: -------------------------------------------------------------------------------- 1 | use { 2 | pinocchio::{ 3 | account_info::AccountInfo, 4 | program_error::ProgramError, 5 | sysvars::{rent::Rent, Sysvar}, 6 | ProgramResult, 7 | }, 8 | spl_token_interface::{ 9 | error::TokenError, 10 | state::{load_mut_unchecked, multisig::Multisig, Initializable}, 11 | }, 12 | }; 13 | 14 | #[inline(always)] 15 | pub fn process_initialize_multisig( 16 | accounts: &[AccountInfo], 17 | m: u8, 18 | rent_sysvar_account_provided: bool, 19 | ) -> ProgramResult { 20 | // Accounts expected depend on whether we have the `rent_sysvar` account or not. 21 | 22 | let (multisig_info, rent_sysvar_info, remaining) = if rent_sysvar_account_provided { 23 | let [multisig_info, rent_sysvar_info, remaining @ ..] = accounts else { 24 | return Err(ProgramError::NotEnoughAccountKeys); 25 | }; 26 | (multisig_info, Some(rent_sysvar_info), remaining) 27 | } else { 28 | let [multisig_info, remaining @ ..] = accounts else { 29 | return Err(ProgramError::NotEnoughAccountKeys); 30 | }; 31 | (multisig_info, None, remaining) 32 | }; 33 | 34 | let multisig_info_data_len = multisig_info.data_len(); 35 | 36 | let is_exempt = if let Some(rent_sysvar_info) = rent_sysvar_info { 37 | // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length 38 | // are checked by `from_account_info_unchecked`. 39 | let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? }; 40 | rent.is_exempt(multisig_info.lamports(), multisig_info_data_len) 41 | } else { 42 | Rent::get()?.is_exempt(multisig_info.lamports(), multisig_info_data_len) 43 | }; 44 | 45 | // SAFETY: single mutable borrow to `multisig_info` account data. 46 | let multisig = 47 | unsafe { load_mut_unchecked::(multisig_info.borrow_mut_data_unchecked())? }; 48 | 49 | if multisig.is_initialized()? { 50 | return Err(TokenError::AlreadyInUse.into()); 51 | } 52 | 53 | if !is_exempt { 54 | return Err(TokenError::NotRentExempt.into()); 55 | } 56 | 57 | // Initialize the multisig account. 58 | 59 | multisig.m = m; 60 | multisig.n = remaining.len() as u8; 61 | 62 | if !Multisig::is_valid_signer_index(multisig.n) { 63 | return Err(TokenError::InvalidNumberOfProvidedSigners.into()); 64 | } 65 | if !Multisig::is_valid_signer_index(multisig.m) { 66 | return Err(TokenError::InvalidNumberOfRequiredSigners.into()); 67 | } 68 | 69 | for (i, signer_info) in remaining.iter().enumerate() { 70 | multisig.signers[i] = *signer_info.key(); 71 | } 72 | 73 | multisig.set_initialized(true); 74 | 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /p-token/src/processor/shared/mint_to.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::processor::{check_account_owner, validate_owner}, 3 | pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, 4 | spl_token_interface::{ 5 | error::TokenError, 6 | state::{account::Account, load_mut, mint::Mint}, 7 | }, 8 | }; 9 | 10 | #[inline(always)] 11 | #[allow(clippy::arithmetic_side_effects)] 12 | pub fn process_mint_to( 13 | accounts: &[AccountInfo], 14 | amount: u64, 15 | expected_decimals: Option, 16 | ) -> ProgramResult { 17 | let [mint_info, destination_account_info, owner_info, remaining @ ..] = accounts else { 18 | return Err(ProgramError::NotEnoughAccountKeys); 19 | }; 20 | 21 | // Validates the destination account. 22 | 23 | // SAFETY: single mutable borrow to `destination_account_info` account data and 24 | // `load_mut` validates that the account is initialized. 25 | let destination_account = 26 | unsafe { load_mut::(destination_account_info.borrow_mut_data_unchecked())? }; 27 | 28 | if destination_account.is_frozen()? { 29 | return Err(TokenError::AccountFrozen.into()); 30 | } 31 | 32 | if destination_account.is_native() { 33 | return Err(TokenError::NativeNotSupported.into()); 34 | } 35 | 36 | if mint_info.key() != &destination_account.mint { 37 | return Err(TokenError::MintMismatch.into()); 38 | } 39 | 40 | // SAFETY: single mutable borrow to `mint_info` account data and 41 | // `load_mut` validates that the mint is initialized. 42 | let mint = unsafe { load_mut::(mint_info.borrow_mut_data_unchecked())? }; 43 | 44 | if let Some(expected_decimals) = expected_decimals { 45 | if expected_decimals != mint.decimals { 46 | return Err(TokenError::MintDecimalsMismatch.into()); 47 | } 48 | } 49 | 50 | match mint.mint_authority() { 51 | Some(mint_authority) => validate_owner(mint_authority, owner_info, remaining)?, 52 | None => return Err(TokenError::FixedSupply.into()), 53 | } 54 | 55 | if amount == 0 { 56 | // Validates the accounts' owner since we are not writing 57 | // to these account. 58 | check_account_owner(mint_info)?; 59 | check_account_owner(destination_account_info)?; 60 | } else { 61 | let mint_supply = mint 62 | .supply() 63 | .checked_add(amount) 64 | .ok_or(TokenError::Overflow)?; 65 | mint.set_supply(mint_supply); 66 | 67 | // This should not fail since there is no overflow on the mint supply. 68 | destination_account.set_amount(destination_account.amount() + amount); 69 | } 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /p-token/src/processor/shared/mod.rs: -------------------------------------------------------------------------------- 1 | //! Shared processor functions. 2 | //! 3 | //! This module contains the shared processor functions that are used by 4 | //! the multiple instruction processors. 5 | 6 | pub mod approve; 7 | pub mod burn; 8 | pub mod initialize_account; 9 | pub mod initialize_mint; 10 | pub mod initialize_multisig; 11 | pub mod mint_to; 12 | pub mod toggle_account_state; 13 | pub mod transfer; 14 | -------------------------------------------------------------------------------- /p-token/src/processor/shared/toggle_account_state.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::processor::validate_owner, 3 | pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, 4 | spl_token_interface::{ 5 | error::TokenError, 6 | state::{account::Account, account_state::AccountState, load, load_mut, mint::Mint}, 7 | }, 8 | }; 9 | 10 | #[inline(always)] 11 | pub fn process_toggle_account_state(accounts: &[AccountInfo], freeze: bool) -> ProgramResult { 12 | let [source_account_info, mint_info, authority_info, remaining @ ..] = accounts else { 13 | return Err(ProgramError::NotEnoughAccountKeys); 14 | }; 15 | 16 | // SAFETY: single mutable borrow to `source_account_info` account data and 17 | // `load_mut` validates that the account is initialized. 18 | let source_account = 19 | unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; 20 | 21 | if freeze == source_account.is_frozen()? { 22 | return Err(TokenError::InvalidState.into()); 23 | } 24 | if source_account.is_native() { 25 | return Err(TokenError::NativeNotSupported.into()); 26 | } 27 | if mint_info.key() != &source_account.mint { 28 | return Err(TokenError::MintMismatch.into()); 29 | } 30 | 31 | // SAFETY: single immutable borrow of `mint_info` account data and 32 | // `load` validates that the mint is initialized; additionally, an 33 | // account cannot be both a token account and a mint, so if duplicates are 34 | // passed in, one of them will fail the `load` check. 35 | let mint = unsafe { load::(mint_info.borrow_data_unchecked())? }; 36 | 37 | match mint.freeze_authority() { 38 | Some(authority) => validate_owner(authority, authority_info, remaining), 39 | None => Err(TokenError::MintCannotFreeze.into()), 40 | }?; 41 | 42 | source_account.set_account_state(if freeze { 43 | AccountState::Frozen 44 | } else { 45 | AccountState::Initialized 46 | }); 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /p-token/src/processor/sync_native.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::check_account_owner, 3 | pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, 4 | spl_token_interface::{ 5 | error::TokenError, 6 | state::{account::Account, load_mut}, 7 | }, 8 | }; 9 | 10 | #[inline(always)] 11 | pub fn process_sync_native(accounts: &[AccountInfo]) -> ProgramResult { 12 | let native_account_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; 13 | 14 | check_account_owner(native_account_info)?; 15 | 16 | // SAFETY: single mutable borrow to `native_account_info` account data and 17 | // `load_mut` validates that the account is initialized. 18 | let native_account = 19 | unsafe { load_mut::(native_account_info.borrow_mut_data_unchecked())? }; 20 | 21 | if let Option::Some(rent_exempt_reserve) = native_account.native_amount() { 22 | let new_amount = native_account_info 23 | .lamports() 24 | .checked_sub(rent_exempt_reserve) 25 | .ok_or(TokenError::Overflow)?; 26 | 27 | if new_amount < native_account.amount() { 28 | return Err(TokenError::InvalidState.into()); 29 | } 30 | native_account.set_amount(new_amount); 31 | } else { 32 | return Err(TokenError::NonNativeNotSupported.into()); 33 | } 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /p-token/src/processor/thaw_account.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::shared::toggle_account_state::process_toggle_account_state, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_thaw_account(accounts: &[AccountInfo]) -> ProgramResult { 8 | process_toggle_account_state(accounts, false) 9 | } 10 | -------------------------------------------------------------------------------- /p-token/src/processor/transfer.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{shared, unpack_amount}, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_transfer(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { 8 | let amount = unpack_amount(instruction_data)?; 9 | 10 | shared::transfer::process_transfer(accounts, amount, None) 11 | } 12 | -------------------------------------------------------------------------------- /p-token/src/processor/transfer_checked.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{shared, unpack_amount_and_decimals}, 3 | pinocchio::{account_info::AccountInfo, ProgramResult}, 4 | }; 5 | 6 | #[inline(always)] 7 | pub fn process_transfer_checked( 8 | accounts: &[AccountInfo], 9 | instruction_data: &[u8], 10 | ) -> ProgramResult { 11 | let (amount, decimals) = unpack_amount_and_decimals(instruction_data)?; 12 | 13 | shared::transfer::process_transfer(accounts, amount, Some(decimals)) 14 | } 15 | -------------------------------------------------------------------------------- /p-token/src/processor/ui_amount_to_amount.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::{check_account_owner, try_ui_amount_into_amount}, 3 | core::str::from_utf8, 4 | pinocchio::{ 5 | account_info::AccountInfo, program::set_return_data, program_error::ProgramError, 6 | ProgramResult, 7 | }, 8 | spl_token_interface::{ 9 | error::TokenError, 10 | state::{load, mint::Mint}, 11 | }, 12 | }; 13 | 14 | pub fn process_ui_amount_to_amount( 15 | accounts: &[AccountInfo], 16 | instruction_data: &[u8], 17 | ) -> ProgramResult { 18 | let ui_amount = from_utf8(instruction_data).map_err(|_error| TokenError::InvalidInstruction)?; 19 | 20 | let mint_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; 21 | check_account_owner(mint_info)?; 22 | // SAFETY: single immutable borrow to `mint_info` account data and 23 | // `load` validates that the mint is initialized. 24 | let mint = unsafe { 25 | load::(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)? 26 | }; 27 | 28 | let amount = try_ui_amount_into_amount(ui_amount, mint.decimals)?; 29 | set_return_data(&amount.to_le_bytes()); 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /p-token/src/processor/withdraw_excess_lamports.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::validate_owner, 3 | pinocchio::{ 4 | account_info::AccountInfo, 5 | program_error::ProgramError, 6 | sysvars::{rent::Rent, Sysvar}, 7 | ProgramResult, 8 | }, 9 | spl_token_interface::{ 10 | error::TokenError, 11 | state::{account::Account, load, mint::Mint, multisig::Multisig, Transmutable}, 12 | }, 13 | }; 14 | 15 | #[allow(clippy::arithmetic_side_effects)] 16 | pub fn process_withdraw_excess_lamports(accounts: &[AccountInfo]) -> ProgramResult { 17 | let [source_account_info, destination_info, authority_info, remaining @ ..] = accounts else { 18 | return Err(ProgramError::NotEnoughAccountKeys); 19 | }; 20 | 21 | // SAFETY: single immutable borrow to `source_account_info` account data 22 | let source_data = unsafe { source_account_info.borrow_data_unchecked() }; 23 | 24 | match source_data.len() { 25 | Account::LEN => { 26 | // SAFETY: `source_data` has the same length as `Account`. 27 | let account = unsafe { load::(source_data)? }; 28 | 29 | if account.is_native() { 30 | return Err(TokenError::NativeNotSupported.into()); 31 | } 32 | 33 | validate_owner(&account.owner, authority_info, remaining)?; 34 | } 35 | Mint::LEN => { 36 | // SAFETY: `source_data` has the same length as `Mint`. 37 | let mint = unsafe { load::(source_data)? }; 38 | 39 | match mint.mint_authority() { 40 | Some(mint_authority) => { 41 | validate_owner(mint_authority, authority_info, remaining)?; 42 | } 43 | None if source_account_info == authority_info => { 44 | // Comparing whether the AccountInfo's "point" to the same account or 45 | // not - this is a faster comparison since it just checks the internal 46 | // raw pointer. 47 | // 48 | // This is a special case where there is no mint authority set but the mint 49 | // account is the same as the authority account and, therefore, needs to be 50 | // a signer. 51 | if !authority_info.is_signer() { 52 | return Err(ProgramError::MissingRequiredSignature); 53 | } 54 | } 55 | _ => { 56 | return Err(TokenError::AuthorityTypeNotSupported.into()); 57 | } 58 | } 59 | } 60 | Multisig::LEN => { 61 | validate_owner(source_account_info.key(), authority_info, remaining)?; 62 | } 63 | _ => return Err(TokenError::InvalidState.into()), 64 | } 65 | 66 | // Withdraws the excess lamports from the source account. 67 | 68 | let source_rent_exempt_reserve = Rent::get()?.minimum_balance(source_data.len()); 69 | 70 | let transfer_amount = source_account_info 71 | .lamports() 72 | .checked_sub(source_rent_exempt_reserve) 73 | .ok_or(TokenError::NotRentExempt)?; 74 | 75 | let source_starting_lamports = source_account_info.lamports(); 76 | // SAFETY: single mutable borrow to `source_account_info` lamports. 77 | unsafe { 78 | // Moves the lamports out of the source account. 79 | // 80 | // Note: The `transfer_amount` is guaranteed to be less than the source 81 | // account's lamports. 82 | *source_account_info.borrow_mut_lamports_unchecked() = 83 | source_starting_lamports - transfer_amount; 84 | } 85 | 86 | let destination_starting_lamports = destination_info.lamports(); 87 | // SAFETY: single mutable borrow to `destination_info` lamports. 88 | unsafe { 89 | // Moves the lamports to the destination account. 90 | *destination_info.borrow_mut_lamports_unchecked() = destination_starting_lamports 91 | .checked_add(transfer_amount) 92 | .ok_or(TokenError::Overflow)?; 93 | } 94 | 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /p-token/tests/amount_to_ui_amount.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}, 7 | }; 8 | 9 | #[tokio::test] 10 | async fn amount_to_ui_amount() { 11 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 12 | .start_with_context() 13 | .await; 14 | 15 | // Given a mint account. 16 | 17 | let mint_authority = Pubkey::new_unique(); 18 | let freeze_authority = Pubkey::new_unique(); 19 | 20 | let mint = mint::initialize( 21 | &mut context, 22 | mint_authority, 23 | Some(freeze_authority), 24 | &TOKEN_PROGRAM_ID, 25 | ) 26 | .await 27 | .unwrap(); 28 | 29 | let amount_to_ui_amount_ix = 30 | spl_token::instruction::amount_to_ui_amount(&spl_token::ID, &mint, 1000).unwrap(); 31 | 32 | let tx = Transaction::new_signed_with_payer( 33 | &[amount_to_ui_amount_ix], 34 | Some(&context.payer.pubkey()), 35 | &[&context.payer], 36 | context.last_blockhash, 37 | ); 38 | context.banks_client.process_transaction(tx).await.unwrap(); 39 | 40 | // Then the transaction should succeed. 41 | 42 | let account = context.banks_client.get_account(mint).await.unwrap(); 43 | 44 | assert!(account.is_some()); 45 | } 46 | -------------------------------------------------------------------------------- /p-token/tests/approve.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | transaction::Transaction, 11 | }, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn approve() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Pubkey::new_unique(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account with 100 tokens. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | mint::mint( 42 | &mut context, 43 | &mint, 44 | &account, 45 | &mint_authority, 46 | 100, 47 | &TOKEN_PROGRAM_ID, 48 | ) 49 | .await 50 | .unwrap(); 51 | 52 | // When we approve a delegate. 53 | 54 | let delegate = Pubkey::new_unique(); 55 | 56 | let approve_ix = spl_token::instruction::approve( 57 | &spl_token::ID, 58 | &account, 59 | &delegate, 60 | &owner.pubkey(), 61 | &[], 62 | 50, 63 | ) 64 | .unwrap(); 65 | 66 | let tx = Transaction::new_signed_with_payer( 67 | &[approve_ix], 68 | Some(&context.payer.pubkey()), 69 | &[&context.payer, &owner], 70 | context.last_blockhash, 71 | ); 72 | context.banks_client.process_transaction(tx).await.unwrap(); 73 | 74 | // Then the account should have the delegate and delegated amount. 75 | 76 | let account = context.banks_client.get_account(account).await.unwrap(); 77 | 78 | assert!(account.is_some()); 79 | 80 | let account = account.unwrap(); 81 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 82 | 83 | assert!(account.delegate.is_some()); 84 | assert!(account.delegate.unwrap() == delegate); 85 | assert!(account.delegated_amount == 50); 86 | } 87 | -------------------------------------------------------------------------------- /p-token/tests/approve_checked.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | transaction::Transaction, 11 | }, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn approve_checked() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Pubkey::new_unique(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account with 100 tokens. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | mint::mint( 42 | &mut context, 43 | &mint, 44 | &account, 45 | &mint_authority, 46 | 100, 47 | &TOKEN_PROGRAM_ID, 48 | ) 49 | .await 50 | .unwrap(); 51 | 52 | // When we approve a delegate. 53 | 54 | let delegate = Pubkey::new_unique(); 55 | 56 | let approve_ix = spl_token::instruction::approve_checked( 57 | &TOKEN_PROGRAM_ID, 58 | &account, 59 | &mint, 60 | &delegate, 61 | &owner.pubkey(), 62 | &[], 63 | 50, 64 | 4, 65 | ) 66 | .unwrap(); 67 | 68 | let tx = Transaction::new_signed_with_payer( 69 | &[approve_ix], 70 | Some(&context.payer.pubkey()), 71 | &[&context.payer, &owner], 72 | context.last_blockhash, 73 | ); 74 | context.banks_client.process_transaction(tx).await.unwrap(); 75 | 76 | // Then the account should have the delegate and delegated amount. 77 | 78 | let account = context.banks_client.get_account(account).await.unwrap(); 79 | 80 | assert!(account.is_some()); 81 | 82 | let account = account.unwrap(); 83 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 84 | 85 | assert!(account.delegate.is_some()); 86 | assert!(account.delegate.unwrap() == delegate); 87 | assert!(account.delegated_amount == 50); 88 | } 89 | -------------------------------------------------------------------------------- /p-token/tests/burn.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | transaction::Transaction, 11 | }, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn burn() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Pubkey::new_unique(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account with 100 tokens. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | mint::mint( 42 | &mut context, 43 | &mint, 44 | &account, 45 | &mint_authority, 46 | 100, 47 | &TOKEN_PROGRAM_ID, 48 | ) 49 | .await 50 | .unwrap(); 51 | 52 | // When we burn 50 tokens. 53 | 54 | let burn_ix = 55 | spl_token::instruction::burn(&spl_token::ID, &account, &mint, &owner.pubkey(), &[], 50) 56 | .unwrap(); 57 | 58 | let tx = Transaction::new_signed_with_payer( 59 | &[burn_ix], 60 | Some(&context.payer.pubkey()), 61 | &[&context.payer, &owner], 62 | context.last_blockhash, 63 | ); 64 | context.banks_client.process_transaction(tx).await.unwrap(); 65 | 66 | // Then the account should have 50 tokens remaining. 67 | 68 | let account = context.banks_client.get_account(account).await.unwrap(); 69 | 70 | assert!(account.is_some()); 71 | 72 | let account = account.unwrap(); 73 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 74 | 75 | assert!(account.amount == 50); 76 | } 77 | -------------------------------------------------------------------------------- /p-token/tests/burn_checked.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | transaction::Transaction, 11 | }, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn burn_checked() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Pubkey::new_unique(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account with 100 tokens. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | mint::mint( 42 | &mut context, 43 | &mint, 44 | &account, 45 | &mint_authority, 46 | 100, 47 | &TOKEN_PROGRAM_ID, 48 | ) 49 | .await 50 | .unwrap(); 51 | 52 | // When we burn 50 tokens. 53 | 54 | let burn_ix = spl_token::instruction::burn_checked( 55 | &spl_token::ID, 56 | &account, 57 | &mint, 58 | &owner.pubkey(), 59 | &[], 60 | 50, 61 | 4, 62 | ) 63 | .unwrap(); 64 | 65 | let tx = Transaction::new_signed_with_payer( 66 | &[burn_ix], 67 | Some(&context.payer.pubkey()), 68 | &[&context.payer, &owner], 69 | context.last_blockhash, 70 | ); 71 | context.banks_client.process_transaction(tx).await.unwrap(); 72 | 73 | // Then the account should have 50 tokens remaining. 74 | 75 | let account = context.banks_client.get_account(account).await.unwrap(); 76 | 77 | assert!(account.is_some()); 78 | 79 | let account = account.unwrap(); 80 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 81 | 82 | assert!(account.amount == 50); 83 | } 84 | -------------------------------------------------------------------------------- /p-token/tests/close_account.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | pubkey::Pubkey, 8 | signature::{Keypair, Signer}, 9 | transaction::Transaction, 10 | }, 11 | }; 12 | 13 | #[tokio::test] 14 | async fn close_account() { 15 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 16 | .start_with_context() 17 | .await; 18 | 19 | // Given a mint account. 20 | 21 | let mint_authority = Keypair::new(); 22 | let freeze_authority = Pubkey::new_unique(); 23 | 24 | let mint = mint::initialize( 25 | &mut context, 26 | mint_authority.pubkey(), 27 | Some(freeze_authority), 28 | &TOKEN_PROGRAM_ID, 29 | ) 30 | .await 31 | .unwrap(); 32 | 33 | // And a token account. 34 | 35 | let owner = Keypair::new(); 36 | 37 | let account = 38 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 39 | 40 | let token_account = context.banks_client.get_account(account).await.unwrap(); 41 | assert!(token_account.is_some()); 42 | 43 | // When we close the account. 44 | 45 | let close_account_ix = spl_token::instruction::close_account( 46 | &spl_token::ID, 47 | &account, 48 | &owner.pubkey(), 49 | &owner.pubkey(), 50 | &[], 51 | ) 52 | .unwrap(); 53 | 54 | let tx = Transaction::new_signed_with_payer( 55 | &[close_account_ix], 56 | Some(&context.payer.pubkey()), 57 | &[&context.payer, &owner], 58 | context.last_blockhash, 59 | ); 60 | context.banks_client.process_transaction(tx).await.unwrap(); 61 | 62 | // Then an account must not exist. 63 | 64 | let token_account = context.banks_client.get_account(account).await.unwrap(); 65 | assert!(token_account.is_none()); 66 | } 67 | -------------------------------------------------------------------------------- /p-token/tests/freeze_account.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | signature::{Keypair, Signer}, 9 | transaction::Transaction, 10 | }, 11 | spl_token::state::AccountState, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn freeze_account() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Keypair::new(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority.pubkey()), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | let token_account = context.banks_client.get_account(account).await.unwrap(); 42 | assert!(token_account.is_some()); 43 | 44 | // When we freeze the account. 45 | 46 | let freeze_account_ix = spl_token::instruction::freeze_account( 47 | &spl_token::ID, 48 | &account, 49 | &mint, 50 | &freeze_authority.pubkey(), 51 | &[], 52 | ) 53 | .unwrap(); 54 | 55 | let tx = Transaction::new_signed_with_payer( 56 | &[freeze_account_ix], 57 | Some(&context.payer.pubkey()), 58 | &[&context.payer, &freeze_authority], 59 | context.last_blockhash, 60 | ); 61 | context.banks_client.process_transaction(tx).await.unwrap(); 62 | 63 | // Then the account is frozen. 64 | 65 | let token_account = context.banks_client.get_account(account).await.unwrap(); 66 | assert!(token_account.is_some()); 67 | 68 | let token_account = token_account.unwrap(); 69 | let token_account = spl_token::state::Account::unpack(&token_account.data).unwrap(); 70 | 71 | assert_eq!(token_account.state, AccountState::Frozen); 72 | } 73 | -------------------------------------------------------------------------------- /p-token/tests/initialize_account.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | system_instruction, 11 | transaction::Transaction, 12 | }, 13 | }; 14 | 15 | #[tokio::test] 16 | async fn initialize_account() { 17 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 18 | .start_with_context() 19 | .await; 20 | 21 | // Given a mint account. 22 | 23 | let mint_authority = Pubkey::new_unique(); 24 | let freeze_authority = Pubkey::new_unique(); 25 | 26 | let mint = mint::initialize( 27 | &mut context, 28 | mint_authority, 29 | Some(freeze_authority), 30 | &TOKEN_PROGRAM_ID, 31 | ) 32 | .await 33 | .unwrap(); 34 | 35 | // Given a mint authority, freeze authority and an account keypair. 36 | 37 | let owner = Pubkey::new_unique(); 38 | let account = Keypair::new(); 39 | 40 | let account_size = 165; 41 | let rent = context.banks_client.get_rent().await.unwrap(); 42 | 43 | let initialize_ix = spl_token::instruction::initialize_account( 44 | &spl_token::ID, 45 | &account.pubkey(), 46 | &mint, 47 | &owner, 48 | ) 49 | .unwrap(); 50 | 51 | // When a new mint account is created and initialized. 52 | 53 | let instructions = vec![ 54 | system_instruction::create_account( 55 | &context.payer.pubkey(), 56 | &account.pubkey(), 57 | rent.minimum_balance(account_size), 58 | account_size as u64, 59 | &TOKEN_PROGRAM_ID, 60 | ), 61 | initialize_ix, 62 | ]; 63 | 64 | let tx = Transaction::new_signed_with_payer( 65 | &instructions, 66 | Some(&context.payer.pubkey()), 67 | &[&context.payer, &account], 68 | context.last_blockhash, 69 | ); 70 | context.banks_client.process_transaction(tx).await.unwrap(); 71 | 72 | // Then an account has the correct data. 73 | 74 | let account = context 75 | .banks_client 76 | .get_account(account.pubkey()) 77 | .await 78 | .unwrap(); 79 | 80 | assert!(account.is_some()); 81 | 82 | let account = account.unwrap(); 83 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 84 | 85 | assert!(!account.is_frozen()); 86 | assert!(account.owner == owner); 87 | assert!(account.mint == mint); 88 | } 89 | -------------------------------------------------------------------------------- /p-token/tests/initialize_account2.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | system_instruction, 11 | transaction::Transaction, 12 | }, 13 | }; 14 | 15 | #[tokio::test] 16 | async fn initialize_account2() { 17 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 18 | .start_with_context() 19 | .await; 20 | 21 | // Given a mint account. 22 | 23 | let mint_authority = Pubkey::new_unique(); 24 | let freeze_authority = Pubkey::new_unique(); 25 | 26 | let mint = mint::initialize( 27 | &mut context, 28 | mint_authority, 29 | Some(freeze_authority), 30 | &TOKEN_PROGRAM_ID, 31 | ) 32 | .await 33 | .unwrap(); 34 | 35 | // Given a mint authority, freeze authority and an account keypair. 36 | 37 | let owner = Pubkey::new_unique(); 38 | let account = Keypair::new(); 39 | 40 | let account_size = 165; 41 | let rent = context.banks_client.get_rent().await.unwrap(); 42 | 43 | let initialize_ix = spl_token::instruction::initialize_account2( 44 | &spl_token::ID, 45 | &account.pubkey(), 46 | &mint, 47 | &owner, 48 | ) 49 | .unwrap(); 50 | 51 | // When a new mint account is created and initialized. 52 | 53 | let instructions = vec![ 54 | system_instruction::create_account( 55 | &context.payer.pubkey(), 56 | &account.pubkey(), 57 | rent.minimum_balance(account_size), 58 | account_size as u64, 59 | &TOKEN_PROGRAM_ID, 60 | ), 61 | initialize_ix, 62 | ]; 63 | 64 | let tx = Transaction::new_signed_with_payer( 65 | &instructions, 66 | Some(&context.payer.pubkey()), 67 | &[&context.payer, &account], 68 | context.last_blockhash, 69 | ); 70 | context.banks_client.process_transaction(tx).await.unwrap(); 71 | 72 | // Then an account has the correct data. 73 | 74 | let account = context 75 | .banks_client 76 | .get_account(account.pubkey()) 77 | .await 78 | .unwrap(); 79 | 80 | assert!(account.is_some()); 81 | 82 | let account = account.unwrap(); 83 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 84 | 85 | assert!(!account.is_frozen()); 86 | assert!(account.owner == owner); 87 | assert!(account.mint == mint); 88 | } 89 | -------------------------------------------------------------------------------- /p-token/tests/initialize_account3.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | system_instruction, 11 | transaction::Transaction, 12 | }, 13 | }; 14 | 15 | #[tokio::test] 16 | async fn initialize_account3() { 17 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 18 | .start_with_context() 19 | .await; 20 | 21 | // Given a mint account. 22 | 23 | let mint_authority = Pubkey::new_unique(); 24 | let freeze_authority = Pubkey::new_unique(); 25 | 26 | let mint = mint::initialize( 27 | &mut context, 28 | mint_authority, 29 | Some(freeze_authority), 30 | &TOKEN_PROGRAM_ID, 31 | ) 32 | .await 33 | .unwrap(); 34 | 35 | // Given a mint authority, freeze authority and an account keypair. 36 | 37 | let owner = Pubkey::new_unique(); 38 | let account = Keypair::new(); 39 | 40 | let account_size = 165; 41 | let rent = context.banks_client.get_rent().await.unwrap(); 42 | 43 | let initialize_ix = spl_token::instruction::initialize_account3( 44 | &spl_token::ID, 45 | &account.pubkey(), 46 | &mint, 47 | &owner, 48 | ) 49 | .unwrap(); 50 | 51 | // When a new mint account is created and initialized. 52 | 53 | let instructions = vec![ 54 | system_instruction::create_account( 55 | &context.payer.pubkey(), 56 | &account.pubkey(), 57 | rent.minimum_balance(account_size), 58 | account_size as u64, 59 | &TOKEN_PROGRAM_ID, 60 | ), 61 | initialize_ix, 62 | ]; 63 | 64 | let tx = Transaction::new_signed_with_payer( 65 | &instructions, 66 | Some(&context.payer.pubkey()), 67 | &[&context.payer, &account], 68 | context.last_blockhash, 69 | ); 70 | context.banks_client.process_transaction(tx).await.unwrap(); 71 | 72 | // Then an account has the correct data. 73 | 74 | let account = context 75 | .banks_client 76 | .get_account(account.pubkey()) 77 | .await 78 | .unwrap(); 79 | 80 | assert!(account.is_some()); 81 | 82 | let account = account.unwrap(); 83 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 84 | 85 | assert!(!account.is_frozen()); 86 | assert!(account.owner == owner); 87 | assert!(account.mint == mint); 88 | } 89 | -------------------------------------------------------------------------------- /p-token/tests/initialize_mint.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::TOKEN_PROGRAM_ID, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_option::COption, 8 | program_pack::Pack, 9 | pubkey::Pubkey, 10 | signature::{Keypair, Signer}, 11 | system_instruction, 12 | transaction::Transaction, 13 | }, 14 | spl_token_interface::state::mint::Mint, 15 | std::mem::size_of, 16 | }; 17 | 18 | #[tokio::test] 19 | async fn initialize_mint() { 20 | let context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 21 | .start_with_context() 22 | .await; 23 | 24 | // Given a mint authority, freeze authority and an account keypair. 25 | 26 | let mint_authority = Pubkey::new_unique(); 27 | let freeze_authority = Pubkey::new_unique(); 28 | let account = Keypair::new(); 29 | 30 | let account_size = size_of::(); 31 | let rent = context.banks_client.get_rent().await.unwrap(); 32 | 33 | let initialize_ix = spl_token::instruction::initialize_mint( 34 | &spl_token::ID, 35 | &account.pubkey(), 36 | &mint_authority, 37 | Some(&freeze_authority), 38 | 0, 39 | ) 40 | .unwrap(); 41 | 42 | // When a new mint account is created and initialized. 43 | 44 | let instructions = vec![ 45 | system_instruction::create_account( 46 | &context.payer.pubkey(), 47 | &account.pubkey(), 48 | rent.minimum_balance(account_size), 49 | account_size as u64, 50 | &TOKEN_PROGRAM_ID, 51 | ), 52 | initialize_ix, 53 | ]; 54 | 55 | let tx = Transaction::new_signed_with_payer( 56 | &instructions, 57 | Some(&context.payer.pubkey()), 58 | &[&context.payer, &account], 59 | context.last_blockhash, 60 | ); 61 | context.banks_client.process_transaction(tx).await.unwrap(); 62 | 63 | // Then an account has the correct data. 64 | 65 | let account = context 66 | .banks_client 67 | .get_account(account.pubkey()) 68 | .await 69 | .unwrap(); 70 | 71 | assert!(account.is_some()); 72 | 73 | let account = account.unwrap(); 74 | let mint = spl_token::state::Mint::unpack(&account.data).unwrap(); 75 | 76 | assert!(mint.is_initialized); 77 | assert!(mint.mint_authority == COption::Some(mint_authority)); 78 | assert!(mint.freeze_authority == COption::Some(freeze_authority)); 79 | assert!(mint.decimals == 0) 80 | } 81 | -------------------------------------------------------------------------------- /p-token/tests/initialize_mint2.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::TOKEN_PROGRAM_ID, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_option::COption, 8 | program_pack::Pack, 9 | pubkey::Pubkey, 10 | signature::{Keypair, Signer}, 11 | system_instruction, 12 | transaction::Transaction, 13 | }, 14 | spl_token_interface::state::mint::Mint, 15 | std::mem::size_of, 16 | }; 17 | 18 | #[tokio::test] 19 | async fn initialize_mint2() { 20 | let context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 21 | .start_with_context() 22 | .await; 23 | 24 | // Given a mint authority, freeze authority and an account keypair. 25 | 26 | let mint_authority = Pubkey::new_unique(); 27 | let freeze_authority = Pubkey::new_unique(); 28 | let account = Keypair::new(); 29 | 30 | let account_size = size_of::(); 31 | let rent = context.banks_client.get_rent().await.unwrap(); 32 | 33 | let initialize_ix = spl_token::instruction::initialize_mint2( 34 | &spl_token::ID, 35 | &account.pubkey(), 36 | &mint_authority, 37 | Some(&freeze_authority), 38 | 0, 39 | ) 40 | .unwrap(); 41 | 42 | // When a new mint account is created and initialized. 43 | 44 | let instructions = vec![ 45 | system_instruction::create_account( 46 | &context.payer.pubkey(), 47 | &account.pubkey(), 48 | rent.minimum_balance(account_size), 49 | account_size as u64, 50 | &TOKEN_PROGRAM_ID, 51 | ), 52 | initialize_ix, 53 | ]; 54 | 55 | let tx = Transaction::new_signed_with_payer( 56 | &instructions, 57 | Some(&context.payer.pubkey()), 58 | &[&context.payer, &account], 59 | context.last_blockhash, 60 | ); 61 | context.banks_client.process_transaction(tx).await.unwrap(); 62 | 63 | // Then an account has the correct data. 64 | 65 | let account = context 66 | .banks_client 67 | .get_account(account.pubkey()) 68 | .await 69 | .unwrap(); 70 | 71 | assert!(account.is_some()); 72 | 73 | let account = account.unwrap(); 74 | let mint = spl_token::state::Mint::unpack(&account.data).unwrap(); 75 | 76 | assert!(mint.is_initialized); 77 | assert!(mint.mint_authority == COption::Some(mint_authority)); 78 | assert!(mint.freeze_authority == COption::Some(freeze_authority)); 79 | assert!(mint.decimals == 0) 80 | } 81 | -------------------------------------------------------------------------------- /p-token/tests/initialize_multisig.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::TOKEN_PROGRAM_ID, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | system_instruction, 11 | transaction::Transaction, 12 | }, 13 | spl_token::state::Multisig, 14 | }; 15 | 16 | #[tokio::test] 17 | async fn initialize_multisig() { 18 | let context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 19 | .start_with_context() 20 | .await; 21 | 22 | // Given an account 23 | 24 | let multisig = Keypair::new(); 25 | let signer1 = Pubkey::new_unique(); 26 | let signer2 = Pubkey::new_unique(); 27 | let signer3 = Pubkey::new_unique(); 28 | let signers = vec![&signer1, &signer2, &signer3]; 29 | 30 | let rent = context.banks_client.get_rent().await.unwrap(); 31 | 32 | let initialize_ix = spl_token::instruction::initialize_multisig( 33 | &spl_token::ID, 34 | &multisig.pubkey(), 35 | &signers, 36 | 2, 37 | ) 38 | .unwrap(); 39 | 40 | // When a new multisig account is created and initialized. 41 | 42 | let instructions = vec![ 43 | system_instruction::create_account( 44 | &context.payer.pubkey(), 45 | &multisig.pubkey(), 46 | rent.minimum_balance(Multisig::LEN), 47 | Multisig::LEN as u64, 48 | &TOKEN_PROGRAM_ID, 49 | ), 50 | initialize_ix, 51 | ]; 52 | 53 | let tx = Transaction::new_signed_with_payer( 54 | &instructions, 55 | Some(&context.payer.pubkey()), 56 | &[&context.payer, &multisig], 57 | context.last_blockhash, 58 | ); 59 | context.banks_client.process_transaction(tx).await.unwrap(); 60 | 61 | // Then the multisig has the correct data. 62 | 63 | let account = context 64 | .banks_client 65 | .get_account(multisig.pubkey()) 66 | .await 67 | .unwrap(); 68 | 69 | assert!(account.is_some()); 70 | 71 | let account = account.unwrap(); 72 | let multisig = spl_token::state::Multisig::unpack(&account.data).unwrap(); 73 | 74 | assert!(multisig.is_initialized); 75 | assert_eq!(multisig.n, 3); 76 | assert_eq!(multisig.m, 2); 77 | } 78 | -------------------------------------------------------------------------------- /p-token/tests/initialize_multisig2.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::TOKEN_PROGRAM_ID, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | system_instruction, 11 | transaction::Transaction, 12 | }, 13 | spl_token::state::Multisig, 14 | }; 15 | 16 | #[tokio::test] 17 | async fn initialize_multisig2() { 18 | let context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 19 | .start_with_context() 20 | .await; 21 | 22 | // Given an account 23 | 24 | let multisig = Keypair::new(); 25 | let signer1 = Pubkey::new_unique(); 26 | let signer2 = Pubkey::new_unique(); 27 | let signer3 = Pubkey::new_unique(); 28 | let signers = vec![&signer1, &signer2, &signer3]; 29 | 30 | let rent = context.banks_client.get_rent().await.unwrap(); 31 | 32 | let initialize_ix = spl_token::instruction::initialize_multisig2( 33 | &spl_token::ID, 34 | &multisig.pubkey(), 35 | &signers, 36 | 2, 37 | ) 38 | .unwrap(); 39 | 40 | // When a new multisig account is created and initialized. 41 | 42 | let instructions = vec![ 43 | system_instruction::create_account( 44 | &context.payer.pubkey(), 45 | &multisig.pubkey(), 46 | rent.minimum_balance(Multisig::LEN), 47 | Multisig::LEN as u64, 48 | &TOKEN_PROGRAM_ID, 49 | ), 50 | initialize_ix, 51 | ]; 52 | 53 | let tx = Transaction::new_signed_with_payer( 54 | &instructions, 55 | Some(&context.payer.pubkey()), 56 | &[&context.payer, &multisig], 57 | context.last_blockhash, 58 | ); 59 | context.banks_client.process_transaction(tx).await.unwrap(); 60 | 61 | // Then the multisig has the correct data. 62 | 63 | let account = context 64 | .banks_client 65 | .get_account(multisig.pubkey()) 66 | .await 67 | .unwrap(); 68 | 69 | assert!(account.is_some()); 70 | 71 | let account = account.unwrap(); 72 | let multisig = spl_token::state::Multisig::unpack(&account.data).unwrap(); 73 | 74 | assert!(multisig.is_initialized); 75 | assert_eq!(multisig.n, 3); 76 | assert_eq!(multisig.m, 2); 77 | } 78 | -------------------------------------------------------------------------------- /p-token/tests/mint_to.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | transaction::Transaction, 11 | }, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn mint_to() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Pubkey::new_unique(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | // When we mint tokens to it. 42 | 43 | let mint_ix = spl_token::instruction::mint_to( 44 | &spl_token::ID, 45 | &mint, 46 | &account, 47 | &mint_authority.pubkey(), 48 | &[], 49 | 100, 50 | ) 51 | .unwrap(); 52 | 53 | let tx = Transaction::new_signed_with_payer( 54 | &[mint_ix], 55 | Some(&context.payer.pubkey()), 56 | &[&context.payer, &mint_authority], 57 | context.last_blockhash, 58 | ); 59 | context.banks_client.process_transaction(tx).await.unwrap(); 60 | 61 | // Then an account has the correct data. 62 | 63 | let account = context.banks_client.get_account(account).await.unwrap(); 64 | 65 | assert!(account.is_some()); 66 | 67 | let account = account.unwrap(); 68 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 69 | 70 | assert!(account.amount == 100); 71 | } 72 | -------------------------------------------------------------------------------- /p-token/tests/mint_to_checked.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | transaction::Transaction, 11 | }, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn mint_to_checked() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Pubkey::new_unique(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | // When we mint tokens to it. 42 | 43 | let mint_ix = spl_token::instruction::mint_to_checked( 44 | &spl_token::ID, 45 | &mint, 46 | &account, 47 | &mint_authority.pubkey(), 48 | &[], 49 | 100, 50 | 4, 51 | ) 52 | .unwrap(); 53 | 54 | let tx = Transaction::new_signed_with_payer( 55 | &[mint_ix], 56 | Some(&context.payer.pubkey()), 57 | &[&context.payer, &mint_authority], 58 | context.last_blockhash, 59 | ); 60 | context.banks_client.process_transaction(tx).await.unwrap(); 61 | 62 | // Then an account has the correct data. 63 | 64 | let account = context.banks_client.get_account(account).await.unwrap(); 65 | 66 | assert!(account.is_some()); 67 | 68 | let account = account.unwrap(); 69 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 70 | 71 | assert!(account.amount == 100); 72 | } 73 | -------------------------------------------------------------------------------- /p-token/tests/revoke.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | transaction::Transaction, 11 | }, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn revoke() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Pubkey::new_unique(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account with 100 tokens. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | mint::mint( 42 | &mut context, 43 | &mint, 44 | &account, 45 | &mint_authority, 46 | 100, 47 | &TOKEN_PROGRAM_ID, 48 | ) 49 | .await 50 | .unwrap(); 51 | 52 | // And 50 tokens delegated. 53 | 54 | let delegate = Pubkey::new_unique(); 55 | 56 | account::approve( 57 | &mut context, 58 | &account, 59 | &delegate, 60 | &owner, 61 | 50, 62 | &TOKEN_PROGRAM_ID, 63 | ) 64 | .await; 65 | 66 | // When we revoke the delegation. 67 | 68 | let revoke_ix = 69 | spl_token::instruction::revoke(&spl_token::ID, &account, &owner.pubkey(), &[]).unwrap(); 70 | 71 | let tx = Transaction::new_signed_with_payer( 72 | &[revoke_ix], 73 | Some(&context.payer.pubkey()), 74 | &[&context.payer, &owner], 75 | context.last_blockhash, 76 | ); 77 | context.banks_client.process_transaction(tx).await.unwrap(); 78 | 79 | // Then the account should not have a delegate nor delegated amount. 80 | 81 | let account = context.banks_client.get_account(account).await.unwrap(); 82 | 83 | assert!(account.is_some()); 84 | 85 | let account = account.unwrap(); 86 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 87 | 88 | assert!(account.delegate.is_none()); 89 | assert!(account.delegated_amount == 0); 90 | } 91 | -------------------------------------------------------------------------------- /p-token/tests/set_authority.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_option::COption, 8 | program_pack::Pack, 9 | pubkey::Pubkey, 10 | signature::{Keypair, Signer}, 11 | transaction::Transaction, 12 | }, 13 | spl_token::instruction::AuthorityType, 14 | }; 15 | 16 | #[tokio::test] 17 | async fn set_authority() { 18 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 19 | .start_with_context() 20 | .await; 21 | 22 | // Given a mint account. 23 | 24 | let mint_authority = Keypair::new(); 25 | let freeze_authority = Keypair::new(); 26 | 27 | let mint = mint::initialize( 28 | &mut context, 29 | mint_authority.pubkey(), 30 | Some(freeze_authority.pubkey()), 31 | &TOKEN_PROGRAM_ID, 32 | ) 33 | .await 34 | .unwrap(); 35 | 36 | // When we set a new freeze authority. 37 | 38 | let new_authority = Pubkey::new_unique(); 39 | 40 | let set_authority_ix = spl_token::instruction::set_authority( 41 | &spl_token::ID, 42 | &mint, 43 | Some(&new_authority), 44 | AuthorityType::FreezeAccount, 45 | &freeze_authority.pubkey(), 46 | &[], 47 | ) 48 | .unwrap(); 49 | 50 | let tx = Transaction::new_signed_with_payer( 51 | &[set_authority_ix], 52 | Some(&context.payer.pubkey()), 53 | &[&context.payer, &freeze_authority], 54 | context.last_blockhash, 55 | ); 56 | context.banks_client.process_transaction(tx).await.unwrap(); 57 | 58 | // Then the account should have the delegate and delegated amount. 59 | 60 | let account = context.banks_client.get_account(mint).await.unwrap(); 61 | 62 | assert!(account.is_some()); 63 | 64 | let account = account.unwrap(); 65 | let mint = spl_token::state::Mint::unpack(&account.data).unwrap(); 66 | 67 | assert!(mint.freeze_authority == COption::Some(new_authority)); 68 | } 69 | -------------------------------------------------------------------------------- /p-token/tests/setup/account.rs: -------------------------------------------------------------------------------- 1 | use { 2 | solana_program_test::ProgramTestContext, 3 | solana_sdk::{ 4 | pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, 5 | transaction::Transaction, 6 | }, 7 | }; 8 | 9 | pub async fn initialize( 10 | context: &mut ProgramTestContext, 11 | mint: &Pubkey, 12 | owner: &Pubkey, 13 | program_id: &Pubkey, 14 | ) -> Pubkey { 15 | let account = Keypair::new(); 16 | 17 | let account_size = 165; 18 | let rent = context.banks_client.get_rent().await.unwrap(); 19 | 20 | let mut initialize_ix = 21 | spl_token::instruction::initialize_account(&spl_token::ID, &account.pubkey(), mint, owner) 22 | .unwrap(); 23 | initialize_ix.program_id = *program_id; 24 | 25 | let instructions = vec![ 26 | system_instruction::create_account( 27 | &context.payer.pubkey(), 28 | &account.pubkey(), 29 | rent.minimum_balance(account_size), 30 | account_size as u64, 31 | program_id, 32 | ), 33 | initialize_ix, 34 | ]; 35 | 36 | let tx = Transaction::new_signed_with_payer( 37 | &instructions, 38 | Some(&context.payer.pubkey()), 39 | &[&context.payer, &account], 40 | context.last_blockhash, 41 | ); 42 | context.banks_client.process_transaction(tx).await.unwrap(); 43 | 44 | account.pubkey() 45 | } 46 | 47 | pub async fn approve( 48 | context: &mut ProgramTestContext, 49 | account: &Pubkey, 50 | delegate: &Pubkey, 51 | owner: &Keypair, 52 | amount: u64, 53 | program_id: &Pubkey, 54 | ) { 55 | let mut approve_ix = spl_token::instruction::approve( 56 | &spl_token::ID, 57 | account, 58 | delegate, 59 | &owner.pubkey(), 60 | &[], 61 | amount, 62 | ) 63 | .unwrap(); 64 | approve_ix.program_id = *program_id; 65 | 66 | let tx = Transaction::new_signed_with_payer( 67 | &[approve_ix], 68 | Some(&context.payer.pubkey()), 69 | &[&context.payer, owner], 70 | context.last_blockhash, 71 | ); 72 | context.banks_client.process_transaction(tx).await.unwrap(); 73 | } 74 | 75 | pub async fn freeze( 76 | context: &mut ProgramTestContext, 77 | account: &Pubkey, 78 | mint: &Pubkey, 79 | freeze_authority: &Keypair, 80 | program_id: &Pubkey, 81 | ) { 82 | let mut freeze_account_ix = spl_token::instruction::freeze_account( 83 | &spl_token::ID, 84 | account, 85 | mint, 86 | &freeze_authority.pubkey(), 87 | &[], 88 | ) 89 | .unwrap(); 90 | freeze_account_ix.program_id = *program_id; 91 | 92 | let tx = Transaction::new_signed_with_payer( 93 | &[freeze_account_ix], 94 | Some(&context.payer.pubkey()), 95 | &[&context.payer, freeze_authority], 96 | context.last_blockhash, 97 | ); 98 | context.banks_client.process_transaction(tx).await.unwrap(); 99 | } 100 | -------------------------------------------------------------------------------- /p-token/tests/setup/mint.rs: -------------------------------------------------------------------------------- 1 | use { 2 | solana_program_test::{BanksClientError, ProgramTestContext}, 3 | solana_sdk::{ 4 | program_error::ProgramError, pubkey::Pubkey, signature::Keypair, signer::Signer, 5 | system_instruction, transaction::Transaction, 6 | }, 7 | spl_token_interface::state::mint::Mint, 8 | std::mem::size_of, 9 | }; 10 | 11 | pub async fn initialize( 12 | context: &mut ProgramTestContext, 13 | mint_authority: Pubkey, 14 | freeze_authority: Option, 15 | program_id: &Pubkey, 16 | ) -> Result { 17 | // Mint account keypair. 18 | let account = Keypair::new(); 19 | 20 | let account_size = size_of::(); 21 | let rent = context.banks_client.get_rent().await.unwrap(); 22 | 23 | let mut initialize_ix = spl_token::instruction::initialize_mint( 24 | &spl_token::ID, 25 | &account.pubkey(), 26 | &mint_authority, 27 | freeze_authority.as_ref(), 28 | 4, 29 | ) 30 | .unwrap(); 31 | // Switches the program id in case we are using a "custom" one. 32 | initialize_ix.program_id = *program_id; 33 | 34 | // Create a new account and initialize as a mint. 35 | 36 | let instructions = vec![ 37 | system_instruction::create_account( 38 | &context.payer.pubkey(), 39 | &account.pubkey(), 40 | rent.minimum_balance(account_size), 41 | account_size as u64, 42 | program_id, 43 | ), 44 | initialize_ix, 45 | ]; 46 | 47 | let tx = Transaction::new_signed_with_payer( 48 | &instructions, 49 | Some(&context.payer.pubkey()), 50 | &[&context.payer, &account], 51 | context.last_blockhash, 52 | ); 53 | context.banks_client.process_transaction(tx).await.unwrap(); 54 | 55 | Ok(account.pubkey()) 56 | } 57 | 58 | pub async fn mint( 59 | context: &mut ProgramTestContext, 60 | mint: &Pubkey, 61 | account: &Pubkey, 62 | mint_authority: &Keypair, 63 | amount: u64, 64 | program_id: &Pubkey, 65 | ) -> Result<(), BanksClientError> { 66 | let mut mint_ix = spl_token::instruction::mint_to( 67 | &spl_token::ID, 68 | mint, 69 | account, 70 | &mint_authority.pubkey(), 71 | &[], 72 | amount, 73 | ) 74 | .unwrap(); 75 | // Switches the program id to the token program. 76 | mint_ix.program_id = *program_id; 77 | 78 | let tx = Transaction::new_signed_with_payer( 79 | &[mint_ix], 80 | Some(&context.payer.pubkey()), 81 | &[&context.payer, mint_authority], 82 | context.last_blockhash, 83 | ); 84 | context.banks_client.process_transaction(tx).await 85 | } 86 | -------------------------------------------------------------------------------- /p-token/tests/setup/mod.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::pubkey::Pubkey; 2 | 3 | #[allow(dead_code)] 4 | pub mod account; 5 | #[allow(dead_code)] 6 | pub mod mint; 7 | 8 | pub const TOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array(spl_token_interface::program::ID); 9 | -------------------------------------------------------------------------------- /p-token/tests/thaw_account.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | signature::{Keypair, Signer}, 9 | transaction::Transaction, 10 | }, 11 | spl_token::state::AccountState, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn thaw_account() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Keypair::new(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority.pubkey()), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a frozen token account. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | let token_account = context.banks_client.get_account(account).await.unwrap(); 42 | assert!(token_account.is_some()); 43 | 44 | account::freeze( 45 | &mut context, 46 | &account, 47 | &mint, 48 | &freeze_authority, 49 | &TOKEN_PROGRAM_ID, 50 | ) 51 | .await; 52 | 53 | // When we thaw the account. 54 | 55 | let thaw_account_ix = spl_token::instruction::thaw_account( 56 | &spl_token::ID, 57 | &account, 58 | &mint, 59 | &freeze_authority.pubkey(), 60 | &[], 61 | ) 62 | .unwrap(); 63 | 64 | let tx = Transaction::new_signed_with_payer( 65 | &[thaw_account_ix], 66 | Some(&context.payer.pubkey()), 67 | &[&context.payer, &freeze_authority], 68 | context.last_blockhash, 69 | ); 70 | context.banks_client.process_transaction(tx).await.unwrap(); 71 | 72 | // Then the account is frozen. 73 | 74 | let token_account = context.banks_client.get_account(account).await.unwrap(); 75 | assert!(token_account.is_some()); 76 | 77 | let token_account = token_account.unwrap(); 78 | let token_account = spl_token::state::Account::unpack(&token_account.data).unwrap(); 79 | 80 | assert_eq!(token_account.state, AccountState::Initialized); 81 | } 82 | -------------------------------------------------------------------------------- /p-token/tests/transfer.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | transaction::Transaction, 11 | }, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn transfer() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Pubkey::new_unique(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account with 100 tokens. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | mint::mint( 42 | &mut context, 43 | &mint, 44 | &account, 45 | &mint_authority, 46 | 100, 47 | &TOKEN_PROGRAM_ID, 48 | ) 49 | .await 50 | .unwrap(); 51 | 52 | // When we transfer the tokens. 53 | 54 | let destination = Pubkey::new_unique(); 55 | 56 | let destination_account = 57 | account::initialize(&mut context, &mint, &destination, &TOKEN_PROGRAM_ID).await; 58 | 59 | let transfer_ix = spl_token::instruction::transfer( 60 | &spl_token::ID, 61 | &account, 62 | &destination_account, 63 | &owner.pubkey(), 64 | &[], 65 | 100, 66 | ) 67 | .unwrap(); 68 | 69 | let tx = Transaction::new_signed_with_payer( 70 | &[transfer_ix], 71 | Some(&context.payer.pubkey()), 72 | &[&context.payer, &owner], 73 | context.last_blockhash, 74 | ); 75 | context.banks_client.process_transaction(tx).await.unwrap(); 76 | 77 | // Then an account has the correct data. 78 | 79 | let account = context.banks_client.get_account(account).await.unwrap(); 80 | 81 | assert!(account.is_some()); 82 | 83 | let account = account.unwrap(); 84 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 85 | 86 | assert!(account.amount == 0); 87 | } 88 | -------------------------------------------------------------------------------- /p-token/tests/transfer_checked.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{account, mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{ 7 | program_pack::Pack, 8 | pubkey::Pubkey, 9 | signature::{Keypair, Signer}, 10 | transaction::Transaction, 11 | }, 12 | }; 13 | 14 | #[tokio::test] 15 | async fn transfer_checked() { 16 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 17 | .start_with_context() 18 | .await; 19 | 20 | // Given a mint account. 21 | 22 | let mint_authority = Keypair::new(); 23 | let freeze_authority = Pubkey::new_unique(); 24 | 25 | let mint = mint::initialize( 26 | &mut context, 27 | mint_authority.pubkey(), 28 | Some(freeze_authority), 29 | &TOKEN_PROGRAM_ID, 30 | ) 31 | .await 32 | .unwrap(); 33 | 34 | // And a token account with 100 tokens. 35 | 36 | let owner = Keypair::new(); 37 | 38 | let account = 39 | account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; 40 | 41 | mint::mint( 42 | &mut context, 43 | &mint, 44 | &account, 45 | &mint_authority, 46 | 100, 47 | &TOKEN_PROGRAM_ID, 48 | ) 49 | .await 50 | .unwrap(); 51 | 52 | // When we transfer the tokens. 53 | 54 | let destination = Pubkey::new_unique(); 55 | 56 | let destination_account = 57 | account::initialize(&mut context, &mint, &destination, &TOKEN_PROGRAM_ID).await; 58 | 59 | let transfer_ix = spl_token::instruction::transfer_checked( 60 | &spl_token::ID, 61 | &account, 62 | &mint, 63 | &destination_account, 64 | &owner.pubkey(), 65 | &[], 66 | 100, 67 | 4, 68 | ) 69 | .unwrap(); 70 | 71 | let tx = Transaction::new_signed_with_payer( 72 | &[transfer_ix], 73 | Some(&context.payer.pubkey()), 74 | &[&context.payer, &owner], 75 | context.last_blockhash, 76 | ); 77 | context.banks_client.process_transaction(tx).await.unwrap(); 78 | 79 | // Then an account has the correct data. 80 | 81 | let account = context.banks_client.get_account(account).await.unwrap(); 82 | 83 | assert!(account.is_some()); 84 | 85 | let account = account.unwrap(); 86 | let account = spl_token::state::Account::unpack(&account.data).unwrap(); 87 | 88 | assert!(account.amount == 0); 89 | } 90 | -------------------------------------------------------------------------------- /p-token/tests/ui_amount_to_amount.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | setup::{mint, TOKEN_PROGRAM_ID}, 5 | solana_program_test::{tokio, ProgramTest}, 6 | solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}, 7 | }; 8 | 9 | #[tokio::test] 10 | async fn ui_amount_to_amount() { 11 | let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) 12 | .start_with_context() 13 | .await; 14 | 15 | // Given a mint account. 16 | 17 | let mint_authority = Pubkey::new_unique(); 18 | let freeze_authority = Pubkey::new_unique(); 19 | 20 | let mint = mint::initialize( 21 | &mut context, 22 | mint_authority, 23 | Some(freeze_authority), 24 | &TOKEN_PROGRAM_ID, 25 | ) 26 | .await 27 | .unwrap(); 28 | 29 | let ui_amount_to_amount_ix = 30 | spl_token::instruction::ui_amount_to_amount(&spl_token::ID, &mint, "1000.00").unwrap(); 31 | 32 | let tx = Transaction::new_signed_with_payer( 33 | &[ui_amount_to_amount_ix], 34 | Some(&context.payer.pubkey()), 35 | &[&context.payer], 36 | context.last_blockhash, 37 | ); 38 | context.banks_client.process_transaction(tx).await.unwrap(); 39 | 40 | // Then the transaction should succeed. 41 | 42 | let account = context.banks_client.get_account(mint).await.unwrap(); 43 | 44 | assert!(account.is_some()); 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "programs:build": "zx ./scripts/rust/build-sbf.mjs program", 5 | "programs:format": "zx ./scripts/rust/format.mjs program", 6 | "programs:lint": "zx ./scripts/rust/lint.mjs program", 7 | "programs:test": "zx ./scripts/rust/test.mjs program", 8 | "solana:check": "zx ./scripts/check-solana-version.mjs", 9 | "solana:link": "zx ./scripts/link-solana-version.mjs", 10 | "generate": "pnpm generate:clients", 11 | "generate:clients": "zx ./scripts/generate-clients.mjs", 12 | "validator:start": "zx ./scripts/start-validator.mjs", 13 | "validator:restart": "pnpm validator:start --restart", 14 | "validator:stop": "zx ./scripts/stop-validator.mjs", 15 | "clients:js:format": "zx ./scripts/js/format.mjs", 16 | "clients:js:lint": "zx ./scripts/js/lint.mjs", 17 | "clients:js:publish": "zx ./scripts/js/publish.mjs", 18 | "clients:js:test": "zx ./scripts/js/test.mjs", 19 | "clients:rust:format": "zx ./scripts/rust/format.mjs clients/rust", 20 | "clients:rust:lint": "zx ./scripts/rust/lint.mjs clients/rust", 21 | "clients:rust:test": "zx ./scripts/rust/test.mjs clients/rust", 22 | "template:upgrade": "zx ./scripts/upgrade-template.mjs", 23 | "rust:spellcheck": "cargo spellcheck --code 1", 24 | "rust:audit": "zx ./scripts/rust/audit.mjs", 25 | "rust:publish": "zx ./scripts/rust/publish.mjs", 26 | "rust:semver": "cargo semver-checks", 27 | "p-token:build": "zx ./scripts/rust/build-sbf.mjs p-token", 28 | "p-token:format": "zx ./scripts/rust/format.mjs p-token", 29 | "p-token:lint": "zx ./scripts/rust/lint.mjs p-token", 30 | "p-token:test": "zx ./scripts/rust/test.mjs p-token", 31 | "fixtures:clean": "zx ./scripts/rust/fixtures.mjs clean", 32 | "fixtures:generate": "zx ./scripts/rust/fixtures.mjs generate", 33 | "fixtures:run": "zx ./scripts/rust/fixtures.mjs run", 34 | "interface:format": "zx ./scripts/rust/format.mjs interface", 35 | "interface:lint": "zx ./scripts/rust/lint.mjs interface" 36 | }, 37 | "devDependencies": { 38 | "@codama/renderers-js": "^1.2.7", 39 | "@codama/renderers-rust": "^1.0.16", 40 | "@iarna/toml": "^2.2.5", 41 | "codama": "^1.2.8", 42 | "typescript": "^5.5.2", 43 | "zx": "^7.2.3" 44 | }, 45 | "engines": { 46 | "node": ">=v20.0.0" 47 | }, 48 | "packageManager": "pnpm@9.1.0" 49 | } 50 | -------------------------------------------------------------------------------- /program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spl-token" 3 | version = "8.0.0" 4 | description = "Solana Program Library Token" 5 | authors = { workspace = true} 6 | repository = { workspace = true} 7 | license = { workspace = true} 8 | edition = { workspace = true} 9 | 10 | [features] 11 | no-entrypoint = [] 12 | test-sbf = [] 13 | 14 | [dependencies] 15 | arrayref = "0.3.9" 16 | bytemuck = "1.20.0" 17 | num-derive = "0.4" 18 | num-traits = "0.2" 19 | num_enum = "0.7.3" 20 | solana-account-info = "2.2.0" 21 | solana-cpi = "2.2.0" 22 | solana-decode-error = "2.2.0" 23 | solana-instruction = "2.2.0" 24 | solana-msg = "2.2.0" 25 | solana-program-entrypoint = "2.2.0" 26 | solana-program-error = "2.2.0" 27 | solana-program-memory = "2.2.0" 28 | solana-program-option = "2.2.0" 29 | solana-program-pack = "2.2.0" 30 | solana-pubkey = { version = "2.2.0", features = ["bytemuck"] } 31 | solana-rent = "2.2.0" 32 | solana-sdk-ids = "2.2.0" 33 | solana-sysvar = { version = "2.2.0", features = ["bincode"] } 34 | thiserror = "2.0" 35 | 36 | [dev-dependencies] 37 | lazy_static = "1.5.0" 38 | mollusk-svm = "0.1.0" 39 | proptest = "1.5" 40 | serial_test = "3.2.0" 41 | solana-clock = "2.2.1" 42 | solana-native-token = "2.2.1" 43 | solana-sdk = "2.2.1" 44 | 45 | [lib] 46 | crate-type = ["cdylib", "lib"] 47 | 48 | [package.metadata.docs.rs] 49 | targets = ["x86_64-unknown-linux-gnu"] 50 | 51 | [lints] 52 | workspace = true 53 | 54 | [package.metadata.solana] 55 | program-id = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 56 | -------------------------------------------------------------------------------- /program/README.md: -------------------------------------------------------------------------------- 1 | # Token program 2 | 3 | A token program on the Solana blockchain, usable for fungible and non-fungible tokens. 4 | 5 | This program provides an interface and implementation that third parties can 6 | utilize to create and use their tokens. 7 | 8 | Full documentation is available at the [SPL Token docs](https://spl.solana.com/token). 9 | 10 | ## Audit 11 | 12 | The repository [README](https://github.com/solana-labs/solana-program-library#audits) 13 | contains information about program audits. 14 | -------------------------------------------------------------------------------- /program/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | //! Program entrypoint 2 | 3 | use { 4 | crate::{error::TokenError, processor::Processor}, 5 | solana_account_info::AccountInfo, 6 | solana_program_error::{PrintProgramError, ProgramResult}, 7 | solana_pubkey::Pubkey, 8 | }; 9 | 10 | solana_program_entrypoint::entrypoint!(process_instruction); 11 | fn process_instruction( 12 | program_id: &Pubkey, 13 | accounts: &[AccountInfo], 14 | instruction_data: &[u8], 15 | ) -> ProgramResult { 16 | if let Err(error) = Processor::process(program_id, accounts, instruction_data) { 17 | // catch the error so we can print it 18 | error.print::(); 19 | return Err(error); 20 | } 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /program/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::arithmetic_side_effects)] 2 | #![deny(missing_docs)] 3 | #![cfg_attr(not(test), forbid(unsafe_code))] 4 | 5 | //! An ERC20-like Token program for the Solana blockchain 6 | 7 | pub mod error; 8 | pub mod instruction; 9 | pub mod native_mint; 10 | pub mod processor; 11 | pub mod state; 12 | 13 | #[cfg(not(feature = "no-entrypoint"))] 14 | mod entrypoint; 15 | 16 | /// Export current sdk types for downstream users building with a different sdk 17 | /// version 18 | pub mod solana_program { 19 | #![allow(missing_docs)] 20 | pub mod entrypoint { 21 | pub use solana_program_error::ProgramResult; 22 | } 23 | pub mod instruction { 24 | pub use solana_instruction::{AccountMeta, Instruction}; 25 | } 26 | pub mod program_error { 27 | pub use solana_program_error::{PrintProgramError, ProgramError}; 28 | } 29 | pub mod program_option { 30 | pub use solana_program_option::COption; 31 | } 32 | pub mod program_pack { 33 | pub use solana_program_pack::{IsInitialized, Pack, Sealed}; 34 | } 35 | pub mod pubkey { 36 | pub use solana_pubkey::{Pubkey, PUBKEY_BYTES}; 37 | } 38 | } 39 | use { 40 | solana_program_error::{ProgramError, ProgramResult}, 41 | solana_pubkey::Pubkey, 42 | }; 43 | 44 | /// Convert the UI representation of a token amount (using the decimals field 45 | /// defined in its mint) to the raw amount 46 | pub fn ui_amount_to_amount(ui_amount: f64, decimals: u8) -> u64 { 47 | (ui_amount * 10_usize.pow(decimals as u32) as f64) as u64 48 | } 49 | 50 | /// Convert a raw amount to its UI representation (using the decimals field 51 | /// defined in its mint) 52 | pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> f64 { 53 | amount as f64 / 10_usize.pow(decimals as u32) as f64 54 | } 55 | 56 | /// Convert a raw amount to its UI representation (using the decimals field 57 | /// defined in its mint) 58 | pub fn amount_to_ui_amount_string(amount: u64, decimals: u8) -> String { 59 | let decimals = decimals as usize; 60 | if decimals > 0 { 61 | // Left-pad zeros to decimals + 1, so we at least have an integer zero 62 | let mut s = format!("{:01$}", amount, decimals + 1); 63 | // Add the decimal point (Sorry, "," locales!) 64 | s.insert(s.len() - decimals, '.'); 65 | s 66 | } else { 67 | amount.to_string() 68 | } 69 | } 70 | 71 | /// Convert a raw amount to its UI representation using the given decimals field 72 | /// Excess zeroes or unneeded decimal point are trimmed. 73 | pub fn amount_to_ui_amount_string_trimmed(amount: u64, decimals: u8) -> String { 74 | let mut s = amount_to_ui_amount_string(amount, decimals); 75 | if decimals > 0 { 76 | let zeros_trimmed = s.trim_end_matches('0'); 77 | s = zeros_trimmed.trim_end_matches('.').to_string(); 78 | } 79 | s 80 | } 81 | 82 | /// Try to convert a UI representation of a token amount to its raw amount using 83 | /// the given decimals field 84 | pub fn try_ui_amount_into_amount(ui_amount: String, decimals: u8) -> Result { 85 | let decimals = decimals as usize; 86 | let mut parts = ui_amount.split('.'); 87 | // splitting a string, even an empty one, will always yield an iterator of 88 | // at least length == 1 89 | let mut amount_str = parts.next().unwrap().to_string(); 90 | let after_decimal = parts.next().unwrap_or(""); 91 | let after_decimal = after_decimal.trim_end_matches('0'); 92 | if (amount_str.is_empty() && after_decimal.is_empty()) 93 | || parts.next().is_some() 94 | || after_decimal.len() > decimals 95 | { 96 | return Err(ProgramError::InvalidArgument); 97 | } 98 | 99 | amount_str.push_str(after_decimal); 100 | for _ in 0..decimals.saturating_sub(after_decimal.len()) { 101 | amount_str.push('0'); 102 | } 103 | amount_str 104 | .parse::() 105 | .map_err(|_| ProgramError::InvalidArgument) 106 | } 107 | 108 | solana_pubkey::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); 109 | 110 | /// Checks that the supplied program ID is the correct one for SPL-token 111 | pub fn check_program_account(spl_token_program_id: &Pubkey) -> ProgramResult { 112 | if spl_token_program_id != &id() { 113 | return Err(ProgramError::IncorrectProgramId); 114 | } 115 | Ok(()) 116 | } 117 | -------------------------------------------------------------------------------- /program/src/native_mint.rs: -------------------------------------------------------------------------------- 1 | //! The Mint that represents the native token 2 | 3 | /// There are `10^9` lamports in one SOL 4 | pub const DECIMALS: u8 = 9; 5 | 6 | // The Mint for native SOL Token accounts 7 | solana_pubkey::declare_id!("So11111111111111111111111111111111111111112"); 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use {super::*, solana_native_token::*}; 12 | 13 | #[test] 14 | fn test_decimals() { 15 | assert!( 16 | (lamports_to_sol(42) - crate::amount_to_ui_amount(42, DECIMALS)).abs() < f64::EPSILON 17 | ); 18 | assert_eq!( 19 | sol_to_lamports(42.), 20 | crate::ui_amount_to_amount(42., DECIMALS) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /program/tests/close_account.rs: -------------------------------------------------------------------------------- 1 | mod setup; 2 | 3 | use { 4 | mollusk_svm::{result::Check, Mollusk}, 5 | solana_sdk::{ 6 | account::{Account as SolanaAccount, ReadableAccount}, 7 | program_error::ProgramError, 8 | program_pack::Pack, 9 | pubkey::Pubkey, 10 | system_instruction, system_program, 11 | }, 12 | spl_token::{instruction, state::Account}, 13 | }; 14 | 15 | #[test] 16 | fn success_init_after_close_account() { 17 | let mollusk = Mollusk::new(&spl_token::id(), "spl_token"); 18 | 19 | let owner = Pubkey::new_unique(); 20 | let mint = Pubkey::new_unique(); 21 | let account = Pubkey::new_unique(); 22 | let destination = Pubkey::new_unique(); 23 | let decimals = 9; 24 | 25 | let owner_account = SolanaAccount::new(1_000_000_000, 0, &system_program::id()); 26 | let mint_account = setup::setup_mint_account(None, None, 0, decimals); 27 | let token_account = setup::setup_token_account(&mint, &owner, 0); 28 | 29 | let expected_destination_lamports = token_account.lamports(); 30 | 31 | mollusk.process_and_validate_instruction_chain( 32 | &[ 33 | ( 34 | &instruction::close_account(&spl_token::id(), &account, &destination, &owner, &[]) 35 | .unwrap(), 36 | &[Check::success()], 37 | ), 38 | ( 39 | &system_instruction::create_account( 40 | &owner, 41 | &account, 42 | 1_000_000_000, 43 | Account::LEN as u64, 44 | &spl_token::id(), 45 | ), 46 | &[Check::success()], 47 | ), 48 | ( 49 | &instruction::initialize_account(&spl_token::id(), &account, &mint, &owner) 50 | .unwrap(), 51 | &[ 52 | Check::success(), 53 | // Account successfully re-initialized. 54 | Check::account(&account) 55 | .data(setup::setup_token_account(&mint, &owner, 0).data()) 56 | .owner(&spl_token::id()) 57 | .build(), 58 | // The destination should have the lamports from the closed account. 59 | Check::account(&destination) 60 | .lamports(expected_destination_lamports) 61 | .build(), 62 | ], 63 | ), 64 | ], 65 | &[ 66 | (mint, mint_account), 67 | (account, token_account), 68 | (owner, owner_account), 69 | (destination, SolanaAccount::default()), 70 | mollusk.sysvars.keyed_account_for_rent_sysvar(), 71 | ], 72 | ); 73 | } 74 | 75 | #[test] 76 | fn fail_init_after_close_account() { 77 | let mollusk = Mollusk::new(&spl_token::id(), "spl_token"); 78 | 79 | let owner = Pubkey::new_unique(); 80 | let mint = Pubkey::new_unique(); 81 | let account = Pubkey::new_unique(); 82 | let destination = Pubkey::new_unique(); 83 | let decimals = 9; 84 | 85 | let owner_account = SolanaAccount::new(1_000_000_000, 0, &system_program::id()); 86 | let mint_account = setup::setup_mint_account(None, None, 0, decimals); 87 | let token_account = setup::setup_token_account(&mint, &owner, 0); 88 | 89 | let expected_destination_lamports = token_account.lamports(); 90 | 91 | mollusk.process_and_validate_instruction_chain( 92 | &[ 93 | ( 94 | &instruction::close_account(&spl_token::id(), &account, &destination, &owner, &[]) 95 | .unwrap(), 96 | &[Check::success()], 97 | ), 98 | ( 99 | &system_instruction::transfer(&owner, &account, 1_000_000_000), 100 | &[Check::success()], 101 | ), 102 | ( 103 | &instruction::initialize_account(&spl_token::id(), &account, &mint, &owner) 104 | .unwrap(), 105 | &[ 106 | Check::err(ProgramError::InvalidAccountData), 107 | // Account not re-initialized. 108 | Check::account(&account) 109 | .lamports(1_000_000_000) 110 | .owner(&system_program::id()) 111 | .build(), 112 | // The destination should have the lamports from the closed account. 113 | Check::account(&destination) 114 | .lamports(expected_destination_lamports) 115 | .build(), 116 | ], 117 | ), 118 | ], 119 | &[ 120 | (mint, mint_account), 121 | (account, token_account), 122 | (owner, owner_account), 123 | (destination, SolanaAccount::default()), 124 | mollusk.sysvars.keyed_account_for_rent_sysvar(), 125 | ], 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /program/tests/setup.rs: -------------------------------------------------------------------------------- 1 | use { 2 | solana_sdk::{ 3 | account::Account as SolanaAccount, program_pack::Pack, pubkey::Pubkey, rent::Rent, 4 | }, 5 | spl_token::state::{Account, AccountState, Mint}, 6 | }; 7 | 8 | pub fn setup_mint_account( 9 | mint_authority: Option<&Pubkey>, 10 | freeze_authority: Option<&Pubkey>, 11 | supply: u64, 12 | decimals: u8, 13 | ) -> SolanaAccount { 14 | let data = { 15 | let mut data = vec![0; Mint::LEN]; 16 | let state = Mint { 17 | mint_authority: mint_authority.cloned().into(), 18 | supply, 19 | decimals, 20 | is_initialized: true, 21 | freeze_authority: freeze_authority.cloned().into(), 22 | }; 23 | state.pack_into_slice(&mut data); 24 | data 25 | }; 26 | 27 | let space = data.len(); 28 | let lamports = Rent::default().minimum_balance(space); 29 | 30 | SolanaAccount { 31 | lamports, 32 | data, 33 | owner: spl_token::id(), 34 | ..Default::default() 35 | } 36 | } 37 | 38 | pub fn setup_token_account(mint: &Pubkey, owner: &Pubkey, amount: u64) -> SolanaAccount { 39 | let data = { 40 | let mut data = vec![0; Account::LEN]; 41 | let state = Account { 42 | mint: *mint, 43 | owner: *owner, 44 | amount, 45 | delegate: None.into(), 46 | state: AccountState::Initialized, 47 | is_native: None.into(), 48 | delegated_amount: 0, 49 | close_authority: None.into(), 50 | }; 51 | state.pack_into_slice(&mut data); 52 | data 53 | }; 54 | 55 | let space = data.len(); 56 | let lamports = Rent::default().minimum_balance(space); 57 | 58 | SolanaAccount { 59 | lamports, 60 | data, 61 | owner: spl_token::id(), 62 | ..Default::default() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.84.1" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | comment_width = 80 2 | edition = "2021" 3 | group_imports = "One" 4 | imports_granularity = "One" 5 | wrap_comments = true 6 | -------------------------------------------------------------------------------- /scripts/check-solana-version.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { getInstalledSolanaVersion, getSolanaVersion } from './utils.mjs'; 4 | 5 | const expectedVersion = getSolanaVersion(); 6 | const installedVersion = await getInstalledSolanaVersion(); 7 | 8 | if (!installedVersion) { 9 | echo( 10 | chalk.red('[ ERROR ]'), 11 | `No Solana installation found. Please install Solana ${expectedVersion} before proceeding.` 12 | ); 13 | process.exit(1); 14 | } else if (installedVersion !== expectedVersion) { 15 | echo( 16 | chalk.yellow('[ WARNING ]'), 17 | `The installed Solana version ${installedVersion} does not match the expected version ${expectedVersion}.` 18 | ); 19 | } else { 20 | echo( 21 | chalk.green('[ SUCCESS ]'), 22 | `The expected Solana version ${expectedVersion} is installed.` 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /scripts/ci/set-env.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import { getSolanaVersion, getToolchain } from '../utils.mjs'; 3 | 4 | await $`echo "SOLANA_VERSION=${getSolanaVersion()}" >> $GITHUB_ENV`; 5 | await $`echo "TOOLCHAIN_FORMAT=${getToolchain('format')}" >> $GITHUB_ENV`; 6 | await $`echo "TOOLCHAIN_LINT=${getToolchain('lint')}" >> $GITHUB_ENV`; 7 | -------------------------------------------------------------------------------- /scripts/generate-clients.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { createFromRoot } from 'codama'; 4 | import { renderVisitor as renderJavaScriptVisitor } from '@codama/renderers-js'; 5 | // import { renderVisitor as renderRustVisitor } from "@codama/renderers-rust"; 6 | import { workingDirectory } from './utils.mjs'; 7 | 8 | // Instanciate Codama. 9 | const codama = createFromRoot( 10 | require(path.join(workingDirectory, 'program', 'idl.json')) 11 | ); 12 | 13 | // Render JavaScript. 14 | const jsClient = path.join(__dirname, '..', 'clients', 'js'); 15 | codama.accept( 16 | renderJavaScriptVisitor(path.join(jsClient, 'src', 'generated'), { 17 | prettier: require(path.join(jsClient, '.prettierrc.json')), 18 | }) 19 | ); 20 | 21 | // Render Rust. 22 | // const rustClient = path.join(__dirname, "..", "clients", "rust"); 23 | // codama.accept( 24 | // renderRustVisitor(path.join(rustClient, "src", "generated"), { 25 | // formatCode: true, 26 | // crateFolder: rustClient, 27 | // }) 28 | // ); 29 | -------------------------------------------------------------------------------- /scripts/js/format.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { cliArguments, workingDirectory } from '../utils.mjs'; 4 | 5 | // Format the client using Prettier. 6 | cd(path.join(workingDirectory, 'clients', 'js')); 7 | await $`pnpm install`; 8 | await $`pnpm format ${cliArguments()}`; 9 | -------------------------------------------------------------------------------- /scripts/js/lint.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { cliArguments, workingDirectory } from '../utils.mjs'; 4 | 5 | // Check the client using ESLint. 6 | cd(path.join(workingDirectory, 'clients', 'js')); 7 | await $`pnpm install`; 8 | await $`pnpm lint ${cliArguments()}`; 9 | -------------------------------------------------------------------------------- /scripts/js/publish.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { cliArguments, workingDirectory } from '../utils.mjs'; 4 | 5 | const [level, tag = 'latest'] = cliArguments(); 6 | if (!level) { 7 | throw new Error('A version level — e.g. "path" — must be provided.'); 8 | } 9 | 10 | // Go to the client directory and install the dependencies. 11 | cd(path.join(workingDirectory, 'clients', 'js')); 12 | await $`pnpm install`; 13 | 14 | // Update the version. 15 | const versionArgs = [ 16 | '--no-git-tag-version', 17 | ...(level.startsWith('pre') ? [`--preid ${tag}`] : []), 18 | ]; 19 | let { stdout } = await $`pnpm version ${level} ${versionArgs}`; 20 | const newVersion = stdout.slice(1).trim(); 21 | 22 | // Expose the new version to CI if needed. 23 | if (process.env.CI) { 24 | await $`echo "new_version=${newVersion}" >> $GITHUB_OUTPUT`; 25 | } 26 | 27 | // Publish the package. 28 | // This will also build the package before publishing (see prepublishOnly script). 29 | await $`pnpm publish --no-git-checks --tag ${tag}`; 30 | 31 | // Commit the new version. 32 | await $`git commit -am "Publish JS client v${newVersion}"`; 33 | 34 | // Tag the new version. 35 | await $`git tag -a js@v${newVersion} -m "JS client v${newVersion}"`; 36 | -------------------------------------------------------------------------------- /scripts/js/test.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { cliArguments, workingDirectory } from '../utils.mjs'; 4 | 5 | // Start the local validator, or restart it if it is already running. 6 | await $`pnpm validator:restart`; 7 | 8 | // Build the client and run the tests. 9 | cd(path.join(workingDirectory, 'clients', 'js')); 10 | await $`pnpm install`; 11 | await $`pnpm build`; 12 | await $`pnpm test ${cliArguments()}`; 13 | -------------------------------------------------------------------------------- /scripts/link-solana-version.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { getInstalledSolanaVersion, getSolanaVersion } from './utils.mjs'; 4 | 5 | const expectedVersion = getSolanaVersion(); 6 | const installedVersion = await getInstalledSolanaVersion(); 7 | 8 | const installPath = path.join( 9 | os.homedir(), 10 | '.local', 11 | 'share', 12 | 'solana', 13 | 'install' 14 | ); 15 | const releasePath = path.join( 16 | installPath, 17 | 'releases', 18 | expectedVersion, 19 | 'solana-release' 20 | ); 21 | const activeReleasePath = path.join(installPath, 'active_release'); 22 | const hasRelease = await fs.exists(releasePath); 23 | 24 | if (!installedVersion) { 25 | echo( 26 | chalk.red('[ ERROR ]'), 27 | `No Solana installation found. Solana ${expectedVersion} is required for this project.` 28 | ); 29 | await askToInstallSolana(expectedVersion); 30 | } else if (installedVersion === expectedVersion) { 31 | echo( 32 | chalk.green('[ SUCCESS ]'), 33 | `The expected Solana version ${expectedVersion} is installed.` 34 | ); 35 | } else if (hasRelease) { 36 | await $`rm -f "${activeReleasePath}"`; 37 | await $`ln -s "${releasePath}" "${activeReleasePath}"`; 38 | echo( 39 | chalk.green('[ SUCCESS ]'), 40 | `Successfully switched from Solana version ${installedVersion} to ${expectedVersion} to match the project's requirements.` 41 | ); 42 | } else { 43 | echo( 44 | chalk.yellow('[ WARNING ]'), 45 | `Cannot switch from Solana version ${installedVersion} to ${expectedVersion} because it is not installed.` 46 | ); 47 | await askToInstallSolana(expectedVersion); 48 | } 49 | 50 | async function askToInstallSolana(version) { 51 | const installRelease = await question('Should we install it now? [y/N] '); 52 | if (installRelease === 'y') { 53 | await installSolana(version); 54 | echo( 55 | chalk.green('[ SUCCESS ]'), 56 | `Successfully installed Solana version ${version}.` 57 | ); 58 | } else { 59 | process.exit(1); 60 | } 61 | } 62 | 63 | async function installSolana(version) { 64 | echo(`Installing Solana ${version}...`); 65 | const cutoff = '1.18.19'; 66 | const isBeforeCutoff = 67 | (await $`[[ "$(printf '%s\n' "${cutoff}" "${version}" | sort -V | head -n1)" = "${version}" ]] && [[ "${cutoff}" != "${version}" ]]`.quiet() 68 | .exitCode) == 0; 69 | if (isBeforeCutoff) { 70 | await $`sh -c "$(curl -sSfL https://release.solana.com/v${version}/install)"`; 71 | } else { 72 | await $`sh -c "$(curl -sSfL https://release.anza.xyz/v${version}/install)"`; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /scripts/rust/audit.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | 4 | const advisories = [ 5 | // ed25519-dalek: Double Public Key Signing Function Oracle Attack 6 | // 7 | // Remove once repo upgrades to ed25519-dalek v2 8 | 'RUSTSEC-2022-0093', 9 | 10 | // curve25519-dalek 11 | // 12 | // Remove once repo upgrades to curve25519-dalek v4 13 | 'RUSTSEC-2024-0344', 14 | 15 | // Crate: tonic 16 | // Version: 0.9.2 17 | // Title: Remotely exploitable Denial of Service in Tonic 18 | // Date: 2024-10-01 19 | // ID: RUSTSEC-2024-0376 20 | // URL: https://rustsec.org/advisories/RUSTSEC-2024-0376 21 | // Solution: Upgrade to >=0.12.3 22 | 'RUSTSEC-2024-0376', 23 | ]; 24 | const ignores = [] 25 | advisories.forEach(x => { 26 | ignores.push('--ignore'); 27 | ignores.push(x); 28 | }); 29 | 30 | // Check Solana version. 31 | await $`cargo audit ${ignores}`; 32 | -------------------------------------------------------------------------------- /scripts/rust/build-sbf.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { cliArguments, workingDirectory } from '../utils.mjs'; 4 | 5 | const [folder, ...args] = cliArguments(); 6 | const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); 7 | await $`cargo-build-sbf --manifest-path ${manifestPath} ${args}`; 8 | 9 | -------------------------------------------------------------------------------- /scripts/rust/fixtures.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { existsSync } from 'fs'; 4 | import { cliArguments, workingDirectory } from '../utils.mjs'; 5 | 6 | // Directory where the fixtures are generated. 7 | const FIXTURES_DIR = path.join(workingDirectory, 'target', 'fixtures'); 8 | // Directory of the SPL Token program. 9 | const SPL_TOKEN_DIR = path.join(workingDirectory, 'program'); 10 | // Directory of the SBF program. 11 | const SBF_OUTPUT_DIR = path.join(workingDirectory, 'target', 'deploy'); 12 | 13 | const [command, ...args] = cliArguments(); 14 | 15 | switch (command) { 16 | case 'clean': 17 | await clean(); 18 | break; 19 | case 'generate': 20 | await generate(); 21 | break; 22 | case 'run': 23 | await run(args); 24 | break; 25 | default: 26 | throw new Error(`Unknown command: ${command}`); 27 | } 28 | 29 | async function clean() { 30 | await $`rm -rf ${FIXTURES_DIR}`; 31 | } 32 | 33 | async function generate() { 34 | if (existsSync(FIXTURES_DIR)) { 35 | echo(chalk.yellow('[ WARNING ]'), `Fixtures directory already exists.`); 36 | } else { 37 | await $`mkdir ${FIXTURES_DIR}`; 38 | 39 | // Fixtures are generated from the SPL Token program. 40 | cd(SPL_TOKEN_DIR); 41 | 42 | await $`RUST_LOG=error EJECT_FUZZ_FIXTURES=${FIXTURES_DIR} cargo test-sbf --features mollusk-svm/fuzz`; 43 | } 44 | } 45 | 46 | async function run(args) { 47 | if (!existsSync(FIXTURES_DIR)) { 48 | throw new Error(`Fixtures directory does not exist: ${FIXTURES_DIR}`); 49 | } 50 | 51 | const [programName] = args; 52 | if (!programName) { 53 | throw new Error('The name of the program file must be provided.'); 54 | } 55 | 56 | await $`mollusk execute-fixture \ 57 | ${path.join(SBF_OUTPUT_DIR, programName + '.so')} \ 58 | ${FIXTURES_DIR} \ 59 | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA \ 60 | --ignore-compute-units`; 61 | } 62 | -------------------------------------------------------------------------------- /scripts/rust/format.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { 4 | cliArguments, 5 | getToolchainArgument, 6 | partitionArguments, 7 | popArgument, 8 | workingDirectory, 9 | } from '../utils.mjs'; 10 | 11 | const [folder, ...formatArgs] = cliArguments(); 12 | 13 | const fix = popArgument(formatArgs, '--fix'); 14 | const [cargoArgs, fmtArgs] = partitionArguments(formatArgs, '--'); 15 | const toolchain = getToolchainArgument('format'); 16 | 17 | const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); 18 | 19 | // Format the client. 20 | if (fix) { 21 | await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- ${fmtArgs}`; 22 | } else { 23 | await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- --check ${fmtArgs}`; 24 | } 25 | -------------------------------------------------------------------------------- /scripts/rust/lint.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { 4 | cliArguments, 5 | getToolchainArgument, 6 | popArgument, 7 | workingDirectory, 8 | } from '../utils.mjs'; 9 | 10 | const [folder, ...args] = cliArguments(); 11 | 12 | // Configure additional arguments here, e.g.: 13 | // ['--arg1', '--arg2', ...cliArguments()] 14 | const lintArgs = [ 15 | '-Zunstable-options', 16 | '--all-targets', 17 | '--all-features', 18 | '--', 19 | '--deny=warnings', 20 | '--deny=clippy::arithmetic_side_effects', 21 | ...args, 22 | ]; 23 | 24 | const fix = popArgument(lintArgs, '--fix'); 25 | const toolchain = getToolchainArgument('lint'); 26 | const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); 27 | 28 | // Check the client using Clippy. 29 | if (fix) { 30 | await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} --fix ${lintArgs}`; 31 | } else { 32 | await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} ${lintArgs}`; 33 | } 34 | -------------------------------------------------------------------------------- /scripts/rust/publish.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { cliArguments, getCargo, workingDirectory } from '../utils.mjs'; 4 | 5 | const dryRun = argv['dry-run'] ?? false; 6 | const [folder, level] = cliArguments(); 7 | if (!folder) { 8 | throw new Error('A path to a directory with a Rust package — e.g. "clients/cli" — must be provided.'); 9 | } 10 | if (!level) { 11 | throw new Error('A version level — e.g. "patch" — must be provided.'); 12 | } 13 | 14 | cd(path.join(workingDirectory, folder)); 15 | 16 | const packageToml = getCargo(folder).package; 17 | const oldVersion = packageToml.version; 18 | const packageName = packageToml.name; 19 | const tagName = path.basename(folder); 20 | 21 | // Publish the new version, commit the repo change, tag it, and push it all. 22 | const releaseArgs = dryRun 23 | ? [] 24 | : ['--tag-name', `${tagName}@v{{version}}`, '--no-confirm', '--execute']; 25 | await $`cargo release ${level} ${releaseArgs}`; 26 | 27 | // Stop here if this is a dry run. 28 | if (dryRun) { 29 | process.exit(0); 30 | } 31 | 32 | // Get the new version. 33 | const newVersion = getCargo(folder).package.version; 34 | const newGitTag = `${tagName}@v${newVersion}`; 35 | const oldGitTag = `${tagName}@v${oldVersion}`; 36 | 37 | // Expose the new version to CI if needed. 38 | if (process.env.CI) { 39 | await $`echo "new_git_tag=${newGitTag}" >> $GITHUB_OUTPUT`; 40 | await $`echo "old_git_tag=${oldGitTag}" >> $GITHUB_OUTPUT`; 41 | } 42 | -------------------------------------------------------------------------------- /scripts/rust/test.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { cliArguments, workingDirectory } from '../utils.mjs'; 4 | 5 | const [folder, ...args] = cliArguments(); 6 | const sbfOutDir = path.join(workingDirectory, 'target', 'deploy'); 7 | const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); 8 | await $`RUST_LOG=error SBF_OUT_DIR=${sbfOutDir} cargo test --manifest-path ${manifestPath} ${args}`; 9 | -------------------------------------------------------------------------------- /scripts/solana.dic: -------------------------------------------------------------------------------- 1 | 1000 2 | config 3 | metadata 4 | json 5 | uri 6 | ui 7 | cli 8 | readme/S 9 | arg/S 10 | vec/S 11 | enum/S 12 | noop/S 13 | realloc/S 14 | overallocate/SGD 15 | namespace 16 | serde 17 | deserialize/SRGD 18 | deserialization 19 | struct/S 20 | param/S 21 | tuple/S 22 | metas 23 | infos 24 | async 25 | subcommand 26 | repo 27 | init 28 | solana 29 | sol/S 30 | blockchain/S 31 | permissionless 32 | composability 33 | runtime 34 | onchain 35 | offchain 36 | keypair/S 37 | decrypt/SGD 38 | lamport/S 39 | validator/S 40 | pubkey/S 41 | sysvar/S 42 | timestamp/S 43 | entrypoint/S 44 | spl 45 | pda/S 46 | multisignature/S 47 | multisig/S 48 | staker/S 49 | APY 50 | codama 51 | autogenerated 52 | sdk 53 | -------------------------------------------------------------------------------- /scripts/spellcheck.toml: -------------------------------------------------------------------------------- 1 | [Hunspell] 2 | use_builtin = true 3 | skip_os_lookups = false 4 | search_dirs = ["."] 5 | extra_dictionaries = ["solana.dic"] 6 | 7 | -------------------------------------------------------------------------------- /scripts/start-validator.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import { spawn } from 'node:child_process'; 3 | import fs from 'node:fs'; 4 | import 'zx/globals'; 5 | import { 6 | getCargo, 7 | getExternalAccountAddresses, 8 | getExternalProgramAddresses, 9 | getExternalProgramOutputDir, 10 | getProgramFolders, 11 | } from './utils.mjs'; 12 | 13 | // Check Solana version. 14 | await $`pnpm solana:check`; 15 | 16 | // Options and arguments. 17 | const restart = argv['restart']; 18 | 19 | // Keep the validator running when not using the restart flag. 20 | const isValidatorRunning = (await $`lsof -t -i:8899`.quiet().exitCode) === 0; 21 | if (!restart && isValidatorRunning) { 22 | echo(chalk.yellow('Local validator is already running.')); 23 | process.exit(); 24 | } 25 | 26 | // Initial message. 27 | const verb = isValidatorRunning ? 'Restarting' : 'Starting'; 28 | 29 | // Get programs and accounts. 30 | const programs = [...getPrograms(), ...getExternalPrograms()]; 31 | const programPluralized = programs.length === 1 ? 'program' : 'programs'; 32 | const accounts = [...getExternalAccounts()]; 33 | const accountsPluralized = accounts.length === 1 ? 'account' : 'accounts'; 34 | 35 | echo( 36 | `${verb} local validator with ${programs.length} custom ${programPluralized}` + 37 | (accounts.length > 0 38 | ? ` and ${accounts.length} external ${accountsPluralized}...` 39 | : `...`) 40 | ); 41 | 42 | // Kill the validator if it's already running. 43 | if (isValidatorRunning) { 44 | await $`pkill -f solana-test-validator`.quiet(); 45 | await sleep(1000); 46 | } 47 | 48 | // Global validator arguments. 49 | const args = [/* Reset ledger */ '-r']; 50 | 51 | // Load programs. 52 | programs.forEach(({ programId, deployPath }) => { 53 | args.push(/* Load BPF program */ '--bpf-program', programId, deployPath); 54 | }); 55 | 56 | // Load accounts. 57 | accounts.forEach(({ account, deployPath }) => { 58 | args.push(/* Load account */ '--account', account, deployPath); 59 | }); 60 | 61 | // Start the validator in detached mode. 62 | const cliLogs = path.join(os.tmpdir(), 'validator-cli.log'); 63 | fs.writeFileSync(cliLogs, '', () => {}); 64 | const out = fs.openSync(cliLogs, 'a'); 65 | const err = fs.openSync(cliLogs, 'a'); 66 | const validator = spawn('solana-test-validator', args, { 67 | detached: true, 68 | stdio: ['ignore', out, err], 69 | }); 70 | validator.unref(); 71 | 72 | // Wait for the validator to stabilize. 73 | const waitForValidator = spinner( 74 | 'Waiting for local validator to stabilize...', 75 | () => 76 | new Promise((resolve, reject) => { 77 | setInterval(() => { 78 | const logs = fs.readFileSync(cliLogs, 'utf8'); 79 | if (validator.exitCode !== null) { 80 | reject(logs); 81 | } else if (logs.includes('Confirmed Slot: 1')) { 82 | resolve(); 83 | } 84 | }, 1000); 85 | }) 86 | ); 87 | 88 | try { 89 | await waitForValidator; 90 | echo(chalk.green('Local validator is up and running!')); 91 | } catch (error) { 92 | echo(error); 93 | echo(chalk.red('Could not start local validator.')); 94 | } finally { 95 | fs.rmSync(cliLogs); 96 | process.exit(); 97 | } 98 | 99 | function getPrograms() { 100 | const binaryDir = path.join(__dirname, '..', 'target', 'deploy'); 101 | return getProgramFolders().map((folder) => { 102 | const cargo = getCargo(folder); 103 | const name = cargo.package.name.replace(/-/g, '_'); 104 | return { 105 | programId: cargo.package.metadata.solana['program-id'], 106 | deployPath: path.join(binaryDir, `${name}.so`), 107 | }; 108 | }); 109 | } 110 | 111 | function getExternalPrograms() { 112 | const binaryDir = getExternalProgramOutputDir(); 113 | return getExternalProgramAddresses().map((address) => ({ 114 | programId: address, 115 | deployPath: path.join(binaryDir, `${address}.so`), 116 | })); 117 | } 118 | 119 | function getExternalAccounts() { 120 | const binaryDir = getExternalProgramOutputDir(); 121 | return getExternalAccountAddresses().map((address) => ({ 122 | account: address, 123 | deployPath: path.join(binaryDir, `${address}.json`), 124 | })); 125 | } 126 | -------------------------------------------------------------------------------- /scripts/stop-validator.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | 4 | const isValidatorRunning = (await $`lsof -t -i:8899`.quiet().exitCode) === 0; 5 | 6 | if (isValidatorRunning) { 7 | // Kill the validator if it's already running. 8 | await $`pkill -f solana-test-validator`.quiet(); 9 | await sleep(1000); 10 | echo(chalk.green('Local validator terminated!')); 11 | } else { 12 | echo(chalk.yellow('Local validator is not running.')); 13 | } 14 | -------------------------------------------------------------------------------- /scripts/upgrade-template.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | import 'zx/globals'; 3 | import { getCargo } from './utils.mjs'; 4 | 5 | // Arguments to pass to the `create-solana-program` command. 6 | const rustClientCargo = getCargo(path.join('clients', 'rust')); 7 | const jsClientPkg = require( 8 | path.join(__dirname, '..', 'clients', 'js', 'package.json') 9 | ); 10 | const templateArgs = [ 11 | 'token', 12 | '--address', 13 | 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 14 | '--org', 15 | 'solana-program', 16 | '--rust-client-crate-name', 17 | rustClientCargo.package.name, 18 | '--js-client-package-name', 19 | jsClientPkg.name, 20 | '--default', 21 | '--force', 22 | ]; 23 | 24 | // File and folder patterns that should not be overwritten by the template upgrade. 25 | const unchangedGlobs = [ 26 | 'clients/**/src/**', 27 | 'clients/**/src/*', 28 | 'clients/js/test/*', 29 | 'clients/rust/tests/*', 30 | 'program/**/*', 31 | 'program/*', 32 | 'scripts/generate-clients.mjs', 33 | 'scripts/generate-idls.mjs', 34 | 'scripts/upgrade-template.mjs', 35 | 'scripts/program/*', 36 | 'Cargo.lock', 37 | '**/pnpm-lock.yaml', 38 | 'pnpm-lock.yaml', 39 | ]; 40 | 41 | // Prevent CLI arguments from being escaped. 42 | $.quote = (command) => command; 43 | 44 | // Re-generate the repo from the parent directory. 45 | cd('..'); 46 | await $`pnpm create solana-program@latest ${templateArgs}`; 47 | 48 | // Go back inside the updated repo. 49 | cd('token'); 50 | 51 | // Restore files and folders that should not be overwritten. 52 | await $`git add --all`; 53 | for (const glob of unchangedGlobs) { 54 | await $`git restore --worktree --staged "${glob}"`; 55 | } 56 | 57 | // Re-install dependencies. 58 | await $`pnpm install`; 59 | -------------------------------------------------------------------------------- /scripts/utils.mjs: -------------------------------------------------------------------------------- 1 | import 'zx/globals'; 2 | import { parse as parseToml } from '@iarna/toml'; 3 | 4 | process.env.FORCE_COLOR = 3; 5 | process.env.CARGO_TERM_COLOR = 'always'; 6 | 7 | export const workingDirectory = (await $`pwd`.quiet()).toString().trim(); 8 | 9 | export function getAllProgramIdls() { 10 | return getAllProgramFolders().map((folder) => 11 | path.join(workingDirectory, folder, 'idl.json') 12 | ); 13 | } 14 | 15 | export function getExternalProgramOutputDir() { 16 | const config = getCargoMetadata()?.solana?.['external-programs-output']; 17 | return path.join(workingDirectory, config ?? 'target/deploy'); 18 | } 19 | 20 | export function getExternalProgramAddresses() { 21 | const addresses = getProgramFolders().flatMap( 22 | (folder) => getCargoMetadata(folder)?.solana?.['program-dependencies'] ?? [] 23 | ); 24 | return Array.from(new Set(addresses)); 25 | } 26 | 27 | export function getExternalAccountAddresses() { 28 | const addresses = getProgramFolders().flatMap( 29 | (folder) => getCargoMetadata(folder)?.solana?.['account-dependencies'] ?? [] 30 | ); 31 | return Array.from(new Set(addresses)); 32 | } 33 | 34 | let didWarnAboutMissingPrograms = false; 35 | export function getProgramFolders() { 36 | let programs; 37 | 38 | if (process.env.PROGRAMS) { 39 | try { 40 | programs = JSON.parse(process.env.PROGRAMS); 41 | } catch (error) { 42 | programs = process.env.PROGRAMS.split(/\s+/); 43 | } 44 | } else { 45 | programs = getAllProgramFolders(); 46 | } 47 | 48 | const filteredPrograms = programs.filter((program) => 49 | fs.existsSync(path.join(workingDirectory, program)) 50 | ); 51 | 52 | if ( 53 | filteredPrograms.length !== programs.length && 54 | !didWarnAboutMissingPrograms 55 | ) { 56 | didWarnAboutMissingPrograms = true; 57 | programs 58 | .filter((program) => !filteredPrograms.includes(program)) 59 | .forEach((program) => { 60 | echo(chalk.yellow(`Program not found: ${workingDirectory}/${program}`)); 61 | }); 62 | } 63 | 64 | return filteredPrograms; 65 | } 66 | 67 | export function getAllProgramFolders() { 68 | return getCargo().workspace.members.filter( 69 | (member) => 70 | (getCargo(member).lib?.['crate-type'] ?? []).includes('cdylib') && 71 | // Exclude the pinocchio-token-program crate. 72 | getCargo(member).package?.name !== 'pinocchio-token-program' 73 | ); 74 | } 75 | 76 | export function getCargo(folder) { 77 | return parseToml( 78 | fs.readFileSync( 79 | path.join(workingDirectory, folder ? folder : '.', 'Cargo.toml'), 80 | 'utf8' 81 | ) 82 | ); 83 | } 84 | 85 | export function getCargoMetadata(folder) { 86 | const cargo = getCargo(folder); 87 | return folder ? cargo?.package?.metadata : cargo?.workspace?.metadata; 88 | } 89 | 90 | export function getSolanaVersion() { 91 | return getCargoMetadata()?.cli?.solana; 92 | } 93 | 94 | export function getToolchain(operation) { 95 | return getCargoMetadata()?.toolchains?.[operation]; 96 | } 97 | 98 | export function getToolchainArgument(operation) { 99 | const channel = getToolchain(operation); 100 | return channel ? `+${channel}` : ''; 101 | } 102 | 103 | export function cliArguments() { 104 | return process.argv.slice(3); 105 | } 106 | 107 | export function popArgument(args, arg) { 108 | const index = args.indexOf(arg); 109 | if (index >= 0) { 110 | args.splice(index, 1); 111 | } 112 | return index >= 0; 113 | } 114 | 115 | export function partitionArguments(args, delimiter) { 116 | const index = args.indexOf(delimiter); 117 | return index >= 0 118 | ? [args.slice(0, index), args.slice(index + 1)] 119 | : [args, []]; 120 | } 121 | 122 | export async function getInstalledSolanaVersion() { 123 | try { 124 | const { stdout } = await $`solana --version`.quiet(); 125 | return stdout.match(/(\d+\.\d+\.\d+)/)?.[1]; 126 | } catch (error) { 127 | return ''; 128 | } 129 | } 130 | --------------------------------------------------------------------------------