diff --git a/.github/workflows/pull-request-feature-proposal.yml b/.github/workflows/pull-request-feature-proposal.yml deleted file mode 100644 index b88fbcb7a30..00000000000 --- a/.github/workflows/pull-request-feature-proposal.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Feature Proposal Pull Request - -on: - pull_request: - paths: - - 'feature-proposal/**' - - 'token/**' - - 'ci/*-version.sh' - - '!token/js/**' - push: - branches: [master] - paths: - - 'feature-proposal/**' - - 'token/**' - - 'ci/*-version.sh' - - '!token/js/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - cargo-test-sbf: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test - run: ./ci/cargo-test-sbf.sh feature-proposal diff --git a/.github/workflows/pull-request-instruction-padding.yml b/.github/workflows/pull-request-instruction-padding.yml deleted file mode 100644 index 17756f7c679..00000000000 --- a/.github/workflows/pull-request-instruction-padding.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Instruction Pad Pull Request - -on: - pull_request: - paths: - - 'instruction-padding/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-instruction-padding.yml' - push: - branches: [master] - paths: - - 'instruction-padding/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-instruction-padding.yml' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - cargo-test-sbf: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test - run: ./ci/cargo-test-sbf.sh instruction-padding diff --git a/.github/workflows/pull-request-js.yml b/.github/workflows/pull-request-js.yml index f75ef69e438..225874ebade 100644 --- a/.github/workflows/pull-request-js.yml +++ b/.github/workflows/pull-request-js.yml @@ -4,14 +4,8 @@ on: pull_request: paths: - 'account-compression/sdk/**' - - 'libraries/type-length-value/js/**' - 'name-service/js/**' - - 'single-pool/js/**' - - 'stake-pool/js/**' - - 'token/js/**' - - 'token-group/js/**' - 'token-lending/js/**' - - 'token-metadata/js/**' - 'token-swap/js/**' - 'pnpm-lock.yaml' - '.github/workflows/pull-request-js.yml' @@ -19,13 +13,7 @@ on: branches: [master] paths: - 'account-compression/sdk/**' - - 'libraries/type-length-value/js/**' - - 'single-pool/js/**' - - 'stake-pool/js/**' - - 'token/js/**' - - 'token-group/js/**' - 'token-lending/js/**' - - 'token-metadata/js/**' - 'token-swap/js/**' - 'pnpm-lock.yaml' - '.github/workflows/pull-request-js.yml' @@ -42,24 +30,14 @@ jobs: package: [ name-service, - stake-pool, - token, token-swap, ] include: # Restrict certain packages to supported Node.js versions. - package: account-compression node-version: 20.x - - package: libraries - node-version: 20.x - - package: single-pool - node-version: 20.5 - - package: token-group - node-version: 20.x - package: token-lending node-version: 18.5 - - package: token-metadata - node-version: 20.x runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pull-request-libraries.yml b/.github/workflows/pull-request-libraries.yml index 256dacd2586..7b7a06e424f 100644 --- a/.github/workflows/pull-request-libraries.yml +++ b/.github/workflows/pull-request-libraries.yml @@ -6,14 +6,12 @@ on: - 'libraries/**' - 'ci/*-version.sh' - '.github/workflows/pull-request-libraries.yml' - - '!libraries/**/js/**' push: branches: [master] paths: - 'libraries/**' - 'ci/*-version.sh' - '.github/workflows/pull-request-libraries.yml' - - '!libraries/**/js/**' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -62,22 +60,3 @@ jobs: - name: Build and test run: ./ci/cargo-test-sbf.sh libraries - - js-test: - runs-on: ubuntu-latest - env: - NODE_VERSION: 20.x - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - uses: pnpm/action-setup@v4 - - uses: actions/cache@v4 - with: - path: ~/.npm - key: node-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - node- - - run: ./ci/js-test-libraries.sh diff --git a/.github/workflows/pull-request-record.yml b/.github/workflows/pull-request-record.yml deleted file mode 100644 index 6199fe98226..00000000000 --- a/.github/workflows/pull-request-record.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Record Pull Request - -on: - pull_request: - paths: - - 'record/**' - - 'ci/*-version.sh' - push: - branches: [master] - paths: - - 'record/**' - - 'ci/*-version.sh' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - cargo-test-sbf: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test - run: ./ci/cargo-test-sbf.sh record diff --git a/.github/workflows/pull-request-single-pool.yml b/.github/workflows/pull-request-single-pool.yml deleted file mode 100644 index 7a8483dc08e..00000000000 --- a/.github/workflows/pull-request-single-pool.yml +++ /dev/null @@ -1,132 +0,0 @@ -name: Single-Validator Stake Pool Pull Request - -on: - pull_request: - paths: - - 'single-pool/**' - - 'token/**' - - 'associated-token-account/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-single-pool.yml' - - '!single-pool/js/**' - - '!token/js/**' - push: - branches: [master] - paths: - - 'single-pool/**' - - 'token/**' - - 'associated-token-account/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-single-pool.yml' - - '!single-pool/js/**' - - '!token/js/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - cargo-test-sbf: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test - run: ./ci/cargo-test-sbf.sh single-pool/program - - cargo-build-test-cli: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build dependent programs - run: | - cargo build-sbf --manifest-path=single-pool/program/Cargo.toml - - - name: Build and test - run: | - cargo build --manifest-path ./single-pool/cli/Cargo.toml - cargo test --manifest-path ./single-pool/cli/Cargo.toml - - js-test: - runs-on: ubuntu-latest - env: - NODE_VERSION: 20.5 - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - uses: pnpm/action-setup@v4 - - uses: actions/cache@v4 - with: - path: ~/.npm - key: node-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - node- - - run: ./ci/js-test-single-pool.sh diff --git a/.github/workflows/pull-request-stake-pool.yml b/.github/workflows/pull-request-stake-pool.yml deleted file mode 100644 index 0b7e686264e..00000000000 --- a/.github/workflows/pull-request-stake-pool.yml +++ /dev/null @@ -1,129 +0,0 @@ -name: Stake Pool Pull Request - -on: - pull_request: - paths: - - 'stake-pool/**' - - 'token/**' - - 'ci/*-version.sh' - - 'ci/warning/purge-ubuntu-runner.sh' - - '.github/workflows/pull-request-stake-pool.yml' - - '!stake-pool/js/**' - - '!token/js/**' - push: - branches: [master] - paths: - - 'stake-pool/**' - - 'token/**' - - 'ci/*-version.sh' - - 'ci/warning/purge-ubuntu-runner.sh' - - '.github/workflows/pull-request-stake-pool.yml' - - '!stake-pool/js/**' - - '!token/js/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - cargo-test-sbf: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Check disk space - run: df -h - - - name: Remove unneeded packages for more space - run: bash ./ci/warning/purge-ubuntu-runner.sh - - - name: Check disk space again - run: df -h - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test - run: ./ci/cargo-test-sbf.sh stake-pool - - - name: Upload programs - uses: actions/upload-artifact@v4 - with: - name: stake-pool-programs - path: "target/deploy/*.so" - if-no-files-found: error - - js-test: - runs-on: ubuntu-latest - env: - NODE_VERSION: 16.x - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - uses: pnpm/action-setup@v4 - - uses: actions/cache@v4 - with: - path: ~/.npm - key: node-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - node- - - run: ./ci/js-test-stake-pool.sh - - py-test: - runs-on: ubuntu-latest - needs: cargo-test-sbf - steps: - - uses: actions/checkout@v4 - - - name: Setup Python version - uses: actions/setup-python@v4 - with: - python-version: 3.8 - - - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: pip-stake-pool-${{ hashFiles('stake-pool/py/requirements.txt') }} - - - name: Download programs - uses: actions/download-artifact@v4 - with: - name: stake-pool-programs - path: target/deploy - - - run: ./ci/py-test-stake-pool.sh diff --git a/.github/workflows/pull-request-token-group.yml b/.github/workflows/pull-request-token-group.yml deleted file mode 100644 index 2e1ef89ef93..00000000000 --- a/.github/workflows/pull-request-token-group.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Token-Group Pull Request - -on: - pull_request: - paths: - - 'token-group/**' - - 'token/program-2022/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-token-group.yml' - - '!token-group/js/**' - push: - branches: [master] - paths: - - 'token-group/**' - - 'token/program-2022/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-token-group.yml' - - '!token-group/js/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - cargo-test-sbf: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Test token-group interface - run: | - cargo test \ - --manifest-path=token-group/interface/Cargo.toml \ - -- --nocapture - - - name: Build and test example - run: ./ci/cargo-test-sbf.sh token-group/example - - js-test: - runs-on: ubuntu-latest - env: - NODE_VERSION: 20.x - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - uses: pnpm/action-setup@v4 - - uses: actions/cache@v4 - with: - path: ~/.npm - key: node-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - node- - - run: ./ci/js-test-token-group.sh diff --git a/.github/workflows/pull-request-token-metadata.yml b/.github/workflows/pull-request-token-metadata.yml deleted file mode 100644 index 3ec26f79970..00000000000 --- a/.github/workflows/pull-request-token-metadata.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Token-Metadata Pull Request - -on: - pull_request: - paths: - - 'token-metadata/**' - - 'token/program-2022/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-token-metadata.yml' - - '!token-metadata/js/**' - push: - branches: [master] - paths: - - 'token-metadata/**' - - 'token/program-2022/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-token-metadata.yml' - - '!token-metadata/js/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - cargo-test-sbf: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Test token-metadata with "serde" activated - run: | - cargo test \ - --manifest-path=token-metadata/interface/Cargo.toml \ - --features serde-traits \ - -- --nocapture - - - name: Build and test example - run: ./ci/cargo-test-sbf.sh token-metadata/example - - js-test: - runs-on: ubuntu-latest - env: - NODE_VERSION: 20.x - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - uses: pnpm/action-setup@v4 - - uses: actions/cache@v4 - with: - path: ~/.npm - key: node-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - node- - - run: ./ci/js-test-token-metadata.sh diff --git a/.github/workflows/pull-request-token.yml b/.github/workflows/pull-request-token.yml deleted file mode 100644 index 18ed249ef75..00000000000 --- a/.github/workflows/pull-request-token.yml +++ /dev/null @@ -1,337 +0,0 @@ -name: Token Pull Request - -on: - pull_request: - paths: - - 'associated-token-account/**' - - 'token/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-token.yml' - - '!token/js/**' - push: - branches: [master] - paths: - - 'associated-token-account/**' - - 'token/**' - - 'ci/*-version.sh' - - '.github/workflows/pull-request-token.yml' - - '!token/js/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - cargo-test-sbf: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Remove unneeded packages for more space - run: bash ./ci/warning/purge-ubuntu-runner.sh - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test token - run: ./ci/cargo-test-sbf.sh token - - cargo-test-token-2022-serde: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - - - name: Test token-2022 with "serde" activated - run: | - cargo test \ - --manifest-path=token/program-2022/Cargo.toml \ - --features serde-traits \ - -- --nocapture - - cargo-test-sbf-transfer-hook: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test transfer hook example - run: ./ci/cargo-test-sbf.sh token/transfer-hook/example - - - name: Upload program - uses: actions/upload-artifact@v4 - with: - name: spl-transfer-hook-example - path: "target/deploy/*.so" - if-no-files-found: error - - cargo-test-sbf-associated-token-account: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/rustfilt - key: cargo-sbf-bins-${{ runner.os }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test ATA - run: ./ci/cargo-test-sbf.sh associated-token-account - - cargo-test-sbf-token-2022: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Remove unneeded packages for more space - run: bash ./ci/warning/purge-ubuntu-runner.sh - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test token-2022 - run: | - cargo build-sbf --manifest-path token/program-2022/Cargo.toml - cargo build-sbf --manifest-path instruction-padding/program/Cargo.toml - ./ci/cargo-test-sbf.sh token/program-2022-test - - js-test: - runs-on: ubuntu-latest - env: - NODE_VERSION: 16.x - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - uses: pnpm/action-setup@v4 - - uses: actions/cache@v4 - with: - path: ~/.npm - key: node-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - node- - - run: ./ci/js-test-token.sh - - cargo-build-test-cli: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Build and test - run: | - BUILD_DEPENDENT_PROGRAMS=1 cargo build --manifest-path ./token/cli/Cargo.toml - cargo test --manifest-path ./token/cli/Cargo.toml - - cargo-build-test-transfer-hook-cli: - runs-on: ubuntu-latest - needs: [cargo-test-sbf] - steps: - - uses: actions/checkout@v4 - - - name: Set env vars - run: | - source ci/rust-version.sh - echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV - source ci/solana-version.sh - echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE }} - - - uses: actions/cache@v4 - with: - path: ~/.cache/solana - key: solana-${{ env.SOLANA_VERSION }} - - - name: Install dependencies - run: | - ./ci/install-build-deps.sh - ./ci/install-program-deps.sh - echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - - - name: Download spl-transfer-hook-example program - uses: actions/download-artifact@v4 - with: - name: spl-transfer-hook-example - path: target/deploy - - - name: Build and test - run: | - cargo test --manifest-path ./token/transfer-hook/cli/Cargo.toml diff --git a/Anchor.toml b/Anchor.toml index e8bd9dfc048..5ec1ebb7bf3 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -6,9 +6,6 @@ solana_version = "2.1.0" members = [ "governance/program", "governance/chat/program", - "stake-pool/program", - "token/program", - "token/program-2022", ] exclude = [ "account-compression/" @@ -21,6 +18,3 @@ wallet = "~/.config/solana/id.json" [programs.mainnet] spl_governance = "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw" spl_governance_chat = "gCHAtYKrUUktTVzE4hEnZdLV4LXrdBf6Hh9qMaJALET" -spl_stake_pool = "SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy" -spl_token = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" -spl_token_2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" diff --git a/Cargo.lock b/Cargo.lock index 0aaac540b50..cdb00c16039 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,70 +178,12 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "anstream" -version = "0.6.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" - -[[package]] -name = "anstyle-parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" -dependencies = [ - "anstyle", - "windows-sys 0.48.0", -] - [[package]] name = "anyhow" version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - [[package]] name = "aquamarine" version = "0.3.3" @@ -445,22 +387,6 @@ dependencies = [ "syn 1.0.107", ] -[[package]] -name = "assert_cmd" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" -dependencies = [ - "anstyle", - "bstr 1.6.0", - "doc-comment", - "libc", - "predicates 3.0.3", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - [[package]] name = "assert_matches" version = "1.5.0" @@ -899,17 +825,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "bstr" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" -dependencies = [ - "memchr", - "regex-automata 0.3.0", - "serde", -] - [[package]] name = "bumpalo" version = "3.12.0" @@ -1144,8 +1059,7 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", - "clap_derive 3.2.25", - "clap_lex 0.2.4", + "clap_lex", "indexmap 1.9.3", "once_cell", "strsim 0.10.0", @@ -1153,53 +1067,6 @@ dependencies = [ "textwrap 0.16.0", ] -[[package]] -name = "clap" -version = "4.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" -dependencies = [ - "clap_builder", - "clap_derive 4.4.7", -] - -[[package]] -name = "clap_builder" -version = "4.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" -dependencies = [ - "anstream", - "anstyle", - "clap_lex 0.6.0", - "strsim 0.10.0", -] - -[[package]] -name = "clap_derive" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" -dependencies = [ - "heck 0.4.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "clap_derive" -version = "4.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "clap_lex" version = "0.2.4" @@ -1209,12 +1076,6 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "clap_lex" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" - [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1225,12 +1086,6 @@ dependencies = [ "unicode-width 0.1.9", ] -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - [[package]] name = "combine" version = "3.8.1" @@ -1591,7 +1446,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", - "serde", ] [[package]] @@ -1746,12 +1600,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "downcast" version = "0.11.0" @@ -1906,15 +1754,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "escape8259" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4911e3666fcd7826997b4745c8224295a6f3072f1418c3067b97a67557ee" -dependencies = [ - "rustversion", -] - [[package]] name = "etcd-client" version = "0.11.1" @@ -2272,7 +2111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" dependencies = [ "aho-corasick 0.7.18", - "bstr 0.2.17", + "bstr", "fnv", "log", "regex", @@ -2419,12 +2258,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.1.19" @@ -2734,7 +2567,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -2746,7 +2578,6 @@ dependencies = [ "equivalent", "hashbrown 0.15.1", "rayon", - "serde", ] [[package]] @@ -2786,12 +2617,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - [[package]] name = "itertools" version = "0.10.5" @@ -3092,18 +2917,6 @@ dependencies = [ "libsecp256k1-core", ] -[[package]] -name = "libtest-mimic" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" -dependencies = [ - "anstream", - "anstyle", - "clap 4.4.8", - "escape8259", -] - [[package]] name = "libz-sys" version = "1.1.5" @@ -3327,7 +3140,7 @@ dependencies = [ "fragile", "lazy_static", "mockall_derive", - "predicates 2.1.5", + "predicates", "predicates-tree", ] @@ -3939,18 +3752,6 @@ dependencies = [ "regex", ] -[[package]] -name = "predicates" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" -dependencies = [ - "anstyle", - "difflib", - "itertools 0.10.5", - "predicates-core", -] - [[package]] name = "predicates-core" version = "1.0.6" @@ -4405,16 +4206,10 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick 1.0.2", "memchr", - "regex-automata 0.4.8", + "regex-automata", "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56" - [[package]] name = "regex-automata" version = "0.4.8" @@ -4762,15 +4557,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96560eea317a9cc4e0bb1f6a2c93c09a19b8c4fc5cb3fcc0ec1c094cd783e2" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.19" @@ -4809,12 +4595,6 @@ dependencies = [ "untrusted 0.7.1", ] -[[package]] -name = "sdd" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" - [[package]] name = "security-framework" version = "2.10.0" @@ -4913,16 +4693,9 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.6.0", "serde", "serde_derive", - "serde_json", "serde_with_macros", - "time", ] [[package]] @@ -4950,31 +4723,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "serial_test" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" -dependencies = [ - "futures 0.3.31", - "log", - "once_cell", - "parking_lot 0.12.0", - "scc", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "sha-1" version = "0.8.2" @@ -5782,8 +5530,8 @@ dependencies = [ "solana-vote", "solana-vote-program", "solana-wen-restart", - "strum 0.24.1", - "strum_macros 0.24.3", + "strum", + "strum_macros", "sys-info", "sysctl", "tempfile", @@ -6176,8 +5924,8 @@ dependencies = [ "spl-token 6.0.0", "spl-token-2022 4.0.0", "static_assertions", - "strum 0.24.1", - "strum_macros 0.24.3", + "strum", + "strum_macros", "tar", "tempfile", "thiserror 1.0.69", @@ -6889,8 +6637,8 @@ dependencies = [ "solana-zk-token-proof-program", "solana-zk-token-sdk", "static_assertions", - "strum 0.24.1", - "strum_macros 0.24.3", + "strum", + "strum_macros", "symlink", "tar", "tempfile", @@ -7765,6 +7513,8 @@ dependencies = [ [[package]] name = "spl-associated-token-account" version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3" dependencies = [ "borsh 1.5.3", "num-derive", @@ -7773,31 +7523,19 @@ dependencies = [ "spl-associated-token-account-client", "spl-token 7.0.0", "spl-token-2022 6.0.0", - "thiserror 2.0.9", + "thiserror 1.0.69", ] [[package]] name = "spl-associated-token-account-client" version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f8349dbcbe575f354f9a533a21f272f3eb3808a49e2fdc1c34393b88ba76cb" dependencies = [ "solana-instruction", - "solana-program", "solana-pubkey", ] -[[package]] -name = "spl-associated-token-account-test" -version = "0.0.1" -dependencies = [ - "solana-program", - "solana-program-test", - "solana-sdk", - "spl-associated-token-account 6.0.0", - "spl-associated-token-account-client", - "spl-token 7.0.0", - "spl-token-2022 6.0.0", -] - [[package]] name = "spl-binary-oracle-pair" version = "0.1.0" @@ -7834,27 +7572,19 @@ checksum = "a38ea8b6dedb7065887f12d62ed62c1743aa70749e8558f963609793f6fb12bc" dependencies = [ "bytemuck", "solana-program", - "spl-discriminator-derive 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-discriminator-derive", ] [[package]] name = "spl-discriminator" -version = "0.4.0" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3" dependencies = [ - "borsh 1.5.3", "bytemuck", "solana-program-error", "solana-sha256-hasher", - "spl-discriminator-derive 0.2.0", -] - -[[package]] -name = "spl-discriminator-derive" -version = "0.2.0" -dependencies = [ - "quote", - "spl-discriminator-syn 0.2.0", - "syn 2.0.87", + "spl-discriminator-derive", ] [[package]] @@ -7864,19 +7594,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ "quote", - "spl-discriminator-syn 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 2.0.87", -] - -[[package]] -name = "spl-discriminator-syn" -version = "0.2.0" -dependencies = [ - "proc-macro2", - "quote", - "sha2 0.10.8", + "spl-discriminator-syn", "syn 2.0.87", - "thiserror 1.0.69", ] [[package]] @@ -7895,6 +7614,8 @@ dependencies = [ [[package]] name = "spl-elgamal-registry" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a157622a63a4d12fbd8b347fd75ee442cb913137fa98647824c992fb049a15b" dependencies = [ "bytemuck", "solana-program", @@ -7958,31 +7679,6 @@ dependencies = [ "spl-token 7.0.0", ] -[[package]] -name = "spl-feature-proposal" -version = "1.0.0" -dependencies = [ - "borsh 1.5.3", - "solana-program", - "solana-program-test", - "solana-sdk", - "spl-token 7.0.0", -] - -[[package]] -name = "spl-feature-proposal-cli" -version = "1.2.0" -dependencies = [ - "chrono", - "clap 2.34.0", - "solana-clap-utils", - "solana-cli-config", - "solana-client", - "solana-logger", - "solana-sdk", - "spl-feature-proposal", -] - [[package]] name = "spl-governance" version = "4.0.0" @@ -8100,23 +7796,6 @@ dependencies = [ "thiserror 2.0.9", ] -[[package]] -name = "spl-instruction-padding" -version = "0.3.0" -dependencies = [ - "num_enum", - "solana-account-info", - "solana-cpi", - "solana-instruction", - "solana-program", - "solana-program-entrypoint", - "solana-program-error", - "solana-program-test", - "solana-pubkey", - "solana-sdk", - "static_assertions", -] - [[package]] name = "spl-managed-token" version = "0.1.0" @@ -8217,22 +7896,21 @@ dependencies = [ [[package]] name = "spl-pod" version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a7d5950993e1ff2680bd989df298eeb169367fb2f9deeef1f132de6e4e8016" dependencies = [ - "base64 0.22.1", "borsh 1.5.3", "bytemuck", "bytemuck_derive", "num-derive", "num-traits", - "serde", - "serde_json", "solana-decode-error", "solana-msg", "solana-program-error", "solana-program-option", "solana-pubkey", "solana-zk-sdk", - "thiserror 2.0.9", + "thiserror 1.0.69", ] [[package]] @@ -8244,32 +7922,21 @@ dependencies = [ "num-derive", "num-traits", "solana-program", - "spl-program-error-derive 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spl-program-error-derive", "thiserror 1.0.69", ] [[package]] name = "spl-program-error" version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" dependencies = [ - "lazy_static", "num-derive", "num-traits", - "serial_test", "solana-program", - "solana-sdk", - "spl-program-error-derive 0.4.1", - "thiserror 2.0.9", -] - -[[package]] -name = "spl-program-error-derive" -version = "0.4.1" -dependencies = [ - "proc-macro2", - "quote", - "sha2 0.10.8", - "syn 2.0.87", + "spl-program-error-derive", + "thiserror 1.0.69", ] [[package]] @@ -8287,6 +7954,8 @@ dependencies = [ [[package]] name = "spl-record" version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1288810a85bbe7e62ee3c6f7b8119e8c1016e90351411d12e4132e98c7ca7344" dependencies = [ "bytemuck", "num-derive", @@ -8298,11 +7967,9 @@ dependencies = [ "solana-program-entrypoint", "solana-program-error", "solana-program-pack", - "solana-program-test", "solana-pubkey", "solana-rent", - "solana-sdk", - "thiserror 2.0.9", + "thiserror 1.0.69", ] [[package]] @@ -8315,143 +7982,6 @@ dependencies = [ "solana-sdk", ] -[[package]] -name = "spl-single-pool" -version = "1.0.1" -dependencies = [ - "approx", - "arrayref", - "bincode", - "borsh 1.5.3", - "num-derive", - "num-traits", - "num_enum", - "rand 0.8.5", - "solana-program", - "solana-program-test", - "solana-sdk", - "solana-security-txt", - "solana-vote-program", - "spl-associated-token-account 6.0.0", - "spl-associated-token-account-client", - "spl-token 7.0.0", - "test-case", - "thiserror 2.0.9", -] - -[[package]] -name = "spl-single-pool-cli" -version = "1.0.0" -dependencies = [ - "bincode", - "borsh 1.5.3", - "clap 3.2.25", - "console", - "serde", - "serde_derive", - "serde_json", - "serde_with", - "serial_test", - "solana-account-decoder", - "solana-clap-v3-utils", - "solana-cli-config", - "solana-cli-output", - "solana-client", - "solana-logger", - "solana-remote-wallet", - "solana-sdk", - "solana-test-validator", - "solana-transaction-status", - "solana-vote-program", - "spl-single-pool", - "spl-token 7.0.0", - "spl-token-client", - "tempfile", - "test-case", - "tokio", -] - -[[package]] -name = "spl-slashing" -version = "0.1.0" -dependencies = [ - "bincode", - "bitflags 2.6.0", - "bytemuck", - "generic-array 0.14.7", - "lazy_static", - "num-derive", - "num-traits", - "num_enum", - "rand 0.8.5", - "serde", - "serde_bytes", - "serde_derive", - "serde_with", - "solana-client", - "solana-entry", - "solana-ledger", - "solana-program", - "solana-program-test", - "solana-sdk", - "spl-pod 0.5.0", - "spl-record", - "thiserror 2.0.9", -] - -[[package]] -name = "spl-stake-pool" -version = "2.0.1" -dependencies = [ - "arrayref", - "assert_matches", - "bincode", - "borsh 1.5.3", - "bytemuck", - "num-derive", - "num-traits", - "num_enum", - "proptest", - "serde", - "serde_derive", - "solana-program", - "solana-program-test", - "solana-sdk", - "solana-security-txt", - "solana-vote-program", - "spl-pod 0.5.0", - "spl-token 7.0.0", - "spl-token-2022 6.0.0", - "test-case", - "thiserror 2.0.9", -] - -[[package]] -name = "spl-stake-pool-cli" -version = "2.0.0" -dependencies = [ - "bincode", - "borsh 1.5.3", - "bs58", - "clap 2.34.0", - "serde", - "serde_derive", - "serde_json", - "solana-account-decoder", - "solana-clap-utils", - "solana-cli-config", - "solana-cli-output", - "solana-client", - "solana-logger", - "solana-program", - "solana-remote-wallet", - "solana-sdk", - "spl-associated-token-account 6.0.0", - "spl-associated-token-account-client", - "spl-stake-pool", - "spl-token 7.0.0", -] - [[package]] name = "spl-tlv-account-resolution" version = "0.7.0" @@ -8469,27 +7999,23 @@ dependencies = [ [[package]] name = "spl-tlv-account-resolution" version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" dependencies = [ "bytemuck", - "futures 0.3.31", - "futures-util", "num-derive", "num-traits", - "serde", "solana-account-info", - "solana-client", "solana-decode-error", "solana-instruction", "solana-msg", "solana-program-error", - "solana-program-test", "solana-pubkey", - "solana-sdk", - "spl-discriminator 0.4.0", + "spl-discriminator 0.4.1", "spl-pod 0.5.0", "spl-program-error 0.6.0", "spl-type-length-value 0.7.0", - "thiserror 2.0.9", + "thiserror 1.0.69", ] [[package]] @@ -8510,19 +8036,16 @@ dependencies = [ [[package]] name = "spl-token" version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" dependencies = [ "arrayref", "bytemuck", - "lazy_static", "num-derive", "num-traits", "num_enum", - "proptest", - "serial_test", "solana-program", - "solana-program-test", - "solana-sdk", - "thiserror 2.0.9", + "thiserror 1.0.69", ] [[package]] @@ -8552,28 +8075,20 @@ dependencies = [ [[package]] name = "spl-token-2022" version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" dependencies = [ "arrayref", - "base64 0.22.1", "bytemuck", - "lazy_static", "num-derive", "num-traits", "num_enum", - "proptest", - "serde", - "serde_json", - "serde_with", - "serial_test", "solana-program", - "solana-program-test", - "solana-sdk", "solana-security-txt", "solana-zk-sdk", "spl-elgamal-registry", "spl-memo 6.0.0", "spl-pod 0.5.0", - "spl-tlv-account-resolution 0.9.0", "spl-token 7.0.0", "spl-token-confidential-transfer-ciphertext-arithmetic", "spl-token-confidential-transfer-proof-extraction", @@ -8582,81 +8097,14 @@ dependencies = [ "spl-token-metadata-interface 0.6.0", "spl-transfer-hook-interface 0.9.0", "spl-type-length-value 0.7.0", - "thiserror 2.0.9", -] - -[[package]] -name = "spl-token-2022-test" -version = "0.0.1" -dependencies = [ - "async-trait", - "borsh 1.5.3", - "bytemuck", - "futures-util", - "solana-program", - "solana-program-test", - "solana-sdk", - "spl-associated-token-account 6.0.0", - "spl-elgamal-registry", - "spl-instruction-padding", - "spl-memo 6.0.0", - "spl-pod 0.5.0", - "spl-record", - "spl-tlv-account-resolution 0.9.0", - "spl-token-2022 6.0.0", - "spl-token-client", - "spl-token-confidential-transfer-proof-extraction", - "spl-token-confidential-transfer-proof-generation", - "spl-token-group-interface 0.5.0", - "spl-token-metadata-interface 0.6.0", - "spl-transfer-hook-example", - "spl-transfer-hook-interface 0.9.0", - "test-case", - "walkdir", -] - -[[package]] -name = "spl-token-cli" -version = "5.0.0" -dependencies = [ - "assert_cmd", - "base64 0.22.1", - "clap 3.2.25", - "console", - "futures 0.3.31", - "libtest-mimic", - "serde", - "serde_derive", - "serde_json", - "serial_test", - "solana-account-decoder", - "solana-clap-v3-utils", - "solana-cli-config", - "solana-cli-output", - "solana-client", - "solana-logger", - "solana-remote-wallet", - "solana-sdk", - "solana-test-validator", - "solana-transaction-status", - "spl-associated-token-account-client", - "spl-memo 6.0.0", - "spl-token 7.0.0", - "spl-token-2022 6.0.0", - "spl-token-client", - "spl-token-confidential-transfer-proof-generation", - "spl-token-group-interface 0.5.0", - "spl-token-metadata-interface 0.6.0", - "strum 0.26.3", - "strum_macros 0.26.4", - "tempfile", - "tokio", - "walkdir", + "thiserror 1.0.69", ] [[package]] name = "spl-token-client" version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9155237581388a928822ce91caf936cf54406d27bf21def44f18b25e304ba0e" dependencies = [ "async-trait", "bincode", @@ -8680,85 +8128,44 @@ dependencies = [ "spl-token-group-interface 0.5.0", "spl-token-metadata-interface 0.6.0", "spl-transfer-hook-interface 0.9.0", - "thiserror 2.0.9", -] - -[[package]] -name = "spl-token-collection" -version = "0.1.0" -dependencies = [ - "solana-program", - "solana-program-test", - "solana-sdk", - "spl-discriminator 0.4.0", - "spl-pod 0.5.0", - "spl-program-error 0.6.0", - "spl-token-2022 6.0.0", - "spl-token-client", - "spl-token-group-example", - "spl-token-group-interface 0.5.0", - "spl-token-metadata-interface 0.6.0", - "spl-type-length-value 0.7.0", + "thiserror 1.0.69", ] [[package]] name = "spl-token-confidential-transfer-ciphertext-arithmetic" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f1bf731fc65546330a7929a9735679add70f828dd076a4e69b59d3afb5423c" dependencies = [ "base64 0.22.1", "bytemuck", - "curve25519-dalek 4.1.3", "solana-curve25519", "solana-zk-sdk", - "spl-token-confidential-transfer-proof-generation", ] [[package]] name = "spl-token-confidential-transfer-proof-extraction" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383937e637ccbe546f736d5115344351ebd4d2a076907582335261da58236816" dependencies = [ "bytemuck", "solana-curve25519", "solana-program", "solana-zk-sdk", "spl-pod 0.5.0", - "thiserror 2.0.9", + "thiserror 1.0.69", ] [[package]] name = "spl-token-confidential-transfer-proof-generation" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8627184782eec1894de8ea26129c61303f1f0adeed65c20e0b10bc584f09356d" dependencies = [ "curve25519-dalek 4.1.3", "solana-zk-sdk", - "thiserror 2.0.9", -] - -[[package]] -name = "spl-token-confidential-transfer-proof-test" -version = "0.0.1" -dependencies = [ - "curve25519-dalek 4.1.3", - "solana-zk-sdk", - "spl-token-confidential-transfer-proof-extraction", - "spl-token-confidential-transfer-proof-generation", - "thiserror 2.0.9", -] - -[[package]] -name = "spl-token-group-example" -version = "0.2.1" -dependencies = [ - "solana-program", - "solana-program-test", - "solana-sdk", - "spl-discriminator 0.4.0", - "spl-pod 0.5.0", - "spl-token-2022 6.0.0", - "spl-token-client", - "spl-token-group-interface 0.5.0", - "spl-token-metadata-interface 0.6.0", - "spl-type-length-value 0.7.0", + "thiserror 1.0.69", ] [[package]] @@ -8777,6 +8184,8 @@ dependencies = [ [[package]] name = "spl-token-group-interface" version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" dependencies = [ "bytemuck", "num-derive", @@ -8786,11 +8195,9 @@ dependencies = [ "solana-msg", "solana-program-error", "solana-pubkey", - "solana-sha256-hasher", - "spl-discriminator 0.4.0", + "spl-discriminator 0.4.1", "spl-pod 0.5.0", - "spl-type-length-value 0.7.0", - "thiserror 2.0.9", + "thiserror 1.0.69", ] [[package]] @@ -8826,21 +8233,6 @@ dependencies = [ "spl-token-lending", ] -[[package]] -name = "spl-token-metadata-example" -version = "0.3.0" -dependencies = [ - "solana-program", - "solana-program-test", - "solana-sdk", - "spl-pod 0.5.0", - "spl-token-2022 6.0.0", - "spl-token-client", - "spl-token-metadata-interface 0.6.0", - "spl-type-length-value 0.7.0", - "test-case", -] - [[package]] name = "spl-token-metadata-interface" version = "0.4.0" @@ -8858,23 +8250,22 @@ dependencies = [ [[package]] name = "spl-token-metadata-interface" version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" dependencies = [ "borsh 1.5.3", "num-derive", "num-traits", - "serde", - "serde_json", "solana-borsh", "solana-decode-error", "solana-instruction", "solana-msg", "solana-program-error", "solana-pubkey", - "solana-sha256-hasher", - "spl-discriminator 0.4.0", + "spl-discriminator 0.4.1", "spl-pod 0.5.0", "spl-type-length-value 0.7.0", - "thiserror 2.0.9", + "thiserror 1.0.69", ] [[package]] @@ -8961,98 +8352,6 @@ dependencies = [ "thiserror 2.0.9", ] -[[package]] -name = "spl-transfer-hook-cli" -version = "0.2.0" -dependencies = [ - "clap 3.2.25", - "futures-util", - "serde", - "serde_json", - "serde_yaml", - "solana-clap-v3-utils", - "solana-cli-config", - "solana-client", - "solana-logger", - "solana-remote-wallet", - "solana-sdk", - "solana-test-validator", - "spl-tlv-account-resolution 0.9.0", - "spl-token-2022 6.0.0", - "spl-token-client", - "spl-transfer-hook-example", - "spl-transfer-hook-interface 0.9.0", - "strum 0.26.3", - "strum_macros 0.26.4", - "tokio", -] - -[[package]] -name = "spl-transfer-hook-example" -version = "0.6.0" -dependencies = [ - "arrayref", - "solana-program", - "solana-program-test", - "solana-sdk", - "spl-tlv-account-resolution 0.9.0", - "spl-token-2022 6.0.0", - "spl-transfer-hook-interface 0.9.0", - "spl-type-length-value 0.7.0", -] - -[[package]] -name = "spl-transfer-hook-example-downgrade" -version = "1.0.0" -dependencies = [ - "solana-account-info", - "solana-program-entrypoint", - "solana-program-error", - "solana-pubkey", -] - -[[package]] -name = "spl-transfer-hook-example-fail" -version = "1.0.0" -dependencies = [ - "solana-account-info", - "solana-program-entrypoint", - "solana-program-error", - "solana-pubkey", -] - -[[package]] -name = "spl-transfer-hook-example-success" -version = "1.0.0" -dependencies = [ - "solana-account-info", - "solana-program-entrypoint", - "solana-program-error", - "solana-pubkey", -] - -[[package]] -name = "spl-transfer-hook-example-swap" -version = "1.0.0" -dependencies = [ - "solana-account-info", - "solana-program-entrypoint", - "solana-program-error", - "solana-pubkey", - "spl-token-2022 6.0.0", -] - -[[package]] -name = "spl-transfer-hook-example-swap-with-fee" -version = "1.0.0" -dependencies = [ - "solana-account-info", - "solana-program-entrypoint", - "solana-program-error", - "solana-pubkey", - "spl-token-2022 6.0.0", -] - [[package]] name = "spl-transfer-hook-interface" version = "0.7.0" @@ -9072,6 +8371,8 @@ dependencies = [ [[package]] name = "spl-transfer-hook-interface" version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" dependencies = [ "arrayref", "bytemuck", @@ -9082,16 +8383,14 @@ dependencies = [ "solana-decode-error", "solana-instruction", "solana-msg", - "solana-program", "solana-program-error", "solana-pubkey", - "spl-discriminator 0.4.0", + "spl-discriminator 0.4.1", "spl-pod 0.5.0", "spl-program-error 0.6.0", "spl-tlv-account-resolution 0.9.0", "spl-type-length-value 0.7.0", - "thiserror 2.0.9", - "tokio", + "thiserror 1.0.69", ] [[package]] @@ -9110,6 +8409,8 @@ dependencies = [ [[package]] name = "spl-type-length-value" version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" dependencies = [ "bytemuck", "num-derive", @@ -9118,29 +8419,9 @@ dependencies = [ "solana-decode-error", "solana-msg", "solana-program-error", - "spl-discriminator 0.4.0", + "spl-discriminator 0.4.1", "spl-pod 0.5.0", - "spl-type-length-value-derive", - "thiserror 2.0.9", -] - -[[package]] -name = "spl-type-length-value-derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] - -[[package]] -name = "spl-type-length-value-derive-test" -version = "0.1.0" -dependencies = [ - "borsh 1.5.3", - "solana-borsh", - "spl-discriminator 0.4.0", - "spl-type-length-value 0.7.0", + "thiserror 1.0.69", ] [[package]] @@ -9191,15 +8472,9 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros 0.24.3", + "strum_macros", ] -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" - [[package]] name = "strum_macros" version = "0.24.3" @@ -9213,19 +8488,6 @@ dependencies = [ "syn 1.0.107", ] -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.87", -] - [[package]] name = "subtle" version = "2.6.1" @@ -10061,12 +9323,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 5b93b9a2544..ef88ac61895 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,6 @@ opt-level = 3 [workspace] members = [ - "associated-token-account/client", - "associated-token-account/program", - "associated-token-account/program-test", "binary-option/program", "binary-oracle-pair/program", "examples/rust/cross-program-invocation", @@ -24,68 +21,31 @@ members = [ "examples/rust/sysvar", "examples/rust/transfer-lamports", "examples/rust/transfer-tokens", - "feature-proposal/program", - "feature-proposal/cli", "governance/addin-mock/program", "governance/addin-api", "governance/program", "governance/test-sdk", "governance/tools", "governance/chat/program", - "instruction-padding/program", - "libraries/discriminator", "libraries/concurrent-merkle-tree", "libraries/math", "libraries/math-example", "libraries/merkle-tree-reference", - "libraries/pod", - "libraries/program-error", - "libraries/tlv-account-resolution", - "libraries/type-length-value", - "libraries/type-length-value-derive-test", "name-service/program", "managed-token/program", - "record/program", "shared-memory/program", - "single-pool/cli", - "single-pool/program", - "slashing/program", - "stake-pool/cli", - "stake-pool/program", "stateless-asks/program", - "token-collection/program", - "token-group/example", - "token-group/interface", + #"token-collection/program", "token-lending/cli", + "token-lending/flash_loan_receiver", "token-lending/program", - "token-metadata/example", - "token-metadata/interface", "token-swap/program", "token-swap/program/fuzz", "token-upgrade/cli", "token-upgrade/program", "token-wrap/program", - "token/cli", - "token/program", - "token/program-2022", - "token/program-2022-test", - "token/program-2022-test/transfer-hook-test-programs/downgrade", - "token/program-2022-test/transfer-hook-test-programs/fail", - "token/program-2022-test/transfer-hook-test-programs/success", - "token/program-2022-test/transfer-hook-test-programs/swap", - "token/program-2022-test/transfer-hook-test-programs/swap-with-fee", - "token/transfer-hook/cli", - "token/transfer-hook/example", - "token/transfer-hook/interface", - "token/confidential-transfer/ciphertext-arithmetic", - "token/confidential-transfer/proof-extraction", - "token/confidential-transfer/proof-generation", - "token/confidential-transfer/proof-tests", - "token/confidential-transfer/elgamal-registry", - "token/client", "utils/cgen", "utils/test-client", - "token-lending/flash_loan_receiver", ] exclude = [ diff --git a/README.md b/README.md index 2a88a26c616..e98b524760b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,31 @@ +# PLEASE READ: This repo no longer contains the SPL program implementations + +This repo still exists in archived form, feel free to fork any reference +implementations it still contains. + +## Migrated Packages + +The Solana Program Library repository has been broken up into separate repos for +each program and set of clients, under the +[solana-program organization](https://github.com/solana-program). + +The following programs have been moved: + +* [Associated-Token-Account](https://github.com/solana-program/associated-token-account) +* [Feature Proposal](https://github.com/solana-program/feature-proposal) +* [Instruction Padding](https://github.com/solana-program/instruction-padding) +* [Libraries](https://github.com/solana-program/libraries) +* [Memo](https://github.com/solana-program/memo) +* [Record](https://github.com/solana-program/record) +* [Single Pool](https://github.com/solana-program/single-pool) +* [Slashing](https://github.com/solana-program/slashing) +* [Stake Pool](https://github.com/solana-program/stake-pool) +* [Token](https://github.com/solana-program/token) +* [Token-2022](https://github.com/solana-program/token-2022) +* [Token-Group](https://github.com/solana-program/token-group) +* [Token-Metadata](https://github.com/solana-program/token-metadata) +* [Token-2022 Transfer Hook](https://github.com/solana-program/transfer-hook) + # Solana Program Library The Solana Program Library (SPL) is a collection of on-chain programs targeting @@ -17,22 +45,17 @@ the Solana Mainnet Beta. Currently, this includes: | Program | Version | | --- | --- | -| [token](https://github.com/solana-labs/solana-program-library/tree/master/token/program) | [3.4.0](https://github.com/solana-labs/solana-program-library/releases/tag/token-v3.4.0) | -| [associated-token-account](https://github.com/solana-labs/solana-program-library/tree/master/associated-token-account/program) | [1.1.0](https://github.com/solana-labs/solana-program-library/releases/tag/associated-token-account-v1.1.0) | -| [token-2022](https://github.com/solana-labs/solana-program-library/tree/master/token/program-2022) | [1.0.0](https://github.com/solana-labs/solana-program-library/releases/tag/token-2022-v1.0.0) | +| [token](https://github.com/solana-program/token/tree/main/program) | [3.4.0](https://github.com/solana-labs/solana-program-library/releases/tag/token-v3.4.0) | +| [associated-token-account](https://github.com/solana-program/associated-token-account/tree/main/program) | [1.1.0](https://github.com/solana-labs/solana-program-library/releases/tag/associated-token-account-v1.1.0) | +| [token-2022](https://github.com/solana-program/token-2022/tree/main/program) | [1.0.0](https://github.com/solana-labs/solana-program-library/releases/tag/token-2022-v1.0.0) | | [governance](https://github.com/solana-labs/solana-program-library/tree/master/governance/program) | [3.1.0](https://github.com/solana-labs/solana-program-library/releases/tag/governance-v3.1.0) | -| [stake-pool](https://github.com/solana-labs/solana-program-library/tree/master/stake-pool/program) | [1.0.0](https://github.com/solana-labs/solana-program-library/releases/tag/stake-pool-v1.0.0) | +| [stake-pool](https://github.com/solana-program/stake-pool/tree/main/program) | [1.0.0](https://github.com/solana-labs/solana-program-library/releases/tag/stake-pool-v1.0.0) | | [account-compression](https://github.com/solana-labs/solana-program-library/tree/master/account-compression/programs/account-compression) | [0.1.3](https://github.com/solana-labs/solana-program-library/releases/tag/account-compression-v0.1.3) | | [shared-memory](https://github.com/solana-labs/solana-program-library/tree/master/shared-memory/program) | [1.0.0](https://github.com/solana-labs/solana-program-library/commit/b40e0dd3fd6c0e509dc1e8dd3da0a6d609035bbd) | -| [feature-proposal](https://github.com/solana-labs/solana-program-library/tree/master/feature-proposal/program) | [1.0.0](https://github.com/solana-labs/solana-program-library/releases/tag/feature-proposal-v1.0.0) | +| [feature-proposal](https://github.com/solana-program/feature-proposal/tree/main/program) | [1.0.0](https://github.com/solana-labs/solana-program-library/releases/tag/feature-proposal-v1.0.0) | | [name-service](https://github.com/solana-labs/solana-program-library/tree/master/name-service/program) | [0.3.0](https://github.com/solana-labs/solana-program-library/releases/tag/name-service-v0.3.0) | -| [memo](https://github.com/solana-program/memo/tree/master/program) | [3.0.0](https://github.com/solana-labs/solana-program-library/releases/tag/memo-v3.0.0) | - -In addition, one program is planned for deployment to Solana Mainnet Beta: - -| Program | Version | -| --- | --- | -| [single-pool](https://github.com/solana-labs/solana-program-library/tree/master/single-pool/program) | [1.0.1](https://github.com/solana-labs/solana-program-library/releases/tag/single-pool-v1.0.1) | +| [memo](https://github.com/solana-program/memo/tree/main/program) | [3.0.0](https://github.com/solana-labs/solana-program-library/releases/tag/memo-v3.0.0) | +| [single-pool](https://github.com/solana-program/single-pool/tree/main/program) | [1.0.1](https://github.com/solana-labs/solana-program-library/releases/tag/single-pool-v1.0.1) | ## Audits @@ -40,13 +63,13 @@ Only a subset of programs within the Solana Program Library repo are audited. Cu | Program | Last Audit Date | Version | | --- | --- | --- | -| [token](https://github.com/solana-labs/solana-program-library/tree/master/token/program) | 2022-08-04 (Peer review) | [4fadd55](https://github.com/solana-labs/solana-program-library/commit/4fadd553e1c549afd1d62aeb5ffa7ef31d1999d1) | -| [associated-token-account](https://github.com/solana-labs/solana-program-library/tree/master/associated-token-account/program) | 2022-08-04 (Peer review) | [c00194d](https://github.com/solana-labs/solana-program-library/commit/c00194d2257302f028f44a403c6dee95c0f9c3bc) | -| [token-2022](https://github.com/solana-labs/solana-program-library/tree/master/token/program-2022) | [2023-11-03](https://github.com/solana-labs/security-audits/blob/master/spl/OtterSecToken2022Audit-2023-11-03.pdf) | [e924132](https://github.com/solana-labs/solana-program-library/tree/e924132d65ba0896249fb4983f6f97caff15721a) | -| [stake-pool](https://github.com/solana-labs/solana-program-library/tree/master/stake-pool/program) | [2023-12-31](https://github.com/solana-labs/security-audits/blob/master/spl/HalbornStakePoolAudit-2023-12-31.pdf) | [a17fffe](https://github.com/solana-labs/solana-program-library/commit/a17fffe70d6cc13742abfbc4a4a375b087580bc1) | +| [token](https://github.com/solana-program/token) | 2022-08-04 (Peer review) | [4fadd55](https://github.com/solana-labs/solana-program-library/commit/4fadd553e1c549afd1d62aeb5ffa7ef31d1999d1) | +| [associated-token-account](https://github.com/solana-program/associated-token-account) | 2022-08-04 (Peer review) | [c00194d](https://github.com/solana-labs/solana-program-library/commit/c00194d2257302f028f44a403c6dee95c0f9c3bc) | +| [token-2022](https://github.com/solana-program/token-2022) | [2023-11-03](https://github.com/solana-labs/security-audits/blob/master/spl/OtterSecToken2022Audit-2023-11-03.pdf) | [e924132](https://github.com/solana-labs/solana-program-library/tree/e924132d65ba0896249fb4983f6f97caff15721a) | +| [stake-pool](https://github.com/solana-program/stake-pool) | [2023-12-31](https://github.com/solana-labs/security-audits/blob/master/spl/HalbornStakePoolAudit-2023-12-31.pdf) | [a17fffe](https://github.com/solana-labs/solana-program-library/commit/a17fffe70d6cc13742abfbc4a4a375b087580bc1) | | [account-compression](https://github.com/solana-labs/solana-program-library/tree/master/account-compression/programs/account-compression) | [2022-12-05](https://github.com/solana-labs/security-audits/blob/master/spl/OtterSecAccountCompressionAudit-2022-12-03.pdf) | [6e81794](https://github.com/solana-labs/solana-program-library/commit/6e81794) | | [shared-memory](https://github.com/solana-labs/solana-program-library/tree/master/shared-memory/program) | [2021-02-25](https://github.com/solana-labs/security-audits/blob/master/spl/KudelskiTokenSwapSharedMemAudit-2021-02-25.pdf) | [b40e0dd](https://github.com/solana-labs/solana-program-library/commit/b40e0dd3fd6c0e509dc1e8dd3da0a6d609035bbd) | -| [single-pool](https://github.com/solana-labs/solana-program-library/tree/master/single-pool/program) | [2024-01-02](https://github.com/solana-labs/security-audits/blob/master/spl/ZellicSinglePoolAudit-2024-01-02.pdf) | [ef44df9](https://github.com/solana-labs/solana-program-library/commit/ef44df985e76a697ee9a8aabb3a223610e4cf1dc) | +| [single-pool](https://github.com/solana-program/single-pool) | [2024-01-02](https://github.com/solana-labs/security-audits/blob/master/spl/ZellicSinglePoolAudit-2024-01-02.pdf) | [ef44df9](https://github.com/solana-labs/solana-program-library/commit/ef44df985e76a697ee9a8aabb3a223610e4cf1dc) | All other programs may be updated from time to time. These programs are not audited, so fork and deploy them at your own risk. Here is the full list of @@ -54,11 +77,11 @@ unaudited programs: * [binary-option](https://github.com/solana-labs/solana-program-library/tree/master/binary-option/program) * [binary-oracle-pair](https://github.com/solana-labs/solana-program-library/tree/master/binary-oracle-pair/program) -* [feature-proposal](https://github.com/solana-labs/solana-program-library/tree/master/feature-proposal/program) -* [instruction-padding](https://github.com/solana-labs/solana-program-library/tree/master/instruction-padding/program) +* [feature-proposal](https://github.com/solana-program/feature-proposal) +* [instruction-padding](https://github.com/solana-program/instruction-padding) * [managed-token](https://github.com/solana-labs/solana-program-library/tree/master/managed-token/program) * [name-service](https://github.com/solana-labs/solana-program-library/tree/master/name-service/program) -* [record](https://github.com/solana-labs/solana-program-library/tree/master/record/program) +* [record](https://github.com/solana-program/record) * [stateless-asks](https://github.com/solana-labs/solana-program-library/tree/master/stateless-asks/program) * [token-lending](https://github.com/solana-labs/solana-program-library/tree/master/token-lending/program) * [token-swap](https://github.com/solana-labs/solana-program-library/tree/master/token-swap/program) @@ -70,13 +93,6 @@ More information about the repository's security policy at The [security-audits repo](https://github.com/solana-labs/security-audits) contains all past and present program audits. -## Migrated Packages - -The Solana Program Library repository is being broken up into separate repos for -each program and set of clients. The following programs have been moved: - -* [Memo](https://github.com/solana-program/memo) - ## Program Packages | Package | Description | Version | Docs | diff --git a/SECURITY.md b/SECURITY.md index 91e69bcda73..57d453670fd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,73 +1,13 @@ -# Security Policy - -1. [Reporting security problems](#reporting) -1. [Security Bug Bounties](#bounty) -1. [Scope](#scope) -1. [Incident Response Process](#process) - - -## Reporting security problems in the Solana Program Library - -**DO NOT CREATE A GITHUB ISSUE** to report a security problem. - -Instead please use this [Report a Vulnerability](https://github.com/solana-labs/solana-program-library/security/advisories/new) link. -Provide a helpful title and detailed description of the problem. - -If you haven't done so already, please **enable two-factor auth** in your GitHub account. - -Expect a response as fast as possible in the advisory, typically within 72 hours. - --- - -If you do not receive a response in the advisory, send an email to -security@solana.com with the full URL of the advisory you have created. DO NOT -include attachments or provide detail sufficient for exploitation regarding the -security issue in this email. **Only provide such details in the advisory**. - -If you do not receive a response from security@solana.com please followup with -the team directly. You can do this in the `#core-technology` channel of the -[Solana Tech discord server](https://solana.com/discord), by pinging the admins -in the channel and referencing the fact that you submitted a security problem. - - - - -## Security Bug Bounties -The Solana Foundation offer bounties for critical Solana security issues. Please -see the [Solana Security Bug -Bounties](https://github.com/solana-labs/solana/security/policy#security-bug-bounties) -for details on classes of bugs and payment amounts. - - -## Scope - -Only a subset of programs within the Solana Program Library repo are deployed to -the Solana Mainnet Beta. Currently, this includes: - -* [associated-token-account](https://github.com/solana-labs/solana-program-library/tree/master/associated-token-account/program) -* [feature-proposal](https://github.com/solana-labs/solana-program-library/tree/master/feature-proposal/program) -* [governance](https://github.com/solana-labs/solana-program-library/tree/master/governance/program) -* [memo](https://github.com/solana-program/memo) -* [name-service](https://github.com/solana-labs/solana-program-library/tree/master/name-service/program) -* [stake-pool](https://github.com/solana-labs/solana-program-library/tree/master/stake-pool/program) -* [token](https://github.com/solana-labs/solana-program-library/tree/master/token/program) -* [token-2022](https://github.com/solana-labs/solana-program-library/tree/master/token/program-2022) - -If you discover a critical security issue in an out-of-scope program, your finding -may still be valuable. - -Many programs, including -[token-swap](https://github.com/solana-labs/solana-program-library/tree/master/token-swap/program) -and [token-lending](https://github.com/solana-labs/solana-program-library/tree/master/token-lending/program), -have been forked and deployed by prominent ecosystem projects, many of which -have their own bug bounty programs. - -While we cannot guarantee a bounty from another entity, we can help determine who -may be affected and put you in touch with the corresponding teams. - - -## Incident Response Process - -In case an incident is discovered or reported, the -[Solana Security Incident Response Process](https://github.com/solana-labs/solana/security/policy#incident-response-process) -will be followed to contain, respond and remediate. +## This repo will be archived soon +Active development in this repo is ending 2024-03-02. Anza is continuing development +in program-specific repos under the +[solana-program organization](https://github.com/solana-program). Please refer to +the security policy in individual repos: + +* [associated-token-account](https://github.com/solana-program/associated-token-account/security) +* [feature-proposal](https://github.com/solana-program/feature-proposal/security) +* [memo](https://github.com/solana-program/memo/security) +* [single-pool](https://github.com/solana-program/single-pool/security) +* [stake-pool](https://github.com/solana-program/stake-pool/security) +* [token](https://github.com/solana-program/token/security) +* [token-2022](https://github.com/solana-program/token-2022/security) diff --git a/associated-token-account/README.md b/associated-token-account/README.md new file mode 100644 index 00000000000..5ed8fdcbbe1 --- /dev/null +++ b/associated-token-account/README.md @@ -0,0 +1,2 @@ +NOTE: The associated-token-account program and clients are now maintained at +[solana-program/associated-token-account](https://github.com/solana-program/associated-token-account). diff --git a/associated-token-account/client/Cargo.toml b/associated-token-account/client/Cargo.toml deleted file mode 100644 index 7a52979693f..00000000000 --- a/associated-token-account/client/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "spl-associated-token-account-client" -version = "2.0.0" -description = "Solana Program Library Associated Token Account Client" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -solana-instruction = { version = "2.1.0", features = ["std"] } -solana-pubkey = { version = "2.1.0", features = ["curve25519"] } - -[dev-dependencies] -solana-program = "2.1.0" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/associated-token-account/client/src/address.rs b/associated-token-account/client/src/address.rs deleted file mode 100644 index 6a608299600..00000000000 --- a/associated-token-account/client/src/address.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Address derivation functions - -use solana_pubkey::Pubkey; - -/// Derives the associated token account address and bump seed -/// for the given wallet address, token mint and token program id -pub fn get_associated_token_address_and_bump_seed( - wallet_address: &Pubkey, - token_mint_address: &Pubkey, - program_id: &Pubkey, - token_program_id: &Pubkey, -) -> (Pubkey, u8) { - get_associated_token_address_and_bump_seed_internal( - wallet_address, - token_mint_address, - program_id, - token_program_id, - ) -} - -mod inline_spl_token { - solana_pubkey::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); -} - -/// Derives the associated token account address for the given wallet address -/// and token mint -pub fn get_associated_token_address( - wallet_address: &Pubkey, - token_mint_address: &Pubkey, -) -> Pubkey { - get_associated_token_address_with_program_id( - wallet_address, - token_mint_address, - &inline_spl_token::ID, - ) -} - -/// Derives the associated token account address for the given wallet address, -/// token mint and token program id -pub fn get_associated_token_address_with_program_id( - wallet_address: &Pubkey, - token_mint_address: &Pubkey, - token_program_id: &Pubkey, -) -> Pubkey { - get_associated_token_address_and_bump_seed( - wallet_address, - token_mint_address, - &crate::program::id(), - token_program_id, - ) - .0 -} - -/// For internal use only. -#[doc(hidden)] -pub fn get_associated_token_address_and_bump_seed_internal( - wallet_address: &Pubkey, - token_mint_address: &Pubkey, - program_id: &Pubkey, - token_program_id: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[ - &wallet_address.to_bytes(), - &token_program_id.to_bytes(), - &token_mint_address.to_bytes(), - ], - program_id, - ) -} diff --git a/associated-token-account/client/src/instruction.rs b/associated-token-account/client/src/instruction.rs deleted file mode 100644 index 4f000d619d0..00000000000 --- a/associated-token-account/client/src/instruction.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Instruction creators for the program -use { - crate::{address::get_associated_token_address_with_program_id, program::id}, - solana_instruction::{AccountMeta, Instruction}, - solana_pubkey::Pubkey, -}; - -const SYSTEM_PROGRAM_ID: Pubkey = Pubkey::from_str_const("11111111111111111111111111111111"); - -fn build_associated_token_account_instruction( - funding_address: &Pubkey, - wallet_address: &Pubkey, - token_mint_address: &Pubkey, - token_program_id: &Pubkey, - instruction: u8, -) -> Instruction { - let associated_account_address = get_associated_token_address_with_program_id( - wallet_address, - token_mint_address, - token_program_id, - ); - // safety check, assert if not a creation instruction, which is only 0 or 1 - assert!(instruction <= 1); - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(*funding_address, true), - AccountMeta::new(associated_account_address, false), - AccountMeta::new_readonly(*wallet_address, false), - AccountMeta::new_readonly(*token_mint_address, false), - AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false), - AccountMeta::new_readonly(*token_program_id, false), - ], - data: vec![instruction], - } -} - -/// Creates Create instruction -pub fn create_associated_token_account( - funding_address: &Pubkey, - wallet_address: &Pubkey, - token_mint_address: &Pubkey, - token_program_id: &Pubkey, -) -> Instruction { - build_associated_token_account_instruction( - funding_address, - wallet_address, - token_mint_address, - token_program_id, - 0, // AssociatedTokenAccountInstruction::Create - ) -} - -/// Creates CreateIdempotent instruction -pub fn create_associated_token_account_idempotent( - funding_address: &Pubkey, - wallet_address: &Pubkey, - token_mint_address: &Pubkey, - token_program_id: &Pubkey, -) -> Instruction { - build_associated_token_account_instruction( - funding_address, - wallet_address, - token_mint_address, - token_program_id, - 1, // AssociatedTokenAccountInstruction::CreateIdempotent - ) -} - -/// Creates a `RecoverNested` instruction -pub fn recover_nested( - wallet_address: &Pubkey, - owner_token_mint_address: &Pubkey, - nested_token_mint_address: &Pubkey, - token_program_id: &Pubkey, -) -> Instruction { - let owner_associated_account_address = get_associated_token_address_with_program_id( - wallet_address, - owner_token_mint_address, - token_program_id, - ); - let destination_associated_account_address = get_associated_token_address_with_program_id( - wallet_address, - nested_token_mint_address, - token_program_id, - ); - let nested_associated_account_address = get_associated_token_address_with_program_id( - &owner_associated_account_address, // ATA is wrongly used as a wallet_address - nested_token_mint_address, - token_program_id, - ); - - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(nested_associated_account_address, false), - AccountMeta::new_readonly(*nested_token_mint_address, false), - AccountMeta::new(destination_associated_account_address, false), - AccountMeta::new_readonly(owner_associated_account_address, false), - AccountMeta::new_readonly(*owner_token_mint_address, false), - AccountMeta::new(*wallet_address, true), - AccountMeta::new_readonly(*token_program_id, false), - ], - data: vec![2], // AssociatedTokenAccountInstruction::RecoverNested - } -} - -#[cfg(test)] -mod tests { - use {super::*, solana_program::system_program}; - - #[test] - fn system_program_id() { - assert_eq!(system_program::id(), SYSTEM_PROGRAM_ID); - } -} diff --git a/associated-token-account/client/src/lib.rs b/associated-token-account/client/src/lib.rs deleted file mode 100644 index ef32c233b0a..00000000000 --- a/associated-token-account/client/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Client crate for interacting with the spl-associated-token-account program -#![deny(missing_docs)] -#![forbid(unsafe_code)] - -pub mod address; -pub mod instruction; - -/// Module defining the program id -pub mod program { - solana_pubkey::declare_id!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); -} diff --git a/associated-token-account/program-test/Cargo.toml b/associated-token-account/program-test/Cargo.toml deleted file mode 100644 index e576eb3d654..00000000000 --- a/associated-token-account/program-test/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -authors = ["Solana Labs Maintainers "] -description = "SPL Associated Token Account Program Tests" -edition = "2021" -license = "Apache-2.0" -name = "spl-associated-token-account-test" -repository = "https://github.com/solana-labs/solana-program-library" -version = "0.0.1" - -[features] -test-sbf = [] - -[dev-dependencies] -solana-program = "2.1.0" -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" -spl-associated-token-account = { version = "6.0.0", path = "../program", features = ["no-entrypoint"] } -spl-associated-token-account-client = { version = "2.0.0", path = "../client" } -spl-token = { version = "7.0", path = "../../token/program", features = ["no-entrypoint"] } -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = ["no-entrypoint"] } diff --git a/associated-token-account/program-test/tests/create_idempotent.rs b/associated-token-account/program-test/tests/create_idempotent.rs deleted file mode 100644 index 673366ce14c..00000000000 --- a/associated-token-account/program-test/tests/create_idempotent.rs +++ /dev/null @@ -1,244 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; - -use { - program_test::program_test_2022, - solana_program::{instruction::*, pubkey::Pubkey}, - solana_program_test::*, - solana_sdk::{ - account::Account as SolanaAccount, - program_option::COption, - program_pack::Pack, - signature::Signer, - signer::keypair::Keypair, - system_instruction::create_account, - transaction::{Transaction, TransactionError}, - }, - spl_associated_token_account::{ - error::AssociatedTokenAccountError, - instruction::{ - create_associated_token_account, create_associated_token_account_idempotent, - }, - }, - spl_associated_token_account_client::address::get_associated_token_address_with_program_id, - spl_token_2022::{ - extension::ExtensionType, - instruction::initialize_account, - state::{Account, AccountState}, - }, -}; - -#[tokio::test] -async fn success_account_exists() { - let wallet_address = Pubkey::new_unique(); - let token_mint_address = Pubkey::new_unique(); - let associated_token_address = get_associated_token_address_with_program_id( - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let (mut banks_client, payer, recent_blockhash) = - program_test_2022(token_mint_address, true).start().await; - let rent = banks_client.get_rent().await.unwrap(); - let expected_token_account_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner]) - .unwrap(); - let expected_token_account_balance = rent.minimum_balance(expected_token_account_len); - - let instruction = create_associated_token_account_idempotent( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - // Associated account now exists - let associated_account = banks_client - .get_account(associated_token_address) - .await - .expect("get_account") - .expect("associated_account not none"); - assert_eq!(associated_account.data.len(), expected_token_account_len); - assert_eq!(associated_account.owner, spl_token_2022::id()); - assert_eq!(associated_account.lamports, expected_token_account_balance); - - // Unchecked instruction fails - let instruction = create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::IllegalOwner) - ); - - // Get a new blockhash, succeed with create if non existent - let recent_blockhash = banks_client - .get_new_latest_blockhash(&recent_blockhash) - .await - .unwrap(); - - let instruction = create_associated_token_account_idempotent( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - // Associated account is unchanged - let associated_account = banks_client - .get_account(associated_token_address) - .await - .expect("get_account") - .expect("associated_account not none"); - assert_eq!(associated_account.data.len(), expected_token_account_len); - assert_eq!(associated_account.owner, spl_token_2022::id()); - assert_eq!(associated_account.lamports, expected_token_account_balance); -} - -#[tokio::test] -async fn fail_account_exists_with_wrong_owner() { - let wallet_address = Pubkey::new_unique(); - let token_mint_address = Pubkey::new_unique(); - let associated_token_address = get_associated_token_address_with_program_id( - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let wrong_owner = Pubkey::new_unique(); - let mut associated_token_account = - SolanaAccount::new(1_000_000_000, Account::LEN, &spl_token_2022::id()); - let token_account = Account { - mint: token_mint_address, - owner: wrong_owner, - amount: 0, - delegate: COption::None, - state: AccountState::Initialized, - is_native: COption::None, - delegated_amount: 0, - close_authority: COption::None, - }; - Account::pack(token_account, &mut associated_token_account.data).unwrap(); - let mut pt = program_test_2022(token_mint_address, true); - pt.add_account(associated_token_address, associated_token_account); - let (banks_client, payer, recent_blockhash) = pt.start().await; - - // fail creating token account if non existent - let instruction = create_associated_token_account_idempotent( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(AssociatedTokenAccountError::InvalidOwner as u32) - ) - ); -} - -#[tokio::test] -async fn fail_non_ata() { - let token_mint_address = Pubkey::new_unique(); - let (banks_client, payer, recent_blockhash) = - program_test_2022(token_mint_address, true).start().await; - - let rent = banks_client.get_rent().await.unwrap(); - let token_account_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner]) - .unwrap(); - let token_account_balance = rent.minimum_balance(token_account_len); - - let wallet_address = Pubkey::new_unique(); - let account = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[ - create_account( - &payer.pubkey(), - &account.pubkey(), - token_account_balance, - token_account_len as u64, - &spl_token_2022::id(), - ), - initialize_account( - &spl_token_2022::id(), - &account.pubkey(), - &token_mint_address, - &wallet_address, - ) - .unwrap(), - ], - Some(&payer.pubkey()), - &[&payer, &account], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let mut instruction = create_associated_token_account_idempotent( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - instruction.accounts[1] = AccountMeta::new(account.pubkey(), false); // <-- Invalid associated_account_address - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::InvalidSeeds) - ); -} diff --git a/associated-token-account/program-test/tests/extended_mint.rs b/associated-token-account/program-test/tests/extended_mint.rs deleted file mode 100644 index 1ee0b5513e5..00000000000 --- a/associated-token-account/program-test/tests/extended_mint.rs +++ /dev/null @@ -1,216 +0,0 @@ -// Mark this test as BPF-only due to current `ProgramTest` limitations when -// CPIing into the system program -#![cfg(feature = "test-sbf")] - -mod program_test; - -use { - program_test::program_test_2022, - solana_program::{instruction::*, pubkey::Pubkey, system_instruction}, - solana_program_test::*, - solana_sdk::{ - signature::Signer, - signer::keypair::Keypair, - transaction::{Transaction, TransactionError}, - }, - spl_associated_token_account::instruction::create_associated_token_account, - spl_associated_token_account_client::address::get_associated_token_address_with_program_id, - spl_token_2022::{ - error::TokenError, - extension::{ - transfer_fee, BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned, - }, - state::{Account, Mint}, - }, -}; - -#[tokio::test] -async fn test_associated_token_account_with_transfer_fees() { - let wallet_sender = Keypair::new(); - let wallet_address_sender = wallet_sender.pubkey(); - let wallet_address_receiver = Pubkey::new_unique(); - let (mut banks_client, payer, recent_blockhash) = - program_test_2022(Pubkey::new_unique(), true).start().await; - let rent = banks_client.get_rent().await.unwrap(); - - // create extended mint - // ... in the future, a mint can be pre-loaded in program_test.rs like the - // regular mint - let mint_account = Keypair::new(); - let token_mint_address = mint_account.pubkey(); - let mint_authority = Keypair::new(); - let space = - ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferFeeConfig]) - .unwrap(); - let maximum_fee = 100; - let mut transaction = Transaction::new_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - transfer_fee::instruction::initialize_transfer_fee_config( - &spl_token_2022::id(), - &token_mint_address, - Some(&mint_authority.pubkey()), - Some(&mint_authority.pubkey()), - 1_000, - maximum_fee, - ) - .unwrap(), - spl_token_2022::instruction::initialize_mint( - &spl_token_2022::id(), - &token_mint_address, - &mint_authority.pubkey(), - Some(&mint_authority.pubkey()), - 0, - ) - .unwrap(), - ], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &mint_account], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - // create extended ATAs - let mut transaction = Transaction::new_with_payer( - &[create_associated_token_account( - &payer.pubkey(), - &wallet_address_sender, - &token_mint_address, - &spl_token_2022::id(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let recent_blockhash = banks_client - .get_new_latest_blockhash(&recent_blockhash) - .await - .unwrap(); - - let mut transaction = Transaction::new_with_payer( - &[create_associated_token_account( - &payer.pubkey(), - &wallet_address_receiver, - &token_mint_address, - &spl_token_2022::id(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let associated_token_address_sender = get_associated_token_address_with_program_id( - &wallet_address_sender, - &token_mint_address, - &spl_token_2022::id(), - ); - let associated_token_address_receiver = get_associated_token_address_with_program_id( - &wallet_address_receiver, - &token_mint_address, - &spl_token_2022::id(), - ); - - // mint tokens - let sender_amount = 50 * maximum_fee; - let mut transaction = Transaction::new_with_payer( - &[spl_token_2022::instruction::mint_to( - &spl_token_2022::id(), - &token_mint_address, - &associated_token_address_sender, - &mint_authority.pubkey(), - &[], - sender_amount, - ) - .unwrap()], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &mint_authority], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - // not enough tokens - let mut transaction = Transaction::new_with_payer( - &[transfer_fee::instruction::transfer_checked_with_fee( - &spl_token_2022::id(), - &associated_token_address_sender, - &token_mint_address, - &associated_token_address_receiver, - &wallet_address_sender, - &[], - 10_001, - 0, - maximum_fee, - ) - .unwrap()], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &wallet_sender], recent_blockhash); - let err = banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InsufficientFunds as u32) - ) - ); - - let recent_blockhash = banks_client - .get_new_latest_blockhash(&recent_blockhash) - .await - .unwrap(); - - // success - let transfer_amount = 500; - let fee = 50; - let mut transaction = Transaction::new_with_payer( - &[transfer_fee::instruction::transfer_checked_with_fee( - &spl_token_2022::id(), - &associated_token_address_sender, - &token_mint_address, - &associated_token_address_receiver, - &wallet_address_sender, - &[], - transfer_amount, - 0, - fee, - ) - .unwrap()], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &wallet_sender], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let sender_account = banks_client - .get_account(associated_token_address_sender) - .await - .unwrap() - .unwrap(); - let sender_state = StateWithExtensionsOwned::::unpack(sender_account.data).unwrap(); - assert_eq!(sender_state.base.amount, sender_amount - transfer_amount); - let extension = sender_state - .get_extension::() - .unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - - let receiver_account = banks_client - .get_account(associated_token_address_receiver) - .await - .unwrap() - .unwrap(); - let receiver_state = - StateWithExtensionsOwned::::unpack(receiver_account.data).unwrap(); - assert_eq!(receiver_state.base.amount, transfer_amount - fee); - let extension = receiver_state - .get_extension::() - .unwrap(); - assert_eq!(extension.withheld_amount, fee.into()); -} diff --git a/associated-token-account/program-test/tests/fixtures/token-mint-data.bin b/associated-token-account/program-test/tests/fixtures/token-mint-data.bin deleted file mode 100644 index 4a48512c02a..00000000000 Binary files a/associated-token-account/program-test/tests/fixtures/token-mint-data.bin and /dev/null differ diff --git a/associated-token-account/program-test/tests/process_create_associated_token_account.rs b/associated-token-account/program-test/tests/process_create_associated_token_account.rs deleted file mode 100644 index e693310385c..00000000000 --- a/associated-token-account/program-test/tests/process_create_associated_token_account.rs +++ /dev/null @@ -1,318 +0,0 @@ -// Mark this test as BPF-only due to current `ProgramTest` limitations when -// CPIing into the system program -#![cfg(feature = "test-sbf")] - -mod program_test; - -use { - program_test::program_test_2022, - solana_program::{instruction::*, pubkey::Pubkey, system_instruction, sysvar}, - solana_program_test::*, - solana_sdk::{ - signature::Signer, - transaction::{Transaction, TransactionError}, - }, - spl_associated_token_account::instruction::create_associated_token_account, - spl_associated_token_account_client::address::get_associated_token_address_with_program_id, - spl_token_2022::{extension::ExtensionType, state::Account}, -}; - -#[tokio::test] -async fn test_associated_token_address() { - let wallet_address = Pubkey::new_unique(); - let token_mint_address = Pubkey::new_unique(); - let associated_token_address = get_associated_token_address_with_program_id( - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let (banks_client, payer, recent_blockhash) = - program_test_2022(token_mint_address, true).start().await; - let rent = banks_client.get_rent().await.unwrap(); - - let expected_token_account_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner]) - .unwrap(); - let expected_token_account_balance = rent.minimum_balance(expected_token_account_len); - - // Associated account does not exist - assert_eq!( - banks_client - .get_account(associated_token_address) - .await - .expect("get_account"), - None, - ); - - let mut transaction = Transaction::new_with_payer( - &[create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - // Associated account now exists - let associated_account = banks_client - .get_account(associated_token_address) - .await - .expect("get_account") - .expect("associated_account not none"); - assert_eq!(associated_account.data.len(), expected_token_account_len,); - assert_eq!(associated_account.owner, spl_token_2022::id()); - assert_eq!(associated_account.lamports, expected_token_account_balance); -} - -#[tokio::test] -async fn test_create_with_fewer_lamports() { - let wallet_address = Pubkey::new_unique(); - let token_mint_address = Pubkey::new_unique(); - let associated_token_address = get_associated_token_address_with_program_id( - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let (banks_client, payer, recent_blockhash) = - program_test_2022(token_mint_address, true).start().await; - let rent = banks_client.get_rent().await.unwrap(); - let expected_token_account_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner]) - .unwrap(); - let expected_token_account_balance = rent.minimum_balance(expected_token_account_len); - - // Transfer lamports into `associated_token_address` before creating it - enough - // to be rent-exempt for 0 data, but not for an initialized token account - let mut transaction = Transaction::new_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - &associated_token_address, - rent.minimum_balance(0), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - assert_eq!( - banks_client - .get_balance(associated_token_address) - .await - .unwrap(), - rent.minimum_balance(0) - ); - - // Check that the program adds the extra lamports - let mut transaction = Transaction::new_with_payer( - &[create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - assert_eq!( - banks_client - .get_balance(associated_token_address) - .await - .unwrap(), - expected_token_account_balance, - ); -} - -#[tokio::test] -async fn test_create_with_excess_lamports() { - let wallet_address = Pubkey::new_unique(); - let token_mint_address = Pubkey::new_unique(); - let associated_token_address = get_associated_token_address_with_program_id( - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let (banks_client, payer, recent_blockhash) = - program_test_2022(token_mint_address, true).start().await; - let rent = banks_client.get_rent().await.unwrap(); - - let expected_token_account_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner]) - .unwrap(); - let expected_token_account_balance = rent.minimum_balance(expected_token_account_len); - - // Transfer 1 lamport into `associated_token_address` before creating it - let mut transaction = Transaction::new_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - &associated_token_address, - expected_token_account_balance + 1, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - assert_eq!( - banks_client - .get_balance(associated_token_address) - .await - .unwrap(), - expected_token_account_balance + 1 - ); - - // Check that the program doesn't add any lamports - let mut transaction = Transaction::new_with_payer( - &[create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - assert_eq!( - banks_client - .get_balance(associated_token_address) - .await - .unwrap(), - expected_token_account_balance + 1 - ); -} - -#[tokio::test] -async fn test_create_account_mismatch() { - let wallet_address = Pubkey::new_unique(); - let token_mint_address = Pubkey::new_unique(); - let _associated_token_address = get_associated_token_address_with_program_id( - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let (banks_client, payer, recent_blockhash) = - program_test_2022(token_mint_address, true).start().await; - - let mut instruction = create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - instruction.accounts[1] = AccountMeta::new(Pubkey::default(), false); // <-- Invalid associated_account_address - - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::InvalidSeeds) - ); - - let mut instruction = create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - instruction.accounts[2] = AccountMeta::new(Pubkey::default(), false); // <-- Invalid wallet_address - - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::InvalidSeeds) - ); - - let mut instruction = create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - instruction.accounts[3] = AccountMeta::new(Pubkey::default(), false); // <-- Invalid token_mint_address - - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - assert_eq!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::InvalidSeeds) - ); -} - -#[tokio::test] -async fn test_create_associated_token_account_using_legacy_implicit_instruction() { - let wallet_address = Pubkey::new_unique(); - let token_mint_address = Pubkey::new_unique(); - let associated_token_address = get_associated_token_address_with_program_id( - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - let (banks_client, payer, recent_blockhash) = - program_test_2022(token_mint_address, true).start().await; - let rent = banks_client.get_rent().await.unwrap(); - let expected_token_account_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner]) - .unwrap(); - let expected_token_account_balance = rent.minimum_balance(expected_token_account_len); - - // Associated account does not exist - assert_eq!( - banks_client - .get_account(associated_token_address) - .await - .expect("get_account"), - None, - ); - - let mut create_associated_token_account_ix = create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token_2022::id(), - ); - - // Use implicit instruction and rent account to replicate the legacy invocation - create_associated_token_account_ix.data = vec![]; - create_associated_token_account_ix - .accounts - .push(AccountMeta::new_readonly(sysvar::rent::id(), false)); - - let mut transaction = - Transaction::new_with_payer(&[create_associated_token_account_ix], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - // Associated account now exists - let associated_account = banks_client - .get_account(associated_token_address) - .await - .expect("get_account") - .expect("associated_account not none"); - assert_eq!(associated_account.data.len(), expected_token_account_len); - assert_eq!(associated_account.owner, spl_token_2022::id()); - assert_eq!(associated_account.lamports, expected_token_account_balance); -} diff --git a/associated-token-account/program-test/tests/program_test.rs b/associated-token-account/program-test/tests/program_test.rs deleted file mode 100644 index 7008931e7ef..00000000000 --- a/associated-token-account/program-test/tests/program_test.rs +++ /dev/null @@ -1,84 +0,0 @@ -use { - solana_program::pubkey::Pubkey, - solana_program_test::{ProgramTest, *}, - spl_associated_token_account::{id, processor::process_instruction}, -}; - -#[allow(dead_code)] -pub fn program_test(token_mint_address: Pubkey, use_latest_spl_token: bool) -> ProgramTest { - let mut pc = ProgramTest::new( - "spl_associated_token_account", - id(), - processor!(process_instruction), - ); - - if use_latest_spl_token { - pc.prefer_bpf(false); - // TODO: Remove when spl-token is available by default in program-test - pc.add_program( - "spl_token", - spl_token::id(), - processor!(spl_token::processor::Processor::process), - ); - } - - // Add a token mint account - // - // The account data was generated by running: - // $ solana account EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ - // --output-file tests/fixtures/token-mint-data.bin - // - pc.add_account_with_file_data( - token_mint_address, - 1461600, - spl_token::id(), - "token-mint-data.bin", - ); - - // Dial down the BPF compute budget to detect if the program gets bloated in the - // future - pc.set_compute_max_units(60_000); - - pc -} - -#[allow(dead_code)] -pub fn program_test_2022( - token_mint_address: Pubkey, - use_latest_spl_token_2022: bool, -) -> ProgramTest { - let mut pc = ProgramTest::new( - "spl_associated_token_account", - id(), - processor!(process_instruction), - ); - - if use_latest_spl_token_2022 { - pc.prefer_bpf(false); - // TODO: Remove when spl-token-2022 is available by default in program-test - pc.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(spl_token_2022::processor::Processor::process), - ); - } - - // Add a token mint account - // - // The account data was generated by running: - // $ solana account EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ - // --output-file tests/fixtures/token-mint-data.bin - // - pc.add_account_with_file_data( - token_mint_address, - 1461600, - spl_token_2022::id(), - "token-mint-data.bin", - ); - - // Dial down the BPF compute budget to detect if the program gets bloated in the - // future - pc.set_compute_max_units(50_000); - - pc -} diff --git a/associated-token-account/program-test/tests/recover_nested.rs b/associated-token-account/program-test/tests/recover_nested.rs deleted file mode 100644 index bd06ae529d6..00000000000 --- a/associated-token-account/program-test/tests/recover_nested.rs +++ /dev/null @@ -1,678 +0,0 @@ -// Mark this test as BPF-only due to current `ProgramTest` limitations when -// CPIing into the system program -#![cfg(feature = "test-sbf")] - -mod program_test; - -use { - program_test::{program_test, program_test_2022}, - solana_program::{pubkey::Pubkey, system_instruction}, - solana_program_test::*, - solana_sdk::{ - instruction::{AccountMeta, InstructionError}, - signature::Signer, - signer::keypair::Keypair, - transaction::{Transaction, TransactionError}, - }, - spl_associated_token_account::instruction, - spl_associated_token_account_client::address::get_associated_token_address_with_program_id, - spl_token_2022::{ - extension::{ExtensionType, StateWithExtensionsOwned}, - state::{Account, Mint}, - }, -}; - -async fn create_mint(context: &mut ProgramTestContext, program_id: &Pubkey) -> (Pubkey, Keypair) { - let mint_account = Keypair::new(); - let token_mint_address = mint_account.pubkey(); - let mint_authority = Keypair::new(); - let space = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let rent = context.banks_client.get_rent().await.unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(space), - space as u64, - program_id, - ), - spl_token_2022::instruction::initialize_mint( - program_id, - &token_mint_address, - &mint_authority.pubkey(), - Some(&mint_authority.pubkey()), - 0, - ) - .unwrap(), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_account], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - (token_mint_address, mint_authority) -} - -async fn create_associated_token_account( - context: &mut ProgramTestContext, - owner: &Pubkey, - mint: &Pubkey, - program_id: &Pubkey, -) -> Pubkey { - let transaction = Transaction::new_signed_with_payer( - &[instruction::create_associated_token_account( - &context.payer.pubkey(), - owner, - mint, - program_id, - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - get_associated_token_address_with_program_id(owner, mint, program_id) -} - -#[allow(clippy::too_many_arguments)] -async fn try_recover_nested( - context: &mut ProgramTestContext, - program_id: &Pubkey, - nested_mint: Pubkey, - nested_mint_authority: Keypair, - nested_associated_token_address: Pubkey, - destination_token_address: Pubkey, - wallet: Keypair, - recover_transaction: Transaction, - expected_error: Option, -) { - let nested_account = context - .banks_client - .get_account(nested_associated_token_address) - .await - .unwrap() - .unwrap(); - let lamports = nested_account.lamports; - - // mint to nested account - let amount = 100; - let transaction = Transaction::new_signed_with_payer( - &[spl_token_2022::instruction::mint_to( - program_id, - &nested_mint, - &nested_associated_token_address, - &nested_mint_authority.pubkey(), - &[], - amount, - ) - .unwrap()], - Some(&context.payer.pubkey()), - &[&context.payer, &nested_mint_authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // transfer / close nested account - let result = context - .banks_client - .process_transaction(recover_transaction) - .await; - - if let Some(expected_error) = expected_error { - let error = result.unwrap_err().unwrap(); - assert_eq!(error, TransactionError::InstructionError(0, expected_error)); - } else { - result.unwrap(); - // nested account is gone - assert!(context - .banks_client - .get_account(nested_associated_token_address) - .await - .unwrap() - .is_none()); - let destination_account = context - .banks_client - .get_account(destination_token_address) - .await - .unwrap() - .unwrap(); - let destination_state = - StateWithExtensionsOwned::::unpack(destination_account.data).unwrap(); - assert_eq!(destination_state.base.amount, amount); - let wallet_account = context - .banks_client - .get_account(wallet.pubkey()) - .await - .unwrap() - .unwrap(); - assert_eq!(wallet_account.lamports, lamports); - } -} - -async fn check_same_mint(context: &mut ProgramTestContext, program_id: &Pubkey) { - let wallet = Keypair::new(); - let (mint, mint_authority) = create_mint(context, program_id).await; - - let owner_associated_token_address = - create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await; - let nested_associated_token_address = create_associated_token_account( - context, - &owner_associated_token_address, - &mint, - program_id, - ) - .await; - - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::recover_nested( - &wallet.pubkey(), - &mint, - &mint, - program_id, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wallet], - context.last_blockhash, - ); - try_recover_nested( - context, - program_id, - mint, - mint_authority, - nested_associated_token_address, - owner_associated_token_address, - wallet, - transaction, - None, - ) - .await; -} - -#[tokio::test] -async fn success_same_mint_2022() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test_2022(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_same_mint(&mut context, &spl_token_2022::id()).await; -} - -#[tokio::test] -async fn success_same_mint() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_same_mint(&mut context, &spl_token::id()).await; -} - -async fn check_different_mints(context: &mut ProgramTestContext, program_id: &Pubkey) { - let wallet = Keypair::new(); - let (owner_mint, _owner_mint_authority) = create_mint(context, program_id).await; - let (nested_mint, nested_mint_authority) = create_mint(context, program_id).await; - - let owner_associated_token_address = - create_associated_token_account(context, &wallet.pubkey(), &owner_mint, program_id).await; - let nested_associated_token_address = create_associated_token_account( - context, - &owner_associated_token_address, - &nested_mint, - program_id, - ) - .await; - let destination_token_address = - create_associated_token_account(context, &wallet.pubkey(), &nested_mint, program_id).await; - - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::recover_nested( - &wallet.pubkey(), - &owner_mint, - &nested_mint, - program_id, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wallet], - context.last_blockhash, - ); - try_recover_nested( - context, - program_id, - nested_mint, - nested_mint_authority, - nested_associated_token_address, - destination_token_address, - wallet, - transaction, - None, - ) - .await; -} - -#[tokio::test] -async fn success_different_mints() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_different_mints(&mut context, &spl_token::id()).await; -} - -#[tokio::test] -async fn success_different_mints_2022() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test_2022(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_different_mints(&mut context, &spl_token_2022::id()).await; -} - -async fn check_missing_wallet_signature(context: &mut ProgramTestContext, program_id: &Pubkey) { - let wallet = Keypair::new(); - let (mint, mint_authority) = create_mint(context, program_id).await; - - let owner_associated_token_address = - create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await; - - let nested_associated_token_address = create_associated_token_account( - context, - &owner_associated_token_address, - &mint, - program_id, - ) - .await; - - let mut recover = instruction::recover_nested(&wallet.pubkey(), &mint, &mint, program_id); - recover.accounts[5] = AccountMeta::new(wallet.pubkey(), false); - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[recover], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - try_recover_nested( - context, - program_id, - mint, - mint_authority, - nested_associated_token_address, - owner_associated_token_address, - wallet, - transaction, - Some(InstructionError::MissingRequiredSignature), - ) - .await; -} - -#[tokio::test] -async fn fail_missing_wallet_signature_2022() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test_2022(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_missing_wallet_signature(&mut context, &spl_token_2022::id()).await; -} - -#[tokio::test] -async fn fail_missing_wallet_signature() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_missing_wallet_signature(&mut context, &spl_token::id()).await; -} - -async fn check_wrong_signer(context: &mut ProgramTestContext, program_id: &Pubkey) { - let wallet = Keypair::new(); - let wrong_wallet = Keypair::new(); - let (mint, mint_authority) = create_mint(context, program_id).await; - - let owner_associated_token_address = - create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await; - let nested_associated_token_address = create_associated_token_account( - context, - &owner_associated_token_address, - &mint, - program_id, - ) - .await; - - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::recover_nested( - &wrong_wallet.pubkey(), - &mint, - &mint, - program_id, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_wallet], - context.last_blockhash, - ); - try_recover_nested( - context, - program_id, - mint, - mint_authority, - nested_associated_token_address, - owner_associated_token_address, - wrong_wallet, - transaction, - Some(InstructionError::IllegalOwner), - ) - .await; -} - -#[tokio::test] -async fn fail_wrong_signer_2022() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test_2022(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_wrong_signer(&mut context, &spl_token_2022::id()).await; -} - -#[tokio::test] -async fn fail_wrong_signer() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_wrong_signer(&mut context, &spl_token::id()).await; -} - -async fn check_not_nested(context: &mut ProgramTestContext, program_id: &Pubkey) { - let wallet = Keypair::new(); - let wrong_wallet = Pubkey::new_unique(); - let (mint, mint_authority) = create_mint(context, program_id).await; - - let owner_associated_token_address = - create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await; - let nested_associated_token_address = - create_associated_token_account(context, &wrong_wallet, &mint, program_id).await; - - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::recover_nested( - &wallet.pubkey(), - &mint, - &mint, - program_id, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wallet], - context.last_blockhash, - ); - try_recover_nested( - context, - program_id, - mint, - mint_authority, - nested_associated_token_address, - owner_associated_token_address, - wallet, - transaction, - Some(InstructionError::IllegalOwner), - ) - .await; -} - -#[tokio::test] -async fn fail_not_nested_2022() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test_2022(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_not_nested(&mut context, &spl_token_2022::id()).await; -} - -#[tokio::test] -async fn fail_not_nested() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_not_nested(&mut context, &spl_token::id()).await; -} - -async fn check_wrong_address_derivation_owner( - context: &mut ProgramTestContext, - program_id: &Pubkey, -) { - let wallet = Keypair::new(); - let wrong_wallet = Pubkey::new_unique(); - let (mint, mint_authority) = create_mint(context, program_id).await; - - let owner_associated_token_address = - create_associated_token_account(context, &wallet.pubkey(), &mint, program_id).await; - let nested_associated_token_address = create_associated_token_account( - context, - &owner_associated_token_address, - &mint, - program_id, - ) - .await; - - let wrong_owner_associated_token_address = - get_associated_token_address_with_program_id(&mint, &wrong_wallet, program_id); - let mut recover = instruction::recover_nested(&wallet.pubkey(), &mint, &mint, program_id); - recover.accounts[3] = AccountMeta::new(wrong_owner_associated_token_address, false); - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[recover], - Some(&context.payer.pubkey()), - &[&context.payer, &wallet], - context.last_blockhash, - ); - try_recover_nested( - context, - program_id, - mint, - mint_authority, - nested_associated_token_address, - wrong_owner_associated_token_address, - wallet, - transaction, - Some(InstructionError::InvalidSeeds), - ) - .await; -} - -#[tokio::test] -async fn fail_wrong_address_derivation_owner_2022() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test_2022(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_wrong_address_derivation_owner(&mut context, &spl_token_2022::id()).await; -} - -#[tokio::test] -async fn fail_wrong_address_derivation_owner() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_wrong_address_derivation_owner(&mut context, &spl_token::id()).await; -} - -async fn check_owner_account_does_not_exist(context: &mut ProgramTestContext, program_id: &Pubkey) { - let wallet = Keypair::new(); - let (mint, mint_authority) = create_mint(context, program_id).await; - - let owner_associated_token_address = - get_associated_token_address_with_program_id(&wallet.pubkey(), &mint, program_id); - let nested_associated_token_address = create_associated_token_account( - context, - &owner_associated_token_address, - &mint, - program_id, - ) - .await; - - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::recover_nested( - &wallet.pubkey(), - &mint, - &mint, - program_id, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wallet], - context.last_blockhash, - ); - try_recover_nested( - context, - program_id, - mint, - mint_authority, - nested_associated_token_address, - owner_associated_token_address, - wallet, - transaction, - Some(InstructionError::IllegalOwner), - ) - .await; -} - -#[tokio::test] -async fn fail_owner_account_does_not_exist() { - let dummy_mint = Pubkey::new_unique(); - let pt = program_test_2022(dummy_mint, true); - let mut context = pt.start_with_context().await; - check_owner_account_does_not_exist(&mut context, &spl_token_2022::id()).await; -} - -#[tokio::test] -async fn fail_wrong_spl_token_program() { - let wallet = Keypair::new(); - let dummy_mint = Pubkey::new_unique(); - let pt = program_test_2022(dummy_mint, true); - let mut context = pt.start_with_context().await; - let program_id = spl_token_2022::id(); - let wrong_program_id = spl_token::id(); - let (mint, mint_authority) = create_mint(&mut context, &program_id).await; - - let owner_associated_token_address = - create_associated_token_account(&mut context, &wallet.pubkey(), &mint, &program_id).await; - let nested_associated_token_address = create_associated_token_account( - &mut context, - &owner_associated_token_address, - &mint, - &program_id, - ) - .await; - - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::recover_nested( - &wallet.pubkey(), - &mint, - &mint, - &wrong_program_id, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wallet], - context.last_blockhash, - ); - try_recover_nested( - &mut context, - &program_id, - mint, - mint_authority, - nested_associated_token_address, - owner_associated_token_address, - wallet, - transaction, - Some(InstructionError::IllegalOwner), - ) - .await; -} - -#[tokio::test] -async fn fail_destination_not_wallet_ata() { - let wallet = Keypair::new(); - let wrong_wallet = Pubkey::new_unique(); - let dummy_mint = Pubkey::new_unique(); - let pt = program_test_2022(dummy_mint, true); - let program_id = spl_token_2022::id(); - let mut context = pt.start_with_context().await; - let (mint, mint_authority) = create_mint(&mut context, &program_id).await; - - let owner_associated_token_address = - create_associated_token_account(&mut context, &wallet.pubkey(), &mint, &program_id).await; - let nested_associated_token_address = create_associated_token_account( - &mut context, - &owner_associated_token_address, - &mint, - &program_id, - ) - .await; - let wrong_destination_associated_token_account_address = - create_associated_token_account(&mut context, &wrong_wallet, &mint, &program_id).await; - - let mut recover = instruction::recover_nested(&wallet.pubkey(), &mint, &mint, &program_id); - recover.accounts[2] = - AccountMeta::new(wrong_destination_associated_token_account_address, false); - - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[recover], - Some(&context.payer.pubkey()), - &[&context.payer, &wallet], - context.last_blockhash, - ); - try_recover_nested( - &mut context, - &program_id, - mint, - mint_authority, - nested_associated_token_address, - owner_associated_token_address, - wallet, - transaction, - Some(InstructionError::InvalidSeeds), - ) - .await; -} diff --git a/associated-token-account/program-test/tests/spl_token_create.rs b/associated-token-account/program-test/tests/spl_token_create.rs deleted file mode 100644 index 21970414946..00000000000 --- a/associated-token-account/program-test/tests/spl_token_create.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Mark this test as BPF-only due to current `ProgramTest` limitations when -// CPIing into the system program -#![cfg(feature = "test-sbf")] - -mod program_test; - -#[allow(deprecated)] -use spl_associated_token_account::create_associated_token_account as deprecated_create_associated_token_account; -use { - program_test::program_test, - solana_program::pubkey::Pubkey, - solana_program_test::*, - solana_sdk::{program_pack::Pack, signature::Signer, transaction::Transaction}, - spl_associated_token_account::instruction::create_associated_token_account, - spl_associated_token_account_client::address::get_associated_token_address, - spl_token::state::Account, -}; - -#[tokio::test] -async fn success_create() { - let wallet_address = Pubkey::new_unique(); - let token_mint_address = Pubkey::new_unique(); - let associated_token_address = - get_associated_token_address(&wallet_address, &token_mint_address); - - let (banks_client, payer, recent_blockhash) = - program_test(token_mint_address, true).start().await; - let rent = banks_client.get_rent().await.unwrap(); - let expected_token_account_len = Account::LEN; - let expected_token_account_balance = rent.minimum_balance(expected_token_account_len); - - // Associated account does not exist - assert_eq!( - banks_client - .get_account(associated_token_address) - .await - .expect("get_account"), - None, - ); - - let transaction = Transaction::new_signed_with_payer( - &[create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - &spl_token::id(), - )], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - // Associated account now exists - let associated_account = banks_client - .get_account(associated_token_address) - .await - .expect("get_account") - .expect("associated_account not none"); - assert_eq!(associated_account.data.len(), expected_token_account_len); - assert_eq!(associated_account.owner, spl_token::id()); - assert_eq!(associated_account.lamports, expected_token_account_balance); -} - -#[tokio::test] -async fn success_using_deprecated_instruction_creator() { - let wallet_address = Pubkey::new_unique(); - let token_mint_address = Pubkey::new_unique(); - let associated_token_address = - get_associated_token_address(&wallet_address, &token_mint_address); - - let (banks_client, payer, recent_blockhash) = - program_test(token_mint_address, true).start().await; - let rent = banks_client.get_rent().await.unwrap(); - let expected_token_account_len = Account::LEN; - let expected_token_account_balance = rent.minimum_balance(expected_token_account_len); - - // Associated account does not exist - assert_eq!( - banks_client - .get_account(associated_token_address) - .await - .expect("get_account"), - None, - ); - - // Use legacy instruction creator - #[allow(deprecated)] - let create_associated_token_account_ix = deprecated_create_associated_token_account( - &payer.pubkey(), - &wallet_address, - &token_mint_address, - ); - - let transaction = Transaction::new_signed_with_payer( - &[create_associated_token_account_ix], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - // Associated account now exists - let associated_account = banks_client - .get_account(associated_token_address) - .await - .expect("get_account") - .expect("associated_account not none"); - assert_eq!(associated_account.data.len(), expected_token_account_len); - assert_eq!(associated_account.owner, spl_token::id()); - assert_eq!(associated_account.lamports, expected_token_account_balance); -} diff --git a/associated-token-account/program/Cargo.toml b/associated-token-account/program/Cargo.toml deleted file mode 100644 index 8360d217eb0..00000000000 --- a/associated-token-account/program/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "spl-associated-token-account" -version = "6.0.0" -description = "Solana Program Library Associated Token Account" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -borsh = "1.5.3" -num-derive = "0.4" -num-traits = "0.2" -solana-program = "2.1.0" -spl-associated-token-account-client = { version = "2.0.0", path = "../client" } -spl-token = { version = "7.0", path = "../../token/program", features = [ - "no-entrypoint", -] } -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = [ - "no-entrypoint", -] } -thiserror = "2.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/associated-token-account/program/Xargo.toml b/associated-token-account/program/Xargo.toml deleted file mode 100644 index 1744f098ae1..00000000000 --- a/associated-token-account/program/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] \ No newline at end of file diff --git a/associated-token-account/program/program-id.md b/associated-token-account/program/program-id.md deleted file mode 100644 index de8233b720f..00000000000 --- a/associated-token-account/program/program-id.md +++ /dev/null @@ -1 +0,0 @@ -ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL diff --git a/associated-token-account/program/run-tests.sh b/associated-token-account/program/run-tests.sh deleted file mode 100755 index 603e3c552b2..00000000000 --- a/associated-token-account/program/run-tests.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -ex -cd "$(dirname "$0")" -cargo clippy -cargo build -cargo build-sbf - -if [[ $1 = -v ]]; then - export RUST_LOG=solana=debug -fi - -cargo test -cargo test-sbf diff --git a/associated-token-account/program/src/entrypoint.rs b/associated-token-account/program/src/entrypoint.rs deleted file mode 100644 index 8876e45b78b..00000000000 --- a/associated-token-account/program/src/entrypoint.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Program entrypoint - -#![cfg(not(feature = "no-entrypoint"))] - -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - crate::processor::process_instruction(program_id, accounts, instruction_data) -} diff --git a/associated-token-account/program/src/error.rs b/associated-token-account/program/src/error.rs deleted file mode 100644 index ae858a0d537..00000000000 --- a/associated-token-account/program/src/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Error types - -use { - num_derive::FromPrimitive, - solana_program::{decode_error::DecodeError, program_error::ProgramError}, - thiserror::Error, -}; - -/// Errors that may be returned by the program. -#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] -pub enum AssociatedTokenAccountError { - // 0 - /// Associated token account owner does not match address derivation - #[error("Associated token account owner does not match address derivation")] - InvalidOwner, -} -impl From for ProgramError { - fn from(e: AssociatedTokenAccountError) -> Self { - ProgramError::Custom(e as u32) - } -} -impl DecodeError for AssociatedTokenAccountError { - fn type_of() -> &'static str { - "AssociatedTokenAccountError" - } -} diff --git a/associated-token-account/program/src/instruction.rs b/associated-token-account/program/src/instruction.rs deleted file mode 100644 index 51453b9ae14..00000000000 --- a/associated-token-account/program/src/instruction.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Program instructions - -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -pub use spl_associated_token_account_client::instruction::*; - -/// Instructions supported by the AssociatedTokenAccount program -#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub enum AssociatedTokenAccountInstruction { - /// Creates an associated token account for the given wallet address and - /// token mint Returns an error if the account exists. - /// - /// 0. `[writeable,signer]` Funding account (must be a system account) - /// 1. `[writeable]` Associated token account address to be created - /// 2. `[]` Wallet address for the new associated token account - /// 3. `[]` The token mint for the new associated token account - /// 4. `[]` System program - /// 5. `[]` SPL Token program - Create, - /// Creates an associated token account for the given wallet address and - /// token mint, if it doesn't already exist. Returns an error if the - /// account exists, but with a different owner. - /// - /// 0. `[writeable,signer]` Funding account (must be a system account) - /// 1. `[writeable]` Associated token account address to be created - /// 2. `[]` Wallet address for the new associated token account - /// 3. `[]` The token mint for the new associated token account - /// 4. `[]` System program - /// 5. `[]` SPL Token program - CreateIdempotent, - /// Transfers from and closes a nested associated token account: an - /// associated token account owned by an associated token account. - /// - /// The tokens are moved from the nested associated token account to the - /// wallet's associated token account, and the nested account lamports are - /// moved to the wallet. - /// - /// Note: Nested token accounts are an anti-pattern, and almost always - /// created unintentionally, so this instruction should only be used to - /// recover from errors. - /// - /// 0. `[writeable]` Nested associated token account, must be owned by `3` - /// 1. `[]` Token mint for the nested associated token account - /// 2. `[writeable]` Wallet's associated token account - /// 3. `[]` Owner associated token account address, must be owned by `5` - /// 4. `[]` Token mint for the owner associated token account - /// 5. `[writeable, signer]` Wallet address for the owner associated token - /// account - /// 6. `[]` SPL Token program - RecoverNested, -} diff --git a/associated-token-account/program/src/lib.rs b/associated-token-account/program/src/lib.rs deleted file mode 100644 index 5778456aa27..00000000000 --- a/associated-token-account/program/src/lib.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Convention for associating token accounts with a user wallet -#![deny(missing_docs)] -#![forbid(unsafe_code)] - -mod entrypoint; -pub mod error; -pub mod instruction; -pub mod processor; -pub mod tools; - -// Export current SDK types for downstream users building with a different SDK -// version -pub use solana_program; -use solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - sysvar, -}; -#[deprecated( - since = "4.1.0", - note = "Use `spl-associated-token-account-client` crate instead." -)] -pub use spl_associated_token_account_client::address::{ - get_associated_token_address, get_associated_token_address_with_program_id, -}; -// Export current SDK types for downstream users building with a different SDK -// version -pub use spl_associated_token_account_client::program::{check_id, id, ID}; - -/// Create an associated token account for the given wallet address and token -/// mint -/// -/// Accounts expected by this instruction: -/// -/// 0. `[writeable,signer]` Funding account (must be a system account) -/// 1. `[writeable]` Associated token account address to be created -/// 2. `[]` Wallet address for the new associated token account -/// 3. `[]` The token mint for the new associated token account -/// 4. `[]` System program -/// 5. `[]` SPL Token program -#[deprecated( - since = "1.0.5", - note = "please use `instruction::create_associated_token_account` instead" -)] -pub fn create_associated_token_account( - funding_address: &Pubkey, - wallet_address: &Pubkey, - token_mint_address: &Pubkey, -) -> Instruction { - let associated_account_address = - get_associated_token_address(wallet_address, token_mint_address); - - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(*funding_address, true), - AccountMeta::new(associated_account_address, false), - AccountMeta::new_readonly(*wallet_address, false), - AccountMeta::new_readonly(*token_mint_address, false), - AccountMeta::new_readonly(solana_program::system_program::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - ], - data: vec![], - } -} diff --git a/associated-token-account/program/src/processor.rs b/associated-token-account/program/src/processor.rs deleted file mode 100644 index f0063908c78..00000000000 --- a/associated-token-account/program/src/processor.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! Program state processor - -use { - crate::{ - error::AssociatedTokenAccountError, - instruction::AssociatedTokenAccountInstruction, - tools::account::{create_pda_account, get_account_len}, - }, - borsh::BorshDeserialize, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program::{invoke, invoke_signed}, - program_error::ProgramError, - pubkey::Pubkey, - rent::Rent, - system_program, - sysvar::Sysvar, - }, - spl_associated_token_account_client::address::get_associated_token_address_and_bump_seed_internal, - spl_token_2022::{ - extension::{ExtensionType, StateWithExtensions}, - state::{Account, Mint}, - }, -}; - -/// Specify when to create the associated token account -#[derive(PartialEq)] -enum CreateMode { - /// Always try to create the ATA - Always, - /// Only try to create the ATA if non-existent - Idempotent, -} - -/// Instruction processor -pub fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - let instruction = if input.is_empty() { - AssociatedTokenAccountInstruction::Create - } else { - AssociatedTokenAccountInstruction::try_from_slice(input) - .map_err(|_| ProgramError::InvalidInstructionData)? - }; - - msg!("{:?}", instruction); - - match instruction { - AssociatedTokenAccountInstruction::Create => { - process_create_associated_token_account(program_id, accounts, CreateMode::Always) - } - AssociatedTokenAccountInstruction::CreateIdempotent => { - process_create_associated_token_account(program_id, accounts, CreateMode::Idempotent) - } - AssociatedTokenAccountInstruction::RecoverNested => { - process_recover_nested(program_id, accounts) - } - } -} - -/// Processes CreateAssociatedTokenAccount instruction -fn process_create_associated_token_account( - program_id: &Pubkey, - accounts: &[AccountInfo], - create_mode: CreateMode, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let funder_info = next_account_info(account_info_iter)?; - let associated_token_account_info = next_account_info(account_info_iter)?; - let wallet_account_info = next_account_info(account_info_iter)?; - let spl_token_mint_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - let spl_token_program_info = next_account_info(account_info_iter)?; - let spl_token_program_id = spl_token_program_info.key; - - let (associated_token_address, bump_seed) = get_associated_token_address_and_bump_seed_internal( - wallet_account_info.key, - spl_token_mint_info.key, - program_id, - spl_token_program_id, - ); - if associated_token_address != *associated_token_account_info.key { - msg!("Error: Associated address does not match seed derivation"); - return Err(ProgramError::InvalidSeeds); - } - - if create_mode == CreateMode::Idempotent - && associated_token_account_info.owner == spl_token_program_id - { - let ata_data = associated_token_account_info.data.borrow(); - if let Ok(associated_token_account) = StateWithExtensions::::unpack(&ata_data) { - if associated_token_account.base.owner != *wallet_account_info.key { - let error = AssociatedTokenAccountError::InvalidOwner; - msg!("{}", error); - return Err(error.into()); - } - if associated_token_account.base.mint != *spl_token_mint_info.key { - return Err(ProgramError::InvalidAccountData); - } - return Ok(()); - } - } - if *associated_token_account_info.owner != system_program::id() { - return Err(ProgramError::IllegalOwner); - } - - let rent = Rent::get()?; - - let associated_token_account_signer_seeds: &[&[_]] = &[ - &wallet_account_info.key.to_bytes(), - &spl_token_program_id.to_bytes(), - &spl_token_mint_info.key.to_bytes(), - &[bump_seed], - ]; - - let account_len = get_account_len( - spl_token_mint_info, - spl_token_program_info, - &[ExtensionType::ImmutableOwner], - )?; - - create_pda_account( - funder_info, - &rent, - account_len, - spl_token_program_id, - system_program_info, - associated_token_account_info, - associated_token_account_signer_seeds, - )?; - - msg!("Initialize the associated token account"); - invoke( - &spl_token_2022::instruction::initialize_immutable_owner( - spl_token_program_id, - associated_token_account_info.key, - )?, - &[ - associated_token_account_info.clone(), - spl_token_program_info.clone(), - ], - )?; - invoke( - &spl_token_2022::instruction::initialize_account3( - spl_token_program_id, - associated_token_account_info.key, - spl_token_mint_info.key, - wallet_account_info.key, - )?, - &[ - associated_token_account_info.clone(), - spl_token_mint_info.clone(), - wallet_account_info.clone(), - spl_token_program_info.clone(), - ], - ) -} - -/// Processes `RecoverNested` instruction -pub fn process_recover_nested(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let nested_associated_token_account_info = next_account_info(account_info_iter)?; - let nested_token_mint_info = next_account_info(account_info_iter)?; - let destination_associated_token_account_info = next_account_info(account_info_iter)?; - let owner_associated_token_account_info = next_account_info(account_info_iter)?; - let owner_token_mint_info = next_account_info(account_info_iter)?; - let wallet_account_info = next_account_info(account_info_iter)?; - let spl_token_program_info = next_account_info(account_info_iter)?; - let spl_token_program_id = spl_token_program_info.key; - - // Check owner address derivation - let (owner_associated_token_address, bump_seed) = - get_associated_token_address_and_bump_seed_internal( - wallet_account_info.key, - owner_token_mint_info.key, - program_id, - spl_token_program_id, - ); - if owner_associated_token_address != *owner_associated_token_account_info.key { - msg!("Error: Owner associated address does not match seed derivation"); - return Err(ProgramError::InvalidSeeds); - } - - // Check nested address derivation - let (nested_associated_token_address, _) = get_associated_token_address_and_bump_seed_internal( - owner_associated_token_account_info.key, - nested_token_mint_info.key, - program_id, - spl_token_program_id, - ); - if nested_associated_token_address != *nested_associated_token_account_info.key { - msg!("Error: Nested associated address does not match seed derivation"); - return Err(ProgramError::InvalidSeeds); - } - - // Check destination address derivation - let (destination_associated_token_address, _) = - get_associated_token_address_and_bump_seed_internal( - wallet_account_info.key, - nested_token_mint_info.key, - program_id, - spl_token_program_id, - ); - if destination_associated_token_address != *destination_associated_token_account_info.key { - msg!("Error: Destination associated address does not match seed derivation"); - return Err(ProgramError::InvalidSeeds); - } - - if !wallet_account_info.is_signer { - msg!("Wallet of the owner associated token account must sign"); - return Err(ProgramError::MissingRequiredSignature); - } - - if owner_token_mint_info.owner != spl_token_program_id { - msg!("Owner mint not owned by provided token program"); - return Err(ProgramError::IllegalOwner); - } - - // Account data is dropped at the end of this, so the CPI can succeed - // without a double-borrow - let (amount, decimals) = { - // Check owner associated token account data - if owner_associated_token_account_info.owner != spl_token_program_id { - msg!("Owner associated token account not owned by provided token program, recreate the owner associated token account first"); - return Err(ProgramError::IllegalOwner); - } - let owner_account_data = owner_associated_token_account_info.data.borrow(); - let owner_account = StateWithExtensions::::unpack(&owner_account_data)?; - if owner_account.base.owner != *wallet_account_info.key { - msg!("Owner associated token account not owned by provided wallet"); - return Err(AssociatedTokenAccountError::InvalidOwner.into()); - } - - // Check nested associated token account data - if nested_associated_token_account_info.owner != spl_token_program_id { - msg!("Nested associated token account not owned by provided token program"); - return Err(ProgramError::IllegalOwner); - } - let nested_account_data = nested_associated_token_account_info.data.borrow(); - let nested_account = StateWithExtensions::::unpack(&nested_account_data)?; - if nested_account.base.owner != *owner_associated_token_account_info.key { - msg!("Nested associated token account not owned by provided associated token account"); - return Err(AssociatedTokenAccountError::InvalidOwner.into()); - } - let amount = nested_account.base.amount; - - // Check nested token mint data - if nested_token_mint_info.owner != spl_token_program_id { - msg!("Nested mint account not owned by provided token program"); - return Err(ProgramError::IllegalOwner); - } - let nested_mint_data = nested_token_mint_info.data.borrow(); - let nested_mint = StateWithExtensions::::unpack(&nested_mint_data)?; - let decimals = nested_mint.base.decimals; - (amount, decimals) - }; - - // Transfer everything out - let owner_associated_token_account_signer_seeds: &[&[_]] = &[ - &wallet_account_info.key.to_bytes(), - &spl_token_program_id.to_bytes(), - &owner_token_mint_info.key.to_bytes(), - &[bump_seed], - ]; - invoke_signed( - &spl_token_2022::instruction::transfer_checked( - spl_token_program_id, - nested_associated_token_account_info.key, - nested_token_mint_info.key, - destination_associated_token_account_info.key, - owner_associated_token_account_info.key, - &[], - amount, - decimals, - )?, - &[ - nested_associated_token_account_info.clone(), - nested_token_mint_info.clone(), - destination_associated_token_account_info.clone(), - owner_associated_token_account_info.clone(), - spl_token_program_info.clone(), - ], - &[owner_associated_token_account_signer_seeds], - )?; - - // Close the nested account so it's never used again - invoke_signed( - &spl_token_2022::instruction::close_account( - spl_token_program_id, - nested_associated_token_account_info.key, - wallet_account_info.key, - owner_associated_token_account_info.key, - &[], - )?, - &[ - nested_associated_token_account_info.clone(), - wallet_account_info.clone(), - owner_associated_token_account_info.clone(), - spl_token_program_info.clone(), - ], - &[owner_associated_token_account_signer_seeds], - ) -} diff --git a/associated-token-account/program/src/tools/account.rs b/associated-token-account/program/src/tools/account.rs deleted file mode 100644 index 2001e227e50..00000000000 --- a/associated-token-account/program/src/tools/account.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! Account utility functions - -use { - solana_program::{ - account_info::AccountInfo, - entrypoint::ProgramResult, - program::{get_return_data, invoke, invoke_signed}, - program_error::ProgramError, - pubkey::Pubkey, - rent::Rent, - system_instruction, - }, - spl_token_2022::extension::ExtensionType, - std::convert::TryInto, -}; - -/// Creates associated token account using Program Derived Address for the given -/// seeds -pub fn create_pda_account<'a>( - payer: &AccountInfo<'a>, - rent: &Rent, - space: usize, - owner: &Pubkey, - system_program: &AccountInfo<'a>, - new_pda_account: &AccountInfo<'a>, - new_pda_signer_seeds: &[&[u8]], -) -> ProgramResult { - if new_pda_account.lamports() > 0 { - let required_lamports = rent - .minimum_balance(space) - .max(1) - .saturating_sub(new_pda_account.lamports()); - - if required_lamports > 0 { - invoke( - &system_instruction::transfer(payer.key, new_pda_account.key, required_lamports), - &[ - payer.clone(), - new_pda_account.clone(), - system_program.clone(), - ], - )?; - } - - invoke_signed( - &system_instruction::allocate(new_pda_account.key, space as u64), - &[new_pda_account.clone(), system_program.clone()], - &[new_pda_signer_seeds], - )?; - - invoke_signed( - &system_instruction::assign(new_pda_account.key, owner), - &[new_pda_account.clone(), system_program.clone()], - &[new_pda_signer_seeds], - ) - } else { - invoke_signed( - &system_instruction::create_account( - payer.key, - new_pda_account.key, - rent.minimum_balance(space).max(1), - space as u64, - owner, - ), - &[ - payer.clone(), - new_pda_account.clone(), - system_program.clone(), - ], - &[new_pda_signer_seeds], - ) - } -} - -/// Determines the required initial data length for a new token account based on -/// the extensions initialized on the Mint -pub fn get_account_len<'a>( - mint: &AccountInfo<'a>, - spl_token_program: &AccountInfo<'a>, - extension_types: &[ExtensionType], -) -> Result { - invoke( - &spl_token_2022::instruction::get_account_data_size( - spl_token_program.key, - mint.key, - extension_types, - )?, - &[mint.clone(), spl_token_program.clone()], - )?; - get_return_data() - .ok_or(ProgramError::InvalidInstructionData) - .and_then(|(key, data)| { - if key != *spl_token_program.key { - return Err(ProgramError::IncorrectProgramId); - } - data.try_into() - .map(usize::from_le_bytes) - .map_err(|_| ProgramError::InvalidInstructionData) - }) -} diff --git a/associated-token-account/program/src/tools/mod.rs b/associated-token-account/program/src/tools/mod.rs deleted file mode 100644 index 5cb1ab5f753..00000000000 --- a/associated-token-account/program/src/tools/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Utility functions - -pub mod account; diff --git a/binary-option/program/Cargo.toml b/binary-option/program/Cargo.toml index fba2c8613f7..b9b64a6b6e1 100644 --- a/binary-option/program/Cargo.toml +++ b/binary-option/program/Cargo.toml @@ -11,7 +11,7 @@ test-sbf = [] [dependencies] solana-program = "2.1.0" thiserror = "2.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ +spl-token = { version = "7.0", features = [ "no-entrypoint", ] } arrayref = "0.3.9" diff --git a/binary-oracle-pair/program/Cargo.toml b/binary-oracle-pair/program/Cargo.toml index 72d27062941..afea331bd9d 100644 --- a/binary-oracle-pair/program/Cargo.toml +++ b/binary-oracle-pair/program/Cargo.toml @@ -14,7 +14,7 @@ test-sbf = [] num-derive = "0.4" num-traits = "0.2" solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ +spl-token = { version = "7.0", features = [ "no-entrypoint", ] } thiserror = "2.0" diff --git a/ci/js-test-libraries.sh b/ci/js-test-libraries.sh deleted file mode 100755 index 9d8ef554e54..00000000000 --- a/ci/js-test-libraries.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -e -cd "$(dirname "$0")/.." - -set -x -pnpm install -pnpm format - -cd libraries/type-length-value/js -pnpm lint -pnpm build -pnpm test diff --git a/ci/js-test-single-pool.sh b/ci/js-test-single-pool.sh deleted file mode 100755 index 63ae054e328..00000000000 --- a/ci/js-test-single-pool.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -set -ex -cd "$(dirname "$0")/.." -source ./ci/solana-version.sh install - -pnpm install -pnpm format - -cd single-pool/js/packages/modern -pnpm lint -pnpm build - -cd ../classic -pnpm build:program -pnpm lint -pnpm build -pnpm test diff --git a/ci/js-test-stake-pool.sh b/ci/js-test-stake-pool.sh deleted file mode 100755 index 500e96acfd5..00000000000 --- a/ci/js-test-stake-pool.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -ex -cd "$(dirname "$0")/.." - -pnpm install -pnpm format -pnpm build - -cd stake-pool/js -pnpm lint -pnpm test diff --git a/ci/js-test-token-group.sh b/ci/js-test-token-group.sh deleted file mode 100755 index daead80d8bf..00000000000 --- a/ci/js-test-token-group.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -e -cd "$(dirname "$0")/.." - -set -x -pnpm install -pnpm format -pnpm build - -cd token-group/js -pnpm lint -pnpm test diff --git a/ci/js-test-token-metadata.sh b/ci/js-test-token-metadata.sh deleted file mode 100755 index c12a3ebcb40..00000000000 --- a/ci/js-test-token-metadata.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -e -cd "$(dirname "$0")/.." - -set -x -pnpm install -pnpm format -pnpm build - -cd token-metadata/js -pnpm lint -pnpm test diff --git a/ci/js-test-token.sh b/ci/js-test-token.sh deleted file mode 100755 index 089ee200c66..00000000000 --- a/ci/js-test-token.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -e -cd "$(dirname "$0")/.." -source ./ci/solana-version.sh install - -set -x -pnpm install -pnpm format -pnpm build - -cd token/js -pnpm build:program -pnpm lint -pnpm test diff --git a/ci/py-test-stake-pool.sh b/ci/py-test-stake-pool.sh deleted file mode 100755 index 4c3ca8cfe72..00000000000 --- a/ci/py-test-stake-pool.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -set -ex -cd "$(dirname "$0")/.." -source ./ci/solana-version.sh install - -cd stake-pool/py -python3 -m venv venv -source ./venv/bin/activate -pip3 install -r requirements.txt -pip3 install -r optional-requirements.txt -check_dirs=( - "bot" - "spl_token" - "stake" - "stake_pool" - "system" - "tests" - "vote" -) -flake8 "${check_dirs[@]}" -mypy "${check_dirs[@]}" -python3 -m pytest diff --git a/coverage.sh b/coverage.sh index cb12b4b493a..edaca0b88ba 100755 --- a/coverage.sh +++ b/coverage.sh @@ -22,7 +22,6 @@ reportName="lcov-${CI_COMMIT:0:9}" if [[ -z $1 ]]; then programs=( libraries/math - token/program token-lending/program token-swap/program ) diff --git a/docs/src/associated-token-account.md b/docs/src/associated-token-account.md index 0fefb54858c..ff23010aa7c 100644 --- a/docs/src/associated-token-account.md +++ b/docs/src/associated-token-account.md @@ -38,10 +38,10 @@ document are available at: ## Source The Associated Token Account Program's source is available on -[GitHub](https://github.com/solana-labs/solana-program-library). - +[GitHub](https://github.com/solana-program/associated-token-account). ## Interface + The Associated Token Account Program is written in Rust and available on [crates.io](https://crates.io/crates/spl-associated-token-account) and [docs.rs](https://docs.rs/spl-associated-token-account). diff --git a/docs/src/feature-proposal.md b/docs/src/feature-proposal.md index ec560b2378e..705d991ad0c 100644 --- a/docs/src/feature-proposal.md +++ b/docs/src/feature-proposal.md @@ -35,7 +35,7 @@ when appropriate. ## Source The Feature Proposal Program's source is available on -[GitHub](https://github.com/solana-labs/solana-program-library) +[GitHub](https://github.com/solana-program/feature-proposal). ## Interface The Feature Proposal Program is written in Rust and available on [crates.io](https://crates.io/crates/spl-feature-proposal) and [docs.rs](https://docs.rs/spl-feature-proposal). @@ -58,7 +58,7 @@ This section describes the life cycle of a feature proposal. ### Implement the Feature The first step is to conceive of the new feature and realize it in the -Solana code base, working with the core Solana developers at https://github.com/solana-labs/solana. +Solana code base, working with the core Solana developers at https://github.com/anza-xyz/agave During the implementation, a *feature id* will be required to identify the new feature in the code base to avoid the new functionality until its activation. diff --git a/docs/src/single-pool.mdx b/docs/src/single-pool.mdx index 197c3cacbae..357a025d296 100644 --- a/docs/src/single-pool.mdx +++ b/docs/src/single-pool.mdx @@ -24,7 +24,7 @@ The program is a stripped-down adaptation of the existing multi-validator stake ## Source The Single Pool Program's source is available on -[GitHub](https://github.com/solana-labs/solana-program-library/tree/master/single-pool/program). +[GitHub](https://github.com/solana-program/single-pool). ## Security Audits @@ -32,13 +32,13 @@ The Single Pool Program has received three audits to ensure total safety of fund * Zellic (2024-01-02) - Review commit hash [`ef44df9`](https://github.com/solana-labs/solana-program-library/commit/ef44df985e76a697ee9a8aabb3a223610e4cf1dc) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/ZellicSinglePoolAudit-2024-01-02.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/ZellicSinglePoolAudit-2024-01-02.pdf * Neodyme (2023-08-08) - Review commit hash [`735d729`](https://github.com/solana-labs/solana-program-library/commit/735d7292e35d35101750a4452d2647bdbf848e8b) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/NeodymeSinglePoolAudit-2023-08-08.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/NeodymeSinglePoolAudit-2023-08-08.pdf * Zellic (2023-06-21) - Review commit hash [`9dbdc3b`](https://github.com/solana-labs/solana-program-library/commit/9dbdc3bdae31dda1dcb35346aab2d879deecf194) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/ZellicSinglePoolAudit-2023-06-21.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/ZellicSinglePoolAudit-2023-06-21.pdf ## Interface diff --git a/docs/src/stake-pool.md b/docs/src/stake-pool.md index 8f630abb81d..637b32c33e8 100644 --- a/docs/src/stake-pool.md +++ b/docs/src/stake-pool.md @@ -27,7 +27,7 @@ To get started with stake pools: ## Source The Stake Pool Program's source is available on -[GitHub](https://github.com/solana-labs/solana-program-library/tree/master/stake-pool). +[GitHub](https://github.com/solana-program/stake-pool). For information about the types and instructions, the Stake Pool Rust docs are available at [docs.rs](https://docs.rs/spl-stake-pool/latest/spl_stake_pool/index.html). @@ -41,28 +41,28 @@ chronological order, and the commit hash that each was reviewed at: * Quantstamp - Initial review commit hash [`99914c9`](https://github.com/solana-labs/solana-program-library/tree/99914c9fc7246b22ef04416586ab1722c89576de) - Re-review commit hash [`3b48fa0`](https://github.com/solana-labs/solana-program-library/tree/3b48fa09d38d1b66ffb4fef186b606f1bc4fdb31) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/QuantstampStakePoolAudit-2021-10-22.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/QuantstampStakePoolAudit-2021-10-22.pdf * Neodyme - Review commit hash [`0a85a9a`](https://github.com/solana-labs/solana-program-library/tree/0a85a9a533795b6338ea144e433893c6c0056210) - - Report https://github.com/solana-labs/security-audits/blob/master/spl/NeodymeStakePoolAudit-2021-10-16.pdf + - Report https://github.com/anza-xyz/security-audits/blob/master/spl/NeodymeStakePoolAudit-2021-10-16.pdf * Kudelski - Review commit hash [`3dd6767`](https://github.com/solana-labs/solana-program-library/tree/3dd67672974f92d3b648bb50ee74f4747a5f8973) - - Report https://github.com/solana-labs/security-audits/blob/master/spl/KudelskiStakePoolAudit-2021-07-07.pdf + - Report https://github.com/anza-xyz/security-audits/blob/master/spl/KudelskiStakePoolAudit-2021-07-07.pdf * Neodyme Second Audit - Review commit hash [`fd92ccf`](https://github.com/solana-labs/solana-program-library/tree/fd92ccf9e9308508b719d6e5f36474f57023b0b2) - - Report https://github.com/solana-labs/security-audits/blob/master/spl/NeodymeStakePoolAudit-2022-12-10.pdf + - Report https://github.com/anza-xyz/security-audits/blob/master/spl/NeodymeStakePoolAudit-2022-12-10.pdf * OtterSec - Review commit hash [`eba709b`](https://github.com/solana-labs/solana-program-library/tree/eba709b9317f8c7b8b197045161cb744241f0bff) - - Report https://github.com/solana-labs/security-audits/blob/master/spl/OtterSecStakePoolAudit-2023-01-20.pdf + - Report https://github.com/anza-xyz/security-audits/blob/master/spl/OtterSecStakePoolAudit-2023-01-20.pdf * Neodyme Third Audit - Review commit hash [`b341022`](https://github.com/solana-labs/solana-program-library/tree/b34102211f2a5ea6b83f3ee22f045fb115d87813) - - Report https://github.com/solana-labs/security-audits/blob/master/spl/NeodymeStakePoolAudit-2023-01-31.pdf + - Report https://github.com/anza-xyz/security-audits/blob/master/spl/NeodymeStakePoolAudit-2023-01-31.pdf * Halborn - Review commit hash [`eba709b`](https://github.com/solana-labs/solana-program-library/tree/eba709b9317f8c7b8b197045161cb744241f0bff) - - Report https://github.com/solana-labs/security-audits/blob/master/spl/HalbornStakePoolAudit-2023-01-25.pdf + - Report https://github.com/anza-xyz/security-audits/blob/master/spl/HalbornStakePoolAudit-2023-01-25.pdf * Neodyme Fourth Audit - Review commit hash [`6ed7254`](https://github.com/solana-labs/solana-program-library/tree/6ed7254d1a578ffbc2b091d28cb92b25e7cc511d) - - Report https://github.com/solana-labs/security-audits/blob/master/spl/NeodymeStakePoolAudit-2023-11-14.pdf + - Report https://github.com/anza-xyz/security-audits/blob/master/spl/NeodymeStakePoolAudit-2023-11-14.pdf * Halborn Second Audit - Review commit hash [`a17fffe`](https://github.com/solana-labs/solana-program-library/tree/a17fffe70d6cc13742abfbc4a4a375b087580bc1) - - Report https://github.com/solana-labs/security-audits/blob/master/spl/HalbornStakePoolAudit-2023-12-31.pdf + - Report https://github.com/anza-xyz/security-audits/blob/master/spl/HalbornStakePoolAudit-2023-12-31.pdf diff --git a/docs/src/token-2022.md b/docs/src/token-2022.md index 21fb3d866b2..c4aa63f2055 100644 --- a/docs/src/token-2022.md +++ b/docs/src/token-2022.md @@ -82,7 +82,7 @@ is written after the end of the `Account` in Token, which is the byte at index `165`. This means it is always possible to differentiate mints and accounts. You can read more about how this is done at the -[source code](https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/mod.rs). +[source code](https://github.com/solana-program/token-2022/blob/main/program/src/extension/mod.rs). Mint extensions currently include: @@ -128,7 +128,7 @@ The Token functionality will always apply to Token-2022. ## Source The Token-2022 Program's source is available on -[GitHub](https://github.com/solana-labs/solana-program-library/tree/master/token/program-2022). +[GitHub](https://github.com/solana-program/token-2022). For information about the types and instructions, the Rust docs are available at [docs.rs](https://docs.rs/spl-token-2022/latest/spl_token_2022/). @@ -142,19 +142,19 @@ Here are the completed audits as of 13 December 2023: * Halborn - Review commit hash [`c3137a`](https://github.com/solana-labs/solana-program-library/tree/c3137af9dfa2cc0873cc84c4418dea88ac542965/token/program-2022) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/HalbornToken2022Audit-2022-07-27.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/HalbornToken2022Audit-2022-07-27.pdf * Zellic - Review commit hash [`54695b`](https://github.com/solana-labs/solana-program-library/tree/54695b233484722458b18c0e26ebb8334f98422c/token/program-2022) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/ZellicToken2022Audit-2022-12-05.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/ZellicToken2022Audit-2022-12-05.pdf * Trail of Bits - Review commit hash [`50abad`](https://github.com/solana-labs/solana-program-library/tree/50abadd819df2e406567d6eca31c213264c1c7cd/token/program-2022) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/TrailOfBitsToken2022Audit-2023-02-10.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/TrailOfBitsToken2022Audit-2023-02-10.pdf * NCC Group - Review commit hash [`4e43aa`](https://github.com/solana-labs/solana/tree/4e43aa6c18e6bb4d98559f80eb004de18bc6b418/zk-token-sdk) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/NCCToken2022Audit-2023-04-05.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/NCCToken2022Audit-2023-04-05.pdf * OtterSec - Review commit hash [`e92413`](https://github.com/solana-labs/solana-program-library/tree/e924132d65ba0896249fb4983f6f97caff15721a) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/OtterSecToken2022Audit-2023-11-03.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/OtterSecToken2022Audit-2023-11-03.pdf * OtterSec (ZK Token SDK) - Review commit hash [`9e703f8`](https://github.com/solana-labs/solana/tree/9e703f8/zk-token-sdk) - - Final report https://github.com/solana-labs/security-audits/blob/master/spl/OtterSecZkTokenSdkAudit-2023-11-04.pdf + - Final report https://github.com/anza-xyz/security-audits/blob/master/spl/OtterSecZkTokenSdkAudit-2023-11-04.pdf diff --git a/docs/src/token-2022/extensions.mdx b/docs/src/token-2022/extensions.mdx index 12965fee813..4fd48665e1f 100644 --- a/docs/src/token-2022/extensions.mdx +++ b/docs/src/token-2022/extensions.mdx @@ -22,7 +22,7 @@ See the [Token Setup Guide](../token#setup) to install the client utilities. Token-2022 shares the same CLI and NPM packages for maximal compatibility. All JS examples are adapted from the tests, and available in full at the -[Token JS examples](https://github.com/solana-labs/solana-program-library/tree/master/token/js/examples). +[Token JS examples](https://github.com/solana-program/token-2022/tree/main/clients/js-legacy/examples). ## Extensions @@ -1528,29 +1528,29 @@ explained in detail in many of the linked `README` files below under #### Resources The interface description and structs exist at -[spl-transfer-hook-interface](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/interface), +[spl-transfer-hook-interface](https://github.com/solana-program/transfer-hook/tree/main/interface), along with a sample minimal program implementation. You can find detailed instructions on how to implement this interface for an on-chain program or interact with a program that implements transfer-hook in the repository's -[README](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/interface/README.md). +[README](https://github.com/solana-program/transfer-hook/tree/main/interface/README.md). The `spl-transfer-hook-interface` library provides offchain and onchain helpers for resolving the additional accounts required. See -[onchain.rs](https://github.com/solana-labs/solana-program-library/blob/master/token/transfer-hook/interface/src/onchain.rs) +[onchain.rs](https://github.com/solana-program/transfer-hook/blob/main/interface/src/onchain.rs) for usage on-chain, and -[offchain.rs](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/interface/src/offchain.rs) +[offchain.rs](https://github.com/solana-program/transfer-hook/blob/main/interface/src/offchain.rs) for fetching the additional required account metas with any async off-chain client like `BanksClient` or `RpcClient`. A usable example program exists at -[spl-transfer-hook-example](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/example). +[spl-transfer-hook-example](https://github.com/solana-program/transfer-hook/tree/main/program). Token-2022 uses this example program in tests to ensure that it properly uses the transfer hook interface. The example program and the interface are powered by the -[spl-tlv-account-resolution](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution) +[spl-tlv-account-resolution](https://github.com/solana-program/libraries/tree/main/tlv-account-resolution) library, which is explained in detail in the repository's -[README](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution/README.md) +[README](https://github.com/solana-program/libraries/tree/main/tlv-account-resolution/README.md) #### Example: Create a mint with a transfer hook @@ -1660,7 +1660,7 @@ await updateTransferHook( #### Example: Manage a transfer-hook program A sample CLI for managing a transfer-hook program exists at -[spl-transfer-hook-cli](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/cli). +[spl-transfer-hook-cli](https://github.com/solana-program/transfer-hook/tree/main/clients/cli). A mint manager can fork the tool for their own program. It only contains a command to create the required transfer-hook account for the @@ -1720,7 +1720,7 @@ To facilitate token-metadata usage, Token-2022 allows a mint creator to include their token's metadata directly in the mint account. Token-2022 implements all of the instructions from the -[spl-token-metadata-interface](https://github.com/solana-labs/solana-program-library/tree/master/token-metadata/interface). +[spl-token-metadata-interface](https://github.com/solana-program/token-metadata/tree/main/interface). The metadata extension should work directly with the metadata-pointer extension. During mint creation, you should also add the metadata-pointer extension, pointed @@ -1966,7 +1966,7 @@ configurations for a group, which describe things like the update authority and the group's maximum size, can be stored directly in the mint itself. Token-2022 implements all of the instructions from the -[spl-token-group-interface](https://github.com/solana-labs/solana-program-library/tree/master/token-group/interface). +[spl-token-group-interface](https://github.com/solana-program/token-group/tree/main/interface). The group extension works directly with the group-pointer extension. To initialize group configurations within a mint, you must add the group-pointer diff --git a/docs/src/token.mdx b/docs/src/token.mdx index 65397ac1658..cb68488ce67 100644 --- a/docs/src/token.mdx +++ b/docs/src/token.mdx @@ -22,17 +22,17 @@ document are available at: ## Source The Token Program's source is available on -[GitHub](https://github.com/solana-labs/solana-program-library) +[GitHub](https://github.com/solana-program/token). ## Interface The Token Program is written in Rust and available on [crates.io](https://crates.io/crates/spl-token) and [docs.rs](https://docs.rs/spl-token). Auto-generated C bindings are also available -[here](https://github.com/solana-labs/solana-program-library/blob/master/token/program/inc/token.h) +[here](https://github.com/solana-program/token/blob/main/program/inc/token.h) [JavaScript -bindings](https://github.com/solana-labs/solana-program-library/tree/master/token/js) +bindings](https://github.com/solana-program/token-2022/tree/main/clients/js-legacy) are available that support loading the Token Program on to a chain and issue instructions. diff --git a/docs/src/transfer-hook-interface.md b/docs/src/transfer-hook-interface.md index a39f36adf87..b7d12bdc4c4 100644 --- a/docs/src/transfer-hook-interface.md +++ b/docs/src/transfer-hook-interface.md @@ -9,7 +9,7 @@ During transfers, Token-2022 calls a mint's configured transfer hook program using this interface, as described in the [Transfer Hook Extension Guide](../../token-2022/extensions#transfer-hook). Additionally, a -[reference implementation](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/example) +[reference implementation](https://github.com/solana-program/transfer-hook/tree/main/program) can be found in the SPL GitHub repository, detailing how one might implement this interface in their own program. diff --git a/docs/src/transfer-hook-interface/configuring-extra-accounts.md b/docs/src/transfer-hook-interface/configuring-extra-accounts.md index 7de29932ccb..2587b69929d 100644 --- a/docs/src/transfer-hook-interface/configuring-extra-accounts.md +++ b/docs/src/transfer-hook-interface/configuring-extra-accounts.md @@ -43,7 +43,7 @@ of entries. This custom slice structure is called a `PodSlice` and is part of the Solana Program Library's -[Pod](https://github.com/solana-labs/solana-program-library/tree/master/libraries/pod) +[Pod](https://github.com/solana-program/libraries/tree/main/pod) library. The Pod library provides a handful of fixed-length types that implement the `bytemuck` [`Pod`](https://docs.rs/bytemuck/latest/bytemuck/trait.Pod.html) trait, as well @@ -51,7 +51,7 @@ as the `PodSlice`. Another SPL library useful for Type-Length-Value encoded data is -[Type-Length-Value](https://github.com/solana-labs/solana-program-library/tree/master/libraries/type-length-value) +[Type-Length-Value](https://github.com/solana-program/libraries/tree/main/type-length-value) which is used extensively to manage TLV-encoded data structures. ### Dynamic Account Resolution @@ -62,7 +62,7 @@ required accounts you've specified in the validation account. These additional accounts must be _resolved_, and another library used to pull off the resolution of additional accounts for transfer hooks is -[TLV Account Resolution](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution). +[TLV Account Resolution](https://github.com/solana-program/libraries/tree/main/tlv-account-resolution). Using the TLV Account Resolution library, transfer hook programs can empower **dynamic account resolution** of additional required accounts. This means that @@ -72,9 +72,9 @@ validation account's data. In fact, the Transfer Hook interface offers helpers that perform this account resolution in the -[onchain](https://github.com/solana-labs/solana-program-library/blob/master/token/transfer-hook/interface/src/onchain.rs) +[onchain](https://github.com/solana-program/transfer-hook/blob/main/interface/src/onchain.rs) and -[offchain](https://github.com/solana-labs/solana-program-library/blob/master/token/transfer-hook/interface/src/offchain.rs) +[offchain](https://github.com/solana-program/transfer-hook/blob/main/interface/src/offchain.rs) modules of the Transfer Hook interface crate. The account resolution is powered by the way configurations for additional @@ -84,7 +84,7 @@ and roles (signer, writeable, etc.) for accounts. ### The `ExtraAccountMeta` Struct A member of the TLV Account Resolution library, the -[`ExtraAccountMeta`](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/libraries/tlv-account-resolution/src/account.rs#L75) +[`ExtraAccountMeta`](https://github.com/solana-program/libraries/blob/c5cc979f188ddc136de2ff556173a6f655322915/tlv-account-resolution/src/account.rs#L123) struct allows account configurations to be serialized into a fixed-length data format of length 35 bytes. @@ -130,7 +130,7 @@ Well, you don't. Instead, you tell the account resolution functionality _where_ to find the seeds you need. To do this, the transfer hook program can use the -[`Seed`](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/libraries/tlv-account-resolution/src/seeds.rs#L38) +[`Seed`](https://github.com/solana-program/libraries/blob/c5cc979f188ddc136de2ff556173a6f655322915/tlv-account-resolution/src/seeds.rs#L38) enum to describe their seeds and where to find them. With the exception of literals, these seed configurations comprise only a small handful of bytes. diff --git a/docs/src/transfer-hook-interface/examples.md b/docs/src/transfer-hook-interface/examples.md index 7cc01943081..7a4fc1ed0b8 100644 --- a/docs/src/transfer-hook-interface/examples.md +++ b/docs/src/transfer-hook-interface/examples.md @@ -3,14 +3,14 @@ title: Examples --- More examples can be found in the -[Transfer Hook example tests](https://github.com/solana-labs/solana-program-library/blob/master/token/transfer-hook/example/tests/functional.rs), +[Transfer Hook example tests](https://github.com/solana-program/transfer-hook/blob/main/program/tests/functional.rs), as well as the -[TLV Account Resolution tests](https://github.com/solana-labs/solana-program-library/blob/master/libraries/tlv-account-resolution/src/state.rs). +[TLV Account Resolution tests](https://github.com/solana-program/libraries/blob/main/tlv-account-resolution/src/state.rs). ### Initializing Extra Account Metas On-Chain The -[`ExtraAccountMetaList`](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/libraries/tlv-account-resolution/src/state.rs#L167) +[`ExtraAccountMetaList`](https://github.com/solana-program/libraries/blob/c5cc979f188ddc136de2ff556173a6f655322915/tlv-account-resolution/src/state.rs#L164) struct is designed to make working with extra account configurations as seamless as possible. @@ -19,12 +19,12 @@ serialized `ExtraAccountMeta` configurations by simply providing a mutable reference to the buffer and a slice of `ExtraAccountMeta`. The generic `T` is the instruction whose discriminator the extra account configurations should be assigned to. In our case, this will be -[`spl_transfer_hook_interface::instruction::ExecuteInstruction`](https://github.com/solana-labs/solana-program-library/blob/eb32c5e72c6d917e732bded9863db7657b23e428/token/transfer-hook/interface/src/instruction.rs#L68) +[`spl_transfer_hook_interface::instruction::ExecuteInstruction`](https://github.com/solana-program/transfer-hook/blob/e00f3b5c591fd55b4aed6a1e9b1ccc502cb6da05/interface/src/instruction.rs#L67) from the Transfer Hook interface. > Note: All instructions from the SPL Transfer Hook interface implement the > trait -> [`SplDiscriminate`](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/libraries/discriminator/src/discriminator.rs#L9), +> [`SplDiscriminate`](https://github.com/solana-program/libraries/blob/c5cc979f188ddc136de2ff556173a6f655322915/discriminator/src/discriminator.rs#L10), > which provides a constant 8-byte discriminator that > can be used to create a TLV data entry. @@ -83,7 +83,7 @@ program directly or for a program that will CPI to your transfer hook program, you must include all required accounts - including the extra accounts. Below is an example of the logic contained in the Transfer Hook interface's -[offchain helper](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/token/transfer-hook/interface/src/offchain.rs#L50). +[offchain helper](https://github.com/solana-program/transfer-hook/blob/e00f3b5c591fd55b4aed6a1e9b1ccc502cb6da05/interface/src/offchain.rs#L48). ```rust // You'll need to provide an "account data function", which is a function that @@ -151,7 +151,7 @@ offchain account resolution, the executing program has to know how to build a CPI instruction with the proper accounts as well! Below is an example of the logic contained in the Transfer Hook interface's -[onchain helper](https://github.com/solana-labs/solana-program-library/blob/65a92e6e0a4346920582d9b3893cacafd85bb017/token/transfer-hook/interface/src/onchain.rs#L67). +[onchain helper](https://github.com/solana-program/transfer-hook/blob/e00f3b5c591fd55b4aed6a1e9b1ccc502cb6da05/interface/src/onchain.rs#L15). ```rust // Find the validation account from the list of `AccountInfo`s and load its diff --git a/examples/rust/transfer-tokens/Cargo.toml b/examples/rust/transfer-tokens/Cargo.toml index 05d0fd85b67..52f0e02c6e1 100644 --- a/examples/rust/transfer-tokens/Cargo.toml +++ b/examples/rust/transfer-tokens/Cargo.toml @@ -13,7 +13,7 @@ test-sbf = [] [dependencies] solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../../token/program", features = [ "no-entrypoint" ] } +spl-token = { version = "7.0", features = [ "no-entrypoint" ] } [dev-dependencies] solana-program-test = "2.1.0" diff --git a/feature-proposal/README.md b/feature-proposal/README.md new file mode 100644 index 00000000000..b5637f9a4a5 --- /dev/null +++ b/feature-proposal/README.md @@ -0,0 +1,2 @@ +NOTE: The feature-proposal program and clients are now maintained at +[solana-program/feature-proposal](https://github.com/solana-program/feature-proposal). diff --git a/feature-proposal/cli/Cargo.toml b/feature-proposal/cli/Cargo.toml deleted file mode 100644 index bc62d44e532..00000000000 --- a/feature-proposal/cli/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "spl-feature-proposal-cli" -version = "1.2.0" -description = "SPL Feature Proposal Command-line Utility" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -chrono = "0.4.39" -clap = "2.33.3" -solana-clap-utils = "2.1.0" -solana-cli-config = "2.1.0" -solana-client = "2.1.0" -solana-logger = "2.1.0" -solana-sdk = "2.1.0" -spl-feature-proposal = { version = "1.0", path = "../program", features = ["no-entrypoint"] } - -[[bin]] -name = "spl-feature-proposal" -path = "src/main.rs" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/feature-proposal/cli/src/main.rs b/feature-proposal/cli/src/main.rs deleted file mode 100644 index e3a40fee622..00000000000 --- a/feature-proposal/cli/src/main.rs +++ /dev/null @@ -1,486 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -use { - chrono::{DateTime, SecondsFormat, Utc}, - clap::{ - crate_description, crate_name, crate_version, value_t_or_exit, App, AppSettings, Arg, - SubCommand, - }, - solana_clap_utils::{ - input_parsers::{keypair_of, pubkey_of}, - input_validators::{is_keypair, is_url, is_valid_percentage, is_valid_pubkey}, - }, - solana_client::rpc_client::RpcClient, - solana_sdk::{ - clock::UnixTimestamp, - commitment_config::CommitmentConfig, - program_pack::Pack, - pubkey::Pubkey, - signature::{read_keypair_file, Keypair, Signer}, - transaction::Transaction, - }, - spl_feature_proposal::state::{AcceptanceCriteria, FeatureProposal}, - std::{ - collections::HashMap, - fs::File, - io::Write, - time::{Duration, SystemTime, UNIX_EPOCH}, - }, -}; - -struct Config { - keypair: Keypair, - json_rpc_url: String, - verbose: bool, -} - -fn main() -> Result<(), Box> { - let app_matches = App::new(crate_name!()) - .about(crate_description!()) - .version(crate_version!()) - .setting(AppSettings::SubcommandRequiredElseHelp) - .arg({ - let arg = Arg::with_name("config_file") - .short("C") - .long("config") - .value_name("PATH") - .takes_value(true) - .global(true) - .help("Configuration file to use"); - if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE { - arg.default_value(config_file) - } else { - arg - } - }) - .arg( - Arg::with_name("keypair") - .long("keypair") - .value_name("KEYPAIR") - .validator(is_keypair) - .takes_value(true) - .global(true) - .help("Filepath or URL to a keypair [default: client keypair]"), - ) - .arg( - Arg::with_name("verbose") - .long("verbose") - .short("v") - .takes_value(false) - .global(true) - .help("Show additional information"), - ) - .arg( - Arg::with_name("json_rpc_url") - .long("url") - .value_name("URL") - .takes_value(true) - .global(true) - .validator(is_url) - .help("JSON RPC URL for the cluster [default: value from configuration file]"), - ) - .subcommand( - SubCommand::with_name("address") - .about("Display address information for the feature proposal") - .arg( - Arg::with_name("feature_proposal") - .value_name("FEATURE_PROPOSAL_ADDRESS") - .validator(is_valid_pubkey) - .index(1) - .required(true) - .help("The address of the feature proposal"), - ), - ) - .subcommand( - SubCommand::with_name("propose") - .about("Initiate a feature proposal") - .arg( - Arg::with_name("feature_proposal") - .value_name("FEATURE_PROPOSAL_KEYPAIR") - .validator(is_keypair) - .index(1) - .required(true) - .help("The keypair of the feature proposal"), - ) - .arg( - Arg::with_name("percent_stake_required") - .long("percent-stake-required") - .value_name("PERCENTAGE") - .validator(is_valid_percentage) - .required(true) - .default_value("67") - .help("Percentage of the active stake required for the proposal to pass"), - ) - .arg( - Arg::with_name("distribution_file") - .long("distribution-file") - .value_name("FILENAME") - .required(true) - .default_value("feature-proposal.csv") - .help("Allocations CSV file for use with solana-tokens"), - ) - .arg( - Arg::with_name("confirm") - .long("confirm") - .help("Confirm that the feature proposal should actually be initiated"), - ), - ) - .subcommand( - SubCommand::with_name("tally") - .about("Tally the current results for a proposed feature") - .arg( - Arg::with_name("feature_proposal") - .value_name("FEATURE_PROPOSAL_ADDRESS") - .validator(is_valid_pubkey) - .index(1) - .required(true) - .help("The address of the feature proposal"), - ), - ) - .get_matches(); - - let (sub_command, sub_matches) = app_matches.subcommand(); - let matches = sub_matches.unwrap(); - - let config = { - let cli_config = if let Some(config_file) = matches.value_of("config_file") { - solana_cli_config::Config::load(config_file).unwrap_or_default() - } else { - solana_cli_config::Config::default() - }; - - Config { - json_rpc_url: matches - .value_of("json_rpc_url") - .unwrap_or(&cli_config.json_rpc_url) - .to_string(), - keypair: read_keypair_file( - matches - .value_of("keypair") - .unwrap_or(&cli_config.keypair_path), - )?, - verbose: matches.is_present("verbose"), - } - }; - solana_logger::setup_with_default("solana=info"); - let rpc_client = - RpcClient::new_with_commitment(config.json_rpc_url.clone(), CommitmentConfig::confirmed()); - - match (sub_command, sub_matches) { - ("address", Some(arg_matches)) => { - let feature_proposal_address = pubkey_of(arg_matches, "feature_proposal").unwrap(); - - println!( - "Feature Id: {}", - spl_feature_proposal::get_feature_id_address(&feature_proposal_address) - ); - println!( - "Token Mint Address: {}", - spl_feature_proposal::get_mint_address(&feature_proposal_address) - ); - println!( - "Acceptance Token Address: {}", - spl_feature_proposal::get_acceptance_token_address(&feature_proposal_address) - ); - - Ok(()) - } - ("propose", Some(arg_matches)) => { - let feature_proposal_keypair = keypair_of(arg_matches, "feature_proposal").unwrap(); - let distribution_file = value_t_or_exit!(arg_matches, "distribution_file", String); - let percent_stake_required = - value_t_or_exit!(arg_matches, "percent_stake_required", u8); - - // Hard code deadline for now... - let fortnight = Duration::from_secs(60 * 60 * 24 * 14); - let deadline = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .checked_add(fortnight) - .unwrap() - .as_secs() as UnixTimestamp; - - process_propose( - &rpc_client, - &config, - &feature_proposal_keypair, - distribution_file, - percent_stake_required, - deadline, - arg_matches.is_present("confirm"), - ) - } - ("tally", Some(arg_matches)) => { - if config.verbose { - println!("JSON RPC URL: {}", config.json_rpc_url); - } - - let feature_proposal_address = pubkey_of(arg_matches, "feature_proposal").unwrap(); - process_tally(&rpc_client, &config, &feature_proposal_address) - } - _ => unreachable!(), - } -} - -fn get_feature_proposal( - rpc_client: &RpcClient, - feature_proposal_address: &Pubkey, -) -> Result { - let account = rpc_client - .get_multiple_accounts(&[*feature_proposal_address]) - .map_err(|err| err.to_string())? - .into_iter() - .next() - .unwrap(); - - match account { - None => Err(format!( - "Feature proposal {} does not exist", - feature_proposal_address - )), - Some(account) => FeatureProposal::unpack_from_slice(&account.data).map_err(|err| { - format!( - "Failed to deserialize feature proposal {}: {}", - feature_proposal_address, err - ) - }), - } -} - -fn unix_timestamp_to_string(unix_timestamp: UnixTimestamp) -> String { - format!( - "{} (UnixTimestamp: {})", - DateTime::::from_timestamp(unix_timestamp, 0) - .map(|x| x.to_rfc3339_opts(SecondsFormat::Secs, true)) - .unwrap_or("unknown".to_string()), - unix_timestamp, - ) -} - -fn process_propose( - rpc_client: &RpcClient, - config: &Config, - feature_proposal_keypair: &Keypair, - distribution_file: String, - percent_stake_required: u8, - deadline: UnixTimestamp, - confirm: bool, -) -> Result<(), Box> { - let distributor_token_address = - spl_feature_proposal::get_distributor_token_address(&feature_proposal_keypair.pubkey()); - let feature_id_address = - spl_feature_proposal::get_feature_id_address(&feature_proposal_keypair.pubkey()); - let acceptance_token_address = - spl_feature_proposal::get_acceptance_token_address(&feature_proposal_keypair.pubkey()); - let mint_address = spl_feature_proposal::get_mint_address(&feature_proposal_keypair.pubkey()); - - println!("Feature Id: {}", feature_id_address); - println!("Token Mint Address: {}", mint_address); - println!("Distributor Token Address: {}", distributor_token_address); - println!("Acceptance Token Address: {}", acceptance_token_address); - - let vote_accounts = rpc_client.get_vote_accounts()?; - let mut distribution = HashMap::new(); - for (pubkey, activated_stake) in vote_accounts - .current - .into_iter() - .chain(vote_accounts.delinquent) - .map(|vote_account| (vote_account.node_pubkey, vote_account.activated_stake)) - { - distribution - .entry(pubkey) - .and_modify(|e| *e += activated_stake) - .or_insert(activated_stake); - } - - let tokens_to_mint: u64 = distribution.iter().map(|x| x.1).sum(); - let tokens_required = tokens_to_mint * percent_stake_required as u64 / 100; - - println!("Number of validators: {}", distribution.len()); - println!( - "Tokens to be minted: {}", - spl_feature_proposal::amount_to_ui_amount(tokens_to_mint) - ); - println!( - "Tokens required for acceptance: {} ({}%)", - spl_feature_proposal::amount_to_ui_amount(tokens_required), - percent_stake_required - ); - - println!("Token distribution file: {}", distribution_file); - { - let mut file = File::create(&distribution_file)?; - file.write_all(b"recipient,amount\n")?; - for (node_address, activated_stake) in distribution.iter() { - file.write_all(format!("{},{}\n", node_address, activated_stake).as_bytes())?; - } - } - - let mut transaction = Transaction::new_with_payer( - &[spl_feature_proposal::instruction::propose( - &config.keypair.pubkey(), - &feature_proposal_keypair.pubkey(), - tokens_to_mint, - AcceptanceCriteria { - tokens_required, - deadline, - }, - )], - Some(&config.keypair.pubkey()), - ); - let blockhash = rpc_client.get_latest_blockhash()?; - transaction.try_sign(&[&config.keypair, feature_proposal_keypair], blockhash)?; - - println!("JSON RPC URL: {}", config.json_rpc_url); - - println!(); - println!("Distribute the proposal tokens to all validators by running:"); - println!( - " $ solana-tokens distribute-spl-tokens \ - --from {} \ - --input-csv {} \ - --db-path db.{} \ - --fee-payer ~/.config/solana/id.json \ - --owner ", - distributor_token_address, - distribution_file, - &feature_proposal_keypair.pubkey().to_string()[..8] - ); - println!( - " $ solana-tokens spl-token-balances \ - --mint {} --input-csv {}", - mint_address, distribution_file - ); - println!(); - - println!( - "Once the distribution is complete, request validators vote for \ - the proposal by first looking up their token account address:" - ); - println!( - " $ spl-token accounts --owner ~/validator-keypair.json {}", - mint_address - ); - println!("and then submit their vote by running:"); - println!( - " $ spl-token transfer --owner ~/validator-keypair.json ALL {}", - acceptance_token_address - ); - println!(); - println!("Periodically the votes must be tallied by running:"); - println!( - " $ spl-feature-proposal tally {}", - feature_proposal_keypair.pubkey() - ); - println!("Tallying is permissionless and may be run by anybody."); - println!("Once this feature proposal is accepted, the {} feature will be activated at the next epoch.", feature_id_address); - - println!(); - println!( - "Proposal will expire at {}", - unix_timestamp_to_string(deadline) - ); - println!(); - if !confirm { - println!("Add --confirm flag to initiate the feature proposal"); - return Ok(()); - } - rpc_client.send_and_confirm_transaction_with_spinner(&transaction)?; - - println!(); - println!("Feature proposal created!"); - Ok(()) -} - -fn process_tally( - rpc_client: &RpcClient, - config: &Config, - feature_proposal_address: &Pubkey, -) -> Result<(), Box> { - let feature_proposal = get_feature_proposal(rpc_client, feature_proposal_address)?; - - let feature_id_address = spl_feature_proposal::get_feature_id_address(feature_proposal_address); - let acceptance_token_address = - spl_feature_proposal::get_acceptance_token_address(feature_proposal_address); - - println!("Feature Id: {}", feature_id_address); - println!("Acceptance Token Address: {}", acceptance_token_address); - - match feature_proposal { - FeatureProposal::Uninitialized => { - return Err("Feature proposal is uninitialized".into()); - } - FeatureProposal::Pending(acceptance_criteria) => { - let acceptance_token_address = - spl_feature_proposal::get_acceptance_token_address(feature_proposal_address); - let acceptance_token_balance = rpc_client - .get_token_account_balance(&acceptance_token_address)? - .amount - .parse::() - .unwrap_or(0); - - println!(); - println!( - "{} tokens required to accept the proposal", - spl_feature_proposal::amount_to_ui_amount(acceptance_criteria.tokens_required) - ); - println!( - "{} tokens have been received", - spl_feature_proposal::amount_to_ui_amount(acceptance_token_balance) - ); - println!( - "Proposal will expire at {}", - unix_timestamp_to_string(acceptance_criteria.deadline) - ); - println!(); - - // Don't bother issuing a transaction if it's clear the Tally won't succeed - if acceptance_token_balance < acceptance_criteria.tokens_required - && (SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as UnixTimestamp) - < acceptance_criteria.deadline - { - println!("Feature proposal pending"); - return Ok(()); - } - } - FeatureProposal::Accepted { .. } => { - println!("Feature proposal accepted"); - return Ok(()); - } - FeatureProposal::Expired => { - println!("Feature proposal expired"); - return Ok(()); - } - } - - let mut transaction = Transaction::new_with_payer( - &[spl_feature_proposal::instruction::tally( - feature_proposal_address, - )], - Some(&config.keypair.pubkey()), - ); - let blockhash = rpc_client.get_latest_blockhash()?; - transaction.try_sign(&[&config.keypair], blockhash)?; - - rpc_client.send_and_confirm_transaction_with_spinner(&transaction)?; - - // Check the status of the proposal after the tally completes - let feature_proposal = get_feature_proposal(rpc_client, feature_proposal_address)?; - match feature_proposal { - FeatureProposal::Uninitialized => Err("Feature proposal is uninitialized".into()), - FeatureProposal::Pending { .. } => { - println!("Feature proposal pending"); - Ok(()) - } - FeatureProposal::Accepted { .. } => { - println!("Feature proposal accepted"); - Ok(()) - } - FeatureProposal::Expired => { - println!("Feature proposal expired"); - Ok(()) - } - } -} diff --git a/feature-proposal/program/Cargo.toml b/feature-proposal/program/Cargo.toml deleted file mode 100644 index 79c9ea1e37c..00000000000 --- a/feature-proposal/program/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "spl-feature-proposal" -version = "1.0.0" -description = "Solana Program Library Feature Proposal Program" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -borsh = "1.5.3" -solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ - "no-entrypoint", -] } - -[dev-dependencies] -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[lints] -workspace = true diff --git a/feature-proposal/program/Xargo.toml b/feature-proposal/program/Xargo.toml deleted file mode 100644 index 1744f098ae1..00000000000 --- a/feature-proposal/program/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] \ No newline at end of file diff --git a/feature-proposal/program/program-id.md b/feature-proposal/program/program-id.md deleted file mode 100644 index f1475c155c4..00000000000 --- a/feature-proposal/program/program-id.md +++ /dev/null @@ -1 +0,0 @@ -Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse diff --git a/feature-proposal/program/run-tests.sh b/feature-proposal/program/run-tests.sh deleted file mode 100755 index 7ee90bf4b3e..00000000000 --- a/feature-proposal/program/run-tests.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -ex -cd "$(dirname "$0")" -cargo fmt -- --check -cargo clippy -cargo build -cargo build-sbf - -if [[ $1 = -v ]]; then - export RUST_LOG=solana=debug -fi - -cargo test -cargo test-sbf diff --git a/feature-proposal/program/src/entrypoint.rs b/feature-proposal/program/src/entrypoint.rs deleted file mode 100644 index 62a02f2a465..00000000000 --- a/feature-proposal/program/src/entrypoint.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Program entrypoint - -#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] - -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - crate::processor::process_instruction(program_id, accounts, instruction_data) -} diff --git a/feature-proposal/program/src/instruction.rs b/feature-proposal/program/src/instruction.rs deleted file mode 100644 index ea8238e4149..00000000000 --- a/feature-proposal/program/src/instruction.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! Program instructions - -use { - crate::{state::AcceptanceCriteria, *}, - borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - msg, - program_error::ProgramError, - program_pack::{Pack, Sealed}, - pubkey::Pubkey, - sysvar, - }, -}; - -/// Instructions supported by the Feature Proposal program -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)] -pub enum FeatureProposalInstruction { - /// Propose a new feature. - /// - /// This instruction will create a variety of accounts to support the - /// feature proposal, all funded by account 0: - /// * A new token mint with a supply of `tokens_to_mint`, owned by the - /// program and never modified again - /// * A new "distributor" token account that holds the total supply, owned - /// by account 0. - /// * A new "acceptance" token account that holds 0 tokens, owned by the - /// program. Tokens transfers to this address are irrevocable and - /// permanent. - /// * A new feature id account that has been funded and allocated (as - /// described in `solana_program::feature`) - /// - /// On successful execution of the instruction, the feature proposer is - /// expected to distribute the tokens in the distributor token account - /// out to all participating parties. - /// - /// Based on the provided acceptance criteria, if - /// `AcceptanceCriteria::tokens_required` tokens are transferred into - /// the acceptance token account before `AcceptanceCriteria::deadline` - /// then the proposal is eligible to be accepted. - /// - /// The `FeatureProposalInstruction::Tally` instruction must be executed, by - /// any party, to complete the feature acceptance process. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writeable,signer]` Funding account (must be a system account) - /// 1. `[writeable,signer]` Unallocated feature proposal account to create - /// 2. `[writeable]` Token mint address from `get_mint_address` - /// 3. `[writeable]` Distributor token account address from - /// `get_distributor_token_address` - /// 4. `[writeable]` Acceptance token account address from - /// `get_acceptance_token_address` - /// 5. `[writeable]` Feature id account address from - /// `get_feature_id_address` - /// 6. `[]` System program - /// 7. `[]` SPL Token program - /// 8. `[]` Rent sysvar - Propose { - /// Total number of tokens to mint for this proposal - #[allow(dead_code)] // not dead code.. - tokens_to_mint: u64, - - /// Criteria for how this proposal may be activated - #[allow(dead_code)] // not dead code.. - acceptance_criteria: AcceptanceCriteria, - }, - - /// `Tally` is a permission-less instruction to check the acceptance - /// criteria for the feature proposal, which may result in: - /// * No action - /// * Feature proposal acceptance - /// * Feature proposal expiration - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writeable]` Feature proposal account - /// 1. `[]` Acceptance token account address from - /// `get_acceptance_token_address` - /// 2. `[writeable]` Derived feature id account address from - /// `get_feature_id_address` - /// 3. `[]` System program - /// 4. `[]` Clock sysvar - Tally, -} - -impl Sealed for FeatureProposalInstruction {} -impl Pack for FeatureProposalInstruction { - const LEN: usize = 25; // see `test_get_packed_len()` for justification of "18" - - fn pack_into_slice(&self, dst: &mut [u8]) { - let data = self.pack_into_vec(); - dst[..data.len()].copy_from_slice(&data); - } - - fn unpack_from_slice(src: &[u8]) -> Result { - let mut mut_src: &[u8] = src; - Self::deserialize(&mut mut_src).map_err(|err| { - msg!( - "Error: failed to deserialize feature proposal instruction: {}", - err - ); - ProgramError::InvalidInstructionData - }) - } -} - -impl FeatureProposalInstruction { - fn pack_into_vec(&self) -> Vec { - borsh::to_vec(self).expect("try_to_vec") - } -} - -/// Create a `FeatureProposalInstruction::Propose` instruction -pub fn propose( - funding_address: &Pubkey, - feature_proposal_address: &Pubkey, - tokens_to_mint: u64, - acceptance_criteria: AcceptanceCriteria, -) -> Instruction { - let mint_address = get_mint_address(feature_proposal_address); - let distributor_token_address = get_distributor_token_address(feature_proposal_address); - let acceptance_token_address = get_acceptance_token_address(feature_proposal_address); - let feature_id_address = get_feature_id_address(feature_proposal_address); - - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(*funding_address, true), - AccountMeta::new(*feature_proposal_address, true), - AccountMeta::new(mint_address, false), - AccountMeta::new(distributor_token_address, false), - AccountMeta::new(acceptance_token_address, false), - AccountMeta::new(feature_id_address, false), - AccountMeta::new_readonly(solana_program::system_program::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - ], - data: FeatureProposalInstruction::Propose { - tokens_to_mint, - acceptance_criteria, - } - .pack_into_vec(), - } -} - -/// Create a `FeatureProposalInstruction::Tally` instruction -pub fn tally(feature_proposal_address: &Pubkey) -> Instruction { - let acceptance_token_address = get_acceptance_token_address(feature_proposal_address); - let feature_id_address = get_feature_id_address(feature_proposal_address); - - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(*feature_proposal_address, false), - AccountMeta::new_readonly(acceptance_token_address, false), - AccountMeta::new(feature_id_address, false), - AccountMeta::new_readonly(solana_program::system_program::id(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - ], - data: FeatureProposalInstruction::Tally.pack_into_vec(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_packed_len() { - assert_eq!( - FeatureProposalInstruction::get_packed_len(), - solana_program::borsh1::get_packed_len::() - ) - } - - #[test] - fn test_serialize_bytes() { - assert_eq!( - borsh::to_vec(&FeatureProposalInstruction::Tally).unwrap(), - vec![1] - ); - - assert_eq!( - borsh::to_vec(&FeatureProposalInstruction::Propose { - tokens_to_mint: 42, - acceptance_criteria: AcceptanceCriteria { - tokens_required: 0xdeadbeefdeadbeef, - deadline: -1, - } - }) - .unwrap(), - vec![ - 0, 42, 0, 0, 0, 0, 0, 0, 0, 239, 190, 173, 222, 239, 190, 173, 222, 255, 255, 255, - 255, 255, 255, 255, 255 - ] - ); - } - - #[test] - fn test_serialize_large_slice() { - let mut dst = vec![0xff; 4]; - FeatureProposalInstruction::Tally.pack_into_slice(&mut dst); - - // Extra bytes (0xff) ignored - assert_eq!(dst, vec![1, 0xff, 0xff, 0xff]); - } - - #[test] - fn state_deserialize_invalid() { - assert_eq!( - FeatureProposalInstruction::unpack_from_slice(&[1]), - Ok(FeatureProposalInstruction::Tally), - ); - - // Extra bytes (0xff) ignored... - assert_eq!( - FeatureProposalInstruction::unpack_from_slice(&[1, 0xff, 0xff, 0xff]), - Ok(FeatureProposalInstruction::Tally), - ); - - assert_eq!( - FeatureProposalInstruction::unpack_from_slice(&[2]), - Err(ProgramError::InvalidInstructionData), - ); - } -} diff --git a/feature-proposal/program/src/lib.rs b/feature-proposal/program/src/lib.rs deleted file mode 100644 index 52b44bb0bc4..00000000000 --- a/feature-proposal/program/src/lib.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Feature Proposal program -#![deny(missing_docs)] -#![forbid(unsafe_code)] - -mod entrypoint; -pub mod instruction; -pub mod processor; -pub mod state; - -// Export current SDK types for downstream users building with a different SDK -// version -pub use solana_program; -use solana_program::{program_pack::Pack, pubkey::Pubkey}; - -solana_program::declare_id!("Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse"); - -pub(crate) fn get_mint_address_with_seed(feature_proposal_address: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address(&[&feature_proposal_address.to_bytes(), br"mint"], &id()) -} - -pub(crate) fn get_distributor_token_address_with_seed( - feature_proposal_address: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[&feature_proposal_address.to_bytes(), br"distributor"], - &id(), - ) -} - -pub(crate) fn get_acceptance_token_address_with_seed( - feature_proposal_address: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[&feature_proposal_address.to_bytes(), br"acceptance"], - &id(), - ) -} - -pub(crate) fn get_feature_id_address_with_seed(feature_proposal_address: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[&feature_proposal_address.to_bytes(), br"feature-id"], - &id(), - ) -} - -/// Derive the SPL Token mint address associated with a feature proposal -pub fn get_mint_address(feature_proposal_address: &Pubkey) -> Pubkey { - get_mint_address_with_seed(feature_proposal_address).0 -} - -/// Derive the SPL Token token address associated with a feature proposal that -/// receives the initial minted tokens -pub fn get_distributor_token_address(feature_proposal_address: &Pubkey) -> Pubkey { - get_distributor_token_address_with_seed(feature_proposal_address).0 -} - -/// Derive the SPL Token token address associated with a feature proposal that -/// users send their tokens to accept the proposal -pub fn get_acceptance_token_address(feature_proposal_address: &Pubkey) -> Pubkey { - get_acceptance_token_address_with_seed(feature_proposal_address).0 -} - -/// Derive the feature id address associated with the feature proposal -pub fn get_feature_id_address(feature_proposal_address: &Pubkey) -> Pubkey { - get_feature_id_address_with_seed(feature_proposal_address).0 -} - -/// Convert the UI representation of a token amount (using the decimals field -/// defined in its mint) to the raw amount -pub fn ui_amount_to_amount(ui_amount: f64) -> u64 { - (ui_amount * 10_usize.pow(spl_token::native_mint::DECIMALS as u32) as f64) as u64 -} - -/// Convert a raw amount to its UI representation (using the decimals field -/// defined in its mint) -pub fn amount_to_ui_amount(amount: u64) -> f64 { - amount as f64 / 10_usize.pow(spl_token::native_mint::DECIMALS as u32) as f64 -} diff --git a/feature-proposal/program/src/processor.rs b/feature-proposal/program/src/processor.rs deleted file mode 100644 index 6aa00a610f8..00000000000 --- a/feature-proposal/program/src/processor.rs +++ /dev/null @@ -1,371 +0,0 @@ -//! Program state processor - -use { - crate::{instruction::*, state::*, *}, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - clock::Clock, - entrypoint::ProgramResult, - feature::{self, Feature}, - msg, - program::{invoke, invoke_signed}, - program_error::ProgramError, - pubkey::Pubkey, - rent::Rent, - system_instruction, - sysvar::Sysvar, - }, -}; - -/// Instruction processor -pub fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - let instruction = FeatureProposalInstruction::unpack_from_slice(input)?; - let account_info_iter = &mut accounts.iter(); - - match instruction { - FeatureProposalInstruction::Propose { - tokens_to_mint, - acceptance_criteria, - } => { - msg!("FeatureProposalInstruction::Propose"); - - let funder_info = next_account_info(account_info_iter)?; - let feature_proposal_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let distributor_token_info = next_account_info(account_info_iter)?; - let acceptance_token_info = next_account_info(account_info_iter)?; - let feature_id_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - let spl_token_program_info = next_account_info(account_info_iter)?; - let rent_sysvar_info = next_account_info(account_info_iter)?; - let rent = &Rent::from_account_info(rent_sysvar_info)?; - - let (mint_address, mint_bump_seed) = - get_mint_address_with_seed(feature_proposal_info.key); - if mint_address != *mint_info.key { - msg!("Error: mint address derivation mismatch"); - return Err(ProgramError::InvalidArgument); - } - - let (distributor_token_address, distributor_token_bump_seed) = - get_distributor_token_address_with_seed(feature_proposal_info.key); - if distributor_token_address != *distributor_token_info.key { - msg!("Error: distributor token address derivation mismatch"); - return Err(ProgramError::InvalidArgument); - } - - let (acceptance_token_address, acceptance_token_bump_seed) = - get_acceptance_token_address_with_seed(feature_proposal_info.key); - if acceptance_token_address != *acceptance_token_info.key { - msg!("Error: acceptance token address derivation mismatch"); - return Err(ProgramError::InvalidArgument); - } - - let (feature_id_address, feature_id_bump_seed) = - get_feature_id_address_with_seed(feature_proposal_info.key); - if feature_id_address != *feature_id_info.key { - msg!("Error: feature-id address derivation mismatch"); - return Err(ProgramError::InvalidArgument); - } - - let mint_signer_seeds: &[&[_]] = &[ - &feature_proposal_info.key.to_bytes(), - br"mint", - &[mint_bump_seed], - ]; - - let distributor_token_signer_seeds: &[&[_]] = &[ - &feature_proposal_info.key.to_bytes(), - br"distributor", - &[distributor_token_bump_seed], - ]; - - let acceptance_token_signer_seeds: &[&[_]] = &[ - &feature_proposal_info.key.to_bytes(), - br"acceptance", - &[acceptance_token_bump_seed], - ]; - - let feature_id_signer_seeds: &[&[_]] = &[ - &feature_proposal_info.key.to_bytes(), - br"feature-id", - &[feature_id_bump_seed], - ]; - - msg!("Creating feature proposal account"); - invoke( - &system_instruction::create_account( - funder_info.key, - feature_proposal_info.key, - 1.max(rent.minimum_balance(FeatureProposal::get_packed_len())), - FeatureProposal::get_packed_len() as u64, - program_id, - ), - &[ - funder_info.clone(), - feature_proposal_info.clone(), - system_program_info.clone(), - ], - )?; - FeatureProposal::Pending(acceptance_criteria) - .pack_into_slice(&mut feature_proposal_info.data.borrow_mut()); - - msg!("Creating mint"); - invoke_signed( - &system_instruction::create_account( - funder_info.key, - mint_info.key, - 1.max(rent.minimum_balance(spl_token::state::Mint::get_packed_len())), - spl_token::state::Mint::get_packed_len() as u64, - &spl_token::id(), - ), - &[ - funder_info.clone(), - mint_info.clone(), - system_program_info.clone(), - ], - &[mint_signer_seeds], - )?; - - msg!("Initializing mint"); - invoke( - &spl_token::instruction::initialize_mint( - &spl_token::id(), - mint_info.key, - mint_info.key, - None, - spl_token::native_mint::DECIMALS, - )?, - &[ - mint_info.clone(), - spl_token_program_info.clone(), - rent_sysvar_info.clone(), - ], - )?; - - msg!("Creating distributor token account"); - invoke_signed( - &system_instruction::create_account( - funder_info.key, - distributor_token_info.key, - 1.max(rent.minimum_balance(spl_token::state::Account::get_packed_len())), - spl_token::state::Account::get_packed_len() as u64, - &spl_token::id(), - ), - &[ - funder_info.clone(), - distributor_token_info.clone(), - system_program_info.clone(), - ], - &[distributor_token_signer_seeds], - )?; - - msg!("Initializing distributor token account"); - invoke( - &spl_token::instruction::initialize_account( - &spl_token::id(), - distributor_token_info.key, - mint_info.key, - feature_proposal_info.key, - )?, - &[ - distributor_token_info.clone(), - spl_token_program_info.clone(), - rent_sysvar_info.clone(), - feature_proposal_info.clone(), - mint_info.clone(), - ], - )?; - - msg!("Creating acceptance token account"); - invoke_signed( - &system_instruction::create_account( - funder_info.key, - acceptance_token_info.key, - 1.max(rent.minimum_balance(spl_token::state::Account::get_packed_len())), - spl_token::state::Account::get_packed_len() as u64, - &spl_token::id(), - ), - &[ - funder_info.clone(), - acceptance_token_info.clone(), - system_program_info.clone(), - ], - &[acceptance_token_signer_seeds], - )?; - - msg!("Initializing acceptance token account"); - invoke( - &spl_token::instruction::initialize_account( - &spl_token::id(), - acceptance_token_info.key, - mint_info.key, - feature_proposal_info.key, - )?, - &[ - acceptance_token_info.clone(), - spl_token_program_info.clone(), - rent_sysvar_info.clone(), - feature_proposal_info.clone(), - mint_info.clone(), - ], - )?; - invoke( - &spl_token::instruction::set_authority( - &spl_token::id(), - acceptance_token_info.key, - Some(feature_proposal_info.key), - spl_token::instruction::AuthorityType::CloseAccount, - feature_proposal_info.key, - &[], - )?, - &[ - spl_token_program_info.clone(), - acceptance_token_info.clone(), - feature_proposal_info.clone(), - ], - )?; - invoke( - &spl_token::instruction::set_authority( - &spl_token::id(), - acceptance_token_info.key, - Some(program_id), - spl_token::instruction::AuthorityType::AccountOwner, - feature_proposal_info.key, - &[], - )?, - &[ - spl_token_program_info.clone(), - acceptance_token_info.clone(), - feature_proposal_info.clone(), - ], - )?; - - // Mint `tokens_to_mint` tokens into `distributor_token_account` owned by - // `feature_proposal` - msg!("Minting {} tokens", tokens_to_mint); - invoke_signed( - &spl_token::instruction::mint_to( - &spl_token::id(), - mint_info.key, - distributor_token_info.key, - mint_info.key, - &[], - tokens_to_mint, - )?, - &[ - mint_info.clone(), - distributor_token_info.clone(), - spl_token_program_info.clone(), - ], - &[mint_signer_seeds], - )?; - - // Fully fund the feature id account so the `Tally` instruction will not require - // any lamports from the caller - msg!("Funding feature id account"); - invoke( - &system_instruction::transfer( - funder_info.key, - feature_id_info.key, - 1.max(rent.minimum_balance(Feature::size_of())), - ), - &[ - funder_info.clone(), - feature_id_info.clone(), - system_program_info.clone(), - ], - )?; - - msg!("Allocating feature id account"); - invoke_signed( - &system_instruction::allocate(feature_id_info.key, Feature::size_of() as u64), - &[feature_id_info.clone(), system_program_info.clone()], - &[feature_id_signer_seeds], - )?; - } - - FeatureProposalInstruction::Tally => { - msg!("FeatureProposalInstruction::Tally"); - - let feature_proposal_info = next_account_info(account_info_iter)?; - let feature_proposal_state = - FeatureProposal::unpack_from_slice(&feature_proposal_info.data.borrow())?; - - match feature_proposal_state { - FeatureProposal::Pending(acceptance_criteria) => { - let acceptance_token_info = next_account_info(account_info_iter)?; - let feature_id_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - let clock_sysvar_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_sysvar_info)?; - - // Re-derive the acceptance token and feature id program addresses to confirm - // the caller provided the correct addresses - let acceptance_token_address = - get_acceptance_token_address(feature_proposal_info.key); - if acceptance_token_address != *acceptance_token_info.key { - msg!("Error: acceptance token address derivation mismatch"); - return Err(ProgramError::InvalidArgument); - } - - let (feature_id_address, feature_id_bump_seed) = - get_feature_id_address_with_seed(feature_proposal_info.key); - if feature_id_address != *feature_id_info.key { - msg!("Error: feature-id address derivation mismatch"); - return Err(ProgramError::InvalidArgument); - } - - let feature_id_signer_seeds: &[&[_]] = &[ - &feature_proposal_info.key.to_bytes(), - br"feature-id", - &[feature_id_bump_seed], - ]; - - if clock.unix_timestamp >= acceptance_criteria.deadline { - msg!("Feature proposal expired"); - FeatureProposal::Expired - .pack_into_slice(&mut feature_proposal_info.data.borrow_mut()); - return Ok(()); - } - - msg!("Unpacking acceptance token account"); - let acceptance_token = - spl_token::state::Account::unpack(&acceptance_token_info.data.borrow())?; - - msg!( - "Feature proposal has received {} tokens, and {} tokens required for acceptance", - acceptance_token.amount, acceptance_criteria.tokens_required - ); - if acceptance_token.amount < acceptance_criteria.tokens_required { - msg!("Activation threshold has not been reached"); - return Ok(()); - } - - msg!("Assigning feature id account"); - invoke_signed( - &system_instruction::assign(feature_id_info.key, &feature::id()), - &[feature_id_info.clone(), system_program_info.clone()], - &[feature_id_signer_seeds], - )?; - - msg!("Feature proposal accepted"); - FeatureProposal::Accepted { - tokens_upon_acceptance: acceptance_token.amount, - } - .pack_into_slice(&mut feature_proposal_info.data.borrow_mut()); - } - _ => { - msg!("Error: feature proposal account not in the pending state"); - return Err(ProgramError::InvalidAccountData); - } - } - } - } - - Ok(()) -} diff --git a/feature-proposal/program/src/state.rs b/feature-proposal/program/src/state.rs deleted file mode 100644 index 81d86f2da7a..00000000000 --- a/feature-proposal/program/src/state.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Program state -use { - borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, - solana_program::{ - clock::UnixTimestamp, - msg, - program_error::ProgramError, - program_pack::{Pack, Sealed}, - }, -}; - -/// Criteria for accepting a feature proposal -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)] -pub struct AcceptanceCriteria { - /// The balance of the feature proposal's token account must be greater than - /// this amount, and tallied before the deadline for the feature to be - /// accepted. - pub tokens_required: u64, - - /// If the required tokens are not tallied by this deadline then the - /// proposal will expire. - pub deadline: UnixTimestamp, -} - -/// Contents of a Feature Proposal account -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)] -pub enum FeatureProposal { - /// Default account state after creating it - Uninitialized, - /// Feature proposal is now pending - Pending(AcceptanceCriteria), - /// Feature proposal was accepted and the feature is now active - Accepted { - /// The balance of the feature proposal's token account at the time of - /// activation. - #[allow(dead_code)] // not dead code.. - tokens_upon_acceptance: u64, - }, - /// Feature proposal was not accepted before the deadline - Expired, -} -impl Sealed for FeatureProposal {} - -impl Pack for FeatureProposal { - const LEN: usize = 17; // see `test_get_packed_len()` for justification of "18" - - fn pack_into_slice(&self, dst: &mut [u8]) { - let data = borsh::to_vec(self).unwrap(); - dst[..data.len()].copy_from_slice(&data); - } - - fn unpack_from_slice(src: &[u8]) -> Result { - let mut mut_src: &[u8] = src; - Self::deserialize(&mut mut_src).map_err(|err| { - msg!( - "Error: failed to deserialize feature proposal account: {}", - err - ); - ProgramError::InvalidAccountData - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_packed_len() { - assert_eq!( - FeatureProposal::get_packed_len(), - solana_program::borsh1::get_packed_len::() - ); - } - - #[test] - fn test_serialize_bytes() { - assert_eq!(borsh::to_vec(&FeatureProposal::Expired).unwrap(), vec![3]); - - assert_eq!( - borsh::to_vec(&FeatureProposal::Pending(AcceptanceCriteria { - tokens_required: 0xdeadbeefdeadbeef, - deadline: -1, - })) - .unwrap(), - vec![1, 239, 190, 173, 222, 239, 190, 173, 222, 255, 255, 255, 255, 255, 255, 255, 255], - ); - } - - #[test] - fn test_serialize_large_slice() { - let mut dst = vec![0xff; 4]; - FeatureProposal::Expired.pack_into_slice(&mut dst); - - // Extra bytes (0xff) ignored - assert_eq!(dst, vec![3, 0xff, 0xff, 0xff]); - } - - #[test] - fn state_deserialize_invalid() { - assert_eq!( - FeatureProposal::unpack_from_slice(&[3]), - Ok(FeatureProposal::Expired), - ); - - // Extra bytes (0xff) ignored... - assert_eq!( - FeatureProposal::unpack_from_slice(&[3, 0xff, 0xff, 0xff]), - Ok(FeatureProposal::Expired), - ); - - assert_eq!( - FeatureProposal::unpack_from_slice(&[4]), - Err(ProgramError::InvalidAccountData), - ); - } -} diff --git a/feature-proposal/program/tests/functional.rs b/feature-proposal/program/tests/functional.rs deleted file mode 100644 index 149b68b9b8e..00000000000 --- a/feature-proposal/program/tests/functional.rs +++ /dev/null @@ -1,206 +0,0 @@ -// Mark this test as BPF-only due to current `ProgramTest` limitations when -// CPIing into the system program -#![cfg(feature = "test-sbf")] - -use { - solana_program::{ - feature::{self, Feature}, - program_option::COption, - system_program, - }, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::Transaction, - }, - spl_feature_proposal::{instruction::*, state::*, *}, -}; - -fn program_test() -> ProgramTest { - ProgramTest::new( - "spl_feature_proposal", - id(), - processor!(processor::process_instruction), - ) -} - -#[tokio::test] -async fn test_basic() { - let feature_proposal = Keypair::new(); - - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - - let feature_id_address = get_feature_id_address(&feature_proposal.pubkey()); - let mint_address = get_mint_address(&feature_proposal.pubkey()); - let distributor_token_address = get_distributor_token_address(&feature_proposal.pubkey()); - let acceptance_token_address = get_acceptance_token_address(&feature_proposal.pubkey()); - - // Create a new feature proposal - let mut transaction = Transaction::new_with_payer( - &[propose( - &payer.pubkey(), - &feature_proposal.pubkey(), - 42, - AcceptanceCriteria { - tokens_required: 42, - deadline: i64::MAX, - }, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &feature_proposal], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - // Confirm feature id account is now funded and allocated, but not assigned - let feature_id_account = banks_client - .get_account(feature_id_address) - .await - .expect("success") - .expect("some account"); - assert_eq!(feature_id_account.owner, system_program::id()); - assert_eq!(feature_id_account.data.len(), Feature::size_of()); - - // Confirm mint account state - let mint = banks_client - .get_packed_account_data::(mint_address) - .await - .unwrap(); - assert_eq!(mint.supply, 42); - assert_eq!(mint.decimals, 9); - assert!(mint.freeze_authority.is_none()); - assert_eq!(mint.mint_authority, COption::Some(mint_address)); - - // Confirm distributor token account state - let distributor_token = banks_client - .get_packed_account_data::(distributor_token_address) - .await - .unwrap(); - assert_eq!(distributor_token.amount, 42); - assert_eq!(distributor_token.mint, mint_address); - assert_eq!(distributor_token.owner, feature_proposal.pubkey()); - assert!(distributor_token.close_authority.is_none()); - - // Confirm acceptance token account state - let acceptance_token = banks_client - .get_packed_account_data::(acceptance_token_address) - .await - .unwrap(); - assert_eq!(acceptance_token.amount, 0); - assert_eq!(acceptance_token.mint, mint_address); - assert_eq!(acceptance_token.owner, id()); - assert_eq!( - acceptance_token.close_authority, - COption::Some(feature_proposal.pubkey()) - ); - - // Tally #1: Does nothing because the acceptance criteria has not been met - let mut transaction = - Transaction::new_with_payer(&[tally(&feature_proposal.pubkey())], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - // Confirm feature id account is not yet assigned - let feature_id_account = banks_client - .get_account(feature_id_address) - .await - .expect("success") - .expect("some account"); - assert_eq!(feature_id_account.owner, system_program::id()); - - assert!(matches!( - banks_client - .get_packed_account_data::(feature_proposal.pubkey()) - .await, - Ok(FeatureProposal::Pending(_)) - )); - - // Transfer tokens to the acceptance account - let mut transaction = Transaction::new_with_payer( - &[spl_token::instruction::transfer( - &spl_token::id(), - &distributor_token_address, - &acceptance_token_address, - &feature_proposal.pubkey(), - &[], - 42, - ) - .unwrap()], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &feature_proposal], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - // Fetch a new blockhash to avoid the second Tally transaction having the same - // signature as the first Tally transaction - let recent_blockhash = banks_client - .get_new_latest_blockhash(&recent_blockhash) - .await - .unwrap(); - - // Tally #2: the acceptance criteria is now met - let mut transaction = - Transaction::new_with_payer(&[tally(&feature_proposal.pubkey())], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - // Confirm feature id account is now assigned - let feature_id_account = banks_client - .get_account(feature_id_address) - .await - .expect("success") - .expect("some account"); - assert_eq!(feature_id_account.owner, feature::id()); - - // Confirm feature proposal account state - assert!(matches!( - banks_client - .get_packed_account_data::(feature_proposal.pubkey()) - .await, - Ok(FeatureProposal::Accepted { - tokens_upon_acceptance: 42 - }) - )); -} - -#[tokio::test] -async fn test_expired() { - let feature_proposal = Keypair::new(); - - let (banks_client, payer, recent_blockhash) = program_test().start().await; - - // Create a new feature proposal - let mut transaction = Transaction::new_with_payer( - &[propose( - &payer.pubkey(), - &feature_proposal.pubkey(), - 42, - AcceptanceCriteria { - tokens_required: 42, - deadline: 0, // <=== Already expired - }, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &feature_proposal], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - assert!(matches!( - banks_client - .get_packed_account_data::(feature_proposal.pubkey()) - .await, - Ok(FeatureProposal::Pending(_)) - )); - - // Tally will cause the proposal to expire - let mut transaction = - Transaction::new_with_payer(&[tally(&feature_proposal.pubkey())], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - assert!(matches!( - banks_client - .get_packed_account_data::(feature_proposal.pubkey()) - .await, - Ok(FeatureProposal::Expired) - )); -} diff --git a/governance/addin-mock/program/Cargo.toml b/governance/addin-mock/program/Cargo.toml index 589aa85ffd6..575cd75f2c8 100644 --- a/governance/addin-mock/program/Cargo.toml +++ b/governance/addin-mock/program/Cargo.toml @@ -20,7 +20,7 @@ num-traits = "0.2" serde = "1.0.217" serde_derive = "1.0.103" solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../../token/program", features = [ +spl-token = { version = "7.0", features = [ "no-entrypoint", ] } spl-governance-addin-api = { version = "0.1.4", path = "../../addin-api" } diff --git a/governance/chat/program/Cargo.toml b/governance/chat/program/Cargo.toml index 5e3b36e5858..a6315129bf8 100644 --- a/governance/chat/program/Cargo.toml +++ b/governance/chat/program/Cargo.toml @@ -20,7 +20,7 @@ num-traits = "0.2" serde = "1.0.217" serde_derive = "1.0.103" solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../../token/program", features = [ +spl-token = { version = "7.0", features = [ "no-entrypoint", ] } spl-governance = { version = "4.0.0", path = "../../program", features = [ diff --git a/governance/program/Cargo.toml b/governance/program/Cargo.toml index e85f57e81a3..fd553380c3d 100644 --- a/governance/program/Cargo.toml +++ b/governance/program/Cargo.toml @@ -20,7 +20,7 @@ num-traits = "0.2" serde = "1.0.217" serde_derive = "1.0.103" solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ +spl-token = { version = "7.0", features = [ "no-entrypoint", ] } spl-governance-tools = { version = "0.1.4", path = "../tools" } diff --git a/governance/test-sdk/Cargo.toml b/governance/test-sdk/Cargo.toml index af15d1fdaaf..dd27a6fcf46 100644 --- a/governance/test-sdk/Cargo.toml +++ b/governance/test-sdk/Cargo.toml @@ -19,7 +19,7 @@ serde_derive = "1.0.103" solana-program = "2.1.0" solana-program-test = "2.1.0" solana-sdk = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ +spl-token = { version = "7.0", features = [ "no-entrypoint", ] } thiserror = "2.0" diff --git a/governance/tools/Cargo.toml b/governance/tools/Cargo.toml index bf8cdedef14..766bab14b7c 100644 --- a/governance/tools/Cargo.toml +++ b/governance/tools/Cargo.toml @@ -16,7 +16,7 @@ num-traits = "0.2" serde = "1.0.217" serde_derive = "1.0.103" solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ +spl-token = { version = "7.0", features = [ "no-entrypoint", ] } thiserror = "2.0" diff --git a/instruction-padding/README.md b/instruction-padding/README.md new file mode 100644 index 00000000000..fd71f7e7125 --- /dev/null +++ b/instruction-padding/README.md @@ -0,0 +1,2 @@ +NOTE: The instruction-padding program and clients are now maintained at +[solana-program/instruction-padding](https://github.com/solana-program/instruction-padding). diff --git a/instruction-padding/program/Cargo.toml b/instruction-padding/program/Cargo.toml deleted file mode 100644 index b30412155f9..00000000000 --- a/instruction-padding/program/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "spl-instruction-padding" -version = "0.3.0" -description = "Solana Program Library Instruction Padding Program" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -homepage = "https://solana.com/" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -num_enum = "0.7.3" -solana-account-info = "2.1.0" -solana-cpi = "2.1.0" -solana-instruction = { version = "2.1.0", features = ["std"] } -solana-program-entrypoint = "2.1.0" -solana-program-error = "2.1.0" -solana-pubkey = "2.1.0" - -[dev-dependencies] -solana-program = "2.1.0" -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" -static_assertions = "1.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/instruction-padding/program/README.md b/instruction-padding/program/README.md deleted file mode 100644 index 5fe411f9c4c..00000000000 --- a/instruction-padding/program/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Instruction Pad Program - -A program for padding instructions with additional data or accounts, to be used -for testing larger transactions, either more instruction data, or more accounts. - -The main use-case is with solana-bench-tps, where we can see the impact of larger -transactions through TPS numbers. With that data, we can develop a fair fee model -for large transactions. - -It operates with two instructions: no-op and wrap. - -* No-op: simply an instruction with as much data and as many accounts as desired, -of which none will be used for processing. -* Wrap: before the padding data and accounts, accepts a real instruction and -required accounts, and performs a CPI into the program specified by the instruction - -Both of these modes add the general overhead of calling a BPF program, and -the wrap mode adds the CPI overhead. - -Because of the overhead, it's best to use the instruction padding program with -all large transaction tests, and comparing TPS numbers between: - -* using the program with no padding -* using the program with data and account padding - -## Audit - -The repository [README](https://github.com/solana-labs/solana-program-library#audits) -contains information about program audits. diff --git a/instruction-padding/program/src/entrypoint.rs b/instruction-padding/program/src/entrypoint.rs deleted file mode 100644 index d5e799e5937..00000000000 --- a/instruction-padding/program/src/entrypoint.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Program entrypoint - -#![cfg(not(feature = "no-entrypoint"))] - -use { - solana_account_info::AccountInfo, solana_program_error::ProgramResult, solana_pubkey::Pubkey, -}; - -solana_program_entrypoint::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - crate::processor::process(program_id, accounts, instruction_data) -} diff --git a/instruction-padding/program/src/instruction.rs b/instruction-padding/program/src/instruction.rs deleted file mode 100644 index 68c50b8aeb4..00000000000 --- a/instruction-padding/program/src/instruction.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! Instruction creators for large instructions - -use { - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - std::{convert::TryInto, mem::size_of}, -}; - -const MAX_CPI_ACCOUNT_INFOS: usize = 128; -const MAX_CPI_INSTRUCTION_DATA_LEN: u64 = 10 * 1024; - -#[cfg(test)] -static_assertions::const_assert_eq!( - MAX_CPI_ACCOUNT_INFOS, - solana_program::syscalls::MAX_CPI_ACCOUNT_INFOS -); -#[cfg(test)] -static_assertions::const_assert_eq!( - MAX_CPI_INSTRUCTION_DATA_LEN, - solana_program::syscalls::MAX_CPI_INSTRUCTION_DATA_LEN -); - -/// Instructions supported by the padding program, which takes in additional -/// account data or accounts and does nothing with them. It's meant for testing -/// larger transactions with bench-tps. -#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] -#[repr(u8)] -pub enum PadInstruction { - /// Does no work, but accepts a large amount of data and accounts - Noop, - /// Wraps the provided instruction, calling the provided program via CPI - /// - /// Accounts expected by this instruction: - /// - /// * All accounts required for the inner instruction - /// * The program invoked by the inner instruction - /// * Additional padding accounts - /// - /// Data expected by this instruction: - /// * WrapData - Wrap, -} - -/// Data wrapping any inner instruction -pub struct WrapData<'a> { - /// Number of accounts required by the inner instruction - pub num_accounts: u32, - /// the size of the inner instruction data - pub instruction_size: u32, - /// actual inner instruction data - pub instruction_data: &'a [u8], - // additional padding bytes come after, not captured in this struct -} - -const U32_BYTES: usize = 4; -fn unpack_u32(input: &[u8]) -> Result<(u32, &[u8]), ProgramError> { - let value = input - .get(..U32_BYTES) - .and_then(|slice| slice.try_into().ok()) - .map(u32::from_le_bytes) - .ok_or(ProgramError::InvalidInstructionData)?; - Ok((value, &input[U32_BYTES..])) -} - -impl<'a> WrapData<'a> { - /// Unpacks instruction data - pub fn unpack(data: &'a [u8]) -> Result { - let (num_accounts, rest) = unpack_u32(data)?; - let (instruction_size, rest) = unpack_u32(rest)?; - - let (instruction_data, _rest) = rest.split_at(instruction_size as usize); - Ok(Self { - num_accounts, - instruction_size, - instruction_data, - }) - } -} - -pub fn noop( - program_id: Pubkey, - padding_accounts: Vec, - padding_data: u32, -) -> Result { - let total_data_size = size_of::().saturating_add(padding_data as usize); - // crude, but can find a potential issue right away - if total_data_size > MAX_CPI_INSTRUCTION_DATA_LEN as usize { - return Err(ProgramError::InvalidInstructionData); - } - let mut data = Vec::with_capacity(total_data_size); - data.push(PadInstruction::Noop.into()); - for i in 0..padding_data { - data.push(i.checked_rem(u8::MAX as u32).unwrap() as u8); - } - - let num_accounts = padding_accounts.len().saturating_add(1); - if num_accounts > MAX_CPI_ACCOUNT_INFOS { - return Err(ProgramError::InvalidAccountData); - } - let mut accounts = Vec::with_capacity(num_accounts); - accounts.extend(padding_accounts); - - Ok(Instruction { - program_id, - accounts, - data, - }) -} - -pub fn wrap_instruction( - program_id: Pubkey, - instruction: Instruction, - padding_accounts: Vec, - padding_data: u32, -) -> Result { - let total_data_size = size_of::() - .saturating_add(size_of::()) - .saturating_add(size_of::()) - .saturating_add(instruction.data.len()) - .saturating_add(padding_data as usize); - // crude, but can find a potential issue right away - if total_data_size > MAX_CPI_INSTRUCTION_DATA_LEN as usize { - return Err(ProgramError::InvalidInstructionData); - } - let mut data = Vec::with_capacity(total_data_size); - data.push(PadInstruction::Wrap.into()); - let num_accounts: u32 = instruction - .accounts - .len() - .try_into() - .map_err(|_| ProgramError::InvalidInstructionData)?; - data.extend(num_accounts.to_le_bytes().iter()); - - let data_size: u32 = instruction - .data - .len() - .try_into() - .map_err(|_| ProgramError::InvalidInstructionData)?; - data.extend(data_size.to_le_bytes().iter()); - data.extend(instruction.data); - for i in 0..padding_data { - data.push(i.checked_rem(u8::MAX as u32).unwrap() as u8); - } - - // The format for account data goes: - // * accounts required for the CPI - // * program account to call into - // * additional accounts may be included as padding or to test loading / locks - let num_accounts = instruction - .accounts - .len() - .saturating_add(1) - .saturating_add(padding_accounts.len()); - if num_accounts > MAX_CPI_ACCOUNT_INFOS { - return Err(ProgramError::InvalidAccountData); - } - let mut accounts = Vec::with_capacity(num_accounts); - accounts.extend(instruction.accounts); - accounts.push(AccountMeta::new_readonly(instruction.program_id, false)); - accounts.extend(padding_accounts); - - Ok(Instruction { - program_id, - accounts, - data, - }) -} diff --git a/instruction-padding/program/src/lib.rs b/instruction-padding/program/src/lib.rs deleted file mode 100644 index fc69c9d8a66..00000000000 --- a/instruction-padding/program/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod entrypoint; -pub mod instruction; -pub mod processor; - -pub use { - solana_account_info, solana_cpi, solana_instruction, solana_program_entrypoint, - solana_program_error, solana_pubkey, -}; -solana_pubkey::declare_id!("iXpADd6AW1k5FaaXum5qHbSqyd7TtoN6AD7suVa83MF"); diff --git a/instruction-padding/program/src/processor.rs b/instruction-padding/program/src/processor.rs deleted file mode 100644 index 37439bd317d..00000000000 --- a/instruction-padding/program/src/processor.rs +++ /dev/null @@ -1,54 +0,0 @@ -use { - crate::instruction::{PadInstruction, WrapData}, - solana_account_info::AccountInfo, - solana_cpi::invoke, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::{ProgramError, ProgramResult}, - solana_pubkey::Pubkey, - std::convert::TryInto, -}; - -pub fn process( - _program_id: &Pubkey, - account_infos: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let (tag, rest) = instruction_data - .split_first() - .ok_or(ProgramError::InvalidInstructionData)?; - match (*tag) - .try_into() - .map_err(|_| ProgramError::InvalidInstructionData)? - { - PadInstruction::Noop => Ok(()), - PadInstruction::Wrap => { - let WrapData { - num_accounts, - instruction_size, - instruction_data, - } = WrapData::unpack(rest)?; - let mut data = Vec::with_capacity(instruction_size as usize); - data.extend_from_slice(instruction_data); - - let program_id = *account_infos[num_accounts as usize].key; - - let accounts = account_infos - .iter() - .take(num_accounts as usize) - .map(|a| AccountMeta { - pubkey: *a.key, - is_signer: a.is_signer, - is_writable: a.is_writable, - }) - .collect::>(); - - let instruction = Instruction { - program_id, - accounts, - data, - }; - - invoke(&instruction, &account_infos[..num_accounts as usize]) - } - } -} diff --git a/instruction-padding/program/tests/noop.rs b/instruction-padding/program/tests/noop.rs deleted file mode 100644 index beaba6074bb..00000000000 --- a/instruction-padding/program/tests/noop.rs +++ /dev/null @@ -1,38 +0,0 @@ -#![cfg(feature = "test-sbf")] - -use { - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::AccountMeta, pubkey::Pubkey, signature::Signer, transaction::Transaction, - }, - spl_instruction_padding::{instruction::noop, processor::process}, -}; - -#[tokio::test] -async fn success_with_noop() { - let program_id = Pubkey::new_unique(); - let program_test = ProgramTest::new("spl_instruction_padding", program_id, processor!(process)); - - let context = program_test.start_with_context().await; - - let padding_accounts = vec![ - AccountMeta::new_readonly(Pubkey::new_unique(), false), - AccountMeta::new_readonly(Pubkey::new_unique(), false), - AccountMeta::new_readonly(Pubkey::new_unique(), false), - ]; - - let padding_data = 800; - - let transaction = Transaction::new_signed_with_payer( - &[noop(program_id, padding_accounts, padding_data).unwrap()], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} diff --git a/instruction-padding/program/tests/system.rs b/instruction-padding/program/tests/system.rs deleted file mode 100644 index 335af0bc306..00000000000 --- a/instruction-padding/program/tests/system.rs +++ /dev/null @@ -1,62 +0,0 @@ -#![cfg(feature = "test-sbf")] - -use { - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::AccountMeta, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, - signature::Signer, system_instruction, transaction::Transaction, - }, - spl_instruction_padding::{instruction::wrap_instruction, processor::process}, -}; - -#[tokio::test] -async fn success_with_padded_transfer_data() { - let program_id = Pubkey::new_unique(); - let program_test = ProgramTest::new("spl_instruction_padding", program_id, processor!(process)); - - let context = program_test.start_with_context().await; - let to = Pubkey::new_unique(); - - let transfer_amount = LAMPORTS_PER_SOL; - let transfer_instruction = - system_instruction::transfer(&context.payer.pubkey(), &to, transfer_amount); - - let padding_accounts = vec![ - AccountMeta::new_readonly(Pubkey::new_unique(), false), - AccountMeta::new_readonly(Pubkey::new_unique(), false), - AccountMeta::new_readonly(Pubkey::new_unique(), false), - ]; - - let padding_data = 800; - - let transaction = Transaction::new_signed_with_payer( - &[wrap_instruction( - program_id, - transfer_instruction, - padding_accounts, - padding_data, - ) - .unwrap()], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // make sure the transfer went through - assert_eq!( - transfer_amount, - context - .banks_client - .get_account(to) - .await - .unwrap() - .unwrap() - .lamports - ); -} diff --git a/libraries/README.md b/libraries/README.md new file mode 100644 index 00000000000..7f619ec7993 --- /dev/null +++ b/libraries/README.md @@ -0,0 +1,13 @@ +NOTE: The actively maintained libraries now live at +[solana-program/libraries](https://github.com/solana-program/libraries). + +The other repo includes: + +* discriminator +* pod +* program-error +* tlv-account-resolution +* type-length-value +* type-length-value-derive-test + +Unmaintained libraries still live here and can still be forked. diff --git a/libraries/discriminator/Cargo.toml b/libraries/discriminator/Cargo.toml deleted file mode 100644 index fae99bbeea3..00000000000 --- a/libraries/discriminator/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "spl-discriminator" -version = "0.4.0" -description = "Solana Program Library 8-Byte Discriminator Management" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -borsh = ["dep:borsh"] - -[dependencies] -borsh = { version = "1", optional = true } -bytemuck = { version = "1.21.0", features = ["derive"] } -solana-program-error = "2.1.0" -solana-sha256-hasher = "2.1.0" -spl-discriminator-derive = { version = "0.2.0", path = "./derive" } - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/libraries/discriminator/README.md b/libraries/discriminator/README.md deleted file mode 100644 index 8d31cc70667..00000000000 --- a/libraries/discriminator/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# SPL Discriminator - -This library allows for easy management of 8-byte discriminators. - -### The `ArrayDiscriminator` Struct - -With this crate, you can leverage the `ArrayDiscriminator` type to manage an 8-byte discriminator for generic purposes. - -```rust -let my_discriminator = ArrayDiscriminator::new([8, 5, 1, 56, 10, 53, 9, 198]); -``` - -The `new(..)` function is also a **constant function**, so you can use `ArrayDiscriminator` in constants as well. - -```rust -const MY_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([8, 5, 1, 56, 10, 53, 9, 198]); -``` - -The `ArrayDiscriminator` struct also offers another constant function `as_slice(&self)`, so you can use `as_slice()` in constants as well. - -```rust -const MY_DISCRIMINATOR_SLICE: &[u8] = MY_DISCRIMINATOR.as_slice(); -``` - -### The `SplDiscriminate` Trait - -A trait, `SplDiscriminate` is also available, which will give you the `ArrayDiscriminator` constant type and also a slice representation of the discriminator. This can be particularly handy with match statements. - -```rust -/// A trait for managing 8-byte discriminators in a slab of bytes -pub trait SplDiscriminate { - /// The 8-byte discriminator as a `[u8; 8]` - const SPL_DISCRIMINATOR: ArrayDiscriminator; - /// The 8-byte discriminator as a slice (`&[u8]`) - const SPL_DISCRIMINATOR_SLICE: &'static [u8] = Self::SPL_DISCRIMINATOR.as_slice(); -} -``` - -### The `SplDiscriminate` Derive Macro - -The `SplDiscriminate` derive macro is a particularly useful tool for those who wish to derive their 8-byte discriminator from a particular string literal. Typically, you would have to run a hash function against the string literal, then copy the first 8 bytes, and then hard-code those bytes into a statement like the one above. - -Instead, you can simply annotate a struct or enum with `SplDiscriminate` and provide a **hash input** via the `discriminator_hash_input` attribute, and the macro will automatically derive the 8-byte discriminator for you! - -```rust -#[derive(SplDiscriminate)] // Implements `SplDiscriminate` for your struct/enum using your declared string literal hash_input -#[discriminator_hash_input("some_discriminator_hash_input")] -pub struct MyInstruction1 { - arg1: String, - arg2: u8, -} - -let my_discriminator: ArrayDiscriminator = MyInstruction1::SPL_DISCRIMINATOR; -let my_discriminator_slice: &[u8] = MyInstruction1::SPL_DISCRIMINATOR_SLICE; -``` - -Note: the 8-byte discriminator derived using the macro is always the **first 8 bytes** of the resulting hashed bytes. diff --git a/libraries/discriminator/derive/Cargo.toml b/libraries/discriminator/derive/Cargo.toml deleted file mode 100644 index 32ce6085708..00000000000 --- a/libraries/discriminator/derive/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "spl-discriminator-derive" -version = "0.2.0" -description = "Derive macro library for the `spl-discriminator` library" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -quote = "1.0" -spl-discriminator-syn = { version = "0.2.0", path = "../syn" } -syn = { version = "2.0", features = ["full"] } - -[lib] -proc-macro = true - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/libraries/discriminator/derive/src/lib.rs b/libraries/discriminator/derive/src/lib.rs deleted file mode 100644 index 6080b80b226..00000000000 --- a/libraries/discriminator/derive/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Derive macro library for the `spl-discriminator` library - -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -extern crate proc_macro; - -use { - proc_macro::TokenStream, quote::ToTokens, spl_discriminator_syn::SplDiscriminateBuilder, - syn::parse_macro_input, -}; - -/// Derive macro library to implement the `SplDiscriminate` trait -/// on an enum or struct -#[proc_macro_derive(SplDiscriminate, attributes(discriminator_hash_input))] -pub fn spl_discriminator(input: TokenStream) -> TokenStream { - parse_macro_input!(input as SplDiscriminateBuilder) - .to_token_stream() - .into() -} diff --git a/libraries/discriminator/src/discriminator.rs b/libraries/discriminator/src/discriminator.rs deleted file mode 100644 index aef70065c5e..00000000000 --- a/libraries/discriminator/src/discriminator.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! The traits and types used to create a discriminator for a type - -use { - bytemuck::{Pod, Zeroable}, - solana_program_error::ProgramError, - solana_sha256_hasher::hashv, -}; - -/// A trait for managing 8-byte discriminators in a slab of bytes -pub trait SplDiscriminate { - /// The 8-byte discriminator as a `[u8; 8]` - const SPL_DISCRIMINATOR: ArrayDiscriminator; - /// The 8-byte discriminator as a slice (`&[u8]`) - const SPL_DISCRIMINATOR_SLICE: &'static [u8] = Self::SPL_DISCRIMINATOR.as_slice(); -} - -/// Array Discriminator type -#[cfg_attr( - feature = "borsh", - derive(borsh::BorshSerialize, borsh::BorshDeserialize) -)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct ArrayDiscriminator([u8; ArrayDiscriminator::LENGTH]); -impl ArrayDiscriminator { - /// Size for discriminator in account data - pub const LENGTH: usize = 8; - /// Uninitialized variant of a discriminator - pub const UNINITIALIZED: Self = Self::new([0; Self::LENGTH]); - /// Creates a discriminator from an array - pub const fn new(value: [u8; Self::LENGTH]) -> Self { - Self(value) - } - /// Get the array as a const slice - pub const fn as_slice(&self) -> &[u8] { - self.0.as_slice() - } - /// Creates a new `ArrayDiscriminator` from some hash input string literal - pub fn new_with_hash_input(hash_input: &str) -> Self { - let hash_bytes = hashv(&[hash_input.as_bytes()]).to_bytes(); - let mut discriminator_bytes = [0u8; 8]; - discriminator_bytes.copy_from_slice(&hash_bytes[..8]); - Self(discriminator_bytes) - } -} -impl AsRef<[u8]> for ArrayDiscriminator { - fn as_ref(&self) -> &[u8] { - &self.0[..] - } -} -impl AsRef<[u8; ArrayDiscriminator::LENGTH]> for ArrayDiscriminator { - fn as_ref(&self) -> &[u8; ArrayDiscriminator::LENGTH] { - &self.0 - } -} -impl From for ArrayDiscriminator { - fn from(from: u64) -> Self { - Self(from.to_le_bytes()) - } -} -impl From<[u8; Self::LENGTH]> for ArrayDiscriminator { - fn from(from: [u8; Self::LENGTH]) -> Self { - Self(from) - } -} -impl TryFrom<&[u8]> for ArrayDiscriminator { - type Error = ProgramError; - fn try_from(a: &[u8]) -> Result { - <[u8; Self::LENGTH]>::try_from(a) - .map(Self::from) - .map_err(|_| ProgramError::InvalidAccountData) - } -} -impl From for [u8; 8] { - fn from(from: ArrayDiscriminator) -> Self { - from.0 - } -} -impl From for u64 { - fn from(from: ArrayDiscriminator) -> Self { - u64::from_le_bytes(from.0) - } -} diff --git a/libraries/discriminator/src/lib.rs b/libraries/discriminator/src/lib.rs deleted file mode 100644 index 5f7ddc2989f..00000000000 --- a/libraries/discriminator/src/lib.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Crate defining a discriminator type, which creates a set of bytes -//! meant to be unique for instructions or struct types - -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -extern crate self as spl_discriminator; - -/// Exports the discriminator module -pub mod discriminator; - -// Export for downstream -pub use { - discriminator::{ArrayDiscriminator, SplDiscriminate}, - spl_discriminator_derive::SplDiscriminate, -}; - -#[cfg(test)] -mod tests { - use {super::*, crate::discriminator::ArrayDiscriminator}; - - #[allow(dead_code)] - #[derive(SplDiscriminate)] - #[discriminator_hash_input("my_first_instruction")] - pub struct MyInstruction1 { - arg1: String, - arg2: u8, - } - - #[allow(dead_code)] - #[derive(SplDiscriminate)] - #[discriminator_hash_input("global:my_second_instruction")] - pub enum MyInstruction2 { - One, - Two, - Three, - } - - #[allow(dead_code)] - #[derive(SplDiscriminate)] - #[discriminator_hash_input("global:my_instruction_with_lifetime")] - pub struct MyInstruction3<'a> { - data: &'a [u8], - } - - #[allow(dead_code)] - #[derive(SplDiscriminate)] - #[discriminator_hash_input("global:my_instruction_with_one_generic")] - pub struct MyInstruction4 { - data: T, - } - - #[allow(dead_code)] - #[derive(SplDiscriminate)] - #[discriminator_hash_input("global:my_instruction_with_one_generic_and_lifetime")] - pub struct MyInstruction5<'b, T> { - data: &'b [T], - } - - #[allow(dead_code)] - #[derive(SplDiscriminate)] - #[discriminator_hash_input("global:my_instruction_with_multiple_generics_and_lifetime")] - pub struct MyInstruction6<'c, U, V> { - data1: &'c [U], - data2: &'c [V], - } - - #[allow(dead_code)] - #[derive(SplDiscriminate)] - #[discriminator_hash_input( - "global:my_instruction_with_multiple_generics_and_lifetime_and_where" - )] - pub struct MyInstruction7<'c, U, V> - where - U: Clone + Copy, - V: Clone + Copy, - { - data1: &'c [U], - data2: &'c [V], - } - - fn assert_discriminator( - hash_input: &str, - ) { - let discriminator = build_discriminator(hash_input); - assert_eq!( - T::SPL_DISCRIMINATOR, - discriminator, - "Discriminator mismatch: case: {}", - hash_input - ); - assert_eq!( - T::SPL_DISCRIMINATOR_SLICE, - discriminator.as_slice(), - "Discriminator mismatch: case: {}", - hash_input - ); - } - - fn build_discriminator(hash_input: &str) -> ArrayDiscriminator { - let preimage = solana_sha256_hasher::hashv(&[hash_input.as_bytes()]); - let mut bytes = [0u8; 8]; - bytes.copy_from_slice(&preimage.to_bytes()[..8]); - ArrayDiscriminator::new(bytes) - } - - #[test] - fn test_discrminators() { - let runtime_discrim = ArrayDiscriminator::new_with_hash_input("my_runtime_hash_input"); - assert_eq!( - runtime_discrim, - build_discriminator("my_runtime_hash_input"), - ); - - assert_discriminator::("my_first_instruction"); - assert_discriminator::("global:my_second_instruction"); - assert_discriminator::>("global:my_instruction_with_lifetime"); - assert_discriminator::>("global:my_instruction_with_one_generic"); - assert_discriminator::>( - "global:my_instruction_with_one_generic_and_lifetime", - ); - assert_discriminator::>( - "global:my_instruction_with_multiple_generics_and_lifetime", - ); - assert_discriminator::>( - "global:my_instruction_with_multiple_generics_and_lifetime_and_where", - ); - } -} - -#[cfg(all(test, feature = "borsh"))] -mod borsh_test { - use {super::*, borsh::BorshDeserialize}; - - #[test] - fn borsh_test() { - let my_discrim = ArrayDiscriminator::new_with_hash_input("my_discrim"); - let mut buffer = [0u8; 8]; - borsh::to_writer(&mut buffer[..], &my_discrim).unwrap(); - let my_discrim_again = ArrayDiscriminator::try_from_slice(&buffer).unwrap(); - assert_eq!(my_discrim, my_discrim_again); - assert_eq!(buffer, <[u8; 8]>::from(my_discrim)); - } -} diff --git a/libraries/discriminator/syn/Cargo.toml b/libraries/discriminator/syn/Cargo.toml deleted file mode 100644 index 5bd4f59ca95..00000000000 --- a/libraries/discriminator/syn/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "spl-discriminator-syn" -version = "0.2.0" -description = "Token parsing and generating library for the `spl-discriminator` library" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -proc-macro2 = "1.0" -quote = "1.0" -sha2 = "0.10" -syn = { version = "2.0", features = ["full"] } -thiserror = "1.0" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/libraries/discriminator/syn/src/error.rs b/libraries/discriminator/syn/src/error.rs deleted file mode 100644 index 5dd4c30d3c2..00000000000 --- a/libraries/discriminator/syn/src/error.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Error types for the `hash_input` parser - -/// Error types for the `hash_input` parser -#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)] -pub enum SplDiscriminateError { - /// Discriminator hash_input attribute not provided - #[error("Discriminator `hash_input` attribute not provided")] - HashInputAttributeNotProvided, - /// Error parsing discriminator hash_input attribute - #[error("Error parsing discriminator `hash_input` attribute")] - HashInputAttributeParseError, -} diff --git a/libraries/discriminator/syn/src/lib.rs b/libraries/discriminator/syn/src/lib.rs deleted file mode 100644 index db555916573..00000000000 --- a/libraries/discriminator/syn/src/lib.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Token parsing and generating library for the `spl-discriminator` library - -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -mod error; -pub mod parser; - -use { - crate::{error::SplDiscriminateError, parser::parse_hash_input}, - proc_macro2::{Span, TokenStream}, - quote::{quote, ToTokens}, - sha2::{Digest, Sha256}, - syn::{parse::Parse, Generics, Ident, Item, ItemEnum, ItemStruct, LitByteStr, WhereClause}, -}; - -/// "Builder" struct to implement the `SplDiscriminate` trait -/// on an enum or struct -pub struct SplDiscriminateBuilder { - /// The struct/enum identifier - pub ident: Ident, - /// The item's generic arguments (if any) - pub generics: Generics, - /// The item's where clause for generics (if any) - pub where_clause: Option, - /// The TLV hash_input - pub hash_input: String, -} - -impl TryFrom for SplDiscriminateBuilder { - type Error = SplDiscriminateError; - - fn try_from(item_enum: ItemEnum) -> Result { - let ident = item_enum.ident; - let where_clause = item_enum.generics.where_clause.clone(); - let generics = item_enum.generics; - let hash_input = parse_hash_input(&item_enum.attrs)?; - Ok(Self { - ident, - generics, - where_clause, - hash_input, - }) - } -} - -impl TryFrom for SplDiscriminateBuilder { - type Error = SplDiscriminateError; - - fn try_from(item_struct: ItemStruct) -> Result { - let ident = item_struct.ident; - let where_clause = item_struct.generics.where_clause.clone(); - let generics = item_struct.generics; - let hash_input = parse_hash_input(&item_struct.attrs)?; - Ok(Self { - ident, - generics, - where_clause, - hash_input, - }) - } -} - -impl Parse for SplDiscriminateBuilder { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let item = Item::parse(input)?; - match item { - Item::Enum(item_enum) => item_enum.try_into(), - Item::Struct(item_struct) => item_struct.try_into(), - _ => { - return Err(syn::Error::new( - Span::call_site(), - "Only enums and structs are supported", - )) - } - } - .map_err(|e| syn::Error::new(input.span(), format!("Failed to parse item: {}", e))) - } -} - -impl ToTokens for SplDiscriminateBuilder { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - tokens.extend::(self.into()); - } -} - -impl From<&SplDiscriminateBuilder> for TokenStream { - fn from(builder: &SplDiscriminateBuilder) -> Self { - let ident = &builder.ident; - let generics = &builder.generics; - let where_clause = &builder.where_clause; - let bytes = get_discriminator_bytes(&builder.hash_input); - quote! { - impl #generics spl_discriminator::discriminator::SplDiscriminate for #ident #generics #where_clause { - const SPL_DISCRIMINATOR: spl_discriminator::discriminator::ArrayDiscriminator - = spl_discriminator::discriminator::ArrayDiscriminator::new(*#bytes); - } - } - } -} - -/// Returns the bytes for the TLV hash_input discriminator -fn get_discriminator_bytes(hash_input: &str) -> LitByteStr { - LitByteStr::new( - &Sha256::digest(hash_input.as_bytes())[..8], - Span::call_site(), - ) -} diff --git a/libraries/discriminator/syn/src/parser.rs b/libraries/discriminator/syn/src/parser.rs deleted file mode 100644 index 1d70c3ab304..00000000000 --- a/libraries/discriminator/syn/src/parser.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Parser for the `syn` crate to parse the -//! `#[discriminator_hash_input("...")]` attribute - -use { - crate::error::SplDiscriminateError, - syn::{ - parse::{Parse, ParseStream}, - token::Comma, - Attribute, LitStr, - }, -}; - -/// Struct used for `syn` parsing of the hash_input attribute -/// #[discriminator_hash_input("...")] -struct HashInputValueParser { - value: LitStr, - _comma: Option, -} - -impl Parse for HashInputValueParser { - fn parse(input: ParseStream) -> syn::Result { - let value: LitStr = input.parse()?; - let _comma: Option = input.parse().unwrap_or(None); - Ok(HashInputValueParser { value, _comma }) - } -} - -/// Parses the hash_input from the `#[discriminator_hash_input("...")]` -/// attribute -pub fn parse_hash_input(attrs: &[Attribute]) -> Result { - match attrs - .iter() - .find(|a| a.path().is_ident("discriminator_hash_input")) - { - Some(attr) => { - let parsed_args = attr - .parse_args::() - .map_err(|_| SplDiscriminateError::HashInputAttributeParseError)?; - Ok(parsed_args.value.value()) - } - None => Err(SplDiscriminateError::HashInputAttributeNotProvided), - } -} diff --git a/libraries/math-example/tests/instruction_count.rs b/libraries/math-example/tests/instruction_count.rs index 93025ad86cd..a7dbd26aad2 100644 --- a/libraries/math-example/tests/instruction_count.rs +++ b/libraries/math-example/tests/instruction_count.rs @@ -10,7 +10,7 @@ use { #[tokio::test] async fn test_precise_sqrt_u64_max() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); // This is way too big! It's possible to dial down the numbers to get to // something reasonable, but the better option is to do everything in u64 @@ -28,7 +28,7 @@ async fn test_precise_sqrt_u64_max() { #[tokio::test] async fn test_precise_sqrt_u32_max() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(170_000); @@ -44,7 +44,7 @@ async fn test_precise_sqrt_u32_max() { #[tokio::test] async fn test_sqrt_u64() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); // Dial down the BPF compute budget to detect if the operation gets bloated in // the future @@ -60,7 +60,7 @@ async fn test_sqrt_u64() { #[tokio::test] async fn test_sqrt_u128() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); // Dial down the BPF compute budget to detect if the operation gets bloated in // the future @@ -78,7 +78,7 @@ async fn test_sqrt_u128() { #[tokio::test] async fn test_sqrt_u128_max() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(7_000); @@ -92,7 +92,7 @@ async fn test_sqrt_u128_max() { #[tokio::test] async fn test_u64_multiply() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(1350); @@ -106,7 +106,7 @@ async fn test_u64_multiply() { #[tokio::test] async fn test_u64_divide() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(1650); @@ -120,7 +120,7 @@ async fn test_u64_divide() { #[tokio::test] async fn test_f32_multiply() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(1600); @@ -136,7 +136,7 @@ async fn test_f32_multiply() { #[tokio::test] async fn test_f32_divide() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(1650); @@ -152,7 +152,7 @@ async fn test_f32_divide() { #[tokio::test] async fn test_f32_exponentiate() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(1400); @@ -168,7 +168,7 @@ async fn test_f32_exponentiate() { #[tokio::test] async fn test_f32_natural_log() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(3500); @@ -184,7 +184,7 @@ async fn test_f32_natural_log() { #[tokio::test] async fn test_f32_normal_cdf() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); // Dial down the BPF compute budget to detect if the operation gets bloated in // the future @@ -200,7 +200,7 @@ async fn test_f32_normal_cdf() { #[tokio::test] async fn test_f64_pow() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(30_000); @@ -216,7 +216,7 @@ async fn test_f64_pow() { #[tokio::test] async fn test_u128_multiply() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(10000); @@ -232,7 +232,7 @@ async fn test_u128_multiply() { #[tokio::test] async fn test_u128_divide() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(10000); @@ -248,7 +248,7 @@ async fn test_u128_divide() { #[tokio::test] async fn test_f64_multiply() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(10000); @@ -264,7 +264,7 @@ async fn test_f64_multiply() { #[tokio::test] async fn test_f64_divide() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(10000); @@ -280,7 +280,7 @@ async fn test_f64_divide() { #[tokio::test] async fn test_noop() { - let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction)); + let mut pc = ProgramTest::new("spl_math_example", id(), processor!(process_instruction)); pc.set_compute_max_units(1200); diff --git a/libraries/pod/Cargo.toml b/libraries/pod/Cargo.toml deleted file mode 100644 index ccfc8ba3b63..00000000000 --- a/libraries/pod/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "spl-pod" -version = "0.5.0" -description = "Solana Program Library Plain Old Data (Pod)" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -serde-traits = ["dep:serde"] -borsh = ["dep:borsh"] - -[dependencies] -borsh = { version = "1.5.3", optional = true } -bytemuck = { version = "1.21.0" } -bytemuck_derive = { version = "1.8.1" } -num-derive = "0.4" -num-traits = "0.2" -serde = { version = "1.0.217", optional = true } -solana-decode-error = "2.1.0" -solana-msg = "2.1.0" -solana-program-error = "2.1.0" -solana-program-option = "2.1.0" -solana-pubkey = "2.1.0" -solana-zk-sdk = "2.1.0" -thiserror = "2.0" - -[dev-dependencies] -serde_json = "1.0.135" -base64 = { version = "0.22.1" } - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/libraries/pod/README.md b/libraries/pod/README.md deleted file mode 100644 index 984718b5cbf..00000000000 --- a/libraries/pod/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Pod - -This library contains types shared by SPL libraries that implement the `Pod` trait from `bytemuck` and utils for working with these types. diff --git a/libraries/pod/src/bytemuck.rs b/libraries/pod/src/bytemuck.rs deleted file mode 100644 index f0039e351ed..00000000000 --- a/libraries/pod/src/bytemuck.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! wrappers for bytemuck functions - -use {bytemuck::Pod, solana_program_error::ProgramError}; - -/// On-chain size of a `Pod` type -pub const fn pod_get_packed_len() -> usize { - std::mem::size_of::() -} - -/// Convert a `Pod` into a slice of bytes (zero copy) -pub fn pod_bytes_of(t: &T) -> &[u8] { - bytemuck::bytes_of(t) -} - -/// Convert a slice of bytes into a `Pod` (zero copy) -pub fn pod_from_bytes(bytes: &[u8]) -> Result<&T, ProgramError> { - bytemuck::try_from_bytes(bytes).map_err(|_| ProgramError::InvalidArgument) -} - -/// Maybe convert a slice of bytes into a `Pod` (zero copy) -/// -/// Returns `None` if the slice is empty, or else `Err` if input length is not -/// equal to `pod_get_packed_len::()`. -/// This function exists primarily because `Option` is not a `Pod`. -pub fn pod_maybe_from_bytes(bytes: &[u8]) -> Result, ProgramError> { - if bytes.is_empty() { - Ok(None) - } else { - bytemuck::try_from_bytes(bytes) - .map(Some) - .map_err(|_| ProgramError::InvalidArgument) - } -} - -/// Convert a slice of bytes into a mutable `Pod` (zero copy) -pub fn pod_from_bytes_mut(bytes: &mut [u8]) -> Result<&mut T, ProgramError> { - bytemuck::try_from_bytes_mut(bytes).map_err(|_| ProgramError::InvalidArgument) -} - -/// Convert a slice of bytes into a `Pod` slice (zero copy) -pub fn pod_slice_from_bytes(bytes: &[u8]) -> Result<&[T], ProgramError> { - bytemuck::try_cast_slice(bytes).map_err(|_| ProgramError::InvalidArgument) -} - -/// Convert a slice of bytes into a mutable `Pod` slice (zero copy) -pub fn pod_slice_from_bytes_mut(bytes: &mut [u8]) -> Result<&mut [T], ProgramError> { - bytemuck::try_cast_slice_mut(bytes).map_err(|_| ProgramError::InvalidArgument) -} - -/// Convert a `Pod` slice into a single slice of bytes -pub fn pod_slice_to_bytes(slice: &[T]) -> &[u8] { - bytemuck::cast_slice(slice) -} diff --git a/libraries/pod/src/error.rs b/libraries/pod/src/error.rs deleted file mode 100644 index f67f4eabccf..00000000000 --- a/libraries/pod/src/error.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Error types -use { - solana_decode_error::DecodeError, - solana_msg::msg, - solana_program_error::{PrintProgramError, ProgramError}, -}; - -/// Errors that may be returned by the spl-pod library. -#[repr(u32)] -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, num_derive::FromPrimitive)] -pub enum PodSliceError { - /// Error in checked math operation - #[error("Error in checked math operation")] - CalculationFailure, - /// Provided byte buffer too small for expected type - #[error("Provided byte buffer too small for expected type")] - BufferTooSmall, - /// Provided byte buffer too large for expected type - #[error("Provided byte buffer too large for expected type")] - BufferTooLarge, -} - -impl From for ProgramError { - fn from(e: PodSliceError) -> Self { - ProgramError::Custom(e as u32) - } -} - -impl solana_decode_error::DecodeError for PodSliceError { - fn type_of() -> &'static str { - "PodSliceError" - } -} - -impl PrintProgramError for PodSliceError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - PodSliceError::CalculationFailure => { - msg!("Error in checked math operation") - } - PodSliceError::BufferTooSmall => { - msg!("Provided byte buffer too small for expected type") - } - PodSliceError::BufferTooLarge => { - msg!("Provided byte buffer too large for expected type") - } - } - } -} diff --git a/libraries/pod/src/lib.rs b/libraries/pod/src/lib.rs deleted file mode 100644 index 5be44e71571..00000000000 --- a/libraries/pod/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Crate containing `Pod` types and `bytemuck` utils used in SPL - -pub mod bytemuck; -pub mod error; -pub mod option; -pub mod optional_keys; -pub mod primitives; -pub mod slice; - -// Export current sdk types for downstream users building with a different sdk -// version -pub use { - solana_decode_error, solana_msg, solana_program_error, solana_program_option, solana_pubkey, -}; diff --git a/libraries/pod/src/option.rs b/libraries/pod/src/option.rs deleted file mode 100644 index 02d7edd0ef1..00000000000 --- a/libraries/pod/src/option.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Generic `Option` that can be used as a `Pod` for types that can have -//! a designated `None` value. -//! -//! For example, a 64-bit unsigned integer can designate `0` as a `None` value. -//! This would be equivalent to -//! [`Option`](https://doc.rust-lang.org/std/num/type.NonZeroU64.html) -//! and provide the same memory layout optimization. - -use { - bytemuck::{Pod, Zeroable}, - solana_program_error::ProgramError, - solana_program_option::COption, - solana_pubkey::{Pubkey, PUBKEY_BYTES}, -}; - -/// Trait for types that can be `None`. -/// -/// This trait is used to indicate that a type can be `None` according to a -/// specific value. -pub trait Nullable: PartialEq + Pod + Sized { - /// Value that represents `None` for the type. - const NONE: Self; - - /// Indicates whether the value is `None` or not. - fn is_none(&self) -> bool { - self == &Self::NONE - } - - /// Indicates whether the value is `Some`` value of type `T`` or not. - fn is_some(&self) -> bool { - !self.is_none() - } -} - -/// A "pod-enabled" type that can be used as an `Option` without -/// requiring extra space to indicate if the value is `Some` or `None`. -/// -/// This can be used when a specific value of `T` indicates that its -/// value is `None`. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct PodOption(T); - -impl Default for PodOption { - fn default() -> Self { - Self(T::NONE) - } -} - -impl PodOption { - /// Returns the contained value as an `Option`. - #[inline] - pub fn get(self) -> Option { - if self.0.is_none() { - None - } else { - Some(self.0) - } - } - - /// Returns the contained value as an `Option`. - #[inline] - pub fn as_ref(&self) -> Option<&T> { - if self.0.is_none() { - None - } else { - Some(&self.0) - } - } - - /// Returns the contained value as a mutable `Option`. - #[inline] - pub fn as_mut(&mut self) -> Option<&mut T> { - if self.0.is_none() { - None - } else { - Some(&mut self.0) - } - } -} - -/// ## Safety -/// -/// `PodOption` is a transparent wrapper around a `Pod` type `T` with identical -/// data representation. -unsafe impl Pod for PodOption {} - -/// ## Safety -/// -/// `PodOption` is a transparent wrapper around a `Pod` type `T` with identical -/// data representation. -unsafe impl Zeroable for PodOption {} - -impl From for PodOption { - fn from(value: T) -> Self { - PodOption(value) - } -} - -impl TryFrom> for PodOption { - type Error = ProgramError; - - fn try_from(value: Option) -> Result { - match value { - Some(value) if value.is_none() => Err(ProgramError::InvalidArgument), - Some(value) => Ok(PodOption(value)), - None => Ok(PodOption(T::NONE)), - } - } -} - -impl TryFrom> for PodOption { - type Error = ProgramError; - - fn try_from(value: COption) -> Result { - match value { - COption::Some(value) if value.is_none() => Err(ProgramError::InvalidArgument), - COption::Some(value) => Ok(PodOption(value)), - COption::None => Ok(PodOption(T::NONE)), - } - } -} - -/// Implementation of `Nullable` for `Pubkey`. -impl Nullable for Pubkey { - const NONE: Self = Pubkey::new_from_array([0u8; PUBKEY_BYTES]); -} - -#[cfg(test)] -mod tests { - use {super::*, crate::bytemuck::pod_slice_from_bytes}; - const ID: Pubkey = Pubkey::from_str_const("TestSysvar111111111111111111111111111111111"); - - #[test] - fn test_pod_option_pubkey() { - let some_pubkey = PodOption::from(ID); - assert_eq!(some_pubkey.get(), Some(ID)); - - let none_pubkey = PodOption::from(Pubkey::default()); - assert_eq!(none_pubkey.get(), None); - - let mut data = Vec::with_capacity(64); - data.extend_from_slice(ID.as_ref()); - data.extend_from_slice(&[0u8; 32]); - - let values = pod_slice_from_bytes::>(&data).unwrap(); - assert_eq!(values[0], PodOption::from(ID)); - assert_eq!(values[1], PodOption::from(Pubkey::default())); - - let option_pubkey = Some(ID); - let pod_option_pubkey: PodOption = option_pubkey.try_into().unwrap(); - assert_eq!(pod_option_pubkey, PodOption::from(ID)); - assert_eq!( - pod_option_pubkey, - PodOption::try_from(option_pubkey).unwrap() - ); - - let coption_pubkey = COption::Some(ID); - let pod_option_pubkey: PodOption = coption_pubkey.try_into().unwrap(); - assert_eq!(pod_option_pubkey, PodOption::from(ID)); - assert_eq!( - pod_option_pubkey, - PodOption::try_from(coption_pubkey).unwrap() - ); - } - - #[test] - fn test_try_from_option() { - let some_pubkey = Some(ID); - assert_eq!(PodOption::try_from(some_pubkey).unwrap(), PodOption(ID)); - - let none_pubkey = None; - assert_eq!( - PodOption::try_from(none_pubkey).unwrap(), - PodOption::from(Pubkey::NONE) - ); - - let invalid_option = Some(Pubkey::NONE); - let err = PodOption::try_from(invalid_option).unwrap_err(); - assert_eq!(err, ProgramError::InvalidArgument); - } - - #[test] - fn test_default() { - let def = PodOption::::default(); - assert_eq!(def, None.try_into().unwrap()); - } -} diff --git a/libraries/pod/src/optional_keys.rs b/libraries/pod/src/optional_keys.rs deleted file mode 100644 index 501287f9992..00000000000 --- a/libraries/pod/src/optional_keys.rs +++ /dev/null @@ -1,360 +0,0 @@ -//! Optional pubkeys that can be used a `Pod`s -#[cfg(feature = "borsh")] -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use { - bytemuck_derive::{Pod, Zeroable}, - solana_program_error::ProgramError, - solana_program_option::COption, - solana_pubkey::Pubkey, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, -}; -#[cfg(feature = "serde-traits")] -use { - serde::de::{Error, Unexpected, Visitor}, - serde::{Deserialize, Deserializer, Serialize, Serializer}, - std::{convert::TryFrom, fmt, str::FromStr}, -}; - -/// A Pubkey that encodes `None` as all `0`, meant to be usable as a Pod type, -/// similar to all NonZero* number types from the bytemuck library. -#[cfg_attr( - feature = "borsh", - derive(BorshDeserialize, BorshSerialize, BorshSchema) -)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct OptionalNonZeroPubkey(pub Pubkey); -impl TryFrom> for OptionalNonZeroPubkey { - type Error = ProgramError; - fn try_from(p: Option) -> Result { - match p { - None => Ok(Self(Pubkey::default())), - Some(pubkey) => { - if pubkey == Pubkey::default() { - Err(ProgramError::InvalidArgument) - } else { - Ok(Self(pubkey)) - } - } - } - } -} -impl TryFrom> for OptionalNonZeroPubkey { - type Error = ProgramError; - fn try_from(p: COption) -> Result { - match p { - COption::None => Ok(Self(Pubkey::default())), - COption::Some(pubkey) => { - if pubkey == Pubkey::default() { - Err(ProgramError::InvalidArgument) - } else { - Ok(Self(pubkey)) - } - } - } - } -} -impl From for Option { - fn from(p: OptionalNonZeroPubkey) -> Self { - if p.0 == Pubkey::default() { - None - } else { - Some(p.0) - } - } -} -impl From for COption { - fn from(p: OptionalNonZeroPubkey) -> Self { - if p.0 == Pubkey::default() { - COption::None - } else { - COption::Some(p.0) - } - } -} - -#[cfg(feature = "serde-traits")] -impl Serialize for OptionalNonZeroPubkey { - fn serialize(&self, s: S) -> Result - where - S: Serializer, - { - if self.0 == Pubkey::default() { - s.serialize_none() - } else { - s.serialize_some(&self.0.to_string()) - } - } -} - -#[cfg(feature = "serde-traits")] -/// Visitor for deserializing OptionalNonZeroPubkey -struct OptionalNonZeroPubkeyVisitor; - -#[cfg(feature = "serde-traits")] -impl<'de> Visitor<'de> for OptionalNonZeroPubkeyVisitor { - type Value = OptionalNonZeroPubkey; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a Pubkey in base58 or `null`") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - let pkey = Pubkey::from_str(v) - .map_err(|_| Error::invalid_value(Unexpected::Str(v), &"value string"))?; - - OptionalNonZeroPubkey::try_from(Some(pkey)) - .map_err(|_| Error::custom("Failed to convert from pubkey")) - } - - fn visit_unit(self) -> Result - where - E: Error, - { - OptionalNonZeroPubkey::try_from(None).map_err(|e| Error::custom(e.to_string())) - } -} - -#[cfg(feature = "serde-traits")] -impl<'de> Deserialize<'de> for OptionalNonZeroPubkey { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(OptionalNonZeroPubkeyVisitor) - } -} - -/// An ElGamalPubkey that encodes `None` as all `0`, meant to be usable as a Pod -/// type. -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct OptionalNonZeroElGamalPubkey(PodElGamalPubkey); -impl OptionalNonZeroElGamalPubkey { - /// Checks equality between an OptionalNonZeroElGamalPubkey and an - /// ElGamalPubkey when interpreted as bytes. - pub fn equals(&self, other: &PodElGamalPubkey) -> bool { - &self.0 == other - } -} -impl TryFrom> for OptionalNonZeroElGamalPubkey { - type Error = ProgramError; - fn try_from(p: Option) -> Result { - match p { - None => Ok(Self(PodElGamalPubkey::default())), - Some(elgamal_pubkey) => { - if elgamal_pubkey == PodElGamalPubkey::default() { - Err(ProgramError::InvalidArgument) - } else { - Ok(Self(elgamal_pubkey)) - } - } - } - } -} -impl From for Option { - fn from(p: OptionalNonZeroElGamalPubkey) -> Self { - if p.0 == PodElGamalPubkey::default() { - None - } else { - Some(p.0) - } - } -} - -#[cfg(feature = "serde-traits")] -impl Serialize for OptionalNonZeroElGamalPubkey { - fn serialize(&self, s: S) -> Result - where - S: Serializer, - { - if self.0 == PodElGamalPubkey::default() { - s.serialize_none() - } else { - s.serialize_some(&self.0.to_string()) - } - } -} - -#[cfg(feature = "serde-traits")] -struct OptionalNonZeroElGamalPubkeyVisitor; - -#[cfg(feature = "serde-traits")] -impl<'de> Visitor<'de> for OptionalNonZeroElGamalPubkeyVisitor { - type Value = OptionalNonZeroElGamalPubkey; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an ElGamal public key as base64 or `null`") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - let elgamal_pubkey: PodElGamalPubkey = FromStr::from_str(v).map_err(Error::custom)?; - OptionalNonZeroElGamalPubkey::try_from(Some(elgamal_pubkey)).map_err(Error::custom) - } - - fn visit_unit(self) -> Result - where - E: Error, - { - Ok(OptionalNonZeroElGamalPubkey::default()) - } -} - -#[cfg(feature = "serde-traits")] -impl<'de> Deserialize<'de> for OptionalNonZeroElGamalPubkey { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(OptionalNonZeroElGamalPubkeyVisitor) - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::bytemuck::pod_from_bytes, - base64::{prelude::BASE64_STANDARD, Engine}, - solana_pubkey::PUBKEY_BYTES, - }; - - #[test] - fn test_pod_non_zero_option() { - assert_eq!( - Some(Pubkey::new_from_array([1; PUBKEY_BYTES])), - Option::::from( - *pod_from_bytes::(&[1; PUBKEY_BYTES]).unwrap() - ) - ); - assert_eq!( - None, - Option::::from( - *pod_from_bytes::(&[0; PUBKEY_BYTES]).unwrap() - ) - ); - assert_eq!( - pod_from_bytes::(&[]).unwrap_err(), - ProgramError::InvalidArgument - ); - assert_eq!( - pod_from_bytes::(&[0; 1]).unwrap_err(), - ProgramError::InvalidArgument - ); - assert_eq!( - pod_from_bytes::(&[1; 1]).unwrap_err(), - ProgramError::InvalidArgument - ); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_non_zero_option_serde_some() { - let optional_non_zero_pubkey_some = - OptionalNonZeroPubkey(Pubkey::new_from_array([1; PUBKEY_BYTES])); - let serialized_some = serde_json::to_string(&optional_non_zero_pubkey_some).unwrap(); - assert_eq!( - &serialized_some, - "\"4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi\"" - ); - - let deserialized_some = - serde_json::from_str::(&serialized_some).unwrap(); - assert_eq!(optional_non_zero_pubkey_some, deserialized_some); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_non_zero_option_serde_none() { - let optional_non_zero_pubkey_none = - OptionalNonZeroPubkey(Pubkey::new_from_array([0; PUBKEY_BYTES])); - let serialized_none = serde_json::to_string(&optional_non_zero_pubkey_none).unwrap(); - assert_eq!(&serialized_none, "null"); - - let deserialized_none = - serde_json::from_str::(&serialized_none).unwrap(); - assert_eq!(optional_non_zero_pubkey_none, deserialized_none); - } - - const OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN: usize = 32; - - // Unfortunately, the `solana-zk-sdk` does not expose a constructor interface - // to construct `PodRistrettoPoint` from bytes. As a work-around, encode the - // bytes as base64 string and then convert the string to a - // `PodElGamalCiphertext`. - // - // The constructor will be added (and this function removed) with - // `solana-zk-sdk` 2.1. - fn elgamal_pubkey_from_bytes(bytes: &[u8]) -> PodElGamalPubkey { - let string = BASE64_STANDARD.encode(bytes); - std::str::FromStr::from_str(&string).unwrap() - } - - #[test] - fn test_pod_non_zero_elgamal_option() { - assert_eq!( - Some(elgamal_pubkey_from_bytes( - &[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN] - )), - Option::::from(OptionalNonZeroElGamalPubkey( - elgamal_pubkey_from_bytes(&[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]) - )) - ); - assert_eq!( - None, - Option::::from(OptionalNonZeroElGamalPubkey( - elgamal_pubkey_from_bytes(&[0; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]) - )) - ); - - assert_eq!( - OptionalNonZeroElGamalPubkey(elgamal_pubkey_from_bytes( - &[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN] - )), - *pod_from_bytes::( - &[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN] - ) - .unwrap() - ); - assert!(pod_from_bytes::(&[]).is_err()); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_non_zero_elgamal_option_serde_some() { - let optional_non_zero_elgamal_pubkey_some = OptionalNonZeroElGamalPubkey( - elgamal_pubkey_from_bytes(&[1; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]), - ); - let serialized_some = - serde_json::to_string(&optional_non_zero_elgamal_pubkey_some).unwrap(); - assert_eq!( - &serialized_some, - "\"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=\"" - ); - - let deserialized_some = - serde_json::from_str::(&serialized_some).unwrap(); - assert_eq!(optional_non_zero_elgamal_pubkey_some, deserialized_some); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_non_zero_elgamal_option_serde_none() { - let optional_non_zero_elgamal_pubkey_none = OptionalNonZeroElGamalPubkey( - elgamal_pubkey_from_bytes(&[0; OPTIONAL_NONZERO_ELGAMAL_PUBKEY_LEN]), - ); - let serialized_none = - serde_json::to_string(&optional_non_zero_elgamal_pubkey_none).unwrap(); - assert_eq!(&serialized_none, "null"); - - let deserialized_none = - serde_json::from_str::(&serialized_none).unwrap(); - assert_eq!(optional_non_zero_elgamal_pubkey_none, deserialized_none); - } -} diff --git a/libraries/pod/src/primitives.rs b/libraries/pod/src/primitives.rs deleted file mode 100644 index f6759cf5f32..00000000000 --- a/libraries/pod/src/primitives.rs +++ /dev/null @@ -1,269 +0,0 @@ -//! primitive types that can be used in `Pod`s -#[cfg(feature = "borsh")] -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use bytemuck_derive::{Pod, Zeroable}; -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; - -/// The standard `bool` is not a `Pod`, define a replacement that is -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(from = "bool", into = "bool"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodBool(pub u8); -impl PodBool { - pub const fn from_bool(b: bool) -> Self { - Self(if b { 1 } else { 0 }) - } -} - -impl From for PodBool { - fn from(b: bool) -> Self { - Self::from_bool(b) - } -} - -impl From<&bool> for PodBool { - fn from(b: &bool) -> Self { - Self(if *b { 1 } else { 0 }) - } -} - -impl From<&PodBool> for bool { - fn from(b: &PodBool) -> Self { - b.0 != 0 - } -} - -impl From for bool { - fn from(b: PodBool) -> Self { - b.0 != 0 - } -} - -/// Simple macro for implementing conversion functions between Pod* ints and -/// standard ints. -/// -/// The standard int types can cause alignment issues when placed in a `Pod`, -/// so these replacements are usable in all `Pod`s. -#[macro_export] -macro_rules! impl_int_conversion { - ($P:ty, $I:ty) => { - impl $P { - pub const fn from_primitive(n: $I) -> Self { - Self(n.to_le_bytes()) - } - } - impl From<$I> for $P { - fn from(n: $I) -> Self { - Self::from_primitive(n) - } - } - impl From<$P> for $I { - fn from(pod: $P) -> Self { - Self::from_le_bytes(pod.0) - } - } - }; -} - -/// `u16` type that can be used in `Pod`s -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(from = "u16", into = "u16"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodU16(pub [u8; 2]); -impl_int_conversion!(PodU16, u16); - -/// `i16` type that can be used in Pods -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(from = "i16", into = "i16"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodI16(pub [u8; 2]); -impl_int_conversion!(PodI16, i16); - -/// `u32` type that can be used in `Pod`s -#[cfg_attr( - feature = "borsh", - derive(BorshDeserialize, BorshSerialize, BorshSchema) -)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(from = "u32", into = "u32"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodU32(pub [u8; 4]); -impl_int_conversion!(PodU32, u32); - -/// `u64` type that can be used in Pods -#[cfg_attr( - feature = "borsh", - derive(BorshDeserialize, BorshSerialize, BorshSchema) -)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(from = "u64", into = "u64"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodU64(pub [u8; 8]); -impl_int_conversion!(PodU64, u64); - -/// `i64` type that can be used in Pods -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(from = "i64", into = "i64"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodI64([u8; 8]); -impl_int_conversion!(PodI64, i64); - -/// `u128` type that can be used in Pods -#[cfg_attr( - feature = "borsh", - derive(BorshDeserialize, BorshSerialize, BorshSchema) -)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(from = "u128", into = "u128"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodU128(pub [u8; 16]); -impl_int_conversion!(PodU128, u128); - -#[cfg(test)] -mod tests { - use {super::*, crate::bytemuck::pod_from_bytes}; - - #[test] - fn test_pod_bool() { - assert!(pod_from_bytes::(&[]).is_err()); - assert!(pod_from_bytes::(&[0, 0]).is_err()); - - for i in 0..=u8::MAX { - assert_eq!(i != 0, bool::from(pod_from_bytes::(&[i]).unwrap())); - } - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_bool_serde() { - let pod_false: PodBool = false.into(); - let pod_true: PodBool = true.into(); - - let serialized_false = serde_json::to_string(&pod_false).unwrap(); - let serialized_true = serde_json::to_string(&pod_true).unwrap(); - assert_eq!(&serialized_false, "false"); - assert_eq!(&serialized_true, "true"); - - let deserialized_false = serde_json::from_str::(&serialized_false).unwrap(); - let deserialized_true = serde_json::from_str::(&serialized_true).unwrap(); - assert_eq!(pod_false, deserialized_false); - assert_eq!(pod_true, deserialized_true); - } - - #[test] - fn test_pod_u16() { - assert!(pod_from_bytes::(&[]).is_err()); - assert_eq!(1u16, u16::from(*pod_from_bytes::(&[1, 0]).unwrap())); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_u16_serde() { - let pod_u16: PodU16 = u16::MAX.into(); - - let serialized = serde_json::to_string(&pod_u16).unwrap(); - assert_eq!(&serialized, "65535"); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(pod_u16, deserialized); - } - - #[test] - fn test_pod_i16() { - assert!(pod_from_bytes::(&[]).is_err()); - assert_eq!( - -1i16, - i16::from(*pod_from_bytes::(&[255, 255]).unwrap()) - ); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_i16_serde() { - let pod_i16: PodI16 = i16::MAX.into(); - - println!("pod_i16 {:?}", pod_i16); - - let serialized = serde_json::to_string(&pod_i16).unwrap(); - assert_eq!(&serialized, "32767"); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(pod_i16, deserialized); - } - - #[test] - fn test_pod_u64() { - assert!(pod_from_bytes::(&[]).is_err()); - assert_eq!( - 1u64, - u64::from(*pod_from_bytes::(&[1, 0, 0, 0, 0, 0, 0, 0]).unwrap()) - ); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_u64_serde() { - let pod_u64: PodU64 = u64::MAX.into(); - - let serialized = serde_json::to_string(&pod_u64).unwrap(); - assert_eq!(&serialized, "18446744073709551615"); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(pod_u64, deserialized); - } - - #[test] - fn test_pod_i64() { - assert!(pod_from_bytes::(&[]).is_err()); - assert_eq!( - -1i64, - i64::from( - *pod_from_bytes::(&[255, 255, 255, 255, 255, 255, 255, 255]).unwrap() - ) - ); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_i64_serde() { - let pod_i64: PodI64 = i64::MAX.into(); - - let serialized = serde_json::to_string(&pod_i64).unwrap(); - assert_eq!(&serialized, "9223372036854775807"); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(pod_i64, deserialized); - } - - #[test] - fn test_pod_u128() { - assert!(pod_from_bytes::(&[]).is_err()); - assert_eq!( - 1u128, - u128::from( - *pod_from_bytes::(&[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - .unwrap() - ) - ); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn test_pod_u128_serde() { - let pod_u128: PodU128 = u128::MAX.into(); - - let serialized = serde_json::to_string(&pod_u128).unwrap(); - assert_eq!(&serialized, "340282366920938463463374607431768211455"); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(pod_u128, deserialized); - } -} diff --git a/libraries/pod/src/slice.rs b/libraries/pod/src/slice.rs deleted file mode 100644 index 043f408d1f7..00000000000 --- a/libraries/pod/src/slice.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! Special types for working with slices of `Pod`s - -use { - crate::{ - bytemuck::{ - pod_from_bytes, pod_from_bytes_mut, pod_slice_from_bytes, pod_slice_from_bytes_mut, - }, - error::PodSliceError, - primitives::PodU32, - }, - bytemuck::Pod, - solana_program_error::ProgramError, -}; - -const LENGTH_SIZE: usize = std::mem::size_of::(); -/// Special type for using a slice of `Pod`s in a zero-copy way -pub struct PodSlice<'data, T: Pod> { - length: &'data PodU32, - data: &'data [T], -} -impl<'data, T: Pod> PodSlice<'data, T> { - /// Unpack the buffer into a slice - pub fn unpack<'a>(data: &'a [u8]) -> Result - where - 'a: 'data, - { - if data.len() < LENGTH_SIZE { - return Err(PodSliceError::BufferTooSmall.into()); - } - let (length, data) = data.split_at(LENGTH_SIZE); - let length = pod_from_bytes::(length)?; - let _max_length = max_len_for_type::(data.len())?; - let data = pod_slice_from_bytes(data)?; - Ok(Self { length, data }) - } - - /// Get the slice data - pub fn data(&self) -> &[T] { - let length = u32::from(*self.length) as usize; - &self.data[..length] - } - - /// Get the amount of bytes used by `num_items` - pub fn size_of(num_items: usize) -> Result { - std::mem::size_of::() - .checked_mul(num_items) - .and_then(|len| len.checked_add(LENGTH_SIZE)) - .ok_or_else(|| PodSliceError::CalculationFailure.into()) - } -} - -/// Special type for using a slice of mutable `Pod`s in a zero-copy way -pub struct PodSliceMut<'data, T: Pod> { - length: &'data mut PodU32, - data: &'data mut [T], - max_length: usize, -} -impl<'data, T: Pod> PodSliceMut<'data, T> { - /// Unpack the mutable buffer into a mutable slice, with the option to - /// initialize the data - fn unpack_internal<'a>(data: &'a mut [u8], init: bool) -> Result - where - 'a: 'data, - { - if data.len() < LENGTH_SIZE { - return Err(PodSliceError::BufferTooSmall.into()); - } - let (length, data) = data.split_at_mut(LENGTH_SIZE); - let length = pod_from_bytes_mut::(length)?; - if init { - *length = 0.into(); - } - let max_length = max_len_for_type::(data.len())?; - let data = pod_slice_from_bytes_mut(data)?; - Ok(Self { - length, - data, - max_length, - }) - } - - /// Unpack the mutable buffer into a mutable slice - pub fn unpack<'a>(data: &'a mut [u8]) -> Result - where - 'a: 'data, - { - Self::unpack_internal(data, /* init */ false) - } - - /// Unpack the mutable buffer into a mutable slice, and initialize the - /// slice to 0-length - pub fn init<'a>(data: &'a mut [u8]) -> Result - where - 'a: 'data, - { - Self::unpack_internal(data, /* init */ true) - } - - /// Add another item to the slice - pub fn push(&mut self, t: T) -> Result<(), ProgramError> { - let length = u32::from(*self.length); - if length as usize == self.max_length { - Err(PodSliceError::BufferTooSmall.into()) - } else { - self.data[length as usize] = t; - *self.length = length.saturating_add(1).into(); - Ok(()) - } - } -} - -fn max_len_for_type(data_len: usize) -> Result { - let size: usize = std::mem::size_of::(); - let max_len = data_len - .checked_div(size) - .ok_or(PodSliceError::CalculationFailure)?; - // check that it isn't over or under allocated - if max_len.saturating_mul(size) != data_len { - if max_len == 0 { - // Size of T is greater than buffer size - Err(PodSliceError::BufferTooSmall.into()) - } else { - Err(PodSliceError::BufferTooLarge.into()) - } - } else { - Ok(max_len) - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::bytemuck::pod_slice_to_bytes, - bytemuck_derive::{Pod, Zeroable}, - }; - - #[repr(C)] - #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] - struct TestStruct { - test_field: u8, - test_pubkey: [u8; 32], - } - - #[test] - fn test_pod_slice() { - let test_field_bytes = [0]; - let test_pubkey_bytes = [1; 32]; - let len_bytes = [2, 0, 0, 0]; - - // Slice will contain 2 `TestStruct` - let mut data_bytes = [0; 66]; - data_bytes[0..1].copy_from_slice(&test_field_bytes); - data_bytes[1..33].copy_from_slice(&test_pubkey_bytes); - data_bytes[33..34].copy_from_slice(&test_field_bytes); - data_bytes[34..66].copy_from_slice(&test_pubkey_bytes); - - let mut pod_slice_bytes = [0; 70]; - pod_slice_bytes[0..4].copy_from_slice(&len_bytes); - pod_slice_bytes[4..70].copy_from_slice(&data_bytes); - - let pod_slice = PodSlice::::unpack(&pod_slice_bytes).unwrap(); - let pod_slice_data = pod_slice.data(); - - assert_eq!(*pod_slice.length, PodU32::from(2)); - assert_eq!(pod_slice_to_bytes(pod_slice.data()), data_bytes); - assert_eq!(pod_slice_data[0].test_field, test_field_bytes[0]); - assert_eq!(pod_slice_data[0].test_pubkey, test_pubkey_bytes); - assert_eq!(PodSlice::::size_of(1).unwrap(), 37); - } - - #[test] - fn test_pod_slice_buffer_too_large() { - // 1 `TestStruct` + length = 37 bytes - // we pass 38 to trigger BufferTooLarge - let pod_slice_bytes = [1; 38]; - let err = PodSlice::::unpack(&pod_slice_bytes) - .err() - .unwrap(); - assert_eq!( - err, - PodSliceError::BufferTooLarge.into(), - "Expected an `PodSliceError::BufferTooLarge` error" - ); - } - - #[test] - fn test_pod_slice_buffer_too_small() { - // 1 `TestStruct` + length = 37 bytes - // we pass 36 to trigger BufferTooSmall - let pod_slice_bytes = [1; 36]; - let err = PodSlice::::unpack(&pod_slice_bytes) - .err() - .unwrap(); - assert_eq!( - err, - PodSliceError::BufferTooSmall.into(), - "Expected an `PodSliceError::BufferTooSmall` error" - ); - } - - #[test] - fn test_pod_slice_mut() { - // slice can fit 2 `TestStruct` - let mut pod_slice_bytes = [0; 70]; - // set length to 1, so we have room to push 1 more item - let len_bytes = [1, 0, 0, 0]; - pod_slice_bytes[0..4].copy_from_slice(&len_bytes); - - let mut pod_slice = PodSliceMut::::unpack(&mut pod_slice_bytes).unwrap(); - - assert_eq!(*pod_slice.length, PodU32::from(1)); - pod_slice.push(TestStruct::default()).unwrap(); - assert_eq!(*pod_slice.length, PodU32::from(2)); - let err = pod_slice - .push(TestStruct::default()) - .expect_err("Expected an `PodSliceError::BufferTooSmall` error"); - assert_eq!(err, PodSliceError::BufferTooSmall.into()); - } -} diff --git a/libraries/program-error/Cargo.toml b/libraries/program-error/Cargo.toml deleted file mode 100644 index 352655dc310..00000000000 --- a/libraries/program-error/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "spl-program-error" -version = "0.6.0" -description = "Library for Solana Program error attributes and derive macro for creating them" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -num-derive = "0.4" -num-traits = "0.2" -solana-program = "2.1.0" -spl-program-error-derive = { version = "0.4.1", path = "./derive" } -thiserror = "2.0" - -[dev-dependencies] -lazy_static = "1.5" -serial_test = "3.2" -solana-sdk = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/libraries/program-error/README.md b/libraries/program-error/README.md deleted file mode 100644 index 5ada5934460..00000000000 --- a/libraries/program-error/README.md +++ /dev/null @@ -1,293 +0,0 @@ -# SPL Program Error - -Macros for implementing error-based traits on enums. - -- `#[derive(IntoProgramError)]`: automatically derives the trait `From for solana_program::program_error::ProgramError`. -- `#[derive(DecodeError)]`: automatically derives the trait `solana_program::decode_error::DecodeError`. -- `#[derive(PrintProgramError)]`: automatically derives the trait `solana_program::program_error::PrintProgramError`. -- `#[spl_program_error]`: Automatically derives all below traits: - - `Clone` - - `Debug` - - `Eq` - - `DecodeError` - - `IntoProgramError` - - `PrintProgramError` - - `thiserror::Error` - - `num_derive::FromPrimitive` - - `PartialEq` - -### `#[derive(IntoProgramError)]` - -This derive macro automatically derives the trait `From for solana_program::program_error::ProgramError`. - -Your enum must implement the following traits in order for this macro to work: - -- `Clone` -- `Debug` -- `Eq` -- `thiserror::Error` -- `num_derive::FromPrimitive` -- `PartialEq` - -Sample code: - -```rust -/// Example error -#[derive( - Clone, Debug, Eq, IntoProgramError, thiserror::Error, num_derive::FromPrimitive, PartialEq, -)] -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} -``` - -### `#[derive(DecodeError)]` - -This derive macro automatically derives the trait `solana_program::decode_error::DecodeError`. - -Your enum must implement the following traits in order for this macro to work: - -- `Clone` -- `Debug` -- `Eq` -- `IntoProgramError` (above) -- `thiserror::Error` -- `num_derive::FromPrimitive` -- `PartialEq` - -Sample code: - -```rust -/// Example error -#[derive( - Clone, - Debug, - DecodeError, - Eq, - IntoProgramError, - thiserror::Error, - num_derive::FromPrimitive, - PartialEq, -)] -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} -``` - -### `#[derive(PrintProgramError)]` - -This derive macro automatically derives the trait `solana_program::program_error::PrintProgramError`. - -Your enum must implement the following traits in order for this macro to work: - -- `Clone` -- `Debug` -- `DecodeError` (above) -- `Eq` -- `IntoProgramError` (above) -- `thiserror::Error` -- `num_derive::FromPrimitive` -- `PartialEq` - -Sample code: - -```rust -/// Example error -#[derive( - Clone, - Debug, - DecodeError, - Eq, - IntoProgramError, - thiserror::Error, - num_derive::FromPrimitive, - PartialEq, -)] -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} -``` - -### `#[spl_program_error]` - -It can be cumbersome to ensure your program's defined errors - typically represented -in an enum - implement the required traits and will print to the program's logs when they're -invoked. - -This procedural macro will give you all of the required implementations out of the box: - -- `Clone` -- `Debug` -- `Eq` -- `thiserror::Error` -- `num_derive::FromPrimitive` -- `PartialEq` - -It also imports the required crates so you don't have to in your program: - -- `num_derive` -- `num_traits` -- `thiserror` - ---- - -Just annotate your enum... - -```rust -use solana_program_error_derive::*; - -/// Example error -#[solana_program_error] -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} -``` - -...and get: - -```rust -/// Example error -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} -#[automatically_derived] -impl ::core::clone::Clone for ExampleError { - #[inline] - fn clone(&self) -> ExampleError { - match self { - ExampleError::MintHasNoMintAuthority => ExampleError::MintHasNoMintAuthority, - ExampleError::IncorrectMintAuthority => ExampleError::IncorrectMintAuthority, - } - } -} -#[automatically_derived] -impl ::core::fmt::Debug for ExampleError { - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::write_str( - f, - match self { - ExampleError::MintHasNoMintAuthority => "MintHasNoMintAuthority", - ExampleError::IncorrectMintAuthority => "IncorrectMintAuthority", - }, - ) - } -} -#[automatically_derived] -impl ::core::marker::StructuralEq for ExampleError {} -#[automatically_derived] -impl ::core::cmp::Eq for ExampleError { - #[inline] - #[doc(hidden)] - #[no_coverage] - fn assert_receiver_is_total_eq(&self) -> () {} -} -#[allow(unused_qualifications)] -impl std::error::Error for ExampleError {} -#[allow(unused_qualifications)] -impl std::fmt::Display for ExampleError { - fn fmt(&self, __formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - #[allow(unused_variables, deprecated, clippy::used_underscore_binding)] - match self { - ExampleError::MintHasNoMintAuthority {} => { - __formatter.write_fmt(format_args!("Mint has no mint authority")) - } - ExampleError::IncorrectMintAuthority {} => { - __formatter - .write_fmt( - format_args!( - "Incorrect mint authority has signed the instruction" - ), - ) - } - } - } -} -#[allow(non_upper_case_globals, unused_qualifications)] -const _IMPL_NUM_FromPrimitive_FOR_ExampleError: () = { - #[allow(clippy::useless_attribute)] - #[allow(rust_2018_idioms)] - extern crate num_traits as _num_traits; - impl _num_traits::FromPrimitive for ExampleError { - #[allow(trivial_numeric_casts)] - #[inline] - fn from_i64(n: i64) -> Option { - if n == ExampleError::MintHasNoMintAuthority as i64 { - Some(ExampleError::MintHasNoMintAuthority) - } else if n == ExampleError::IncorrectMintAuthority as i64 { - Some(ExampleError::IncorrectMintAuthority) - } else { - None - } - } - #[inline] - fn from_u64(n: u64) -> Option { - Self::from_i64(n as i64) - } - } -}; -#[automatically_derived] -impl ::core::marker::StructuralPartialEq for ExampleError {} -#[automatically_derived] -impl ::core::cmp::PartialEq for ExampleError { - #[inline] - fn eq(&self, other: &ExampleError) -> bool { - let __self_tag = ::core::intrinsics::discriminant_value(self); - let __arg1_tag = ::core::intrinsics::discriminant_value(other); - __self_tag == __arg1_tag - } -} -impl From for solana_program::program_error::ProgramError { - fn from(e: ExampleError) -> Self { - solana_program::program_error::ProgramError::Custom(e as u32) - } -} -impl solana_program::decode_error::DecodeError for ExampleError { - fn type_of() -> &'static str { - "ExampleError" - } -} -impl solana_program::program_error::PrintProgramError for ExampleError { - fn print(&self) - where - E: 'static + std::error::Error + solana_program::decode_error::DecodeError - + solana_program::program_error::PrintProgramError - + num_traits::FromPrimitive, - { - match self { - ExampleError::MintHasNoMintAuthority => { - ::solana_program::log::sol_log("Mint has no mint authority") - } - ExampleError::IncorrectMintAuthority => { - ::solana_program::log::sol_log( - "Incorrect mint authority has signed the instruction", - ) - } - } - } -} -``` diff --git a/libraries/program-error/derive/Cargo.toml b/libraries/program-error/derive/Cargo.toml deleted file mode 100644 index 74aec23ba4a..00000000000 --- a/libraries/program-error/derive/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "spl-program-error-derive" -version = "0.4.1" -description = "Proc-Macro Library for Solana Program error attributes and derive macro" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1.0" -quote = "1.0" -sha2 = "0.10" -syn = { version = "2.0", features = ["full"] } diff --git a/libraries/program-error/derive/src/lib.rs b/libraries/program-error/derive/src/lib.rs deleted file mode 100644 index 026b0eb7d0f..00000000000 --- a/libraries/program-error/derive/src/lib.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Crate defining a procedural macro for building Solana program errors - -// Required to include `#[allow(clippy::integer_arithmetic)]` -// below since the tokens generated by `quote!` in the implementation -// for `MacroType::PrintProgramError` and `MacroType::SplProgramError` -// trigger the lint upstream through `quote_token_with_context` within the -// `quote` crate -// -// Culprit is `macro_impl.rs:66` -#![allow(clippy::arithmetic_side_effects)] -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -extern crate proc_macro; - -mod macro_impl; -mod parser; - -use { - crate::parser::SplProgramErrorArgs, - macro_impl::MacroType, - proc_macro::TokenStream, - syn::{parse_macro_input, ItemEnum}, -}; - -/// Derive macro to add `Into` -/// trait -#[proc_macro_derive(IntoProgramError)] -pub fn into_program_error(input: TokenStream) -> TokenStream { - let ItemEnum { ident, .. } = parse_macro_input!(input as ItemEnum); - MacroType::IntoProgramError { ident } - .generate_tokens() - .into() -} - -/// Derive macro to add `solana_program::decode_error::DecodeError` trait -#[proc_macro_derive(DecodeError)] -pub fn decode_error(input: TokenStream) -> TokenStream { - let ItemEnum { ident, .. } = parse_macro_input!(input as ItemEnum); - MacroType::DecodeError { ident }.generate_tokens().into() -} - -/// Derive macro to add `solana_program::program_error::PrintProgramError` trait -#[proc_macro_derive(PrintProgramError)] -pub fn print_program_error(input: TokenStream) -> TokenStream { - let ItemEnum { - ident, variants, .. - } = parse_macro_input!(input as ItemEnum); - MacroType::PrintProgramError { ident, variants } - .generate_tokens() - .into() -} - -/// Proc macro attribute to turn your enum into a Solana Program Error -/// -/// Adds: -/// - `Clone` -/// - `Debug` -/// - `Eq` -/// - `PartialEq` -/// - `thiserror::Error` -/// - `num_derive::FromPrimitive` -/// - `Into` -/// - `solana_program::decode_error::DecodeError` -/// - `solana_program::program_error::PrintProgramError` -/// -/// Optionally, you can add `hash_error_code_start: u32` argument to create -/// a unique `u32` _starting_ error codes from the names of the enum variants. -/// Notes: -/// - The _error_ variant will start at this value, and the rest will be -/// incremented by one -/// - The value provided is only for code readability, the actual error code -/// will be a hash of the input string and is checked against your input -/// -/// Syntax: `#[spl_program_error(hash_error_code_start = 1275525928)]` -/// Hash Input: `spl_program_error::` -/// Value: `u32::from_le_bytes([13..17])` -#[proc_macro_attribute] -pub fn spl_program_error(attr: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(attr as SplProgramErrorArgs); - let item_enum = parse_macro_input!(input as ItemEnum); - MacroType::SplProgramError { args, item_enum } - .generate_tokens() - .into() -} diff --git a/libraries/program-error/derive/src/macro_impl.rs b/libraries/program-error/derive/src/macro_impl.rs deleted file mode 100644 index 1f90cd55613..00000000000 --- a/libraries/program-error/derive/src/macro_impl.rs +++ /dev/null @@ -1,207 +0,0 @@ -//! The actual token generator for the macro - -use { - crate::parser::{SolanaProgram, SplProgramErrorArgs}, - proc_macro2::Span, - quote::quote, - sha2::{Digest, Sha256}, - syn::{ - punctuated::Punctuated, token::Comma, Expr, ExprLit, Ident, ItemEnum, Lit, LitInt, LitStr, - Token, Variant, - }, -}; - -const SPL_ERROR_HASH_NAMESPACE: &str = "spl_program_error"; -const SPL_ERROR_HASH_MIN_VALUE: u32 = 7_000; - -/// The type of macro being called, thus directing which tokens to generate -#[allow(clippy::enum_variant_names)] -pub enum MacroType { - IntoProgramError { - ident: Ident, - }, - DecodeError { - ident: Ident, - }, - PrintProgramError { - ident: Ident, - variants: Punctuated, - }, - SplProgramError { - args: SplProgramErrorArgs, - item_enum: ItemEnum, - }, -} - -impl MacroType { - /// Generates the corresponding tokens based on variant selection - pub fn generate_tokens(&mut self) -> proc_macro2::TokenStream { - let default_solana_program = SolanaProgram::default(); - match self { - Self::IntoProgramError { ident } => into_program_error(ident, &default_solana_program), - Self::DecodeError { ident } => decode_error(ident, &default_solana_program), - Self::PrintProgramError { ident, variants } => { - print_program_error(ident, variants, &default_solana_program) - } - Self::SplProgramError { args, item_enum } => spl_program_error(args, item_enum), - } - } -} - -/// Builds the implementation of -/// `Into` More specifically, -/// implements `From for solana_program::program_error::ProgramError` -pub fn into_program_error(ident: &Ident, import: &SolanaProgram) -> proc_macro2::TokenStream { - let this_impl = quote! { - impl From<#ident> for #import::program_error::ProgramError { - fn from(e: #ident) -> Self { - #import::program_error::ProgramError::Custom(e as u32) - } - } - }; - import.wrap(this_impl) -} - -/// Builds the implementation of `solana_program::decode_error::DecodeError` -pub fn decode_error(ident: &Ident, import: &SolanaProgram) -> proc_macro2::TokenStream { - let this_impl = quote! { - impl #import::decode_error::DecodeError for #ident { - fn type_of() -> &'static str { - stringify!(#ident) - } - } - }; - import.wrap(this_impl) -} - -/// Builds the implementation of -/// `solana_program::program_error::PrintProgramError` -pub fn print_program_error( - ident: &Ident, - variants: &Punctuated, - import: &SolanaProgram, -) -> proc_macro2::TokenStream { - let ppe_match_arms = variants.iter().map(|variant| { - let variant_ident = &variant.ident; - let error_msg = get_error_message(variant) - .unwrap_or_else(|| String::from("Unknown custom program error")); - quote! { - #ident::#variant_ident => { - #import::msg!(#error_msg) - } - } - }); - let this_impl = quote! { - impl #import::program_error::PrintProgramError for #ident { - fn print(&self) - where - E: 'static - + std::error::Error - + #import::decode_error::DecodeError - + #import::program_error::PrintProgramError - + num_traits::FromPrimitive, - { - match self { - #(#ppe_match_arms),* - } - } - } - }; - import.wrap(this_impl) -} - -/// Helper to parse out the string literal from the `#[error(..)]` attribute -fn get_error_message(variant: &Variant) -> Option { - let attrs = &variant.attrs; - for attr in attrs { - if attr.path().is_ident("error") { - if let Ok(lit_str) = attr.parse_args::() { - return Some(lit_str.value()); - } - } - } - None -} - -/// The main function that produces the tokens required to turn your -/// error enum into a Solana Program Error -pub fn spl_program_error( - args: &SplProgramErrorArgs, - item_enum: &mut ItemEnum, -) -> proc_macro2::TokenStream { - if let Some(error_code_start) = args.hash_error_code_start { - set_first_discriminant(item_enum, error_code_start); - } - - let ident = &item_enum.ident; - let variants = &item_enum.variants; - let into_program_error = into_program_error(ident, &args.import); - let decode_error = decode_error(ident, &args.import); - let print_program_error = print_program_error(ident, variants, &args.import); - - quote! { - #[repr(u32)] - #[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] - #[num_traits = "num_traits"] - #item_enum - - #into_program_error - - #decode_error - - #print_program_error - } -} - -/// This function adds a discriminant to the first enum variant based on the -/// hash of the `SPL_ERROR_HASH_NAMESPACE` constant, the enum name and variant -/// name. -/// It will then check to make sure the provided `hash_error_code_start` is -/// equal to the hash-produced `u32`. -/// -/// See https://docs.rs/syn/latest/syn/struct.Variant.html -fn set_first_discriminant(item_enum: &mut ItemEnum, error_code_start: u32) { - let enum_ident = &item_enum.ident; - if item_enum.variants.is_empty() { - panic!("Enum must have at least one variant"); - } - let first_variant = &mut item_enum.variants[0]; - let discriminant = u32_from_hash(enum_ident); - if discriminant == error_code_start { - let eq = Token![=](Span::call_site()); - let expr = Expr::Lit(ExprLit { - attrs: Vec::new(), - lit: Lit::Int(LitInt::new(&discriminant.to_string(), Span::call_site())), - }); - first_variant.discriminant = Some((eq, expr)); - } else { - panic!( - "Error code start value from hash must be {0}. Update your macro attribute to \ - `#[spl_program_error(hash_error_code_start = {0})]`.", - discriminant - ); - } -} - -/// Hashes the `SPL_ERROR_HASH_NAMESPACE` constant, the enum name and variant -/// name and returns four middle bytes (13 through 16) as a u32. -fn u32_from_hash(enum_ident: &Ident) -> u32 { - let hash_input = format!("{}:{}", SPL_ERROR_HASH_NAMESPACE, enum_ident); - - // We don't want our error code to start at any number below - // `SPL_ERROR_HASH_MIN_VALUE`! - let mut nonce: u32 = 0; - loop { - let mut hasher = Sha256::new_with_prefix(hash_input.as_bytes()); - hasher.update(nonce.to_le_bytes()); - let d = u32::from_le_bytes( - hasher.finalize()[13..17] - .try_into() - .expect("Unable to convert hash to u32"), - ); - if d >= SPL_ERROR_HASH_MIN_VALUE { - return d; - } - nonce += 1; - } -} diff --git a/libraries/program-error/derive/src/parser.rs b/libraries/program-error/derive/src/parser.rs deleted file mode 100644 index 7e01cb728cc..00000000000 --- a/libraries/program-error/derive/src/parser.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Token parsing - -use { - proc_macro2::{Ident, Span, TokenStream}, - quote::quote, - syn::{ - parse::{Parse, ParseStream}, - token::Comma, - LitInt, LitStr, Token, - }, -}; - -/// Possible arguments to the `#[spl_program_error]` attribute -pub struct SplProgramErrorArgs { - /// Whether to hash the error codes using `solana_program::hash` - /// or to use the default error code assigned by `num_traits`. - pub hash_error_code_start: Option, - /// Crate to use for solana_program - pub import: SolanaProgram, -} - -/// Struct representing the path to a `solana_program` crate, which may be -/// renamed or otherwise. -pub struct SolanaProgram { - import: Ident, - explicit: bool, -} -impl quote::ToTokens for SolanaProgram { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.import.to_tokens(tokens); - } -} -impl SolanaProgram { - pub fn wrap(&self, output: TokenStream) -> TokenStream { - if self.explicit { - output - } else { - anon_const_trick(output) - } - } -} -impl Default for SolanaProgram { - fn default() -> Self { - Self { - import: Ident::new("_solana_program", Span::call_site()), - explicit: false, - } - } -} - -impl Parse for SplProgramErrorArgs { - fn parse(input: ParseStream) -> syn::Result { - let mut hash_error_code_start = None; - let mut import = None; - while !input.is_empty() { - match SplProgramErrorArgParser::parse(input)? { - SplProgramErrorArgParser::HashErrorCodes { value, .. } => { - hash_error_code_start = Some(value.base10_parse::()?); - } - SplProgramErrorArgParser::SolanaProgramCrate { value, .. } => { - import = Some(SolanaProgram { - import: value.parse()?, - explicit: true, - }); - } - } - } - Ok(Self { - hash_error_code_start, - import: import.unwrap_or(SolanaProgram::default()), - }) - } -} - -/// Parser for args to the `#[spl_program_error]` attribute -/// ie. `#[spl_program_error(hash_error_code_start = 1275525928)]` -enum SplProgramErrorArgParser { - HashErrorCodes { - _equals_sign: Token![=], - value: LitInt, - _comma: Option, - }, - SolanaProgramCrate { - _equals_sign: Token![=], - value: LitStr, - _comma: Option, - }, -} - -impl Parse for SplProgramErrorArgParser { - fn parse(input: ParseStream) -> syn::Result { - let ident = input.parse::()?; - match ident.to_string().as_str() { - "hash_error_code_start" => { - let _equals_sign = input.parse::()?; - let value = input.parse::()?; - let _comma: Option = input.parse().unwrap_or(None); - Ok(Self::HashErrorCodes { - _equals_sign, - value, - _comma, - }) - } - "solana_program" => { - let _equals_sign = input.parse::()?; - let value = input.parse::()?; - let _comma: Option = input.parse().unwrap_or(None); - Ok(Self::SolanaProgramCrate { - _equals_sign, - value, - _comma, - }) - } - _ => Err(input.error("Expected argument 'hash_error_code_start' or 'solana_program'")), - } - } -} - -// Within `exp`, you can bring things into scope with `extern crate`. -// -// We don't want to assume that `solana_program::` is in scope - the user may -// have imported it under a different name, or may have imported it in a -// non-toplevel module (common when putting impls behind a feature gate). -// -// Solution: let's just generate `extern crate solana_program as -// _solana_program` and then refer to `_solana_program` in the derived code. -// However, macros are not allowed to produce `extern crate` statements at the -// toplevel. -// -// Solution: let's generate `mod _impl_foo` and import solana_program within -// that. However, now we lose access to private members of the surrounding -// module. This is a problem if, for example, we're deriving for a newtype, -// where the inner type is defined in the same module, but not exported. -// -// Solution: use the anonymous const trick. For some reason, `extern crate` -// statements are allowed here, but everything from the surrounding module is in -// scope. This trick is taken from serde and num_traits. -fn anon_const_trick(exp: TokenStream) -> TokenStream { - quote! { - const _: () = { - extern crate solana_program as _solana_program; - #exp - }; - } -} diff --git a/libraries/program-error/src/lib.rs b/libraries/program-error/src/lib.rs deleted file mode 100644 index 739995dd197..00000000000 --- a/libraries/program-error/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Crate defining a library with a procedural macro and other -//! dependencies for building Solana program errors - -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -extern crate self as spl_program_error; - -// Make these available downstream for the macro to work without -// additional imports -pub use { - num_derive, num_traits, solana_program, - spl_program_error_derive::{ - spl_program_error, DecodeError, IntoProgramError, PrintProgramError, - }, - thiserror, -}; diff --git a/libraries/program-error/tests/bench.rs b/libraries/program-error/tests/bench.rs deleted file mode 100644 index 40f01bb7e90..00000000000 --- a/libraries/program-error/tests/bench.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Bench case with manual implementations -use spl_program_error::*; - -/// Example error -#[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} - -impl From for solana_program::program_error::ProgramError { - fn from(e: ExampleError) -> Self { - solana_program::program_error::ProgramError::Custom(e as u32) - } -} -impl solana_program::decode_error::DecodeError for ExampleError { - fn type_of() -> &'static str { - "ExampleError" - } -} - -impl solana_program::program_error::PrintProgramError for ExampleError { - fn print(&self) - where - E: 'static - + std::error::Error - + solana_program::decode_error::DecodeError - + solana_program::program_error::PrintProgramError - + num_traits::FromPrimitive, - { - match self { - ExampleError::MintHasNoMintAuthority => { - solana_program::msg!("Mint has no mint authority") - } - ExampleError::IncorrectMintAuthority => { - solana_program::msg!("Incorrect mint authority has signed the instruction") - } - } - } -} - -/// Tests that all macros compile -#[test] -fn test_macros_compile() { - let _ = ExampleError::MintHasNoMintAuthority; -} diff --git a/libraries/program-error/tests/decode.rs b/libraries/program-error/tests/decode.rs deleted file mode 100644 index 0c7c209bc14..00000000000 --- a/libraries/program-error/tests/decode.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Tests `#[derive(DecodeError)]` - -use spl_program_error::*; - -/// Example error -#[derive( - Clone, - Debug, - DecodeError, - Eq, - IntoProgramError, - thiserror::Error, - num_derive::FromPrimitive, - PartialEq, -)] -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} - -/// Tests that all macros compile -#[test] -fn test_macros_compile() { - let _ = ExampleError::MintHasNoMintAuthority; -} diff --git a/libraries/program-error/tests/into.rs b/libraries/program-error/tests/into.rs deleted file mode 100644 index 0f32b8f40d3..00000000000 --- a/libraries/program-error/tests/into.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Tests `#[derive(IntoProgramError)]` - -use spl_program_error::*; - -/// Example error -#[derive( - Clone, Debug, Eq, IntoProgramError, thiserror::Error, num_derive::FromPrimitive, PartialEq, -)] -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} - -/// Tests that all macros compile -#[test] -fn test_macros_compile() { - let _ = ExampleError::MintHasNoMintAuthority; -} diff --git a/libraries/program-error/tests/mod.rs b/libraries/program-error/tests/mod.rs deleted file mode 100644 index 413587758e9..00000000000 --- a/libraries/program-error/tests/mod.rs +++ /dev/null @@ -1,141 +0,0 @@ -pub mod bench; -pub mod decode; -pub mod into; -pub mod print; -pub mod spl; - -#[cfg(test)] -mod tests { - use { - super::*, - serial_test::serial, - solana_program::{ - decode_error::DecodeError, - program_error::{PrintProgramError, ProgramError}, - }, - std::sync::{Arc, RwLock}, - }; - - // Used to capture output for `PrintProgramError` for testing - lazy_static::lazy_static! { - static ref EXPECTED_DATA: Arc>> = Arc::new(RwLock::new(Vec::new())); - } - fn set_expected_data(expected_data: Vec) { - *EXPECTED_DATA.write().unwrap() = expected_data; - } - pub struct SyscallStubs {} - impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { - fn sol_log(&self, message: &str) { - assert_eq!( - message, - String::from_utf8_lossy(&EXPECTED_DATA.read().unwrap()) - ); - } - } - - // `#[derive(IntoProgramError)]` - #[test] - fn test_derive_into_program_error() { - // `Into` - assert_eq!( - Into::::into(bench::ExampleError::MintHasNoMintAuthority), - Into::::into(into::ExampleError::MintHasNoMintAuthority), - ); - assert_eq!( - Into::::into(bench::ExampleError::IncorrectMintAuthority), - Into::::into(into::ExampleError::IncorrectMintAuthority), - ); - } - - // `#[derive(DecodeError)]` - #[test] - fn test_derive_decode_error() { - // `Into` - assert_eq!( - Into::::into(bench::ExampleError::MintHasNoMintAuthority), - Into::::into(decode::ExampleError::MintHasNoMintAuthority), - ); - assert_eq!( - Into::::into(bench::ExampleError::IncorrectMintAuthority), - Into::::into(decode::ExampleError::IncorrectMintAuthority), - ); - // `DecodeError` - assert_eq!( - >::type_of(), - >::type_of(), - ); - } - // `#[derive(PrintProgramError)]` - #[test] - #[serial] - fn test_derive_print_program_error() { - use std::sync::Once; - static ONCE: Once = Once::new(); - - ONCE.call_once(|| { - solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); - }); - // `Into` - assert_eq!( - Into::::into(bench::ExampleError::MintHasNoMintAuthority), - Into::::into(print::ExampleError::MintHasNoMintAuthority), - ); - assert_eq!( - Into::::into(bench::ExampleError::IncorrectMintAuthority), - Into::::into(print::ExampleError::IncorrectMintAuthority), - ); - // `DecodeError` - assert_eq!( - >::type_of(), - >::type_of(), - ); - // `PrintProgramError` - set_expected_data("Mint has no mint authority".as_bytes().to_vec()); - PrintProgramError::print::( - &print::ExampleError::MintHasNoMintAuthority, - ); - set_expected_data( - "Incorrect mint authority has signed the instruction" - .as_bytes() - .to_vec(), - ); - PrintProgramError::print::( - &print::ExampleError::IncorrectMintAuthority, - ); - } - - // `#[spl_program_error]` - #[test] - #[serial] - fn test_spl_program_error() { - use std::sync::Once; - static ONCE: Once = Once::new(); - - ONCE.call_once(|| { - solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); - }); - // `Into` - assert_eq!( - Into::::into(bench::ExampleError::MintHasNoMintAuthority), - Into::::into(spl::ExampleError::MintHasNoMintAuthority), - ); - assert_eq!( - Into::::into(bench::ExampleError::IncorrectMintAuthority), - Into::::into(spl::ExampleError::IncorrectMintAuthority), - ); - // `DecodeError` - assert_eq!( - >::type_of(), - >::type_of(), - ); - // `PrintProgramError` - set_expected_data("Mint has no mint authority".as_bytes().to_vec()); - PrintProgramError::print::(&spl::ExampleError::MintHasNoMintAuthority); - set_expected_data( - "Incorrect mint authority has signed the instruction" - .as_bytes() - .to_vec(), - ); - PrintProgramError::print::(&spl::ExampleError::IncorrectMintAuthority); - } -} diff --git a/libraries/program-error/tests/print.rs b/libraries/program-error/tests/print.rs deleted file mode 100644 index 8b68f66a581..00000000000 --- a/libraries/program-error/tests/print.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Tests `#[derive(PrintProgramError)]` - -use spl_program_error::*; - -/// Example error -#[derive( - Clone, - Debug, - DecodeError, - Eq, - IntoProgramError, - PrintProgramError, - thiserror::Error, - num_derive::FromPrimitive, - PartialEq, -)] -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} - -/// Tests that all macros compile -#[test] -fn test_macros_compile() { - let _ = ExampleError::MintHasNoMintAuthority; -} diff --git a/libraries/program-error/tests/spl.rs b/libraries/program-error/tests/spl.rs deleted file mode 100644 index d772d24f6c5..00000000000 --- a/libraries/program-error/tests/spl.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Tests `#[spl_program_error]` - -use spl_program_error::*; - -/// Example error -#[spl_program_error] -pub enum ExampleError { - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, -} - -/// Tests that all macros compile -#[test] -fn test_macros_compile() { - let _ = ExampleError::MintHasNoMintAuthority; -} - -/// Example library error with namespace -#[spl_program_error(hash_error_code_start = 2_056_342_880)] -enum ExampleLibraryError { - /// This is a very informative error - #[error("This is a very informative error")] - VeryInformativeError, - /// This is a super important error - #[error("This is a super important error")] - SuperImportantError, - /// This is a mega serious error - #[error("This is a mega serious error")] - MegaSeriousError, - /// You are toast - #[error("You are toast")] - YouAreToast, -} - -/// Tests hashing of error codes into unique `u32` values -#[test] -fn test_library_error_codes() { - fn get_error_code_check(hash_input: &str) -> u32 { - let mut nonce: u32 = 0; - loop { - let hash = solana_program::hash::hashv(&[hash_input.as_bytes(), &nonce.to_le_bytes()]); - let mut bytes = [0u8; 4]; - bytes.copy_from_slice(&hash.to_bytes()[13..17]); - let error_code = u32::from_le_bytes(bytes); - if error_code >= 10_000 { - return error_code; - } - nonce += 1; - } - } - - let first_error_as_u32 = ExampleLibraryError::VeryInformativeError as u32; - - assert_eq!( - ExampleLibraryError::VeryInformativeError as u32, - get_error_code_check("spl_program_error:ExampleLibraryError"), - ); - assert_eq!( - ExampleLibraryError::SuperImportantError as u32, - first_error_as_u32 + 1, - ); - assert_eq!( - ExampleLibraryError::MegaSeriousError as u32, - first_error_as_u32 + 2, - ); - assert_eq!( - ExampleLibraryError::YouAreToast as u32, - first_error_as_u32 + 3, - ); -} - -/// Example error with solana_program crate set -#[spl_program_error(solana_program = "solana_program")] -enum ExampleSolanaProgramCrateError { - /// This is a very informative error - #[error("This is a very informative error")] - VeryInformativeError, - /// This is a super important error - #[error("This is a super important error")] - SuperImportantError, -} - -/// Tests that all macros compile -#[test] -fn test_macros_compile_with_solana_program_crate() { - let _ = ExampleSolanaProgramCrateError::VeryInformativeError; -} diff --git a/libraries/tlv-account-resolution/Cargo.toml b/libraries/tlv-account-resolution/Cargo.toml deleted file mode 100644 index e95642cd4f3..00000000000 --- a/libraries/tlv-account-resolution/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "spl-tlv-account-resolution" -version = "0.9.0" -description = "Solana Program Library TLV Account Resolution Interface" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -serde-traits = ["dep:serde"] -test-sbf = [] - -[dependencies] -bytemuck = { version = "1.21.0", features = ["derive"] } -num-derive = "0.4" -num-traits = "0.2" -serde = { version = "1.0.217", optional = true } -solana-account-info = "2.1.0" -solana-decode-error = "2.1.0" -solana-instruction = { version = "2.1.0", features = ["std"] } -solana-program-error = "2.1.0" -solana-msg = "2.1.0" -solana-pubkey = "2.1.0" -spl-discriminator = { version = "0.4.0", path = "../discriminator" } -spl-program-error = { version = "0.6.0", path = "../program-error" } -spl-pod = { version = "0.5.0", path = "../pod" } -spl-type-length-value = { version = "0.7.0", path = "../type-length-value" } -thiserror = "2.0" - -[dev-dependencies] -futures = "0.3.31" -futures-util = "0.3" -solana-client = "2.1.0" -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/libraries/tlv-account-resolution/README.md b/libraries/tlv-account-resolution/README.md deleted file mode 100644 index 2b7dd2d0d09..00000000000 --- a/libraries/tlv-account-resolution/README.md +++ /dev/null @@ -1,198 +0,0 @@ -# TLV Account Resolution - -Library defining a generic state interface to encode additional required accounts -for an instruction, using Type-Length-Value structures. - -## Example usage - -If you want to encode the additional required accounts for your instruction -into a TLV entry in an account, you can do the following: - -```rust -use { - solana_account_info::AccountInfo, - solana_instruction::{AccountMeta, Instruction}, - solana_pubkey::Pubkey - spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, - spl_tlv_account_resolution::{ - account::ExtraAccountMeta, - seeds::Seed, - state::ExtraAccountMetaList - }, -}; - -struct MyInstruction; -impl SplDiscriminate for MyInstruction { - // For ease of use, give it the same discriminator as its instruction definition - const SPL_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([1; ArrayDiscriminator::LENGTH]); -} - -// Prepare the additional required account keys and signer / writable -let extra_metas = [ - AccountMeta::new(Pubkey::new_unique(), false).into(), - AccountMeta::new_readonly(Pubkey::new_unique(), true).into(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"some_string".to_vec(), - }, - Seed::InstructionData { - index: 1, - length: 1, // u8 - }, - Seed::AccountKey { index: 1 }, - ], - false, - true, - ).unwrap(), - ExtraAccountMeta::new_external_pda_with_seeds( - 0, - &[Seed::AccountKey { index: 2 }], - false, - false, - ).unwrap(), -]; - -// Allocate a new buffer with the proper `account_size` -let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap(); -let mut buffer = vec![0; account_size]; - -// Initialize the structure for your instruction -ExtraAccountMetaList::init::(&mut buffer, &extra_metas).unwrap(); - -// Off-chain, you can add the additional accounts directly from the account data -// You need to provide the resolver a way to fetch account data off-chain -let client = RpcClient::new_mock("succeeds".to_string()); -let program_id = Pubkey::new_unique(); -let mut instruction = Instruction::new_with_bytes(program_id, &[0, 1, 2], vec![]); -ExtraAccountMetaList::add_to_instruction::<_, _, MyInstruction>( - &mut instruction, - |address: &Pubkey| { - client - .get_account(address) - .map_ok(|acct| Some(acct.data)) - }, - &buffer, -) -.await -.unwrap(); - -// On-chain, you can add the additional accounts *and* account infos -let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[0, 1, 2], vec![]); - -// Include all of the well-known required account infos here first -let mut cpi_account_infos = vec![]; - -// Provide all "remaining_account_infos" that are *not* part of any other known interface -let remaining_account_infos = &[]; -ExtraAccountMetaList::add_to_cpi_instruction::( - &mut cpi_instruction, - &mut cpi_account_infos, - &buffer, - &remaining_account_infos, -).unwrap(); -``` - -For ease of use on-chain, `ExtraAccountMetaList::init` is also -provided to initialize directly from a set of given accounts. - -## Motivation - -The Solana account model presents unique challenges for program interfaces. -Since it's impossible to load additional accounts on-chain, if a program requires -additional accounts to properly implement an instruction, there's no clear way -for clients to fetch these accounts. - -There are two main ways to fetch additional accounts, dynamically through program -simulation, or statically by fetching account data. This library implements -additional account resolution statically. You can find more information about -dynamic account resolution in the Appendix. - -### Static Account Resolution - -It's possible for programs to write the additional required account infos -into account data, so that on-chain and off-chain clients simply need to read -the data to figure out the additional required accounts. - -Rather than exposing this data dynamically through program execution, this method -uses static account data. - -For example, let's imagine there's a `Transferable` interface, along with a -`transfer` instruction. Some programs that implement `transfer` may need more -accounts than just the ones defined in the interface. How does an on-chain or -off-chain client figure out the additional required accounts? - -The "static" approach requires programs to write the extra required accounts to -an account defined at a given address. This could be directly in the `mint`, or -some address derivable from the mint address. - -Off-chain, a client must fetch this additional account and read its data to find -out the additional required accounts, and then include them in the instruction. - -On-chain, a program must have access to "remaining account infos" containing the -special account and all other required accounts to properly create the CPI -instruction and give the correct account infos. - -This approach could also be called a "state interface". - -### Types of Required Accounts - -This library is capable of storing two types of configurations for additional -required accounts: - -- Accounts with a fixed address -- Accounts with a **dynamic program-derived address** derived from seeds that -may come from any combination of the following: - - Hard-coded values, such as string literals or integers - - A slice of the instruction data provided to the transfer-hook program - - The address of another account in the total list of accounts - - A program id from another account in the instruction - -When you store configurations for a dynamic Program-Derived Address within the -additional required accounts, the PDA itself is evaluated (or resolved) at the -time of instruction invocation using the instruction itself. This -occurs in the offchain and onchain helpers mentioned below, which leverage -the SPL TLV Account Resolution library to perform this resolution -automatically. - -## How it Works - -This library uses `spl-type-length-value` to read and write required instruction -accounts from account data. - -Interface instructions must have an 8-byte discriminator, so that the exposed -`ExtraAccountMetaList` type can use the instruction discriminator as an -`ArrayDiscriminator`, which allows that discriminator to serve as a unique TLV -discriminator for identifying entries that correspond to that particular -instruction. - -This can be confusing. Typically, a type implements `SplDiscriminate`, so that -the type can be written into TLV data. In this case, `ExtraAccountMetaList` is -generic over `SplDiscriminate`, meaning that a program can write many different instances of -`ExtraAccountMetaList` into one account, using different `ArrayDiscriminator`s. - -Also, it's reusing an instruction discriminator as a TLV discriminator. For example, -if the `transfer` instruction has a discriminator of `[1, 2, 3, 4, 5, 6, 7, 8]`, -then the account uses a TLV discriminator of `[1, 2, 3, 4, 5, 6, 7, 8]` to denote -where the additional account metas are stored. - -This isn't required, but makes it easier for clients to find the additional -required accounts for an instruction. - -## Appendix - -### Dynamic Account Resolution - -To expose the additional accounts required, instruction interfaces can include -supplemental instructions to return the required accounts. - -For example, in the `Transferable` interface example, along with a `transfer` -instruction, also requires implementations to expose a -`get_additional_accounts_for_transfer` instruction. - -In the program implementation, this instruction writes the additional accounts -into return data, making it easy for on-chain and off-chain clients to consume. - -See the -[relevant sRFC](https://forum.solana.com/t/srfc-00010-additional-accounts-request-transfer-spec/122) -for more information about the dynamic approach. diff --git a/libraries/tlv-account-resolution/src/account.rs b/libraries/tlv-account-resolution/src/account.rs deleted file mode 100644 index 91305680773..00000000000 --- a/libraries/tlv-account-resolution/src/account.rs +++ /dev/null @@ -1,296 +0,0 @@ -//! Struct for managing extra required account configs, ie. defining accounts -//! required for your interface program, which can be `AccountMeta`s - which -//! have fixed addresses - or PDAs - which have addresses derived from a -//! collection of seeds - -use { - crate::{error::AccountResolutionError, pubkey_data::PubkeyData, seeds::Seed}, - bytemuck::{Pod, Zeroable}, - solana_account_info::AccountInfo, - solana_instruction::AccountMeta, - solana_program_error::ProgramError, - solana_pubkey::{Pubkey, PUBKEY_BYTES}, - spl_pod::primitives::PodBool, -}; - -/// Resolve a program-derived address (PDA) from the instruction data -/// and the accounts that have already been resolved -fn resolve_pda<'a, F>( - seeds: &[Seed], - instruction_data: &[u8], - program_id: &Pubkey, - get_account_key_data_fn: F, -) -> Result -where - F: Fn(usize) -> Option<(&'a Pubkey, Option<&'a [u8]>)>, -{ - let mut pda_seeds: Vec<&[u8]> = vec![]; - for config in seeds { - match config { - Seed::Uninitialized => (), - Seed::Literal { bytes } => pda_seeds.push(bytes), - Seed::InstructionData { index, length } => { - let arg_start = *index as usize; - let arg_end = arg_start + *length as usize; - if arg_end > instruction_data.len() { - return Err(AccountResolutionError::InstructionDataTooSmall.into()); - } - pda_seeds.push(&instruction_data[arg_start..arg_end]); - } - Seed::AccountKey { index } => { - let account_index = *index as usize; - let address = get_account_key_data_fn(account_index) - .ok_or::(AccountResolutionError::AccountNotFound.into())? - .0; - pda_seeds.push(address.as_ref()); - } - Seed::AccountData { - account_index, - data_index, - length, - } => { - let account_index = *account_index as usize; - let account_data = get_account_key_data_fn(account_index) - .ok_or::(AccountResolutionError::AccountNotFound.into())? - .1 - .ok_or::(AccountResolutionError::AccountDataNotFound.into())?; - let arg_start = *data_index as usize; - let arg_end = arg_start + *length as usize; - if account_data.len() < arg_end { - return Err(AccountResolutionError::AccountDataTooSmall.into()); - } - pda_seeds.push(&account_data[arg_start..arg_end]); - } - } - } - Ok(Pubkey::find_program_address(&pda_seeds, program_id).0) -} - -/// Resolve a pubkey from a pubkey data configuration. -fn resolve_key_data<'a, F>( - key_data: &PubkeyData, - instruction_data: &[u8], - get_account_key_data_fn: F, -) -> Result -where - F: Fn(usize) -> Option<(&'a Pubkey, Option<&'a [u8]>)>, -{ - match key_data { - PubkeyData::Uninitialized => Err(ProgramError::InvalidAccountData), - PubkeyData::InstructionData { index } => { - let key_start = *index as usize; - let key_end = key_start + PUBKEY_BYTES; - if key_end > instruction_data.len() { - return Err(AccountResolutionError::InstructionDataTooSmall.into()); - } - Ok(Pubkey::new_from_array( - instruction_data[key_start..key_end].try_into().unwrap(), - )) - } - PubkeyData::AccountData { - account_index, - data_index, - } => { - let account_index = *account_index as usize; - let account_data = get_account_key_data_fn(account_index) - .ok_or::(AccountResolutionError::AccountNotFound.into())? - .1 - .ok_or::(AccountResolutionError::AccountDataNotFound.into())?; - let arg_start = *data_index as usize; - let arg_end = arg_start + PUBKEY_BYTES; - if account_data.len() < arg_end { - return Err(AccountResolutionError::AccountDataTooSmall.into()); - } - Ok(Pubkey::new_from_array( - account_data[arg_start..arg_end].try_into().unwrap(), - )) - } - } -} - -/// `Pod` type for defining a required account in a validation account. -/// -/// This can be any of the following: -/// -/// * A standard `AccountMeta` -/// * A PDA (with seed configurations) -/// * A pubkey stored in some data (account or instruction data) -/// -/// Can be used in TLV-encoded data. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct ExtraAccountMeta { - /// Discriminator to tell whether this represents a standard - /// `AccountMeta`, PDA, or pubkey data. - pub discriminator: u8, - /// This `address_config` field can either be the pubkey of the account, - /// the seeds used to derive the pubkey from provided inputs (PDA), or the - /// data used to derive the pubkey (account or instruction data). - pub address_config: [u8; 32], - /// Whether the account should sign - pub is_signer: PodBool, - /// Whether the account should be writable - pub is_writable: PodBool, -} -/// Helper used to know when the top bit is set, to interpret the -/// discriminator as an index rather than as a type -const U8_TOP_BIT: u8 = 1 << 7; -impl ExtraAccountMeta { - /// Create a `ExtraAccountMeta` from a public key, - /// thus representing a standard `AccountMeta` - pub fn new_with_pubkey( - pubkey: &Pubkey, - is_signer: bool, - is_writable: bool, - ) -> Result { - Ok(Self { - discriminator: 0, - address_config: pubkey.to_bytes(), - is_signer: is_signer.into(), - is_writable: is_writable.into(), - }) - } - - /// Create a `ExtraAccountMeta` from a list of seed configurations, - /// thus representing a PDA - pub fn new_with_seeds( - seeds: &[Seed], - is_signer: bool, - is_writable: bool, - ) -> Result { - Ok(Self { - discriminator: 1, - address_config: Seed::pack_into_address_config(seeds)?, - is_signer: is_signer.into(), - is_writable: is_writable.into(), - }) - } - - /// Create a `ExtraAccountMeta` from a pubkey data configuration. - pub fn new_with_pubkey_data( - key_data: &PubkeyData, - is_signer: bool, - is_writable: bool, - ) -> Result { - Ok(Self { - discriminator: 2, - address_config: PubkeyData::pack_into_address_config(key_data)?, - is_signer: is_signer.into(), - is_writable: is_writable.into(), - }) - } - - /// Create a `ExtraAccountMeta` from a list of seed configurations, - /// representing a PDA for an external program - /// - /// This PDA belongs to a program elsewhere in the account list, rather - /// than the executing program. For a PDA on the executing program, use - /// `ExtraAccountMeta::new_with_seeds`. - pub fn new_external_pda_with_seeds( - program_index: u8, - seeds: &[Seed], - is_signer: bool, - is_writable: bool, - ) -> Result { - Ok(Self { - discriminator: program_index - .checked_add(U8_TOP_BIT) - .ok_or(AccountResolutionError::InvalidSeedConfig)?, - address_config: Seed::pack_into_address_config(seeds)?, - is_signer: is_signer.into(), - is_writable: is_writable.into(), - }) - } - - /// Resolve an `ExtraAccountMeta` into an `AccountMeta`, potentially - /// resolving a program-derived address (PDA) if necessary - pub fn resolve<'a, F>( - &self, - instruction_data: &[u8], - program_id: &Pubkey, - get_account_key_data_fn: F, - ) -> Result - where - F: Fn(usize) -> Option<(&'a Pubkey, Option<&'a [u8]>)>, - { - match self.discriminator { - 0 => AccountMeta::try_from(self), - x if x == 1 || x >= U8_TOP_BIT => { - let program_id = if x == 1 { - program_id - } else { - get_account_key_data_fn(x.saturating_sub(U8_TOP_BIT) as usize) - .ok_or::(AccountResolutionError::AccountNotFound.into())? - .0 - }; - let seeds = Seed::unpack_address_config(&self.address_config)?; - Ok(AccountMeta { - pubkey: resolve_pda( - &seeds, - instruction_data, - program_id, - get_account_key_data_fn, - )?, - is_signer: self.is_signer.into(), - is_writable: self.is_writable.into(), - }) - } - 2 => { - let key_data = PubkeyData::unpack(&self.address_config)?; - Ok(AccountMeta { - pubkey: resolve_key_data(&key_data, instruction_data, get_account_key_data_fn)?, - is_signer: self.is_signer.into(), - is_writable: self.is_writable.into(), - }) - } - _ => Err(ProgramError::InvalidAccountData), - } - } -} - -impl From<&AccountMeta> for ExtraAccountMeta { - fn from(meta: &AccountMeta) -> Self { - Self { - discriminator: 0, - address_config: meta.pubkey.to_bytes(), - is_signer: meta.is_signer.into(), - is_writable: meta.is_writable.into(), - } - } -} -impl From for ExtraAccountMeta { - fn from(meta: AccountMeta) -> Self { - ExtraAccountMeta::from(&meta) - } -} -impl From<&AccountInfo<'_>> for ExtraAccountMeta { - fn from(account_info: &AccountInfo) -> Self { - Self { - discriminator: 0, - address_config: account_info.key.to_bytes(), - is_signer: account_info.is_signer.into(), - is_writable: account_info.is_writable.into(), - } - } -} -impl From> for ExtraAccountMeta { - fn from(account_info: AccountInfo) -> Self { - ExtraAccountMeta::from(&account_info) - } -} - -impl TryFrom<&ExtraAccountMeta> for AccountMeta { - type Error = ProgramError; - - fn try_from(pod: &ExtraAccountMeta) -> Result { - if pod.discriminator == 0 { - Ok(AccountMeta { - pubkey: Pubkey::from(pod.address_config), - is_signer: pod.is_signer.into(), - is_writable: pod.is_writable.into(), - }) - } else { - Err(AccountResolutionError::AccountTypeNotAccountMeta.into()) - } - } -} diff --git a/libraries/tlv-account-resolution/src/error.rs b/libraries/tlv-account-resolution/src/error.rs deleted file mode 100644 index 919fe603415..00000000000 --- a/libraries/tlv-account-resolution/src/error.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! Error types - -use { - solana_decode_error::DecodeError, - solana_msg::msg, - solana_program_error::{PrintProgramError, ProgramError}, -}; - -/// Errors that may be returned by the Account Resolution library. -#[repr(u32)] -#[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] -pub enum AccountResolutionError { - /// Incorrect account provided - #[error("Incorrect account provided")] - IncorrectAccount = 2_724_315_840, - /// Not enough accounts provided - #[error("Not enough accounts provided")] - NotEnoughAccounts, - /// No value initialized in TLV data - #[error("No value initialized in TLV data")] - TlvUninitialized, - /// Some value initialized in TLV data - #[error("Some value initialized in TLV data")] - TlvInitialized, - /// Too many pubkeys provided - #[error("Too many pubkeys provided")] - TooManyPubkeys, - /// Failed to parse `Pubkey` from bytes - #[error("Failed to parse `Pubkey` from bytes")] - InvalidPubkey, - /// Attempted to deserialize an `AccountMeta` but the underlying type has - /// PDA configs rather than a fixed address - #[error( - "Attempted to deserialize an `AccountMeta` but the underlying type has PDA configs rather \ - than a fixed address" - )] - AccountTypeNotAccountMeta, - /// Provided list of seed configurations too large for a validation account - #[error("Provided list of seed configurations too large for a validation account")] - SeedConfigsTooLarge, - /// Not enough bytes available to pack seed configuration - #[error("Not enough bytes available to pack seed configuration")] - NotEnoughBytesForSeed, - /// The provided bytes are not valid for a seed configuration - #[error("The provided bytes are not valid for a seed configuration")] - InvalidBytesForSeed, - /// Tried to pack an invalid seed configuration - #[error("Tried to pack an invalid seed configuration")] - InvalidSeedConfig, - /// Instruction data too small for seed configuration - #[error("Instruction data too small for seed configuration")] - InstructionDataTooSmall, - /// Could not find account at specified index - #[error("Could not find account at specified index")] - AccountNotFound, - /// Error in checked math operation - #[error("Error in checked math operation")] - CalculationFailure, - /// Could not find account data at specified index - #[error("Could not find account data at specified index")] - AccountDataNotFound, - /// Account data too small for requested seed configuration - #[error("Account data too small for requested seed configuration")] - AccountDataTooSmall, - /// Failed to fetch account - #[error("Failed to fetch account")] - AccountFetchFailed, - /// Not enough bytes available to pack pubkey data configuration. - #[error("Not enough bytes available to pack pubkey data configuration")] - NotEnoughBytesForPubkeyData, - /// The provided bytes are not valid for a pubkey data configuration - #[error("The provided bytes are not valid for a pubkey data configuration")] - InvalidBytesForPubkeyData, - /// Tried to pack an invalid pubkey data configuration - #[error("Tried to pack an invalid pubkey data configuration")] - InvalidPubkeyDataConfig, -} - -impl From for ProgramError { - fn from(e: AccountResolutionError) -> Self { - ProgramError::Custom(e as u32) - } -} - -impl DecodeError for AccountResolutionError { - fn type_of() -> &'static str { - "AccountResolutionError" - } -} - -impl PrintProgramError for AccountResolutionError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - AccountResolutionError::IncorrectAccount => { - msg!("Incorrect account provided") - } - AccountResolutionError::NotEnoughAccounts => { - msg!("Not enough accounts provided") - } - AccountResolutionError::TlvUninitialized => { - msg!("No value initialized in TLV data") - } - AccountResolutionError::TlvInitialized => { - msg!("Some value initialized in TLV data") - } - AccountResolutionError::TooManyPubkeys => { - msg!("Too many pubkeys provided") - } - AccountResolutionError::InvalidPubkey => { - msg!("Failed to parse `Pubkey` from bytes") - } - AccountResolutionError::AccountTypeNotAccountMeta => { - msg!( - "Attempted to deserialize an `AccountMeta` but the underlying type has PDA configs rather than a fixed address", - ) - } - AccountResolutionError::SeedConfigsTooLarge => { - msg!("Provided list of seed configurations too large for a validation account",) - } - AccountResolutionError::NotEnoughBytesForSeed => { - msg!("Not enough bytes available to pack seed configuration",) - } - AccountResolutionError::InvalidBytesForSeed => { - msg!("The provided bytes are not valid for a seed configuration",) - } - AccountResolutionError::InvalidSeedConfig => { - msg!("Tried to pack an invalid seed configuration",) - } - AccountResolutionError::InstructionDataTooSmall => { - msg!("Instruction data too small for seed configuration",) - } - AccountResolutionError::AccountNotFound => { - msg!("Could not find account at specified index",) - } - AccountResolutionError::CalculationFailure => { - msg!("Error in checked math operation") - } - AccountResolutionError::AccountDataNotFound => { - msg!("Could not find account data at specified index",) - } - AccountResolutionError::AccountDataTooSmall => { - msg!("Account data too small for requested seed configuration",) - } - AccountResolutionError::AccountFetchFailed => { - msg!("Failed to fetch account") - } - AccountResolutionError::NotEnoughBytesForPubkeyData => { - msg!("Not enough bytes available to pack pubkey data configuration",) - } - AccountResolutionError::InvalidBytesForPubkeyData => { - msg!("The provided bytes are not valid for a pubkey data configuration",) - } - AccountResolutionError::InvalidPubkeyDataConfig => { - msg!("Tried to pack an invalid pubkey data configuration",) - } - } - } -} diff --git a/libraries/tlv-account-resolution/src/lib.rs b/libraries/tlv-account-resolution/src/lib.rs deleted file mode 100644 index e015fc972ec..00000000000 --- a/libraries/tlv-account-resolution/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Crate defining a state interface for offchain account resolution. If a -//! program writes the proper state information into one of their accounts, any -//! offchain and onchain client can fetch any additional required accounts for -//! an instruction. - -#![allow(clippy::arithmetic_side_effects)] -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -pub mod account; -pub mod error; -pub mod pubkey_data; -pub mod seeds; -pub mod state; - -// Export current sdk types for downstream users building with a different sdk -// version -pub use { - solana_account_info, solana_decode_error, solana_instruction, solana_msg, solana_program_error, - solana_pubkey, -}; diff --git a/libraries/tlv-account-resolution/src/pubkey_data.rs b/libraries/tlv-account-resolution/src/pubkey_data.rs deleted file mode 100644 index 033d72f3a90..00000000000 --- a/libraries/tlv-account-resolution/src/pubkey_data.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! Types for managing extra account meta keys that may be extracted from some -//! data. -//! -//! This can be either account data from some account in the list of accounts -//! or from the instruction data itself. - -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use {crate::error::AccountResolutionError, solana_program_error::ProgramError}; - -/// Enum to describe a required key stored in some data. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -pub enum PubkeyData { - /// Uninitialized configuration byte space. - Uninitialized, - /// A pubkey to be resolved from the instruction data. - /// - /// Packed as: - /// * 1 - Discriminator - /// * 1 - Start index of instruction data - /// - /// Note: Length is always 32 bytes. - InstructionData { - /// The index where the address bytes begin in the instruction data. - index: u8, - }, - /// A pubkey to be resolved from the inner data of some account. - /// - /// Packed as: - /// * 1 - Discriminator - /// * 1 - Index of account in accounts list - /// * 1 - Start index of account data - /// - /// Note: Length is always 32 bytes. - AccountData { - /// The index of the account in the entire accounts list. - account_index: u8, - /// The index where the address bytes begin in the account data. - data_index: u8, - }, -} -impl PubkeyData { - /// Get the size of a pubkey data configuration. - pub fn tlv_size(&self) -> u8 { - match self { - Self::Uninitialized => 0, - // 1 byte for the discriminator, 1 byte for the index. - Self::InstructionData { .. } => 1 + 1, - // 1 byte for the discriminator, 1 byte for the account index, - // 1 byte for the data index. - Self::AccountData { .. } => 1 + 1 + 1, - } - } - - /// Packs a pubkey data configuration into a slice. - pub fn pack(&self, dst: &mut [u8]) -> Result<(), ProgramError> { - // Because no `PubkeyData` variant is larger than 3 bytes, this check - // is sufficient for the data length. - if dst.len() != self.tlv_size() as usize { - return Err(AccountResolutionError::NotEnoughBytesForPubkeyData.into()); - } - match &self { - Self::Uninitialized => { - return Err(AccountResolutionError::InvalidPubkeyDataConfig.into()) - } - Self::InstructionData { index } => { - dst[0] = 1; - dst[1] = *index; - } - Self::AccountData { - account_index, - data_index, - } => { - dst[0] = 2; - dst[1] = *account_index; - dst[2] = *data_index; - } - } - Ok(()) - } - - /// Packs a pubkey data configuration into a 32-byte array, filling the - /// rest with 0s. - pub fn pack_into_address_config(key_data: &Self) -> Result<[u8; 32], ProgramError> { - let mut packed = [0u8; 32]; - let tlv_size = key_data.tlv_size() as usize; - key_data.pack(&mut packed[..tlv_size])?; - Ok(packed) - } - - /// Unpacks a pubkey data configuration from a slice. - pub fn unpack(bytes: &[u8]) -> Result { - let (discrim, rest) = bytes - .split_first() - .ok_or::(ProgramError::InvalidAccountData)?; - match discrim { - 0 => Ok(Self::Uninitialized), - 1 => { - if rest.is_empty() { - return Err(AccountResolutionError::InvalidBytesForPubkeyData.into()); - } - Ok(Self::InstructionData { index: rest[0] }) - } - 2 => { - if rest.len() < 2 { - return Err(AccountResolutionError::InvalidBytesForPubkeyData.into()); - } - Ok(Self::AccountData { - account_index: rest[0], - data_index: rest[1], - }) - } - _ => Err(ProgramError::InvalidAccountData), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pack() { - // Should fail if the length is too short. - let key = PubkeyData::InstructionData { index: 0 }; - let mut packed = vec![0u8; key.tlv_size() as usize - 1]; - assert_eq!( - key.pack(&mut packed).unwrap_err(), - AccountResolutionError::NotEnoughBytesForPubkeyData.into(), - ); - - // Should fail if the length is too long. - let key = PubkeyData::InstructionData { index: 0 }; - let mut packed = vec![0u8; key.tlv_size() as usize + 1]; - assert_eq!( - key.pack(&mut packed).unwrap_err(), - AccountResolutionError::NotEnoughBytesForPubkeyData.into(), - ); - - // Can't pack a `PubkeyData::Uninitialized`. - let key = PubkeyData::Uninitialized; - let mut packed = vec![0u8; key.tlv_size() as usize]; - assert_eq!( - key.pack(&mut packed).unwrap_err(), - AccountResolutionError::InvalidPubkeyDataConfig.into(), - ); - } - - #[test] - fn test_unpack() { - // Can unpack zeroes. - let zeroes = [0u8; 32]; - let key = PubkeyData::unpack(&zeroes).unwrap(); - assert_eq!(key, PubkeyData::Uninitialized); - - // Should fail for empty bytes. - let bytes = []; - assert_eq!( - PubkeyData::unpack(&bytes).unwrap_err(), - ProgramError::InvalidAccountData - ); - } - - fn test_pack_unpack_key(key: PubkeyData) { - let tlv_size = key.tlv_size() as usize; - let mut packed = vec![0u8; tlv_size]; - key.pack(&mut packed).unwrap(); - let unpacked = PubkeyData::unpack(&packed).unwrap(); - assert_eq!(key, unpacked); - } - - #[test] - fn test_pack_unpack() { - // Instruction data. - test_pack_unpack_key(PubkeyData::InstructionData { index: 0 }); - - // Account data. - test_pack_unpack_key(PubkeyData::AccountData { - account_index: 0, - data_index: 0, - }); - } -} diff --git a/libraries/tlv-account-resolution/src/seeds.rs b/libraries/tlv-account-resolution/src/seeds.rs deleted file mode 100644 index 1f51128e30f..00000000000 --- a/libraries/tlv-account-resolution/src/seeds.rs +++ /dev/null @@ -1,524 +0,0 @@ -//! Types for managing seed configurations in TLV Account Resolution -//! -//! As determined by the `address_config` field of `ExtraAccountMeta`, -//! seed configurations are limited to a maximum of 32 bytes. -//! This means that the maximum number of seed configurations that can be -//! packed into a single `ExtraAccountMeta` will depend directly on the size -//! of the seed configurations themselves. -//! -//! Sizes are as follows: -//! * `Seed::Literal`: 1 + 1 + N -//! * 1 - Discriminator -//! * 1 - Length of literal -//! * N - Literal bytes themselves -//! * `Seed::InstructionData`: 1 + 1 + 1 = 3 -//! * 1 - Discriminator -//! * 1 - Start index of instruction data -//! * 1 - Length of instruction data starting at index -//! * `Seed::AccountKey` - 1 + 1 = 2 -//! * 1 - Discriminator -//! * 1 - Index of account in accounts list -//! * `Seed::AccountData`: 1 + 1 + 1 + 1 = 4 -//! * 1 - Discriminator -//! * 1 - Index of account in accounts list -//! * 1 - Start index of account data -//! * 1 - Length of account data starting at index -//! -//! No matter which types of seeds you choose, the total size of all seed -//! configurations must be less than or equal to 32 bytes. - -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use {crate::error::AccountResolutionError, solana_program_error::ProgramError}; - -/// Enum to describe a required seed for a Program-Derived Address -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -pub enum Seed { - /// Uninitialized configuration byte space - Uninitialized, - /// A literal hard-coded argument - /// Packed as: - /// * 1 - Discriminator - /// * 1 - Length of literal - /// * N - Literal bytes themselves - Literal { - /// The literal value represented as a vector of bytes. - /// - /// For example, if a literal value is a string literal, - /// such as "my-seed", this value would be - /// `"my-seed".as_bytes().to_vec()`. - bytes: Vec, - }, - /// An instruction-provided argument, to be resolved from the instruction - /// data - /// Packed as: - /// * 1 - Discriminator - /// * 1 - Start index of instruction data - /// * 1 - Length of instruction data starting at index - InstructionData { - /// The index where the bytes of an instruction argument begin - index: u8, - /// The length of the instruction argument (number of bytes) - /// - /// Note: Max seed length is 32 bytes, so `u8` is appropriate here - length: u8, - }, - /// The public key of an account from the entire accounts list. - /// Note: This includes an extra accounts required. - /// - /// Packed as: - /// * 1 - Discriminator - /// * 1 - Index of account in accounts list - AccountKey { - /// The index of the account in the entire accounts list - index: u8, - }, - /// An argument to be resolved from the inner data of some account - /// Packed as: - /// * 1 - Discriminator - /// * 1 - Index of account in accounts list - /// * 1 - Start index of account data - /// * 1 - Length of account data starting at index - #[cfg_attr( - feature = "serde-traits", - serde(rename_all = "camelCase", alias = "account_data") - )] - AccountData { - /// The index of the account in the entire accounts list - account_index: u8, - /// The index where the bytes of an account data argument begin - data_index: u8, - /// The length of the argument (number of bytes) - /// - /// Note: Max seed length is 32 bytes, so `u8` is appropriate here - length: u8, - }, -} -impl Seed { - /// Get the size of a seed configuration - pub fn tlv_size(&self) -> u8 { - match &self { - // 1 byte for the discriminator - Self::Uninitialized => 0, - // 1 byte for the discriminator, 1 byte for the length of the bytes, then the raw bytes - Self::Literal { bytes } => 1 + 1 + bytes.len() as u8, - // 1 byte for the discriminator, 1 byte for the index, 1 byte for the length - Self::InstructionData { .. } => 1 + 1 + 1, - // 1 byte for the discriminator, 1 byte for the index - Self::AccountKey { .. } => 1 + 1, - // 1 byte for the discriminator, 1 byte for the account index, - // 1 byte for the data index 1 byte for the length - Self::AccountData { .. } => 1 + 1 + 1 + 1, - } - } - - /// Packs a seed configuration into a slice - pub fn pack(&self, dst: &mut [u8]) -> Result<(), ProgramError> { - if dst.len() != self.tlv_size() as usize { - return Err(AccountResolutionError::NotEnoughBytesForSeed.into()); - } - if dst.len() > 32 { - return Err(AccountResolutionError::SeedConfigsTooLarge.into()); - } - match &self { - Self::Uninitialized => return Err(AccountResolutionError::InvalidSeedConfig.into()), - Self::Literal { bytes } => { - dst[0] = 1; - dst[1] = bytes.len() as u8; - dst[2..].copy_from_slice(bytes); - } - Self::InstructionData { index, length } => { - dst[0] = 2; - dst[1] = *index; - dst[2] = *length; - } - Self::AccountKey { index } => { - dst[0] = 3; - dst[1] = *index; - } - Self::AccountData { - account_index, - data_index, - length, - } => { - dst[0] = 4; - dst[1] = *account_index; - dst[2] = *data_index; - dst[3] = *length; - } - } - Ok(()) - } - - /// Packs a vector of seed configurations into a 32-byte array, - /// filling the rest with 0s. Errors if it overflows. - pub fn pack_into_address_config(seeds: &[Self]) -> Result<[u8; 32], ProgramError> { - let mut packed = [0u8; 32]; - let mut i: usize = 0; - for seed in seeds { - let seed_size = seed.tlv_size() as usize; - let slice_end = i + seed_size; - if slice_end > 32 { - return Err(AccountResolutionError::SeedConfigsTooLarge.into()); - } - seed.pack(&mut packed[i..slice_end])?; - i = slice_end; - } - Ok(packed) - } - - /// Unpacks a seed configuration from a slice - pub fn unpack(bytes: &[u8]) -> Result { - let (discrim, rest) = bytes - .split_first() - .ok_or::(ProgramError::InvalidAccountData)?; - match discrim { - 0 => Ok(Self::Uninitialized), - 1 => unpack_seed_literal(rest), - 2 => unpack_seed_instruction_arg(rest), - 3 => unpack_seed_account_key(rest), - 4 => unpack_seed_account_data(rest), - _ => Err(ProgramError::InvalidAccountData), - } - } - - /// Unpacks all seed configurations from a 32-byte array. - /// Stops when it hits uninitialized data (0s). - pub fn unpack_address_config(address_config: &[u8; 32]) -> Result, ProgramError> { - let mut seeds = vec![]; - let mut i = 0; - while i < 32 { - let seed = Self::unpack(&address_config[i..])?; - let seed_size = seed.tlv_size() as usize; - i += seed_size; - if seed == Self::Uninitialized { - break; - } - seeds.push(seed); - } - Ok(seeds) - } -} - -fn unpack_seed_literal(bytes: &[u8]) -> Result { - let (length, rest) = bytes - .split_first() - // Should be at least 1 byte - .ok_or::(AccountResolutionError::InvalidBytesForSeed.into())?; - let length = *length as usize; - if rest.len() < length { - // Should be at least `length` bytes - return Err(AccountResolutionError::InvalidBytesForSeed.into()); - } - Ok(Seed::Literal { - bytes: rest[..length].to_vec(), - }) -} - -fn unpack_seed_instruction_arg(bytes: &[u8]) -> Result { - if bytes.len() < 2 { - // Should be at least 2 bytes - return Err(AccountResolutionError::InvalidBytesForSeed.into()); - } - Ok(Seed::InstructionData { - index: bytes[0], - length: bytes[1], - }) -} - -fn unpack_seed_account_key(bytes: &[u8]) -> Result { - if bytes.is_empty() { - // Should be at least 1 byte - return Err(AccountResolutionError::InvalidBytesForSeed.into()); - } - Ok(Seed::AccountKey { index: bytes[0] }) -} - -fn unpack_seed_account_data(bytes: &[u8]) -> Result { - if bytes.len() < 3 { - // Should be at least 3 bytes - return Err(AccountResolutionError::InvalidBytesForSeed.into()); - } - Ok(Seed::AccountData { - account_index: bytes[0], - data_index: bytes[1], - length: bytes[2], - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pack() { - // Seed too large - let seed = Seed::Literal { bytes: vec![1; 33] }; - let mut packed = vec![0u8; seed.tlv_size() as usize]; - assert_eq!( - seed.pack(&mut packed).unwrap_err(), - AccountResolutionError::SeedConfigsTooLarge.into() - ); - assert_eq!( - Seed::pack_into_address_config(&[seed]).unwrap_err(), - AccountResolutionError::SeedConfigsTooLarge.into() - ); - - // Should fail if the length is wrong - let seed = Seed::Literal { bytes: vec![1; 12] }; - let mut packed = vec![0u8; seed.tlv_size() as usize - 1]; - assert_eq!( - seed.pack(&mut packed).unwrap_err(), - AccountResolutionError::NotEnoughBytesForSeed.into() - ); - - // Can't pack a `Seed::Uninitialized` - let seed = Seed::Uninitialized; - let mut packed = vec![0u8; seed.tlv_size() as usize]; - assert_eq!( - seed.pack(&mut packed).unwrap_err(), - AccountResolutionError::InvalidSeedConfig.into() - ); - } - - #[test] - fn test_pack_address_config() { - // Should fail if one seed is too large - let seed = Seed::Literal { bytes: vec![1; 36] }; - assert_eq!( - Seed::pack_into_address_config(&[seed]).unwrap_err(), - AccountResolutionError::SeedConfigsTooLarge.into() - ); - - // Should fail if the combination of all seeds is too large - let seed1 = Seed::Literal { bytes: vec![1; 30] }; // 30 bytes - let seed2 = Seed::InstructionData { - index: 0, - length: 4, - }; // 3 bytes - assert_eq!( - Seed::pack_into_address_config(&[seed1, seed2]).unwrap_err(), - AccountResolutionError::SeedConfigsTooLarge.into() - ); - } - - #[test] - fn test_unpack() { - // Can unpack zeroes - let zeroes = [0u8; 32]; - let seeds = Seed::unpack_address_config(&zeroes).unwrap(); - assert_eq!(seeds, vec![]); - - // Should fail for empty bytes - let bytes = []; - assert_eq!( - Seed::unpack(&bytes).unwrap_err(), - ProgramError::InvalidAccountData - ); - - // Should fail if bytes are malformed for literal seed - let bytes = [ - 1, // Discrim (Literal) - 4, // Length - 1, 1, 1, // Incorrect length - ]; - assert_eq!( - Seed::unpack(&bytes).unwrap_err(), - AccountResolutionError::InvalidBytesForSeed.into() - ); - - // Should fail if bytes are malformed for literal seed - let bytes = [ - 2, // Discrim (InstructionData) - 2, // Index (Length missing) - ]; - assert_eq!( - Seed::unpack(&bytes).unwrap_err(), - AccountResolutionError::InvalidBytesForSeed.into() - ); - - // Should fail if bytes are malformed for literal seed - let bytes = [ - 3, // Discrim (AccountKey, Index missing) - ]; - assert_eq!( - Seed::unpack(&bytes).unwrap_err(), - AccountResolutionError::InvalidBytesForSeed.into() - ); - } - - #[test] - fn test_unpack_address_config() { - // Should fail if bytes are malformed - let bytes = [ - 1, // Discrim (Literal) - 4, // Length - 1, 1, 1, 1, // 4 - 6, // Discrim (Invalid) - 2, // Index - 1, // Length - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - assert_eq!( - Seed::unpack_address_config(&bytes).unwrap_err(), - ProgramError::InvalidAccountData - ); - - // Should fail if 32nd byte is not zero, but it would be the - // start of a config - // - // Namely, if a seed config is unpacked and leaves 1 byte remaining, - // it has to be 0, since no valid seed config can be 1 byte long - let bytes = [ - 1, // Discrim (Literal) - 16, // Length - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 16 - 1, // Discrim (Literal) - 11, // Length - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 11 - 2, // Non-zero byte - ]; - assert_eq!( - Seed::unpack_address_config(&bytes).unwrap_err(), - AccountResolutionError::InvalidBytesForSeed.into(), - ); - - // Should pass if 31st byte is not zero, but it would be - // the start of a config - // - // Similar to above, however we now have 2 bytes to work with, - // which could be a valid seed config - let bytes = [ - 1, // Discrim (Literal) - 16, // Length - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 16 - 1, // Discrim (Literal) - 10, // Length - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 10 - 3, // Non-zero byte - Discrim (AccountKey) - 0, // Index - ]; - assert_eq!( - Seed::unpack_address_config(&bytes).unwrap(), - vec![ - Seed::Literal { - bytes: vec![1u8; 16] - }, - Seed::Literal { - bytes: vec![1u8; 10] - }, - Seed::AccountKey { index: 0 } - ], - ); - - // Should fail if 31st byte is not zero and a valid seed config - // discriminator, but the seed config requires more than 2 bytes - let bytes = [ - 1, // Discrim (Literal) - 16, // Length - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 16 - 1, // Discrim (Literal) - 10, // Length - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 10 - 2, // Non-zero byte - Discrim (InstructionData) - 0, // Index (Length missing) - ]; - assert_eq!( - Seed::unpack_address_config(&bytes).unwrap_err(), - AccountResolutionError::InvalidBytesForSeed.into(), - ); - } - - fn test_pack_unpack_seed(seed: Seed) { - let tlv_size = seed.tlv_size() as usize; - let mut packed = vec![0u8; tlv_size]; - seed.pack(&mut packed).unwrap(); - let unpacked = Seed::unpack(&packed).unwrap(); - assert_eq!(seed, unpacked); - } - - #[test] - fn test_pack_unpack() { - let mut mixed = vec![]; - - // Literals - - let bytes = b"hello"; - let seed = Seed::Literal { - bytes: bytes.to_vec(), - }; - test_pack_unpack_seed(seed); - - let bytes = 8u8.to_le_bytes(); - let seed = Seed::Literal { - bytes: bytes.to_vec(), - }; - test_pack_unpack_seed(seed.clone()); - mixed.push(seed); - - let bytes = 32u32.to_le_bytes(); - let seed = Seed::Literal { - bytes: bytes.to_vec(), - }; - test_pack_unpack_seed(seed.clone()); - mixed.push(seed); - - // Instruction args - - let seed = Seed::InstructionData { - index: 0, - length: 0, - }; - test_pack_unpack_seed(seed); - - let seed = Seed::InstructionData { - index: 6, - length: 9, - }; - test_pack_unpack_seed(seed.clone()); - mixed.push(seed); - - // Account keys - - let seed = Seed::AccountKey { index: 0 }; - test_pack_unpack_seed(seed); - - let seed = Seed::AccountKey { index: 9 }; - test_pack_unpack_seed(seed.clone()); - mixed.push(seed); - - // Account data - - let seed = Seed::AccountData { - account_index: 0, - data_index: 0, - length: 0, - }; - test_pack_unpack_seed(seed); - - let seed = Seed::AccountData { - account_index: 0, - data_index: 0, - length: 9, - }; - test_pack_unpack_seed(seed.clone()); - mixed.push(seed); - - // Arrays - - let packed_array = Seed::pack_into_address_config(&mixed).unwrap(); - let unpacked_array = Seed::unpack_address_config(&packed_array).unwrap(); - assert_eq!(mixed, unpacked_array); - - let mut shuffled_mixed = mixed.clone(); - shuffled_mixed.swap(0, 1); - shuffled_mixed.swap(1, 4); - shuffled_mixed.swap(3, 0); - - let packed_array = Seed::pack_into_address_config(&shuffled_mixed).unwrap(); - let unpacked_array = Seed::unpack_address_config(&packed_array).unwrap(); - assert_eq!(shuffled_mixed, unpacked_array); - } -} diff --git a/libraries/tlv-account-resolution/src/state.rs b/libraries/tlv-account-resolution/src/state.rs deleted file mode 100644 index f17ee3d06fb..00000000000 --- a/libraries/tlv-account-resolution/src/state.rs +++ /dev/null @@ -1,1726 +0,0 @@ -//! State transition types - -use { - crate::{account::ExtraAccountMeta, error::AccountResolutionError}, - solana_account_info::AccountInfo, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - spl_discriminator::SplDiscriminate, - spl_pod::slice::{PodSlice, PodSliceMut}, - spl_type_length_value::state::{TlvState, TlvStateBorrowed, TlvStateMut}, - std::future::Future, -}; - -/// Type representing the output of an account fetching function, for easy -/// chaining between APIs -pub type AccountDataResult = Result>, AccountFetchError>; -/// Generic error type that can come out of any client while fetching account -/// data -pub type AccountFetchError = Box; - -/// Helper to convert an `AccountInfo` to an `AccountMeta` -fn account_info_to_meta(account_info: &AccountInfo) -> AccountMeta { - AccountMeta { - pubkey: *account_info.key, - is_signer: account_info.is_signer, - is_writable: account_info.is_writable, - } -} - -/// De-escalate an account meta if necessary -fn de_escalate_account_meta(account_meta: &mut AccountMeta, account_metas: &[AccountMeta]) { - // This is a little tricky to read, but the idea is to see if - // this account is marked as writable or signer anywhere in - // the instruction at the start. If so, DON'T escalate it to - // be a writer or signer in the CPI - let maybe_highest_privileges = account_metas - .iter() - .filter(|&x| x.pubkey == account_meta.pubkey) - .map(|x| (x.is_signer, x.is_writable)) - .reduce(|acc, x| (acc.0 || x.0, acc.1 || x.1)); - // If `Some`, then the account was found somewhere in the instruction - if let Some((is_signer, is_writable)) = maybe_highest_privileges { - if !is_signer && is_signer != account_meta.is_signer { - // Existing account is *NOT* a signer already, but the CPI - // wants it to be, so de-escalate to not be a signer - account_meta.is_signer = false; - } - if !is_writable && is_writable != account_meta.is_writable { - // Existing account is *NOT* writable already, but the CPI - // wants it to be, so de-escalate to not be writable - account_meta.is_writable = false; - } - } -} - -/// Stateless helper for storing additional accounts required for an -/// instruction. -/// -/// This struct works with any `SplDiscriminate`, and stores the extra accounts -/// needed for that specific instruction, using the given `ArrayDiscriminator` -/// as the type-length-value `ArrayDiscriminator`, and then storing all of the -/// given `AccountMeta`s as a zero-copy slice. -/// -/// Sample usage: -/// -/// ```rust -/// use { -/// futures_util::TryFutureExt, -/// solana_client::nonblocking::rpc_client::RpcClient, -/// solana_account_info::AccountInfo, -/// solana_instruction::{AccountMeta, Instruction}, -/// solana_pubkey::Pubkey, -/// spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, -/// spl_tlv_account_resolution::{ -/// account::ExtraAccountMeta, -/// seeds::Seed, -/// state::{AccountDataResult, AccountFetchError, ExtraAccountMetaList} -/// }, -/// }; -/// -/// struct MyInstruction; -/// impl SplDiscriminate for MyInstruction { -/// // Give it a unique discriminator, can also be generated using a hash function -/// const SPL_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([1; ArrayDiscriminator::LENGTH]); -/// } -/// -/// // actually put it in the additional required account keys and signer / writable -/// let extra_metas = [ -/// AccountMeta::new(Pubkey::new_unique(), false).into(), -/// AccountMeta::new_readonly(Pubkey::new_unique(), false).into(), -/// ExtraAccountMeta::new_with_seeds( -/// &[ -/// Seed::Literal { -/// bytes: b"some_string".to_vec(), -/// }, -/// Seed::InstructionData { -/// index: 1, -/// length: 1, // u8 -/// }, -/// Seed::AccountKey { index: 1 }, -/// ], -/// false, -/// true, -/// ).unwrap(), -/// ExtraAccountMeta::new_external_pda_with_seeds( -/// 0, -/// &[Seed::AccountKey { index: 2 }], -/// false, -/// false, -/// ).unwrap(), -/// ]; -/// -/// // assume that this buffer is actually account data, already allocated to `account_size` -/// let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap(); -/// let mut buffer = vec![0; account_size]; -/// -/// // Initialize the structure for your instruction -/// ExtraAccountMetaList::init::(&mut buffer, &extra_metas).unwrap(); -/// -/// // Off-chain, you can add the additional accounts directly from the account data -/// // You need to provide the resolver a way to fetch account data off-chain -/// struct MyClient { -/// client: RpcClient, -/// } -/// impl MyClient { -/// pub fn new() -> Self { -/// Self { -/// client: RpcClient::new_mock("succeeds".to_string()), -/// } -/// } -/// pub async fn get_account_data(&self, address: Pubkey) -> AccountDataResult { -/// self.client.get_account(&address) -/// .await -/// .map(|acct| Some(acct.data)) -/// .map_err(|e| Box::new(e) as AccountFetchError) -/// } -/// } -/// -/// let client = MyClient::new(); -/// let program_id = Pubkey::new_unique(); -/// let mut instruction = Instruction::new_with_bytes(program_id, &[0, 1, 2], vec![]); -/// # futures::executor::block_on(async { -/// // Now use the resolver to add the additional accounts off-chain -/// ExtraAccountMetaList::add_to_instruction::( -/// &mut instruction, -/// |address: Pubkey| client.get_account_data(address), -/// &buffer, -/// ) -/// .await; -/// # }); -/// -/// // On-chain, you can add the additional accounts *and* account infos -/// let mut cpi_instruction = Instruction::new_with_bytes(program_id, &[0, 1, 2], vec![]); -/// let mut cpi_account_infos = vec![]; // assume the other required account infos are already included -/// let remaining_account_infos: &[AccountInfo<'_>] = &[]; // these are the account infos provided to the instruction that are *not* part of any other known interface -/// ExtraAccountMetaList::add_to_cpi_instruction::( -/// &mut cpi_instruction, -/// &mut cpi_account_infos, -/// &buffer, -/// &remaining_account_infos, -/// ); -/// ``` -pub struct ExtraAccountMetaList; -impl ExtraAccountMetaList { - /// Initialize pod slice data for the given instruction and its required - /// list of `ExtraAccountMeta`s - pub fn init( - data: &mut [u8], - extra_account_metas: &[ExtraAccountMeta], - ) -> Result<(), ProgramError> { - let mut state = TlvStateMut::unpack(data).unwrap(); - let tlv_size = PodSlice::::size_of(extra_account_metas.len())?; - let (bytes, _) = state.alloc::(tlv_size, false)?; - let mut validation_data = PodSliceMut::init(bytes)?; - for meta in extra_account_metas { - validation_data.push(*meta)?; - } - Ok(()) - } - - /// Update pod slice data for the given instruction and its required - /// list of `ExtraAccountMeta`s - pub fn update( - data: &mut [u8], - extra_account_metas: &[ExtraAccountMeta], - ) -> Result<(), ProgramError> { - let mut state = TlvStateMut::unpack(data).unwrap(); - let tlv_size = PodSlice::::size_of(extra_account_metas.len())?; - let bytes = state.realloc_first::(tlv_size)?; - let mut validation_data = PodSliceMut::init(bytes)?; - for meta in extra_account_metas { - validation_data.push(*meta)?; - } - Ok(()) - } - - /// Get the underlying `PodSlice` from an unpacked TLV - /// - /// Due to lifetime annoyances, this function can't just take in the bytes, - /// since then we would be returning a reference to a locally created - /// `TlvStateBorrowed`. I hope there's a better way to do this! - pub fn unpack_with_tlv_state<'a, T: SplDiscriminate>( - tlv_state: &'a TlvStateBorrowed, - ) -> Result, ProgramError> { - let bytes = tlv_state.get_first_bytes::()?; - PodSlice::::unpack(bytes) - } - - /// Get the byte size required to hold `num_items` items - pub fn size_of(num_items: usize) -> Result { - Ok(TlvStateBorrowed::get_base_len() - .saturating_add(PodSlice::::size_of(num_items)?)) - } - - /// Checks provided account infos against validation data, using - /// instruction data and program ID to resolve any dynamic PDAs - /// if necessary. - /// - /// Note: this function will also verify all extra required accounts - /// have been provided in the correct order - pub fn check_account_infos( - account_infos: &[AccountInfo], - instruction_data: &[u8], - program_id: &Pubkey, - data: &[u8], - ) -> Result<(), ProgramError> { - let state = TlvStateBorrowed::unpack(data).unwrap(); - let extra_meta_list = ExtraAccountMetaList::unpack_with_tlv_state::(&state)?; - let extra_account_metas = extra_meta_list.data(); - - let initial_accounts_len = account_infos.len() - extra_account_metas.len(); - - // Convert to `AccountMeta` to check resolved metas - let provided_metas = account_infos - .iter() - .map(account_info_to_meta) - .collect::>(); - - for (i, config) in extra_account_metas.iter().enumerate() { - let meta = { - // Create a list of `Ref`s so we can reference account data in the - // resolution step - let account_key_data_refs = account_infos - .iter() - .map(|info| { - let key = *info.key; - let data = info.try_borrow_data()?; - Ok((key, data)) - }) - .collect::, ProgramError>>()?; - - config.resolve(instruction_data, program_id, |usize| { - account_key_data_refs - .get(usize) - .map(|(pubkey, opt_data)| (pubkey, Some(opt_data.as_ref()))) - })? - }; - - // Ensure the account is in the correct position - let expected_index = i - .checked_add(initial_accounts_len) - .ok_or::(AccountResolutionError::CalculationFailure.into())?; - if provided_metas.get(expected_index) != Some(&meta) { - return Err(AccountResolutionError::IncorrectAccount.into()); - } - } - - Ok(()) - } - - /// Add the additional account metas to an existing instruction - pub async fn add_to_instruction( - instruction: &mut Instruction, - fetch_account_data_fn: F, - data: &[u8], - ) -> Result<(), ProgramError> - where - F: Fn(Pubkey) -> Fut, - Fut: Future, - { - let state = TlvStateBorrowed::unpack(data)?; - let bytes = state.get_first_bytes::()?; - let extra_account_metas = PodSlice::::unpack(bytes)?; - - // Fetch account data for each of the instruction accounts - let mut account_key_datas = vec![]; - for meta in instruction.accounts.iter() { - let account_data = fetch_account_data_fn(meta.pubkey) - .await - .map_err::(|_| { - AccountResolutionError::AccountFetchFailed.into() - })?; - account_key_datas.push((meta.pubkey, account_data)); - } - - for extra_meta in extra_account_metas.data().iter() { - let mut meta = - extra_meta.resolve(&instruction.data, &instruction.program_id, |usize| { - account_key_datas - .get(usize) - .map(|(pubkey, opt_data)| (pubkey, opt_data.as_ref().map(|x| x.as_slice()))) - })?; - de_escalate_account_meta(&mut meta, &instruction.accounts); - - // Fetch account data for the new account - account_key_datas.push(( - meta.pubkey, - fetch_account_data_fn(meta.pubkey) - .await - .map_err::(|_| { - AccountResolutionError::AccountFetchFailed.into() - })?, - )); - instruction.accounts.push(meta); - } - Ok(()) - } - - /// Add the additional account metas and account infos for a CPI - pub fn add_to_cpi_instruction<'a, T: SplDiscriminate>( - cpi_instruction: &mut Instruction, - cpi_account_infos: &mut Vec>, - data: &[u8], - account_infos: &[AccountInfo<'a>], - ) -> Result<(), ProgramError> { - let state = TlvStateBorrowed::unpack(data)?; - let bytes = state.get_first_bytes::()?; - let extra_account_metas = PodSlice::::unpack(bytes)?; - - for extra_meta in extra_account_metas.data().iter() { - let mut meta = { - // Create a list of `Ref`s so we can reference account data in the - // resolution step - let account_key_data_refs = cpi_account_infos - .iter() - .map(|info| { - let key = *info.key; - let data = info.try_borrow_data()?; - Ok((key, data)) - }) - .collect::, ProgramError>>()?; - - extra_meta.resolve( - &cpi_instruction.data, - &cpi_instruction.program_id, - |usize| { - account_key_data_refs - .get(usize) - .map(|(pubkey, opt_data)| (pubkey, Some(opt_data.as_ref()))) - }, - )? - }; - de_escalate_account_meta(&mut meta, &cpi_instruction.accounts); - - let account_info = account_infos - .iter() - .find(|&x| *x.key == meta.pubkey) - .ok_or(AccountResolutionError::IncorrectAccount)? - .clone(); - - cpi_instruction.accounts.push(meta); - cpi_account_infos.push(account_info); - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{pubkey_data::PubkeyData, seeds::Seed}, - solana_instruction::AccountMeta, - solana_program_test::tokio, - solana_pubkey::Pubkey, - spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, - std::collections::HashMap, - }; - - pub struct TestInstruction; - impl SplDiscriminate for TestInstruction { - const SPL_DISCRIMINATOR: ArrayDiscriminator = - ArrayDiscriminator::new([1; ArrayDiscriminator::LENGTH]); - } - - pub struct TestOtherInstruction; - impl SplDiscriminate for TestOtherInstruction { - const SPL_DISCRIMINATOR: ArrayDiscriminator = - ArrayDiscriminator::new([2; ArrayDiscriminator::LENGTH]); - } - - pub struct MockRpc<'a> { - cache: HashMap>, - } - impl<'a> MockRpc<'a> { - pub fn setup(account_infos: &'a [AccountInfo<'a>]) -> Self { - let mut cache = HashMap::new(); - for info in account_infos { - cache.insert(*info.key, info); - } - Self { cache } - } - - pub async fn get_account_data(&self, pubkey: Pubkey) -> AccountDataResult { - Ok(self - .cache - .get(&pubkey) - .map(|account| account.try_borrow_data().unwrap().to_vec())) - } - } - - #[tokio::test] - async fn init_with_metas() { - let metas = [ - AccountMeta::new(Pubkey::new_unique(), false).into(), - AccountMeta::new(Pubkey::new_unique(), true).into(), - AccountMeta::new_readonly(Pubkey::new_unique(), true).into(), - AccountMeta::new_readonly(Pubkey::new_unique(), false).into(), - ]; - let account_size = ExtraAccountMetaList::size_of(metas.len()).unwrap(); - let mut buffer = vec![0; account_size]; - - ExtraAccountMetaList::init::(&mut buffer, &metas).unwrap(); - - let mock_rpc = MockRpc::setup(&[]); - - let mut instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); - ExtraAccountMetaList::add_to_instruction::( - &mut instruction, - |pubkey| mock_rpc.get_account_data(pubkey), - &buffer, - ) - .await - .unwrap(); - - let check_metas = metas - .iter() - .map(|e| AccountMeta::try_from(e).unwrap()) - .collect::>(); - - assert_eq!(instruction.accounts, check_metas,); - } - - #[tokio::test] - async fn init_with_infos() { - let program_id = Pubkey::new_unique(); - - let pubkey1 = Pubkey::new_unique(); - let mut lamports1 = 0; - let mut data1 = []; - let pubkey2 = Pubkey::new_unique(); - let mut lamports2 = 0; - let mut data2 = [4, 4, 4, 6, 6, 6, 8, 8]; - let pubkey3 = Pubkey::new_unique(); - let mut lamports3 = 0; - let mut data3 = []; - let owner = Pubkey::new_unique(); - let account_infos = [ - AccountInfo::new( - &pubkey1, - false, - true, - &mut lamports1, - &mut data1, - &owner, - false, - 0, - ), - AccountInfo::new( - &pubkey2, - true, - false, - &mut lamports2, - &mut data2, - &owner, - false, - 0, - ), - AccountInfo::new( - &pubkey3, - false, - false, - &mut lamports3, - &mut data3, - &owner, - false, - 0, - ), - ]; - - let required_pda = ExtraAccountMeta::new_with_seeds( - &[ - Seed::AccountKey { index: 0 }, - Seed::AccountData { - account_index: 1, - data_index: 2, - length: 4, - }, - ], - false, - true, - ) - .unwrap(); - - // Convert to `ExtraAccountMeta` - let required_extra_accounts = [ - ExtraAccountMeta::from(&account_infos[0]), - ExtraAccountMeta::from(&account_infos[1]), - ExtraAccountMeta::from(&account_infos[2]), - required_pda, - ]; - - let account_size = ExtraAccountMetaList::size_of(required_extra_accounts.len()).unwrap(); - let mut buffer = vec![0; account_size]; - - ExtraAccountMetaList::init::(&mut buffer, &required_extra_accounts) - .unwrap(); - - let mock_rpc = MockRpc::setup(&account_infos); - - let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]); - ExtraAccountMetaList::add_to_instruction::( - &mut instruction, - |pubkey| mock_rpc.get_account_data(pubkey), - &buffer, - ) - .await - .unwrap(); - - let (check_required_pda, _) = Pubkey::find_program_address( - &[ - account_infos[0].key.as_ref(), // Account key - &account_infos[1].try_borrow_data().unwrap()[2..6], // Account data - ], - &program_id, - ); - - // Convert to `AccountMeta` to check instruction - let check_metas = [ - account_info_to_meta(&account_infos[0]), - account_info_to_meta(&account_infos[1]), - account_info_to_meta(&account_infos[2]), - AccountMeta::new(check_required_pda, false), - ]; - - assert_eq!(instruction.accounts, check_metas,); - - assert_eq!( - instruction.accounts.get(3).unwrap().pubkey, - check_required_pda - ); - } - - #[tokio::test] - async fn init_with_extra_account_metas() { - let program_id = Pubkey::new_unique(); - - let extra_meta3_literal_str = "seed_prefix"; - - let ix_account1 = AccountMeta::new(Pubkey::new_unique(), false); - let ix_account2 = AccountMeta::new(Pubkey::new_unique(), true); - - let extra_meta1 = AccountMeta::new(Pubkey::new_unique(), false); - let extra_meta2 = AccountMeta::new(Pubkey::new_unique(), true); - let extra_meta3 = ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: extra_meta3_literal_str.as_bytes().to_vec(), - }, - Seed::InstructionData { - index: 1, - length: 1, // u8 - }, - Seed::AccountKey { index: 0 }, - Seed::AccountKey { index: 2 }, - ], - false, - true, - ) - .unwrap(); - let extra_meta4 = ExtraAccountMeta::new_with_pubkey_data( - &PubkeyData::InstructionData { index: 4 }, - false, - true, - ) - .unwrap(); - - let metas = [ - ExtraAccountMeta::from(&extra_meta1), - ExtraAccountMeta::from(&extra_meta2), - extra_meta3, - extra_meta4, - ]; - - let mut ix_data = vec![1, 2, 3, 4]; - let check_extra_meta4_pubkey = Pubkey::new_unique(); - ix_data.extend_from_slice(check_extra_meta4_pubkey.as_ref()); - - let ix_accounts = vec![ix_account1.clone(), ix_account2.clone()]; - let mut instruction = Instruction::new_with_bytes(program_id, &ix_data, ix_accounts); - - let account_size = ExtraAccountMetaList::size_of(metas.len()).unwrap(); - let mut buffer = vec![0; account_size]; - - ExtraAccountMetaList::init::(&mut buffer, &metas).unwrap(); - - let mock_rpc = MockRpc::setup(&[]); - - ExtraAccountMetaList::add_to_instruction::( - &mut instruction, - |pubkey| mock_rpc.get_account_data(pubkey), - &buffer, - ) - .await - .unwrap(); - - let check_extra_meta3_u8_arg = ix_data[1]; - let check_extra_meta3_pubkey = Pubkey::find_program_address( - &[ - extra_meta3_literal_str.as_bytes(), - &[check_extra_meta3_u8_arg], - ix_account1.pubkey.as_ref(), - extra_meta1.pubkey.as_ref(), - ], - &program_id, - ) - .0; - let check_metas = [ - ix_account1, - ix_account2, - extra_meta1, - extra_meta2, - AccountMeta::new(check_extra_meta3_pubkey, false), - AccountMeta::new(check_extra_meta4_pubkey, false), - ]; - - assert_eq!( - instruction.accounts.get(4).unwrap().pubkey, - check_extra_meta3_pubkey, - ); - assert_eq!( - instruction.accounts.get(5).unwrap().pubkey, - check_extra_meta4_pubkey, - ); - assert_eq!(instruction.accounts, check_metas,); - } - - #[tokio::test] - async fn init_multiple() { - let extra_meta5_literal_str = "seed_prefix"; - let extra_meta5_literal_u32 = 4u32; - let other_meta2_literal_str = "other_seed_prefix"; - - let extra_meta1 = AccountMeta::new(Pubkey::new_unique(), false); - let extra_meta2 = AccountMeta::new(Pubkey::new_unique(), true); - let extra_meta3 = AccountMeta::new_readonly(Pubkey::new_unique(), true); - let extra_meta4 = AccountMeta::new_readonly(Pubkey::new_unique(), false); - let extra_meta5 = ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: extra_meta5_literal_str.as_bytes().to_vec(), - }, - Seed::Literal { - bytes: extra_meta5_literal_u32.to_le_bytes().to_vec(), - }, - Seed::InstructionData { - index: 5, - length: 1, // u8 - }, - Seed::AccountKey { index: 2 }, - ], - false, - true, - ) - .unwrap(); - let extra_meta6 = ExtraAccountMeta::new_with_pubkey_data( - &PubkeyData::InstructionData { index: 8 }, - false, - true, - ) - .unwrap(); - - let other_meta1 = AccountMeta::new(Pubkey::new_unique(), false); - let other_meta2 = ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: other_meta2_literal_str.as_bytes().to_vec(), - }, - Seed::InstructionData { - index: 1, - length: 4, // u32 - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(); - let other_meta3 = ExtraAccountMeta::new_with_pubkey_data( - &PubkeyData::InstructionData { index: 7 }, - false, - true, - ) - .unwrap(); - - let metas = [ - ExtraAccountMeta::from(&extra_meta1), - ExtraAccountMeta::from(&extra_meta2), - ExtraAccountMeta::from(&extra_meta3), - ExtraAccountMeta::from(&extra_meta4), - extra_meta5, - extra_meta6, - ]; - let other_metas = [ - ExtraAccountMeta::from(&other_meta1), - other_meta2, - other_meta3, - ]; - - let account_size = ExtraAccountMetaList::size_of(metas.len()).unwrap() - + ExtraAccountMetaList::size_of(other_metas.len()).unwrap(); - let mut buffer = vec![0; account_size]; - - ExtraAccountMetaList::init::(&mut buffer, &metas).unwrap(); - ExtraAccountMetaList::init::(&mut buffer, &other_metas).unwrap(); - - let mock_rpc = MockRpc::setup(&[]); - - let program_id = Pubkey::new_unique(); - - let mut ix_data = vec![0, 0, 0, 0, 0, 7, 0, 0]; - let check_extra_meta6_pubkey = Pubkey::new_unique(); - ix_data.extend_from_slice(check_extra_meta6_pubkey.as_ref()); - - let ix_accounts = vec![]; - - let mut instruction = Instruction::new_with_bytes(program_id, &ix_data, ix_accounts); - ExtraAccountMetaList::add_to_instruction::( - &mut instruction, - |pubkey| mock_rpc.get_account_data(pubkey), - &buffer, - ) - .await - .unwrap(); - - let check_extra_meta5_u8_arg = ix_data[5]; - let check_extra_meta5_pubkey = Pubkey::find_program_address( - &[ - extra_meta5_literal_str.as_bytes(), - extra_meta5_literal_u32.to_le_bytes().as_ref(), - &[check_extra_meta5_u8_arg], - extra_meta3.pubkey.as_ref(), - ], - &program_id, - ) - .0; - let check_metas = [ - extra_meta1, - extra_meta2, - extra_meta3, - extra_meta4, - AccountMeta::new(check_extra_meta5_pubkey, false), - AccountMeta::new(check_extra_meta6_pubkey, false), - ]; - - assert_eq!( - instruction.accounts.get(4).unwrap().pubkey, - check_extra_meta5_pubkey, - ); - assert_eq!( - instruction.accounts.get(5).unwrap().pubkey, - check_extra_meta6_pubkey, - ); - assert_eq!(instruction.accounts, check_metas,); - - let program_id = Pubkey::new_unique(); - - let ix_account1 = AccountMeta::new(Pubkey::new_unique(), false); - let ix_account2 = AccountMeta::new(Pubkey::new_unique(), true); - let ix_accounts = vec![ix_account1.clone(), ix_account2.clone()]; - - let mut ix_data = vec![0, 26, 0, 0, 0, 0, 0]; - let check_other_meta3_pubkey = Pubkey::new_unique(); - ix_data.extend_from_slice(check_other_meta3_pubkey.as_ref()); - - let mut instruction = Instruction::new_with_bytes(program_id, &ix_data, ix_accounts); - ExtraAccountMetaList::add_to_instruction::( - &mut instruction, - |pubkey| mock_rpc.get_account_data(pubkey), - &buffer, - ) - .await - .unwrap(); - - let check_other_meta2_u32_arg = u32::from_le_bytes(ix_data[1..5].try_into().unwrap()); - let check_other_meta2_pubkey = Pubkey::find_program_address( - &[ - other_meta2_literal_str.as_bytes(), - check_other_meta2_u32_arg.to_le_bytes().as_ref(), - ix_account1.pubkey.as_ref(), - ], - &program_id, - ) - .0; - let check_other_metas = [ - ix_account1, - ix_account2, - other_meta1, - AccountMeta::new(check_other_meta2_pubkey, false), - AccountMeta::new(check_other_meta3_pubkey, false), - ]; - - assert_eq!( - instruction.accounts.get(3).unwrap().pubkey, - check_other_meta2_pubkey, - ); - assert_eq!( - instruction.accounts.get(4).unwrap().pubkey, - check_other_meta3_pubkey, - ); - assert_eq!(instruction.accounts, check_other_metas,); - } - - #[tokio::test] - async fn init_mixed() { - let extra_meta5_literal_str = "seed_prefix"; - let extra_meta6_literal_u64 = 28u64; - - let pubkey1 = Pubkey::new_unique(); - let mut lamports1 = 0; - let mut data1 = []; - let pubkey2 = Pubkey::new_unique(); - let mut lamports2 = 0; - let mut data2 = []; - let pubkey3 = Pubkey::new_unique(); - let mut lamports3 = 0; - let mut data3 = []; - let owner = Pubkey::new_unique(); - let account_infos = [ - AccountInfo::new( - &pubkey1, - false, - true, - &mut lamports1, - &mut data1, - &owner, - false, - 0, - ), - AccountInfo::new( - &pubkey2, - true, - false, - &mut lamports2, - &mut data2, - &owner, - false, - 0, - ), - AccountInfo::new( - &pubkey3, - false, - false, - &mut lamports3, - &mut data3, - &owner, - false, - 0, - ), - ]; - - let extra_meta1 = AccountMeta::new(Pubkey::new_unique(), false); - let extra_meta2 = AccountMeta::new(Pubkey::new_unique(), true); - let extra_meta3 = AccountMeta::new_readonly(Pubkey::new_unique(), true); - let extra_meta4 = AccountMeta::new_readonly(Pubkey::new_unique(), false); - let extra_meta5 = ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: extra_meta5_literal_str.as_bytes().to_vec(), - }, - Seed::InstructionData { - index: 1, - length: 8, // [u8; 8] - }, - Seed::InstructionData { - index: 9, - length: 32, // Pubkey - }, - Seed::AccountKey { index: 2 }, - ], - false, - true, - ) - .unwrap(); - let extra_meta6 = ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: extra_meta6_literal_u64.to_le_bytes().to_vec(), - }, - Seed::AccountKey { index: 1 }, - Seed::AccountKey { index: 4 }, - ], - false, - true, - ) - .unwrap(); - let extra_meta7 = ExtraAccountMeta::new_with_pubkey_data( - &PubkeyData::InstructionData { index: 41 }, // After the other pubkey arg. - false, - true, - ) - .unwrap(); - - let test_ix_required_extra_accounts = account_infos - .iter() - .map(ExtraAccountMeta::from) - .collect::>(); - let test_other_ix_required_extra_accounts = [ - ExtraAccountMeta::from(&extra_meta1), - ExtraAccountMeta::from(&extra_meta2), - ExtraAccountMeta::from(&extra_meta3), - ExtraAccountMeta::from(&extra_meta4), - extra_meta5, - extra_meta6, - extra_meta7, - ]; - - let account_size = ExtraAccountMetaList::size_of(test_ix_required_extra_accounts.len()) - .unwrap() - + ExtraAccountMetaList::size_of(test_other_ix_required_extra_accounts.len()).unwrap(); - let mut buffer = vec![0; account_size]; - - ExtraAccountMetaList::init::( - &mut buffer, - &test_ix_required_extra_accounts, - ) - .unwrap(); - ExtraAccountMetaList::init::( - &mut buffer, - &test_other_ix_required_extra_accounts, - ) - .unwrap(); - - let mock_rpc = MockRpc::setup(&account_infos); - - let program_id = Pubkey::new_unique(); - let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]); - ExtraAccountMetaList::add_to_instruction::( - &mut instruction, - |pubkey| mock_rpc.get_account_data(pubkey), - &buffer, - ) - .await - .unwrap(); - - let test_ix_check_metas = account_infos - .iter() - .map(account_info_to_meta) - .collect::>(); - assert_eq!(instruction.accounts, test_ix_check_metas,); - - let program_id = Pubkey::new_unique(); - - let instruction_u8array_arg = [1, 2, 3, 4, 5, 6, 7, 8]; - let instruction_pubkey_arg = Pubkey::new_unique(); - let instruction_key_data_pubkey_arg = Pubkey::new_unique(); - - let mut instruction_data = vec![0]; - instruction_data.extend_from_slice(&instruction_u8array_arg); - instruction_data.extend_from_slice(instruction_pubkey_arg.as_ref()); - instruction_data.extend_from_slice(instruction_key_data_pubkey_arg.as_ref()); - - let mut instruction = Instruction::new_with_bytes(program_id, &instruction_data, vec![]); - ExtraAccountMetaList::add_to_instruction::( - &mut instruction, - |pubkey| mock_rpc.get_account_data(pubkey), - &buffer, - ) - .await - .unwrap(); - - let check_extra_meta5_pubkey = Pubkey::find_program_address( - &[ - extra_meta5_literal_str.as_bytes(), - &instruction_u8array_arg, - instruction_pubkey_arg.as_ref(), - extra_meta3.pubkey.as_ref(), - ], - &program_id, - ) - .0; - - let check_extra_meta6_pubkey = Pubkey::find_program_address( - &[ - extra_meta6_literal_u64.to_le_bytes().as_ref(), - extra_meta2.pubkey.as_ref(), - check_extra_meta5_pubkey.as_ref(), // The first PDA should be at index 4 - ], - &program_id, - ) - .0; - - let test_other_ix_check_metas = vec![ - extra_meta1, - extra_meta2, - extra_meta3, - extra_meta4, - AccountMeta::new(check_extra_meta5_pubkey, false), - AccountMeta::new(check_extra_meta6_pubkey, false), - AccountMeta::new(instruction_key_data_pubkey_arg, false), - ]; - - assert_eq!( - instruction.accounts.get(4).unwrap().pubkey, - check_extra_meta5_pubkey, - ); - assert_eq!( - instruction.accounts.get(5).unwrap().pubkey, - check_extra_meta6_pubkey, - ); - assert_eq!( - instruction.accounts.get(6).unwrap().pubkey, - instruction_key_data_pubkey_arg, - ); - assert_eq!(instruction.accounts, test_other_ix_check_metas,); - } - - #[tokio::test] - async fn cpi_instruction() { - // Say we have a program that CPIs to another program. - // - // Say that _other_ program will need extra account infos. - - // This will be our program - let program_id = Pubkey::new_unique(); - let owner = Pubkey::new_unique(); - - // Some seeds used by the program for PDAs - let required_pda1_literal_string = "required_pda1"; - let required_pda2_literal_u32 = 4u32; - let required_key_data_instruction_data = Pubkey::new_unique(); - - // Define instruction data - // - 0: u8 - // - 1-8: [u8; 8] - // - 9-16: u64 - let instruction_u8array_arg = [1, 2, 3, 4, 5, 6, 7, 8]; - let instruction_u64_arg = 208u64; - let mut instruction_data = vec![0]; - instruction_data.extend_from_slice(&instruction_u8array_arg); - instruction_data.extend_from_slice(instruction_u64_arg.to_le_bytes().as_ref()); - instruction_data.extend_from_slice(required_key_data_instruction_data.as_ref()); - - // Define known instruction accounts - let ix_accounts = vec![ - AccountMeta::new(Pubkey::new_unique(), false), - AccountMeta::new(Pubkey::new_unique(), false), - ]; - - // Define extra account metas required by the program we will CPI to - let extra_meta1 = AccountMeta::new(Pubkey::new_unique(), false); - let extra_meta2 = AccountMeta::new(Pubkey::new_unique(), true); - let extra_meta3 = AccountMeta::new_readonly(Pubkey::new_unique(), false); - let required_accounts = [ - ExtraAccountMeta::from(&extra_meta1), - ExtraAccountMeta::from(&extra_meta2), - ExtraAccountMeta::from(&extra_meta3), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: required_pda1_literal_string.as_bytes().to_vec(), - }, - Seed::InstructionData { - index: 1, - length: 8, // [u8; 8] - }, - Seed::AccountKey { index: 1 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: required_pda2_literal_u32.to_le_bytes().to_vec(), - }, - Seed::InstructionData { - index: 9, - length: 8, // u64 - }, - Seed::AccountKey { index: 5 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 0, - length: 1, // u8 - }, - Seed::AccountData { - account_index: 2, - data_index: 0, - length: 8, - }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::AccountData { - account_index: 5, - data_index: 4, - length: 4, - }, // This one is a PDA! - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey_data( - &PubkeyData::InstructionData { index: 17 }, - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey_data( - &PubkeyData::AccountData { - account_index: 6, - data_index: 0, - }, - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey_data( - &PubkeyData::AccountData { - account_index: 7, - data_index: 8, - }, - false, - true, - ) - .unwrap(), - ]; - - // Now here we're going to build the list of account infos - // We'll need to include: - // - The instruction account infos for the program to CPI to - // - The extra account infos for the program to CPI to - // - Some other arbitrary account infos our program may use - - // First we need to manually derive each PDA - let check_required_pda1_pubkey = Pubkey::find_program_address( - &[ - required_pda1_literal_string.as_bytes(), - &instruction_u8array_arg, - ix_accounts.get(1).unwrap().pubkey.as_ref(), // The second account - ], - &program_id, - ) - .0; - let check_required_pda2_pubkey = Pubkey::find_program_address( - &[ - required_pda2_literal_u32.to_le_bytes().as_ref(), - instruction_u64_arg.to_le_bytes().as_ref(), - check_required_pda1_pubkey.as_ref(), // The first PDA should be at index 5 - ], - &program_id, - ) - .0; - let check_required_pda3_pubkey = Pubkey::find_program_address( - &[ - &[0], // Instruction "discriminator" (u8) - &[8; 8], // The first 8 bytes of the data for account at index 2 (extra account 1) - ], - &program_id, - ) - .0; - let check_required_pda4_pubkey = Pubkey::find_program_address( - &[ - &[7; 4], /* 4 bytes starting at index 4 of the data for account at index 5 (extra - * pda 1) */ - ], - &program_id, - ) - .0; - let check_key_data1_pubkey = required_key_data_instruction_data; - let check_key_data2_pubkey = Pubkey::new_from_array([8; 32]); - let check_key_data3_pubkey = Pubkey::new_from_array([9; 32]); - - // The instruction account infos for the program to CPI to - let pubkey_ix_1 = ix_accounts.first().unwrap().pubkey; - let mut lamports_ix_1 = 0; - let mut data_ix_1 = []; - let pubkey_ix_2 = ix_accounts.get(1).unwrap().pubkey; - let mut lamports_ix_2 = 0; - let mut data_ix_2 = []; - - // The extra account infos for the program to CPI to - let mut lamports1 = 0; - let mut data1 = [8; 12]; - let mut lamports2 = 0; - let mut data2 = []; - let mut lamports3 = 0; - let mut data3 = []; - let mut lamports_pda1 = 0; - let mut data_pda1 = [7; 12]; - let mut lamports_pda2 = 0; - let mut data_pda2 = [8; 32]; - let mut lamports_pda3 = 0; - let mut data_pda3 = [0; 40]; - data_pda3[8..].copy_from_slice(&[9; 32]); // Add pubkey data for pubkey data pubkey 3. - let mut lamports_pda4 = 0; - let mut data_pda4 = []; - let mut data_key_data1 = []; - let mut lamports_key_data1 = 0; - let mut data_key_data2 = []; - let mut lamports_key_data2 = 0; - let mut data_key_data3 = []; - let mut lamports_key_data3 = 0; - - // Some other arbitrary account infos our program may use - let pubkey_arb_1 = Pubkey::new_unique(); - let mut lamports_arb_1 = 0; - let mut data_arb_1 = []; - let pubkey_arb_2 = Pubkey::new_unique(); - let mut lamports_arb_2 = 0; - let mut data_arb_2 = []; - - let all_account_infos = [ - AccountInfo::new( - &pubkey_ix_1, - ix_accounts.first().unwrap().is_signer, - ix_accounts.first().unwrap().is_writable, - &mut lamports_ix_1, - &mut data_ix_1, - &owner, - false, - 0, - ), - AccountInfo::new( - &pubkey_ix_2, - ix_accounts.get(1).unwrap().is_signer, - ix_accounts.get(1).unwrap().is_writable, - &mut lamports_ix_2, - &mut data_ix_2, - &owner, - false, - 0, - ), - AccountInfo::new( - &extra_meta1.pubkey, - required_accounts.first().unwrap().is_signer.into(), - required_accounts.first().unwrap().is_writable.into(), - &mut lamports1, - &mut data1, - &owner, - false, - 0, - ), - AccountInfo::new( - &extra_meta2.pubkey, - required_accounts.get(1).unwrap().is_signer.into(), - required_accounts.get(1).unwrap().is_writable.into(), - &mut lamports2, - &mut data2, - &owner, - false, - 0, - ), - AccountInfo::new( - &extra_meta3.pubkey, - required_accounts.get(2).unwrap().is_signer.into(), - required_accounts.get(2).unwrap().is_writable.into(), - &mut lamports3, - &mut data3, - &owner, - false, - 0, - ), - AccountInfo::new( - &check_required_pda1_pubkey, - required_accounts.get(3).unwrap().is_signer.into(), - required_accounts.get(3).unwrap().is_writable.into(), - &mut lamports_pda1, - &mut data_pda1, - &owner, - false, - 0, - ), - AccountInfo::new( - &check_required_pda2_pubkey, - required_accounts.get(4).unwrap().is_signer.into(), - required_accounts.get(4).unwrap().is_writable.into(), - &mut lamports_pda2, - &mut data_pda2, - &owner, - false, - 0, - ), - AccountInfo::new( - &check_required_pda3_pubkey, - required_accounts.get(5).unwrap().is_signer.into(), - required_accounts.get(5).unwrap().is_writable.into(), - &mut lamports_pda3, - &mut data_pda3, - &owner, - false, - 0, - ), - AccountInfo::new( - &check_required_pda4_pubkey, - required_accounts.get(6).unwrap().is_signer.into(), - required_accounts.get(6).unwrap().is_writable.into(), - &mut lamports_pda4, - &mut data_pda4, - &owner, - false, - 0, - ), - AccountInfo::new( - &check_key_data1_pubkey, - required_accounts.get(7).unwrap().is_signer.into(), - required_accounts.get(7).unwrap().is_writable.into(), - &mut lamports_key_data1, - &mut data_key_data1, - &owner, - false, - 0, - ), - AccountInfo::new( - &check_key_data2_pubkey, - required_accounts.get(8).unwrap().is_signer.into(), - required_accounts.get(8).unwrap().is_writable.into(), - &mut lamports_key_data2, - &mut data_key_data2, - &owner, - false, - 0, - ), - AccountInfo::new( - &check_key_data3_pubkey, - required_accounts.get(9).unwrap().is_signer.into(), - required_accounts.get(9).unwrap().is_writable.into(), - &mut lamports_key_data3, - &mut data_key_data3, - &owner, - false, - 0, - ), - AccountInfo::new( - &pubkey_arb_1, - false, - true, - &mut lamports_arb_1, - &mut data_arb_1, - &owner, - false, - 0, - ), - AccountInfo::new( - &pubkey_arb_2, - false, - true, - &mut lamports_arb_2, - &mut data_arb_2, - &owner, - false, - 0, - ), - ]; - - // Let's use a mock RPC and set up a test instruction to check the CPI - // instruction against later - let rpc_account_infos = all_account_infos.clone(); - let mock_rpc = MockRpc::setup(&rpc_account_infos); - - let account_size = ExtraAccountMetaList::size_of(required_accounts.len()).unwrap(); - let mut buffer = vec![0; account_size]; - ExtraAccountMetaList::init::(&mut buffer, &required_accounts).unwrap(); - - let mut instruction = - Instruction::new_with_bytes(program_id, &instruction_data, ix_accounts.clone()); - ExtraAccountMetaList::add_to_instruction::( - &mut instruction, - |pubkey| mock_rpc.get_account_data(pubkey), - &buffer, - ) - .await - .unwrap(); - - // Perform the account resolution for the CPI instruction - - // Create the instruction itself - let mut cpi_instruction = - Instruction::new_with_bytes(program_id, &instruction_data, ix_accounts); - - // Start with the known account infos - let mut cpi_account_infos = - vec![all_account_infos[0].clone(), all_account_infos[1].clone()]; - - // Mess up the ordering of the account infos to make it harder! - let mut messed_account_infos = all_account_infos.clone(); - messed_account_infos.swap(0, 4); - messed_account_infos.swap(1, 2); - messed_account_infos.swap(3, 4); - messed_account_infos.swap(5, 6); - messed_account_infos.swap(8, 7); - - // Resolve the rest! - ExtraAccountMetaList::add_to_cpi_instruction::( - &mut cpi_instruction, - &mut cpi_account_infos, - &buffer, - &messed_account_infos, - ) - .unwrap(); - - // Our CPI instruction should match the check instruction. - assert_eq!(cpi_instruction, instruction); - - // CPI account infos should have the instruction account infos - // and the extra required account infos from the validation account, - // and they should be in the correct order. - // Note: The two additional arbitrary account infos for the currently - // executing program won't be present in the CPI instruction's account - // infos, so we will omit them (hence the `..9`). - let check_account_infos = &all_account_infos[..12]; - assert_eq!(cpi_account_infos.len(), check_account_infos.len()); - for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) { - assert_eq!(a.key, b.key); - assert_eq!(a.is_signer, b.is_signer); - assert_eq!(a.is_writable, b.is_writable); - } - } - - async fn update_and_assert_metas( - program_id: Pubkey, - buffer: &mut Vec, - updated_metas: &[ExtraAccountMeta], - check_metas: &[AccountMeta], - ) { - // resize buffer if necessary - let account_size = ExtraAccountMetaList::size_of(updated_metas.len()).unwrap(); - if account_size > buffer.len() { - buffer.resize(account_size, 0); - } - - // update - ExtraAccountMetaList::update::(buffer, updated_metas).unwrap(); - - // retrieve metas and assert - let state = TlvStateBorrowed::unpack(buffer).unwrap(); - let unpacked_metas_pod = - ExtraAccountMetaList::unpack_with_tlv_state::(&state).unwrap(); - let unpacked_metas = unpacked_metas_pod.data(); - assert_eq!( - unpacked_metas, updated_metas, - "The ExtraAccountMetas in the buffer should match the expected ones." - ); - - let mock_rpc = MockRpc::setup(&[]); - - let mut instruction = Instruction::new_with_bytes(program_id, &[], vec![]); - ExtraAccountMetaList::add_to_instruction::( - &mut instruction, - |pubkey| mock_rpc.get_account_data(pubkey), - buffer, - ) - .await - .unwrap(); - - assert_eq!(instruction.accounts, check_metas,); - } - - #[tokio::test] - async fn update_extra_account_meta_list() { - let program_id = Pubkey::new_unique(); - - // Create list of initial metas - let initial_metas = [ - ExtraAccountMeta::new_with_pubkey(&Pubkey::new_unique(), false, true).unwrap(), - ExtraAccountMeta::new_with_pubkey(&Pubkey::new_unique(), true, false).unwrap(), - ]; - - // initialize - let initial_account_size = ExtraAccountMetaList::size_of(initial_metas.len()).unwrap(); - let mut buffer = vec![0; initial_account_size]; - ExtraAccountMetaList::init::(&mut buffer, &initial_metas).unwrap(); - - // Create updated metas list of the same size - let updated_metas_1 = [ - ExtraAccountMeta::new_with_pubkey(&Pubkey::new_unique(), true, true).unwrap(), - ExtraAccountMeta::new_with_pubkey(&Pubkey::new_unique(), false, false).unwrap(), - ]; - let check_metas_1 = updated_metas_1 - .iter() - .map(|e| AccountMeta::try_from(e).unwrap()) - .collect::>(); - update_and_assert_metas(program_id, &mut buffer, &updated_metas_1, &check_metas_1).await; - - // Create updated and larger list of metas - let updated_metas_2 = [ - ExtraAccountMeta::new_with_pubkey(&Pubkey::new_unique(), true, true).unwrap(), - ExtraAccountMeta::new_with_pubkey(&Pubkey::new_unique(), false, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&Pubkey::new_unique(), false, true).unwrap(), - ]; - let check_metas_2 = updated_metas_2 - .iter() - .map(|e| AccountMeta::try_from(e).unwrap()) - .collect::>(); - update_and_assert_metas(program_id, &mut buffer, &updated_metas_2, &check_metas_2).await; - - // Create updated and smaller list of metas - let updated_metas_3 = - [ExtraAccountMeta::new_with_pubkey(&Pubkey::new_unique(), true, true).unwrap()]; - let check_metas_3 = updated_metas_3 - .iter() - .map(|e| AccountMeta::try_from(e).unwrap()) - .collect::>(); - update_and_assert_metas(program_id, &mut buffer, &updated_metas_3, &check_metas_3).await; - - // Create updated list of metas with a simple PDA - let seed_pubkey = Pubkey::new_unique(); - let updated_metas_4 = [ - ExtraAccountMeta::new_with_pubkey(&seed_pubkey, true, true).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"seed-prefix".to_vec(), - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ]; - let simple_pda = Pubkey::find_program_address( - &[ - b"seed-prefix", // Literal prefix - seed_pubkey.as_ref(), // Account at index 0 - ], - &program_id, - ) - .0; - let check_metas_4 = [ - AccountMeta::new(seed_pubkey, true), - AccountMeta::new(simple_pda, false), - ]; - - update_and_assert_metas(program_id, &mut buffer, &updated_metas_4, &check_metas_4).await; - } - - #[test] - fn check_account_infos_test() { - let program_id = Pubkey::new_unique(); - let owner = Pubkey::new_unique(); - - // Create a list of required account metas - let pubkey1 = Pubkey::new_unique(); - let pubkey2 = Pubkey::new_unique(); - let required_accounts = [ - ExtraAccountMeta::new_with_pubkey(&pubkey1, false, true).unwrap(), - ExtraAccountMeta::new_with_pubkey(&pubkey2, false, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"lit_seed".to_vec(), - }, - Seed::InstructionData { - index: 0, - length: 4, - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey_data( - &PubkeyData::InstructionData { index: 8 }, - false, - true, - ) - .unwrap(), - ]; - - // Create the validation data - let account_size = ExtraAccountMetaList::size_of(required_accounts.len()).unwrap(); - let mut buffer = vec![0; account_size]; - ExtraAccountMetaList::init::(&mut buffer, &required_accounts).unwrap(); - - // Create the instruction data - let mut instruction_data = vec![0, 1, 2, 3, 4, 5, 6, 7]; - let key_data_pubkey = Pubkey::new_unique(); - instruction_data.extend_from_slice(key_data_pubkey.as_ref()); - - // Set up a list of the required accounts as account infos, - // with two instruction accounts - let pubkey_ix_1 = Pubkey::new_unique(); - let mut lamports_ix_1 = 0; - let mut data_ix_1 = []; - let pubkey_ix_2 = Pubkey::new_unique(); - let mut lamports_ix_2 = 0; - let mut data_ix_2 = []; - let mut lamports1 = 0; - let mut data1 = []; - let mut lamports2 = 0; - let mut data2 = []; - let mut lamports3 = 0; - let mut data3 = []; - let mut lamports4 = 0; - let mut data4 = []; - let pda = Pubkey::find_program_address( - &[b"lit_seed", &instruction_data[..4], pubkey_ix_1.as_ref()], - &program_id, - ) - .0; - let account_infos = [ - // Instruction account 1 - AccountInfo::new( - &pubkey_ix_1, - false, - true, - &mut lamports_ix_1, - &mut data_ix_1, - &owner, - false, - 0, - ), - // Instruction account 2 - AccountInfo::new( - &pubkey_ix_2, - false, - true, - &mut lamports_ix_2, - &mut data_ix_2, - &owner, - false, - 0, - ), - // Required account 1 - AccountInfo::new( - &pubkey1, - false, - true, - &mut lamports1, - &mut data1, - &owner, - false, - 0, - ), - // Required account 2 - AccountInfo::new( - &pubkey2, - false, - false, - &mut lamports2, - &mut data2, - &owner, - false, - 0, - ), - // Required account 3 (PDA) - AccountInfo::new( - &pda, - false, - true, - &mut lamports3, - &mut data3, - &owner, - false, - 0, - ), - // Required account 4 (pubkey data) - AccountInfo::new( - &key_data_pubkey, - false, - true, - &mut lamports4, - &mut data4, - &owner, - false, - 0, - ), - ]; - - // Create another list of account infos to intentionally mess up - let mut messed_account_infos = account_infos.clone().to_vec(); - messed_account_infos.swap(0, 2); - messed_account_infos.swap(1, 4); - messed_account_infos.swap(3, 2); - messed_account_infos.swap(5, 4); - - // Account info check should fail for the messed list - assert_eq!( - ExtraAccountMetaList::check_account_infos::( - &messed_account_infos, - &instruction_data, - &program_id, - &buffer, - ) - .unwrap_err(), - AccountResolutionError::IncorrectAccount.into(), - ); - - // Account info check should pass for the correct list - assert_eq!( - ExtraAccountMetaList::check_account_infos::( - &account_infos, - &instruction_data, - &program_id, - &buffer, - ), - Ok(()), - ); - } -} diff --git a/libraries/type-length-value-derive-test/Cargo.toml b/libraries/type-length-value-derive-test/Cargo.toml deleted file mode 100644 index 8ad6c307f4b..00000000000 --- a/libraries/type-length-value-derive-test/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "spl-type-length-value-derive-test" -version = "0.1.0" -description = "Testing Derive Macro Library for SPL Type Length Value traits" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dev-dependencies] -borsh = "1.5.3" -solana-borsh = "2.1.0" -spl-discriminator = { version = "0.4.0", path = "../discriminator" } -spl-type-length-value = { version = "0.7.0", path = "../type-length-value", features = [ - "derive", -] } diff --git a/libraries/type-length-value-derive-test/src/lib.rs b/libraries/type-length-value-derive-test/src/lib.rs deleted file mode 100644 index 5e32d4f55cc..00000000000 --- a/libraries/type-length-value-derive-test/src/lib.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Test crate to avoid making `borsh` a direct dependency of -//! `spl-type-length-value`. You can't use a derive macro from within the same -//! crate that the macro is defined, so we need this extra crate for just -//! testing the macro itself. - -#[cfg(test)] -pub mod test { - use { - borsh::{BorshDeserialize, BorshSerialize}, - solana_borsh::v1::{get_instance_packed_len, try_from_slice_unchecked}, - spl_discriminator::SplDiscriminate, - spl_type_length_value::{variable_len_pack::VariableLenPack, SplBorshVariableLenPack}, - }; - - #[derive( - Clone, - Debug, - Default, - PartialEq, - BorshDeserialize, - BorshSerialize, - SplDiscriminate, - SplBorshVariableLenPack, - )] - #[discriminator_hash_input("vehicle::my_vehicle")] - pub struct Vehicle { - vin: [u8; 8], - plate: [u8; 7], - } - - #[test] - fn test_derive() { - let vehicle = Vehicle { - vin: [0; 8], - plate: [0; 7], - }; - - assert_eq!( - get_instance_packed_len::(&vehicle).unwrap(), - vehicle.get_packed_len().unwrap() - ); - - let dst1 = &mut [0u8; 15]; - borsh::to_writer(&mut dst1[..], &vehicle).unwrap(); - - let dst2 = &mut [0u8; 15]; - vehicle.pack_into_slice(&mut dst2[..]).unwrap(); - - assert_eq!(dst1, dst2,); - - let mut buffer = [0u8; 15]; - buffer.copy_from_slice(&dst1[..]); - - assert_eq!( - try_from_slice_unchecked::(&buffer).unwrap(), - Vehicle::unpack_from_slice(&buffer).unwrap() - ); - } -} diff --git a/libraries/type-length-value/Cargo.toml b/libraries/type-length-value/Cargo.toml deleted file mode 100644 index 0ef918ebb35..00000000000 --- a/libraries/type-length-value/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "spl-type-length-value" -version = "0.7.0" -description = "Solana Program Library Type-Length-Value Management" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" -exclude = ["js/**"] - -[features] -derive = ["dep:spl-type-length-value-derive"] - -[dependencies] -bytemuck = { version = "1.21.0", features = ["derive"] } -num-derive = "0.4" -num-traits = "0.2" -solana-account-info = "2.1.0" -solana-decode-error = "2.1.0" -solana-msg = "2.1.0" -solana-program-error = "2.1.0" -spl-discriminator = { version = "0.4.0", path = "../discriminator" } -spl-type-length-value-derive = { version = "0.1", path = "./derive", optional = true } -spl-pod = { version = "0.5.0", path = "../pod" } -thiserror = "2.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/libraries/type-length-value/README.md b/libraries/type-length-value/README.md deleted file mode 100644 index aa7a08508e7..00000000000 --- a/libraries/type-length-value/README.md +++ /dev/null @@ -1,196 +0,0 @@ -# Type-Length-Value - -Library with utilities for working with Type-Length-Value structures. - -## Example usage - -This simple examples defines a zero-copy type with its discriminator. - -```rust -use { - bytemuck::{Pod, Zeroable}, - spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, - spl_type_length_value::{ - state::{TlvState, TlvStateBorrowed, TlvStateMut} - }, -}; - -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -struct MyPodValue { - data: [u8; 32], -} -impl SplDiscriminate for MyPodValue { - // Give it a unique discriminator, can also be generated using a hash function - const SPL_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([1; ArrayDiscriminator::LENGTH]); -} -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -struct MyOtherPodValue { - data: u8, -} -// Give this type a non-derivable implementation of `Default` to write some data -impl Default for MyOtherPodValue { - fn default() -> Self { - Self { - data: 10, - } - } -} -impl SplDiscriminate for MyOtherPodValue { - // Some other unique discriminator - const SPL_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([2; ArrayDiscriminator::LENGTH]); -} - -// Account will have two sets of `get_base_len()` (8-byte discriminator and 4-byte length), -// and enough room for a `MyPodValue` and a `MyOtherPodValue` -let account_size = TlvStateMut::get_base_len() - + std::mem::size_of::() - + TlvStateMut::get_base_len() - + std::mem::size_of::() - + TlvStateMut::get_base_len() - + std::mem::size_of::(); - -// Buffer likely comes from a Solana `solana_account_info::AccountInfo`, -// but this example just uses a vector. -let mut buffer = vec![0; account_size]; - -// Unpack the base buffer as a TLV structure -let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - -// Init and write default value -// Note: you'll need to provide a boolean whether or not to allow repeating -// values with the same TLV discriminator. -// If set to false, this function will error when an existing entry is detected. -// Note the function also returns the repetition number, which can be used to -// fetch the value again. -let (value, _repetition_number) = state.init_value::(false).unwrap(); -// Update it in-place -value.data[0] = 1; - -// Init and write another default value -// This time, we're going to allow repeating values. -let (other_value1, other_value1_repetition_number) = - state.init_value::(true).unwrap(); -assert_eq!(other_value1.data, 10); -// Update it in-place -other_value1.data = 2; - -// Let's do it again, since we can now have repeating values! -let (other_value2, other_value2_repetition_number) = - state.init_value::(true).unwrap(); -assert_eq!(other_value2.data, 10); -// Update it in-place -other_value2.data = 4; - -// Later on, to work with it again, we can just get the first value we -// encounter, because we did _not_ allow repeating entries for `MyPodValue`. -let value = state.get_first_value_mut::().unwrap(); - -// Or fetch it from an immutable buffer -let state = TlvStateBorrowed::unpack(&buffer).unwrap(); -let value1 = state.get_first_value::().unwrap(); - -// Since we used repeating entries for `MyOtherPodValue`, we can grab either one by -// its repetition number -let value1 = state - .get_value_with_repetition::(other_value1_repetition_number) - .unwrap(); -let value2 = state - .get_value_with_repetition::(other_value2_repetition_number) - .unwrap(); - -``` - -## Motivation - -The Solana blockchain exposes slabs of bytes to on-chain programs, allowing program -writers to interpret these bytes and change them however they wish. Currently, -programs interpret account bytes as being only of one type. For example, a token -mint account is only ever a token mint, an AMM pool account is only ever an AMM pool, -a token metadata account can only hold token metadata, etc. - -In a world of interfaces, a program will likely implement multiple interfaces. -As a concrete and important example, imagine a token program where mints hold -their own metadata. This means that a single account can be both a mint and -metadata. - -To allow easy implementation of multiple interfaces, accounts must be able to -hold multiple different types within one opaque slab of bytes. The -[type-length-value](https://en.wikipedia.org/wiki/Type%E2%80%93length%E2%80%93value) -scheme facilitates this exact case. - -## How it works - -This library allows for holding multiple disparate types within the same account -by encoding the type, then length, then value. - -The type is an 8-byte `ArrayDiscriminator`, which can be set to anything. - -The length is a little-endian `u32`. - -The value is a slab of `length` bytes that can be used however a program desires. - -When searching through the buffer for a particular type, the library looks at -the first 8-byte discriminator. If it's all zeroes, this means it's uninitialized. -If not, it reads the next 4-byte length. If the discriminator matches, it returns -the next `length` bytes. If not, it jumps ahead `length` bytes and reads the -next 8-byte discriminator. - -## Serialization of variable-length types - -The initial example works using the `bytemuck` crate for zero-copy serialization -and deserialization. It's possible to use Borsh by implementing the `VariableLenPack` -trait on your type. - -```rust -use { - borsh::{BorshDeserialize, BorshSerialize}, - solana_borsh::v1::{get_instance_packed_len, try_from_slice_unchecked}, - solana_program_error::ProgramError, - spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, - spl_type_length_value::{ - state::{TlvState, TlvStateMut}, - variable_len_pack::VariableLenPack - }, -}; -#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize)] -struct MyVariableLenType { - data: String, // variable length type -} -impl SplDiscriminate for MyVariableLenType { - const SPL_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([5; ArrayDiscriminator::LENGTH]); -} -impl VariableLenPack for MyVariableLenType { - fn pack_into_slice(&self, dst: &mut [u8]) -> Result<(), ProgramError> { - borsh::to_writer(&mut dst[..], self).map_err(Into::into) - } - - fn unpack_from_slice(src: &[u8]) -> Result { - try_from_slice_unchecked(src).map_err(Into::into) - } - - fn get_packed_len(&self) -> Result { - get_instance_packed_len(self).map_err(Into::into) - } -} -let initial_data = "This is a pretty cool test!"; -// Allocate exactly the right size for the string, can go bigger if desired -let tlv_size = 4 + initial_data.len(); -let account_size = TlvStateMut::get_base_len() + tlv_size; - -// Buffer likely comes from a Solana `solana_account_info::AccountInfo`, -// but this example just uses a vector. -let mut buffer = vec![0; account_size]; -let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - -// No need to hold onto the bytes since we'll serialize back into the right place -// For this example, let's _not_ allow repeating entries. -let _ = state.alloc::(tlv_size, false).unwrap(); -let my_variable_len = MyVariableLenType { - data: initial_data.to_string() -}; -state.pack_first_variable_len_value(&my_variable_len).unwrap(); -let deser = state.get_first_variable_len_value::().unwrap(); -assert_eq!(deser, my_variable_len); -``` diff --git a/libraries/type-length-value/derive/Cargo.toml b/libraries/type-length-value/derive/Cargo.toml deleted file mode 100644 index 4cc7cfeda25..00000000000 --- a/libraries/type-length-value/derive/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "spl-type-length-value-derive" -version = "0.1.0" -description = "Derive Macro Library for SPL Type Length Value traits" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1.0" -quote = "1.0" -syn = { version = "2.0", features = ["full"] } diff --git a/libraries/type-length-value/derive/src/builder.rs b/libraries/type-length-value/derive/src/builder.rs deleted file mode 100644 index 4f2d5a4aaac..00000000000 --- a/libraries/type-length-value/derive/src/builder.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! The actual token generator for the macro -use { - proc_macro2::{Span, TokenStream}, - quote::{quote, ToTokens}, - syn::{parse::Parse, Generics, Ident, Item, ItemEnum, ItemStruct, WhereClause}, -}; - -pub struct SplBorshVariableLenPackBuilder { - /// The struct/enum identifier - pub ident: Ident, - /// The item's generic arguments (if any) - pub generics: Generics, - /// The item's where clause for generics (if any) - pub where_clause: Option, -} - -impl TryFrom for SplBorshVariableLenPackBuilder { - type Error = syn::Error; - - fn try_from(item_enum: ItemEnum) -> Result { - let ident = item_enum.ident; - let where_clause = item_enum.generics.where_clause.clone(); - let generics = item_enum.generics; - Ok(Self { - ident, - generics, - where_clause, - }) - } -} - -impl TryFrom for SplBorshVariableLenPackBuilder { - type Error = syn::Error; - - fn try_from(item_struct: ItemStruct) -> Result { - let ident = item_struct.ident; - let where_clause = item_struct.generics.where_clause.clone(); - let generics = item_struct.generics; - Ok(Self { - ident, - generics, - where_clause, - }) - } -} - -impl Parse for SplBorshVariableLenPackBuilder { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let item = Item::parse(input)?; - match item { - Item::Enum(item_enum) => item_enum.try_into(), - Item::Struct(item_struct) => item_struct.try_into(), - _ => { - return Err(syn::Error::new( - Span::call_site(), - "Only enums and structs are supported", - )) - } - } - .map_err(|e| syn::Error::new(input.span(), format!("Failed to parse item: {}", e))) - } -} - -impl ToTokens for SplBorshVariableLenPackBuilder { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - tokens.extend::(self.into()); - } -} - -impl From<&SplBorshVariableLenPackBuilder> for TokenStream { - fn from(builder: &SplBorshVariableLenPackBuilder) -> Self { - let ident = &builder.ident; - let generics = &builder.generics; - let where_clause = &builder.where_clause; - quote! { - impl #generics spl_type_length_value::variable_len_pack::VariableLenPack for #ident #generics #where_clause { - fn pack_into_slice(&self, dst: &mut [u8]) -> Result<(), spl_type_length_value::solana_program_error::ProgramError> { - borsh::to_writer(&mut dst[..], self).map_err(Into::into) - } - - fn unpack_from_slice(src: &[u8]) -> Result { - solana_borsh::v1::try_from_slice_unchecked(src).map_err(Into::into) - } - - fn get_packed_len(&self) -> Result { - solana_borsh::v1::get_instance_packed_len(self).map_err(Into::into) - } - } - } - } -} diff --git a/libraries/type-length-value/derive/src/lib.rs b/libraries/type-length-value/derive/src/lib.rs deleted file mode 100644 index b3f0f12a297..00000000000 --- a/libraries/type-length-value/derive/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Crate defining a derive macro for a basic borsh implementation of -//! the trait `VariableLenPack`. - -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -extern crate proc_macro; - -mod builder; - -use { - builder::SplBorshVariableLenPackBuilder, proc_macro::TokenStream, quote::ToTokens, - syn::parse_macro_input, -}; - -/// Derive macro to add `VariableLenPack` trait for borsh-implemented types -#[proc_macro_derive(SplBorshVariableLenPack)] -pub fn spl_borsh_variable_len_pack(input: TokenStream) -> TokenStream { - parse_macro_input!(input as SplBorshVariableLenPackBuilder) - .to_token_stream() - .into() -} diff --git a/libraries/type-length-value/js/.eslintignore b/libraries/type-length-value/js/.eslintignore deleted file mode 100644 index 6da325effab..00000000000 --- a/libraries/type-length-value/js/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -docs -lib -test-ledger - -package-lock.json diff --git a/libraries/type-length-value/js/.eslintrc b/libraries/type-length-value/js/.eslintrc deleted file mode 100644 index 5aef10a4729..00000000000 --- a/libraries/type-length-value/js/.eslintrc +++ /dev/null @@ -1,34 +0,0 @@ -{ - "root": true, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - "plugin:require-extensions/recommended" - ], - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "prettier", - "require-extensions" - ], - "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/consistent-type-imports": "error" - }, - "overrides": [ - { - "files": [ - "examples/**/*", - "test/**/*" - ], - "rules": { - "require-extensions/require-extensions": "off", - "require-extensions/require-index": "off" - } - } - ] -} diff --git a/libraries/type-length-value/js/.gitignore b/libraries/type-length-value/js/.gitignore deleted file mode 100644 index 21f33db819c..00000000000 --- a/libraries/type-length-value/js/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -.idea -.vscode -.DS_Store - -node_modules - -pnpm-lock.yaml -yarn.lock - -docs -lib -test-ledger -*.tsbuildinfo diff --git a/libraries/type-length-value/js/.mocharc.json b/libraries/type-length-value/js/.mocharc.json deleted file mode 100644 index 451c14c3016..00000000000 --- a/libraries/type-length-value/js/.mocharc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extension": ["ts"], - "node-option": ["experimental-specifier-resolution=node", "loader=ts-node/esm"], - "timeout": 5000 -} diff --git a/libraries/type-length-value/js/.nojekyll b/libraries/type-length-value/js/.nojekyll deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/libraries/type-length-value/js/LICENSE b/libraries/type-length-value/js/LICENSE deleted file mode 100644 index d6456956733..00000000000 --- a/libraries/type-length-value/js/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/libraries/type-length-value/js/README.md b/libraries/type-length-value/js/README.md deleted file mode 100644 index d0d1446225c..00000000000 --- a/libraries/type-length-value/js/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Type-Length-Value-js - -Library with utilities for working with Type-Length-Value structures in js. - -## Example usage - -```ts -import { TlvState, SplDiscriminator } from '@solana/spl-type-length-value'; - -const tlv = new TlvState(tlvData, discriminatorSize, lengthSize); -const discriminator = await splDiscriminate("", discriminatorSize); - -const firstValue = tlv.firstBytes(discriminator); - -const allValues = tlv.bytesRepeating(discriminator); - -const firstThreeValues = tlv.bytesRepeating(discriminator, 3); -``` diff --git a/libraries/type-length-value/js/package.json b/libraries/type-length-value/js/package.json deleted file mode 100644 index e6d4fd63bfd..00000000000 --- a/libraries/type-length-value/js/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "@solana/spl-type-length-value", - "description": "SPL Type Length Value Library", - "version": "0.2.0", - "author": "Solana Labs Maintainers ", - "repository": "https://github.com/solana-labs/solana-program-library", - "license": "Apache-2.0", - "type": "module", - "sideEffects": false, - "engines": { - "node": ">=19" - }, - "files": [ - "lib", - "src", - "LICENSE", - "README.md" - ], - "publishConfig": { - "access": "public" - }, - "main": "./lib/cjs/index.js", - "module": "./lib/esm/index.js", - "types": "./lib/types/index.d.ts", - "exports": { - "types": "./lib/types/index.d.ts", - "require": "./lib/cjs/index.js", - "import": "./lib/esm/index.js" - }, - "scripts": { - "build": "tsc --build --verbose tsconfig.all.json", - "clean": "shx rm -rf lib **/*.tsbuildinfo || true", - "deploy": "npm run deploy:docs", - "deploy:docs": "npm run docs && gh-pages --dest type-length-value/js --dist docs --dotfiles", - "docs": "shx rm -rf docs && typedoc && shx cp .nojekyll docs/", - "lint": "eslint --max-warnings 0 .", - "lint:fix": "eslint --fix .", - "nuke": "shx rm -rf node_modules package-lock.json || true", - "postbuild": "shx echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", - "reinstall": "npm run nuke && npm install", - "release": "npm run clean && npm run build", - "test": "mocha test", - "watch": "tsc --build --verbose --watch tsconfig.all.json" - }, - "dependencies": { - "@solana/assertions": "^2.0.0", - "buffer": "^6.0.3" - }, - "devDependencies": { - "@types/chai": "^5.0.1", - "@types/mocha": "^10.0.10", - "@types/node": "^22.10.5", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "@typescript-eslint/parser": "^8.4.0", - "chai": "^5.1.2", - "eslint": "^8.57.0", - "eslint-plugin-require-extensions": "^0.1.1", - "gh-pages": "^6.3.0", - "mocha": "^11.0.1", - "shx": "^0.3.4", - "ts-node": "^10.9.2", - "typedoc": "^0.27.6", - "typescript": "^5.7.2" - } -} diff --git a/libraries/type-length-value/js/src/errors.ts b/libraries/type-length-value/js/src/errors.ts deleted file mode 100644 index 1b128cb8b92..00000000000 --- a/libraries/type-length-value/js/src/errors.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** Base class for errors */ -export abstract class TlvError extends Error { - constructor(message?: string) { - super(message); - } -} - -/** Thrown if the byte length of an tlv buffer doesn't match the expected size */ -export class TlvInvalidAccountDataError extends TlvError { - name = 'TlvInvalidAccountDataError'; -} diff --git a/libraries/type-length-value/js/src/index.ts b/libraries/type-length-value/js/src/index.ts deleted file mode 100644 index 68afecb9af3..00000000000 --- a/libraries/type-length-value/js/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './splDiscriminate.js'; -export * from './tlvState.js'; -export * from './errors.js'; diff --git a/libraries/type-length-value/js/src/splDiscriminate.ts b/libraries/type-length-value/js/src/splDiscriminate.ts deleted file mode 100644 index 01a32ac6400..00000000000 --- a/libraries/type-length-value/js/src/splDiscriminate.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { assertDigestCapabilityIsAvailable } from '@solana/assertions'; - -export async function splDiscriminate(discriminator: string, length = 8): Promise { - assertDigestCapabilityIsAvailable(); - const bytes = new TextEncoder().encode(discriminator); - const digest = await crypto.subtle.digest('SHA-256', bytes); - return new Uint8Array(digest).subarray(0, length); -} diff --git a/libraries/type-length-value/js/src/tlvState.ts b/libraries/type-length-value/js/src/tlvState.ts deleted file mode 100644 index 816a242843d..00000000000 --- a/libraries/type-length-value/js/src/tlvState.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { TlvInvalidAccountDataError } from './errors.js'; - -export type LengthSize = 1 | 2 | 4 | 8; - -export type Discriminator = Uint8Array; - -export class TlvState { - private readonly tlvData: Buffer; - private readonly discriminatorSize: number; - private readonly lengthSize: LengthSize; - - public constructor(buffer: Buffer, discriminatorSize = 2, lengthSize: LengthSize = 2, offset: number = 0) { - this.tlvData = buffer.subarray(offset); - this.discriminatorSize = discriminatorSize; - this.lengthSize = lengthSize; - } - - /** - * Get the raw tlv data - * - * @return the raw tlv data - */ - public get data(): Buffer { - return this.tlvData; - } - - private readEntryLength(size: LengthSize, offset: number, constructor: (x: number | bigint) => T): T { - switch (size) { - case 1: - return constructor(this.tlvData.readUInt8(offset)); - case 2: - return constructor(this.tlvData.readUInt16LE(offset)); - case 4: - return constructor(this.tlvData.readUInt32LE(offset)); - case 8: - return constructor(this.tlvData.readBigUInt64LE(offset)); - } - } - - /** - * Get a single entry from the tlv data. This function returns the first entry with the given type. - * - * @param type the type of the entry to get - * - * @return the entry from the tlv data or null - */ - public firstBytes(discriminator: Discriminator): Buffer | null { - const entries = this.bytesRepeating(discriminator, 1); - return entries.length > 0 ? entries[0] : null; - } - - /** - * Get a multiple entries from the tlv data. This function returns `count` or less entries with the given type. - * - * @param type the type of the entry to get - * @param count the number of entries to get (0 for all entries) - * - * @return the entry from the tlv data or null - */ - public bytesRepeating(discriminator: Discriminator, count = 0): Buffer[] { - const entries: Buffer[] = []; - let offset = 0; - while (offset < this.tlvData.length) { - if (offset + this.discriminatorSize + this.lengthSize > this.tlvData.length) { - throw new TlvInvalidAccountDataError(); - } - const type = this.tlvData.subarray(offset, offset + this.discriminatorSize); - offset += this.discriminatorSize; - const entryLength = this.readEntryLength(this.lengthSize, offset, Number); - offset += this.lengthSize; - if (offset + entryLength > this.tlvData.length) { - throw new TlvInvalidAccountDataError(); - } - if (type.equals(discriminator)) { - entries.push(this.tlvData.subarray(offset, offset + entryLength)); - } - if (count > 0 && entries.length >= count) { - break; - } - offset += entryLength; - } - return entries; - } - - /** - * Get all the discriminators from the tlv data. This function will return a type multiple times if it occurs multiple times in the tlv data. - * - * @return a list of the discriminators. - */ - public discriminators(): Buffer[] { - const discriminators: Buffer[] = []; - let offset = 0; - while (offset < this.tlvData.length) { - if (offset + this.discriminatorSize + this.lengthSize > this.tlvData.length) { - throw new TlvInvalidAccountDataError(); - } - const type = this.tlvData.subarray(offset, offset + this.discriminatorSize); - discriminators.push(type); - offset += this.discriminatorSize; - const entryLength = this.readEntryLength(this.lengthSize, offset, Number); - offset += this.lengthSize; - if (offset + entryLength > this.tlvData.length) { - throw new TlvInvalidAccountDataError(); - } - offset += entryLength; - } - return discriminators; - } -} diff --git a/libraries/type-length-value/js/test/splDiscriminate.test.ts b/libraries/type-length-value/js/test/splDiscriminate.test.ts deleted file mode 100644 index 8d2d62b82db..00000000000 --- a/libraries/type-length-value/js/test/splDiscriminate.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { expect } from 'chai'; -import { splDiscriminate } from '../src/splDiscriminate'; - -const testVectors = [ - 'hello', - 'this-is-a-test', - 'test-namespace:this-is-a-test', - 'test-namespace:this-is-a-test:with-a-longer-name', -]; - -const testExpectedBytes = await Promise.all( - testVectors.map(x => - crypto.subtle.digest('SHA-256', new TextEncoder().encode(x)).then(digest => new Uint8Array(digest)), - ), -); - -describe('splDiscrimintor', () => { - const testSplDiscriminator = async (length: number) => { - for (let i = 0; i < testVectors.length; i++) { - const discriminator = await splDiscriminate(testVectors[i], length); - const expectedBytes = testExpectedBytes[i].subarray(0, length); - expect(discriminator).to.have.length(length); - expect(discriminator).to.deep.equal(expectedBytes); - } - }; - - it('should produce the expected bytes', () => { - testSplDiscriminator(8); - testSplDiscriminator(4); - testSplDiscriminator(2); - }); - - it('should produce the same bytes as rust library', async () => { - const expectedBytes = Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]); - const discriminator = await splDiscriminate('spl-transfer-hook-interface:execute'); - expect(discriminator).to.deep.equal(expectedBytes); - }); -}); diff --git a/libraries/type-length-value/js/test/tlvData.test.ts b/libraries/type-length-value/js/test/tlvData.test.ts deleted file mode 100644 index d43604ff37b..00000000000 --- a/libraries/type-length-value/js/test/tlvData.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { LengthSize } from '../src/tlvState'; -import { TlvState } from '../src/tlvState'; -import { expect } from 'chai'; - -describe('tlvData', () => { - // typeLength 1, lengthSize 2 - const tlvData1 = Buffer.concat([ - Buffer.from([0]), - Buffer.from([0, 0]), - Buffer.from([]), - Buffer.from([1]), - Buffer.from([1, 0]), - Buffer.from([1]), - Buffer.from([2]), - Buffer.from([2, 0]), - Buffer.from([1, 2]), - Buffer.from([0]), - Buffer.from([3, 0]), - Buffer.from([1, 2, 3]), - ]); - - // typeLength 2, lengthSize 1 - const tlvData2 = Buffer.concat([ - Buffer.from([0, 0]), - Buffer.from([0]), - Buffer.from([]), - Buffer.from([1, 0]), - Buffer.from([1]), - Buffer.from([1]), - Buffer.from([2, 0]), - Buffer.from([2]), - Buffer.from([1, 2]), - Buffer.from([0, 0]), - Buffer.from([3]), - Buffer.from([1, 2, 3]), - ]); - - // typeLength 4, lengthSize 8 - const tlvData3 = Buffer.concat([ - Buffer.from([0, 0, 0, 0]), - Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), - Buffer.from([]), - Buffer.from([1, 0, 0, 0]), - Buffer.from([1, 0, 0, 0, 0, 0, 0, 0]), - Buffer.from([1]), - Buffer.from([2, 0, 0, 0]), - Buffer.from([2, 0, 0, 0, 0, 0, 0, 0]), - Buffer.from([1, 2]), - Buffer.from([0, 0, 0, 0]), - Buffer.from([3, 0, 0, 0, 0, 0, 0, 0]), - Buffer.from([1, 2, 3]), - ]); - - // typeLength 8, lengthSize 4 - const tlvData4 = Buffer.concat([ - Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), - Buffer.from([0, 0, 0, 0]), - Buffer.from([]), - Buffer.from([1, 0, 0, 0, 0, 0, 0, 0]), - Buffer.from([1, 0, 0, 0]), - Buffer.from([1]), - Buffer.from([2, 0, 0, 0, 0, 0, 0, 0]), - Buffer.from([2, 0, 0, 0]), - Buffer.from([1, 2]), - Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), - Buffer.from([3, 0, 0, 0]), - Buffer.from([1, 2, 3]), - ]); - - const testRawData = (tlvData: Buffer, discriminatorSize: number, lengthSize: LengthSize) => { - const tlv = new TlvState(tlvData, discriminatorSize, lengthSize); - expect(tlv.data).to.be.deep.equal(tlvData); - const tlvWithOffset = new TlvState(tlvData, discriminatorSize, lengthSize, discriminatorSize + lengthSize); - expect(tlvWithOffset.data).to.be.deep.equal(tlvData.subarray(discriminatorSize + lengthSize)); - }; - - it('should get the raw tlv data', () => { - testRawData(tlvData1, 1, 2); - testRawData(tlvData2, 2, 1); - testRawData(tlvData3, 4, 8); - testRawData(tlvData4, 8, 4); - }); - - const testIndividualEntries = (tlvData: Buffer, discriminatorSize: number, lengthSize: LengthSize) => { - const tlv = new TlvState(tlvData, discriminatorSize, lengthSize); - - const type = Buffer.alloc(discriminatorSize); - type[0] = 0; - expect(tlv.firstBytes(type)).to.be.deep.equal(Buffer.from([])); - type[0] = 1; - expect(tlv.firstBytes(type)).to.be.deep.equal(Buffer.from([1])); - type[0] = 2; - expect(tlv.firstBytes(type)).to.be.deep.equal(Buffer.from([1, 2])); - type[0] = 3; - expect(tlv.firstBytes(type)).to.equal(null); - }; - - it('should get the entries individually', () => { - testIndividualEntries(tlvData1, 1, 2); - testIndividualEntries(tlvData2, 2, 1); - testIndividualEntries(tlvData3, 4, 8); - testIndividualEntries(tlvData4, 8, 4); - }); - - const testRepeatingEntries = (tlvData: Buffer, discriminatorSize: number, lengthSize: LengthSize) => { - const tlv = new TlvState(tlvData, discriminatorSize, lengthSize); - - const bufferDiscriminator = tlv.bytesRepeating(Buffer.alloc(discriminatorSize)); - expect(bufferDiscriminator).to.have.length(2); - expect(bufferDiscriminator[0]).to.be.deep.equal(Buffer.from([])); - expect(bufferDiscriminator[1]).to.be.deep.equal(Buffer.from([1, 2, 3])); - - const bufferDiscriminatorWithCount = tlv.bytesRepeating(Buffer.alloc(discriminatorSize), 1); - expect(bufferDiscriminatorWithCount).to.have.length(1); - expect(bufferDiscriminatorWithCount[0]).to.be.deep.equal(Buffer.from([])); - }; - - it('should get the repeating entries', () => { - testRepeatingEntries(tlvData1, 1, 2); - testRepeatingEntries(tlvData2, 2, 1); - testRepeatingEntries(tlvData3, 4, 8); - testRepeatingEntries(tlvData4, 8, 4); - }); - - const testDiscriminators = (tlvData: Buffer, discriminatorSize: number, lengthSize: LengthSize) => { - const tlv = new TlvState(tlvData, discriminatorSize, lengthSize); - const discriminators = tlv.discriminators(); - expect(discriminators).to.have.length(4); - - const type = Buffer.alloc(discriminatorSize); - type[0] = 0; - expect(discriminators[0]).to.be.deep.equal(type); - type[0] = 1; - expect(discriminators[1]).to.be.deep.equal(type); - type[0] = 2; - expect(discriminators[2]).to.be.deep.equal(type); - type[0] = 0; - expect(discriminators[3]).to.be.deep.equal(type); - }; - - it('should get the discriminators', () => { - testDiscriminators(tlvData1, 1, 2); - testDiscriminators(tlvData2, 2, 1); - testDiscriminators(tlvData3, 4, 8); - testDiscriminators(tlvData4, 8, 4); - }); -}); diff --git a/libraries/type-length-value/js/tsconfig.all.json b/libraries/type-length-value/js/tsconfig.all.json deleted file mode 100644 index 985513259e2..00000000000 --- a/libraries/type-length-value/js/tsconfig.all.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "./tsconfig.root.json", - "references": [ - { - "path": "./tsconfig.cjs.json" - }, - { - "path": "./tsconfig.esm.json" - } - ] -} diff --git a/libraries/type-length-value/js/tsconfig.base.json b/libraries/type-length-value/js/tsconfig.base.json deleted file mode 100644 index 90620c4e485..00000000000 --- a/libraries/type-length-value/js/tsconfig.base.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "include": [], - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Node", - "esModuleInterop": true, - "isolatedModules": true, - "noEmitOnError": true, - "resolveJsonModule": true, - "strict": true, - "stripInternal": true - } -} diff --git a/libraries/type-length-value/js/tsconfig.cjs.json b/libraries/type-length-value/js/tsconfig.cjs.json deleted file mode 100644 index 2db9b71569e..00000000000 --- a/libraries/type-length-value/js/tsconfig.cjs.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "include": ["src"], - "compilerOptions": { - "outDir": "lib/cjs", - "target": "ES2016", - "module": "CommonJS", - "sourceMap": true - } -} diff --git a/libraries/type-length-value/js/tsconfig.esm.json b/libraries/type-length-value/js/tsconfig.esm.json deleted file mode 100644 index 25e7e25e751..00000000000 --- a/libraries/type-length-value/js/tsconfig.esm.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "include": ["src"], - "compilerOptions": { - "outDir": "lib/esm", - "declarationDir": "lib/types", - "target": "ES2020", - "module": "ES2020", - "sourceMap": true, - "declaration": true, - "declarationMap": true - } -} diff --git a/libraries/type-length-value/js/tsconfig.json b/libraries/type-length-value/js/tsconfig.json deleted file mode 100644 index 2f9b239bfca..00000000000 --- a/libraries/type-length-value/js/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.all.json", - "include": ["src", "test"], - "compilerOptions": { - "noEmit": true, - "skipLibCheck": true - } -} diff --git a/libraries/type-length-value/js/tsconfig.root.json b/libraries/type-length-value/js/tsconfig.root.json deleted file mode 100644 index fadf294ab43..00000000000 --- a/libraries/type-length-value/js/tsconfig.root.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "composite": true - } -} diff --git a/libraries/type-length-value/js/typedoc.json b/libraries/type-length-value/js/typedoc.json deleted file mode 100644 index c39fc53aee1..00000000000 --- a/libraries/type-length-value/js/typedoc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entryPoints": ["src/index.ts"], - "out": "docs", - "readme": "README.md" -} diff --git a/libraries/type-length-value/src/error.rs b/libraries/type-length-value/src/error.rs deleted file mode 100644 index 203b200c100..00000000000 --- a/libraries/type-length-value/src/error.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Error types -use { - solana_decode_error::DecodeError, - solana_msg::msg, - solana_program_error::{PrintProgramError, ProgramError}, -}; - -/// Errors that may be returned by the Token program. -#[repr(u32)] -#[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] -pub enum TlvError { - /// Type not found in TLV data - #[error("Type not found in TLV data")] - TypeNotFound = 1_202_666_432, - /// Type already exists in TLV data - #[error("Type already exists in TLV data")] - TypeAlreadyExists, -} - -impl From for ProgramError { - fn from(e: TlvError) -> Self { - ProgramError::Custom(e as u32) - } -} - -impl DecodeError for TlvError { - fn type_of() -> &'static str { - "TlvError" - } -} - -impl PrintProgramError for TlvError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - TlvError::TypeNotFound => { - msg!("Type not found in TLV data") - } - TlvError::TypeAlreadyExists => { - msg!("Type already exists in TLV data") - } - } - } -} diff --git a/libraries/type-length-value/src/length.rs b/libraries/type-length-value/src/length.rs deleted file mode 100644 index 3bab4a26bd0..00000000000 --- a/libraries/type-length-value/src/length.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Module for the length portion of a Type-Length-Value structure -use { - bytemuck::{Pod, Zeroable}, - solana_program_error::ProgramError, - spl_pod::primitives::PodU32, -}; - -/// Length in TLV structure -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct Length(PodU32); -impl TryFrom for usize { - type Error = ProgramError; - fn try_from(n: Length) -> Result { - Self::try_from(u32::from(n.0)).map_err(|_| ProgramError::AccountDataTooSmall) - } -} -impl TryFrom for Length { - type Error = ProgramError; - fn try_from(n: usize) -> Result { - u32::try_from(n) - .map(|v| Self(PodU32::from(v))) - .map_err(|_| ProgramError::AccountDataTooSmall) - } -} diff --git a/libraries/type-length-value/src/lib.rs b/libraries/type-length-value/src/lib.rs deleted file mode 100644 index ed7f0a2aeff..00000000000 --- a/libraries/type-length-value/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Crate defining an interface for managing type-length-value entries in a slab -//! of bytes, to be used with Solana accounts. - -#![allow(clippy::arithmetic_side_effects)] -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -pub mod error; -pub mod length; -pub mod state; -pub mod variable_len_pack; - -// Export current sdk types for downstream users building with a different sdk -// version -// Expose derive macro on feature flag -#[cfg(feature = "derive")] -pub use spl_type_length_value_derive::SplBorshVariableLenPack; -pub use {solana_account_info, solana_decode_error, solana_program_error}; diff --git a/libraries/type-length-value/src/state.rs b/libraries/type-length-value/src/state.rs deleted file mode 100644 index 3016580ea9a..00000000000 --- a/libraries/type-length-value/src/state.rs +++ /dev/null @@ -1,1365 +0,0 @@ -//! Type-length-value structure definition and manipulation - -use { - crate::{error::TlvError, length::Length, variable_len_pack::VariableLenPack}, - bytemuck::Pod, - solana_account_info::AccountInfo, - solana_program_error::ProgramError, - spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, - spl_pod::bytemuck::{pod_from_bytes, pod_from_bytes_mut}, - std::{cmp::Ordering, mem::size_of}, -}; - -/// Get the current TlvIndices from the current spot -const fn get_indices_unchecked(type_start: usize, value_repetition_number: usize) -> TlvIndices { - let length_start = type_start.saturating_add(size_of::()); - let value_start = length_start.saturating_add(size_of::()); - TlvIndices { - type_start, - length_start, - value_start, - value_repetition_number, - } -} - -/// Internal helper struct for returning the indices of the type, length, and -/// value in a TLV entry -#[derive(Debug)] -struct TlvIndices { - pub type_start: usize, - pub length_start: usize, - pub value_start: usize, - pub value_repetition_number: usize, -} - -fn get_indices( - tlv_data: &[u8], - value_discriminator: ArrayDiscriminator, - init: bool, - repetition_number: Option, -) -> Result { - let mut current_repetition_number = 0; - let mut start_index = 0; - while start_index < tlv_data.len() { - let tlv_indices = get_indices_unchecked(start_index, current_repetition_number); - if tlv_data.len() < tlv_indices.value_start { - return Err(ProgramError::InvalidAccountData); - } - let discriminator = ArrayDiscriminator::try_from( - &tlv_data[tlv_indices.type_start..tlv_indices.length_start], - )?; - if discriminator == value_discriminator { - if let Some(desired_repetition_number) = repetition_number { - if current_repetition_number == desired_repetition_number { - return Ok(tlv_indices); - } - } - current_repetition_number += 1; - // got to an empty spot, init here, or error if we're searching, since - // nothing is written after an Uninitialized spot - } else if discriminator == ArrayDiscriminator::UNINITIALIZED { - if init { - return Ok(tlv_indices); - } else { - return Err(TlvError::TypeNotFound.into()); - } - } - let length = - pod_from_bytes::(&tlv_data[tlv_indices.length_start..tlv_indices.value_start])?; - let value_end_index = tlv_indices - .value_start - .saturating_add(usize::try_from(*length)?); - start_index = value_end_index; - } - Err(ProgramError::InvalidAccountData) -} - -// This function is doing two separate things at once, and would probably be -// better served by some custom iterator, but let's leave that for another day. -fn get_discriminators_and_end_index( - tlv_data: &[u8], -) -> Result<(Vec, usize), ProgramError> { - let mut discriminators = vec![]; - let mut start_index = 0; - while start_index < tlv_data.len() { - // This function is not concerned with repetitions, so we can just - // arbitrarily pass `0` here - let tlv_indices = get_indices_unchecked(start_index, 0); - if tlv_data.len() < tlv_indices.length_start { - // we got to the end, but there might be some uninitialized data after - let remainder = &tlv_data[tlv_indices.type_start..]; - if remainder.iter().all(|&x| x == 0) { - return Ok((discriminators, tlv_indices.type_start)); - } else { - return Err(ProgramError::InvalidAccountData); - } - } - let discriminator = ArrayDiscriminator::try_from( - &tlv_data[tlv_indices.type_start..tlv_indices.length_start], - )?; - if discriminator == ArrayDiscriminator::UNINITIALIZED { - return Ok((discriminators, tlv_indices.type_start)); - } else { - if tlv_data.len() < tlv_indices.value_start { - // not enough bytes to store the length, malformed - return Err(ProgramError::InvalidAccountData); - } - discriminators.push(discriminator); - let length = pod_from_bytes::( - &tlv_data[tlv_indices.length_start..tlv_indices.value_start], - )?; - - let value_end_index = tlv_indices - .value_start - .saturating_add(usize::try_from(*length)?); - if value_end_index > tlv_data.len() { - // value blows past the size of the slice, malformed - return Err(ProgramError::InvalidAccountData); - } - start_index = value_end_index; - } - } - Ok((discriminators, start_index)) -} - -fn get_bytes( - tlv_data: &[u8], - repetition_number: usize, -) -> Result<&[u8], ProgramError> { - let TlvIndices { - type_start: _, - length_start, - value_start, - value_repetition_number: _, - } = get_indices( - tlv_data, - V::SPL_DISCRIMINATOR, - false, - Some(repetition_number), - )?; - // get_indices has checked that tlv_data is long enough to include these - // indices - let length = pod_from_bytes::(&tlv_data[length_start..value_start])?; - let value_end = value_start.saturating_add(usize::try_from(*length)?); - if tlv_data.len() < value_end { - return Err(ProgramError::InvalidAccountData); - } - Ok(&tlv_data[value_start..value_end]) -} - -/// Trait for all TLV state -/// -/// Stores data as any number of type-length-value structures underneath, where: -/// -/// * the "type" is an `ArrayDiscriminator`, 8 bytes -/// * the "length" is a `Length`, 4 bytes -/// * the "value" is a slab of "length" bytes -/// -/// With this structure, it's possible to hold onto any number of entries with -/// unique discriminators, provided that the total underlying data has enough -/// bytes for every entry. -/// -/// For example, if we have two distinct types, one which is an 8-byte array -/// of value `[0, 1, 0, 0, 0, 0, 0, 0]` and discriminator -/// `[1, 1, 1, 1, 1, 1, 1, 1]`, and another which is just a single `u8` of value -/// `4` with the discriminator `[2, 2, 2, 2, 2, 2, 2, 2]`, we can deserialize -/// this buffer as follows: -/// -/// ``` -/// use { -/// bytemuck::{Pod, Zeroable}, -/// spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, -/// spl_type_length_value::state::{TlvState, TlvStateBorrowed, TlvStateMut}, -/// }; -/// #[repr(C)] -/// #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -/// struct MyPodValue { -/// data: [u8; 8], -/// } -/// impl SplDiscriminate for MyPodValue { -/// const SPL_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([1; ArrayDiscriminator::LENGTH]); -/// } -/// #[repr(C)] -/// #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -/// struct MyOtherPodValue { -/// data: u8, -/// } -/// impl SplDiscriminate for MyOtherPodValue { -/// const SPL_DISCRIMINATOR: ArrayDiscriminator = ArrayDiscriminator::new([2; ArrayDiscriminator::LENGTH]); -/// } -/// let buffer = [ -/// 1, 1, 1, 1, 1, 1, 1, 1, // first type's discriminator -/// 8, 0, 0, 0, // first type's length -/// 0, 1, 0, 0, 0, 0, 0, 0, // first type's value -/// 2, 2, 2, 2, 2, 2, 2, 2, // second type's discriminator -/// 1, 0, 0, 0, // second type's length -/// 4, // second type's value -/// ]; -/// let state = TlvStateBorrowed::unpack(&buffer).unwrap(); -/// let value = state.get_first_value::().unwrap(); -/// assert_eq!(value.data, [0, 1, 0, 0, 0, 0, 0, 0]); -/// let value = state.get_first_value::().unwrap(); -/// assert_eq!(value.data, 4); -/// ``` -/// -/// See the README and tests for more examples on how to use these types. -pub trait TlvState { - /// Get the full buffer containing all TLV data - fn get_data(&self) -> &[u8]; - - /// Unpack a portion of the TLV data as the desired Pod type for the entry - /// number specified - fn get_value_with_repetition( - &self, - repetition_number: usize, - ) -> Result<&V, ProgramError> { - let data = get_bytes::(self.get_data(), repetition_number)?; - pod_from_bytes::(data) - } - - /// Unpack a portion of the TLV data as the desired Pod type for the first - /// entry found - fn get_first_value(&self) -> Result<&V, ProgramError> { - self.get_value_with_repetition::(0) - } - - /// Unpacks a portion of the TLV data as the desired variable-length type - /// for the entry number specified - fn get_variable_len_value_with_repetition( - &self, - repetition_number: usize, - ) -> Result { - let data = get_bytes::(self.get_data(), repetition_number)?; - V::unpack_from_slice(data) - } - - /// Unpacks a portion of the TLV data as the desired variable-length type - /// for the first entry found - fn get_first_variable_len_value( - &self, - ) -> Result { - self.get_variable_len_value_with_repetition::(0) - } - - /// Unpack a portion of the TLV data as bytes for the entry number specified - fn get_bytes_with_repetition( - &self, - repetition_number: usize, - ) -> Result<&[u8], ProgramError> { - get_bytes::(self.get_data(), repetition_number) - } - - /// Unpack a portion of the TLV data as bytes for the first entry found - fn get_first_bytes(&self) -> Result<&[u8], ProgramError> { - self.get_bytes_with_repetition::(0) - } - - /// Iterates through the TLV entries, returning only the types - fn get_discriminators(&self) -> Result, ProgramError> { - get_discriminators_and_end_index(self.get_data()).map(|v| v.0) - } - - /// Get the base size required for TLV data - fn get_base_len() -> usize { - get_base_len() - } -} - -/// Encapsulates owned TLV data -#[derive(Debug, PartialEq)] -pub struct TlvStateOwned { - /// Raw TLV data, deserialized on demand - data: Vec, -} -impl TlvStateOwned { - /// Unpacks TLV state data - /// - /// Fails if no state is initialized or if data is too small - pub fn unpack(data: Vec) -> Result { - check_data(&data)?; - Ok(Self { data }) - } -} -impl TlvState for TlvStateOwned { - fn get_data(&self) -> &[u8] { - &self.data - } -} - -/// Encapsulates immutable base state data (mint or account) with possible -/// extensions -#[derive(Debug, PartialEq)] -pub struct TlvStateBorrowed<'data> { - /// Slice of data containing all TLV data, deserialized on demand - data: &'data [u8], -} -impl<'data> TlvStateBorrowed<'data> { - /// Unpacks TLV state data - /// - /// Fails if no state is initialized or if data is too small - pub fn unpack(data: &'data [u8]) -> Result { - check_data(data)?; - Ok(Self { data }) - } -} -impl<'a> TlvState for TlvStateBorrowed<'a> { - fn get_data(&self) -> &[u8] { - self.data - } -} - -/// Encapsulates mutable base state data (mint or account) with possible -/// extensions -#[derive(Debug, PartialEq)] -pub struct TlvStateMut<'data> { - /// Slice of data containing all TLV data, deserialized on demand - data: &'data mut [u8], -} -impl<'data> TlvStateMut<'data> { - /// Unpacks TLV state data - /// - /// Fails if no state is initialized or if data is too small - pub fn unpack(data: &'data mut [u8]) -> Result { - check_data(data)?; - Ok(Self { data }) - } - - /// Unpack a portion of the TLV data as the desired type that allows - /// modifying the type for the entry number specified - pub fn get_value_with_repetition_mut( - &mut self, - repetition_number: usize, - ) -> Result<&mut V, ProgramError> { - let data = self.get_bytes_with_repetition_mut::(repetition_number)?; - pod_from_bytes_mut::(data) - } - - /// Unpack a portion of the TLV data as the desired type that allows - /// modifying the type for the first entry found - pub fn get_first_value_mut( - &mut self, - ) -> Result<&mut V, ProgramError> { - self.get_value_with_repetition_mut::(0) - } - - /// Unpack a portion of the TLV data as mutable bytes for the entry number - /// specified - pub fn get_bytes_with_repetition_mut( - &mut self, - repetition_number: usize, - ) -> Result<&mut [u8], ProgramError> { - let TlvIndices { - type_start: _, - length_start, - value_start, - value_repetition_number: _, - } = get_indices( - self.data, - V::SPL_DISCRIMINATOR, - false, - Some(repetition_number), - )?; - - let length = pod_from_bytes::(&self.data[length_start..value_start])?; - let value_end = value_start.saturating_add(usize::try_from(*length)?); - if self.data.len() < value_end { - return Err(ProgramError::InvalidAccountData); - } - Ok(&mut self.data[value_start..value_end]) - } - - /// Unpack a portion of the TLV data as mutable bytes for the first entry - /// found - pub fn get_first_bytes_mut(&mut self) -> Result<&mut [u8], ProgramError> { - self.get_bytes_with_repetition_mut::(0) - } - - /// Packs the default TLV data into the first open slot in the data buffer. - /// Handles repetition based on the boolean arg provided: - /// * `true`: If extension is already found in the buffer, it returns an - /// error. - /// * `false`: Will add a new entry to the next open slot. - pub fn init_value( - &mut self, - allow_repetition: bool, - ) -> Result<(&mut V, usize), ProgramError> { - let length = size_of::(); - let (buffer, repetition_number) = self.alloc::(length, allow_repetition)?; - let extension_ref = pod_from_bytes_mut::(buffer)?; - *extension_ref = V::default(); - Ok((extension_ref, repetition_number)) - } - - /// Packs a variable-length value into its appropriate data segment, where - /// repeating discriminators _are_ allowed - pub fn pack_variable_len_value_with_repetition( - &mut self, - value: &V, - repetition_number: usize, - ) -> Result<(), ProgramError> { - let data = self.get_bytes_with_repetition_mut::(repetition_number)?; - // NOTE: Do *not* use `pack`, since the length check will cause - // reallocations to smaller sizes to fail - value.pack_into_slice(data) - } - - /// Packs a variable-length value into its appropriate data segment, where - /// no repeating discriminators are allowed - pub fn pack_first_variable_len_value( - &mut self, - value: &V, - ) -> Result<(), ProgramError> { - self.pack_variable_len_value_with_repetition::(value, 0) - } - - /// Allocate the given number of bytes for the given SplDiscriminate - pub fn alloc( - &mut self, - length: usize, - allow_repetition: bool, - ) -> Result<(&mut [u8], usize), ProgramError> { - let TlvIndices { - type_start, - length_start, - value_start, - value_repetition_number, - } = get_indices( - self.data, - V::SPL_DISCRIMINATOR, - true, - if allow_repetition { None } else { Some(0) }, - )?; - - let discriminator = ArrayDiscriminator::try_from(&self.data[type_start..length_start])?; - if discriminator == ArrayDiscriminator::UNINITIALIZED { - // write type - let discriminator_ref = &mut self.data[type_start..length_start]; - discriminator_ref.copy_from_slice(V::SPL_DISCRIMINATOR.as_ref()); - // write length - let length_ref = - pod_from_bytes_mut::(&mut self.data[length_start..value_start])?; - *length_ref = Length::try_from(length)?; - - let value_end = value_start.saturating_add(length); - if self.data.len() < value_end { - return Err(ProgramError::InvalidAccountData); - } - Ok(( - &mut self.data[value_start..value_end], - value_repetition_number, - )) - } else { - Err(TlvError::TypeAlreadyExists.into()) - } - } - - /// Allocates and serializes a new TLV entry from a `VariableLenPack` type - pub fn alloc_and_pack_variable_len_entry( - &mut self, - value: &V, - allow_repetition: bool, - ) -> Result { - let length = value.get_packed_len()?; - let (data, repetition_number) = self.alloc::(length, allow_repetition)?; - value.pack_into_slice(data)?; - Ok(repetition_number) - } - - /// Reallocate the given number of bytes for the given SplDiscriminate. If - /// the new length is smaller, it will compact the rest of the buffer - /// and zero out the difference at the end. If it's larger, it will move - /// the rest of the buffer data and zero out the new data. - pub fn realloc_with_repetition( - &mut self, - length: usize, - repetition_number: usize, - ) -> Result<&mut [u8], ProgramError> { - let TlvIndices { - type_start: _, - length_start, - value_start, - value_repetition_number: _, - } = get_indices( - self.data, - V::SPL_DISCRIMINATOR, - false, - Some(repetition_number), - )?; - let (_, end_index) = get_discriminators_and_end_index(self.data)?; - let data_len = self.data.len(); - - let length_ref = pod_from_bytes_mut::(&mut self.data[length_start..value_start])?; - let old_length = usize::try_from(*length_ref)?; - - // check that we're not going to panic during `copy_within` - if old_length < length { - let new_end_index = end_index.saturating_add(length.saturating_sub(old_length)); - if new_end_index > data_len { - return Err(ProgramError::InvalidAccountData); - } - } - - // write new length after the check, to avoid getting into a bad situation - // if trying to recover from an error - *length_ref = Length::try_from(length)?; - - let old_value_end = value_start.saturating_add(old_length); - let new_value_end = value_start.saturating_add(length); - self.data - .copy_within(old_value_end..end_index, new_value_end); - match old_length.cmp(&length) { - Ordering::Greater => { - // realloc to smaller, fill the end - let new_end_index = end_index.saturating_sub(old_length.saturating_sub(length)); - self.data[new_end_index..end_index].fill(0); - } - Ordering::Less => { - // realloc to bigger, fill the moved part - self.data[old_value_end..new_value_end].fill(0); - } - Ordering::Equal => {} // nothing needed! - } - - Ok(&mut self.data[value_start..new_value_end]) - } - - /// Reallocate the given number of bytes for the given SplDiscriminate, - /// where no repeating discriminators are allowed - pub fn realloc_first( - &mut self, - length: usize, - ) -> Result<&mut [u8], ProgramError> { - self.realloc_with_repetition::(length, 0) - } -} - -impl<'a> TlvState for TlvStateMut<'a> { - fn get_data(&self) -> &[u8] { - self.data - } -} - -/// Packs a variable-length value into an existing TLV space, reallocating -/// the account and TLV as needed to accommodate for any change in space -pub fn realloc_and_pack_variable_len_with_repetition( - account_info: &AccountInfo, - value: &V, - repetition_number: usize, -) -> Result<(), ProgramError> { - let previous_length = { - let data = account_info.try_borrow_data()?; - let TlvIndices { - type_start: _, - length_start, - value_start, - value_repetition_number: _, - } = get_indices(&data, V::SPL_DISCRIMINATOR, false, Some(repetition_number))?; - usize::try_from(*pod_from_bytes::(&data[length_start..value_start])?)? - }; - let new_length = value.get_packed_len()?; - let previous_account_size = account_info.try_data_len()?; - if previous_length < new_length { - // size increased, so realloc the account, then the TLV entry, then write data - let additional_bytes = new_length - .checked_sub(previous_length) - .ok_or(ProgramError::AccountDataTooSmall)?; - account_info.realloc(previous_account_size.saturating_add(additional_bytes), true)?; - let mut buffer = account_info.try_borrow_mut_data()?; - let mut state = TlvStateMut::unpack(&mut buffer)?; - state.realloc_with_repetition::(new_length, repetition_number)?; - state.pack_variable_len_value_with_repetition(value, repetition_number)?; - } else { - // do it backwards otherwise, write the state, realloc TLV, then the account - let mut buffer = account_info.try_borrow_mut_data()?; - let mut state = TlvStateMut::unpack(&mut buffer)?; - state.pack_variable_len_value_with_repetition(value, repetition_number)?; - let removed_bytes = previous_length - .checked_sub(new_length) - .ok_or(ProgramError::AccountDataTooSmall)?; - if removed_bytes > 0 { - // we decreased the size, so need to realloc the TLV, then the account - state.realloc_with_repetition::(new_length, repetition_number)?; - // this is probably fine, but be safe and avoid invalidating references - drop(buffer); - account_info.realloc(previous_account_size.saturating_sub(removed_bytes), false)?; - } - } - Ok(()) -} - -/// Packs a variable-length value into an existing TLV space, where no repeating -/// discriminators are allowed -pub fn realloc_and_pack_first_variable_len( - account_info: &AccountInfo, - value: &V, -) -> Result<(), ProgramError> { - realloc_and_pack_variable_len_with_repetition::(account_info, value, 0) -} - -/// Get the base size required for TLV data -const fn get_base_len() -> usize { - get_indices_unchecked(0, 0).value_start -} - -fn check_data(tlv_data: &[u8]) -> Result<(), ProgramError> { - // should be able to iterate through all entries in the TLV structure - let _ = get_discriminators_and_end_index(tlv_data)?; - Ok(()) -} - -#[cfg(test)] -mod test { - use { - super::*, - bytemuck::{Pod, Zeroable}, - }; - - const TEST_BUFFER: &[u8] = &[ - 1, 1, 1, 1, 1, 1, 1, 1, // discriminator - 32, 0, 0, 0, // length - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, // value - 0, 0, // empty, not enough for a discriminator - ]; - - const TEST_BIG_BUFFER: &[u8] = &[ - 1, 1, 1, 1, 1, 1, 1, 1, // discriminator - 32, 0, 0, 0, // length - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, // value - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, // empty, but enough for a discriminator and empty value - ]; - - #[repr(C)] - #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] - struct TestValue { - data: [u8; 32], - } - impl SplDiscriminate for TestValue { - const SPL_DISCRIMINATOR: ArrayDiscriminator = - ArrayDiscriminator::new([1; ArrayDiscriminator::LENGTH]); - } - - #[repr(C)] - #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] - struct TestSmallValue { - data: [u8; 3], - } - impl SplDiscriminate for TestSmallValue { - const SPL_DISCRIMINATOR: ArrayDiscriminator = - ArrayDiscriminator::new([2; ArrayDiscriminator::LENGTH]); - } - - #[repr(transparent)] - #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] - struct TestEmptyValue; - impl SplDiscriminate for TestEmptyValue { - const SPL_DISCRIMINATOR: ArrayDiscriminator = - ArrayDiscriminator::new([3; ArrayDiscriminator::LENGTH]); - } - - #[repr(C)] - #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] - struct TestNonZeroDefault { - data: [u8; 5], - } - const TEST_NON_ZERO_DEFAULT_DATA: [u8; 5] = [4; 5]; - impl SplDiscriminate for TestNonZeroDefault { - const SPL_DISCRIMINATOR: ArrayDiscriminator = - ArrayDiscriminator::new([4; ArrayDiscriminator::LENGTH]); - } - impl Default for TestNonZeroDefault { - fn default() -> Self { - Self { - data: TEST_NON_ZERO_DEFAULT_DATA, - } - } - } - - #[test] - fn unpack_opaque_buffer() { - let state = TlvStateBorrowed::unpack(TEST_BUFFER).unwrap(); - let value = state.get_first_value::().unwrap(); - assert_eq!(value.data, [1; 32]); - assert_eq!( - state.get_first_value::(), - Err(ProgramError::InvalidAccountData) - ); - - let mut test_buffer = TEST_BUFFER.to_vec(); - let state = TlvStateMut::unpack(&mut test_buffer).unwrap(); - let value = state.get_first_value::().unwrap(); - assert_eq!(value.data, [1; 32]); - let state = TlvStateOwned::unpack(test_buffer).unwrap(); - let value = state.get_first_value::().unwrap(); - assert_eq!(value.data, [1; 32]); - } - - #[test] - fn fail_unpack_opaque_buffer() { - // input buffer too small - let mut buffer = vec![0, 3]; - assert_eq!( - TlvStateBorrowed::unpack(&buffer), - Err(ProgramError::InvalidAccountData) - ); - assert_eq!( - TlvStateMut::unpack(&mut buffer), - Err(ProgramError::InvalidAccountData) - ); - assert_eq!( - TlvStateMut::unpack(&mut buffer), - Err(ProgramError::InvalidAccountData) - ); - - // tweak the discriminator - let mut buffer = TEST_BUFFER.to_vec(); - buffer[0] += 1; - let state = TlvStateMut::unpack(&mut buffer).unwrap(); - assert_eq!( - state.get_first_value::(), - Err(ProgramError::InvalidAccountData) - ); - - // tweak the length, too big - let mut buffer = TEST_BUFFER.to_vec(); - buffer[ArrayDiscriminator::LENGTH] += 10; - assert_eq!( - TlvStateMut::unpack(&mut buffer), - Err(ProgramError::InvalidAccountData) - ); - - // tweak the length, too small - let mut buffer = TEST_BIG_BUFFER.to_vec(); - buffer[ArrayDiscriminator::LENGTH] -= 1; - let state = TlvStateMut::unpack(&mut buffer).unwrap(); - assert_eq!( - state.get_first_value::(), - Err(ProgramError::InvalidArgument) - ); - - // data buffer is too small for type - let buffer = &TEST_BUFFER[..TEST_BUFFER.len() - 5]; - assert_eq!( - TlvStateBorrowed::unpack(buffer), - Err(ProgramError::InvalidAccountData) - ); - } - - #[test] - fn get_discriminators_with_opaque_buffer() { - // incorrect due to the length - assert_eq!( - get_discriminators_and_end_index(&[1, 0, 1, 1]).unwrap_err(), - ProgramError::InvalidAccountData, - ); - // correct due to the good discriminator length and zero length - assert_eq!( - get_discriminators_and_end_index(&[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap(), - (vec![ArrayDiscriminator::from(1)], 12) - ); - // correct since it's just uninitialized data - assert_eq!( - get_discriminators_and_end_index(&[0, 0, 0, 0, 0, 0, 0, 0]).unwrap(), - (vec![], 0) - ); - } - - #[test] - fn value_pack_unpack() { - let account_size = - get_base_len() + size_of::() + get_base_len() + size_of::(); - let mut buffer = vec![0; account_size]; - - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - // success init and write value - let value = state.init_value::(false).unwrap().0; - let data = [100; 32]; - value.data = data; - assert_eq!( - &state.get_discriminators().unwrap(), - &[TestValue::SPL_DISCRIMINATOR], - ); - assert_eq!(&state.get_first_value::().unwrap().data, &data,); - - // fail init extension when already initialized - assert_eq!( - state.init_value::(false).unwrap_err(), - TlvError::TypeAlreadyExists.into(), - ); - - // check raw buffer - let mut expect = vec![]; - expect.extend_from_slice(TestValue::SPL_DISCRIMINATOR.as_ref()); - expect.extend_from_slice(&u32::try_from(size_of::()).unwrap().to_le_bytes()); - expect.extend_from_slice(&data); - expect.extend_from_slice(&[0; size_of::()]); - expect.extend_from_slice(&[0; size_of::()]); - expect.extend_from_slice(&[0; size_of::()]); - assert_eq!(expect, buffer); - - // check unpacking - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let unpacked = state.get_first_value_mut::().unwrap(); - assert_eq!(*unpacked, TestValue { data }); - - // update extension - let new_data = [101; 32]; - unpacked.data = new_data; - - // check updates are propagated - let state = TlvStateBorrowed::unpack(&buffer).unwrap(); - let unpacked = state.get_first_value::().unwrap(); - assert_eq!(*unpacked, TestValue { data: new_data }); - - // check raw buffer - let mut expect = vec![]; - expect.extend_from_slice(TestValue::SPL_DISCRIMINATOR.as_ref()); - expect.extend_from_slice(&u32::try_from(size_of::()).unwrap().to_le_bytes()); - expect.extend_from_slice(&new_data); - expect.extend_from_slice(&[0; size_of::()]); - expect.extend_from_slice(&[0; size_of::()]); - expect.extend_from_slice(&[0; size_of::()]); - assert_eq!(expect, buffer); - - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - // init one more value - let new_value = state.init_value::(false).unwrap().0; - let small_data = [102; 3]; - new_value.data = small_data; - - assert_eq!( - &state.get_discriminators().unwrap(), - &[ - TestValue::SPL_DISCRIMINATOR, - TestSmallValue::SPL_DISCRIMINATOR - ] - ); - - // check raw buffer - let mut expect = vec![]; - expect.extend_from_slice(TestValue::SPL_DISCRIMINATOR.as_ref()); - expect.extend_from_slice(&u32::try_from(size_of::()).unwrap().to_le_bytes()); - expect.extend_from_slice(&new_data); - expect.extend_from_slice(TestSmallValue::SPL_DISCRIMINATOR.as_ref()); - expect.extend_from_slice( - &u32::try_from(size_of::()) - .unwrap() - .to_le_bytes(), - ); - expect.extend_from_slice(&small_data); - assert_eq!(expect, buffer); - - // fail to init one more extension that does not fit - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - assert_eq!( - state.init_value::(false), - Err(ProgramError::InvalidAccountData), - ); - } - - #[test] - fn value_any_order() { - let account_size = - get_base_len() + size_of::() + get_base_len() + size_of::(); - let mut buffer = vec![0; account_size]; - - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - let data = [99; 32]; - let small_data = [98; 3]; - - // write values - let value = state.init_value::(false).unwrap().0; - value.data = data; - let value = state.init_value::(false).unwrap().0; - value.data = small_data; - - assert_eq!( - &state.get_discriminators().unwrap(), - &[ - TestValue::SPL_DISCRIMINATOR, - TestSmallValue::SPL_DISCRIMINATOR, - ] - ); - - // write values in a different order - let mut other_buffer = vec![0; account_size]; - let mut state = TlvStateMut::unpack(&mut other_buffer).unwrap(); - - let value = state.init_value::(false).unwrap().0; - value.data = small_data; - let value = state.init_value::(false).unwrap().0; - value.data = data; - - assert_eq!( - &state.get_discriminators().unwrap(), - &[ - TestSmallValue::SPL_DISCRIMINATOR, - TestValue::SPL_DISCRIMINATOR, - ] - ); - - // buffers are NOT the same because written in a different order - assert_ne!(buffer, other_buffer); - let state = TlvStateBorrowed::unpack(&buffer).unwrap(); - let other_state = TlvStateBorrowed::unpack(&other_buffer).unwrap(); - - // BUT values are the same - assert_eq!( - state.get_first_value::().unwrap(), - other_state.get_first_value::().unwrap() - ); - assert_eq!( - state.get_first_value::().unwrap(), - other_state.get_first_value::().unwrap() - ); - } - - #[test] - fn init_nonzero_default() { - let account_size = get_base_len() + size_of::(); - let mut buffer = vec![0; account_size]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let value = state.init_value::(false).unwrap().0; - assert_eq!(value.data, TEST_NON_ZERO_DEFAULT_DATA); - } - - #[test] - fn init_buffer_too_small() { - let account_size = get_base_len() + size_of::(); - let mut buffer = vec![0; account_size - 1]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let err = state.init_value::(false).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - - // hack the buffer to look like it was initialized, still fails - let discriminator_ref = &mut state.data[0..ArrayDiscriminator::LENGTH]; - discriminator_ref.copy_from_slice(TestValue::SPL_DISCRIMINATOR.as_ref()); - state.data[ArrayDiscriminator::LENGTH] = 32; - let err = state.get_first_value::().unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - assert_eq!( - state.get_discriminators().unwrap_err(), - ProgramError::InvalidAccountData - ); - } - - #[test] - fn value_with_no_data() { - let account_size = get_base_len() + size_of::(); - let mut buffer = vec![0; account_size]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - assert_eq!( - state.get_first_value::().unwrap_err(), - TlvError::TypeNotFound.into(), - ); - - state.init_value::(false).unwrap(); - state.get_first_value::().unwrap(); - - // re-init fails - assert_eq!( - state.init_value::(false).unwrap_err(), - TlvError::TypeAlreadyExists.into(), - ); - } - - #[test] - fn alloc_first() { - let tlv_size = 1; - let account_size = get_base_len() + tlv_size; - let mut buffer = vec![0; account_size]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - // not enough room - let data = state.alloc::(tlv_size, false).unwrap().0; - assert_eq!( - pod_from_bytes_mut::(data).unwrap_err(), - ProgramError::InvalidArgument, - ); - - // can't double alloc - assert_eq!( - state.alloc::(tlv_size, false).unwrap_err(), - TlvError::TypeAlreadyExists.into(), - ); - } - - #[test] - fn alloc_with_repetition() { - let tlv_size = 1; - let account_size = (get_base_len() + tlv_size) * 2; - let mut buffer = vec![0; account_size]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - let (data, repetition_number) = state.alloc::(tlv_size, true).unwrap(); - assert_eq!(repetition_number, 0); - - // not enough room - assert_eq!( - pod_from_bytes_mut::(data).unwrap_err(), - ProgramError::InvalidArgument, - ); - - // Can alloc again! - let (_data, repetition_number) = state.alloc::(tlv_size, true).unwrap(); - assert_eq!(repetition_number, 1); - } - - #[test] - fn realloc_first() { - const TLV_SIZE: usize = 10; - const EXTRA_SPACE: usize = 5; - const SMALL_SIZE: usize = 2; - const ACCOUNT_SIZE: usize = get_base_len() - + TLV_SIZE - + EXTRA_SPACE - + get_base_len() - + size_of::(); - let mut buffer = vec![0; ACCOUNT_SIZE]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - // alloc both types - let _ = state.alloc::(TLV_SIZE, false).unwrap(); - let _ = state.init_value::(false).unwrap(); - - // realloc first entry to larger, all 0 - let data = state - .realloc_first::(TLV_SIZE + EXTRA_SPACE) - .unwrap(); - assert_eq!(data, [0; TLV_SIZE + EXTRA_SPACE]); - let value = state.get_first_value::().unwrap(); - assert_eq!(*value, TestNonZeroDefault::default()); - - // realloc to smaller, still all 0 - let data = state.realloc_first::(SMALL_SIZE).unwrap(); - assert_eq!(data, [0; SMALL_SIZE]); - let value = state.get_first_value::().unwrap(); - assert_eq!(*value, TestNonZeroDefault::default()); - let (_, end_index) = get_discriminators_and_end_index(&buffer).unwrap(); - assert_eq!( - &buffer[end_index..ACCOUNT_SIZE], - [0; TLV_SIZE + EXTRA_SPACE - SMALL_SIZE] - ); - - // unpack again since we dropped the last `state` - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - // realloc too much, fails - assert_eq!( - state - .realloc_first::(TLV_SIZE + EXTRA_SPACE + 1) - .unwrap_err(), - ProgramError::InvalidAccountData, - ); - } - - #[test] - fn realloc_with_repeating_entries() { - const TLV_SIZE: usize = 10; - const EXTRA_SPACE: usize = 5; - const SMALL_SIZE: usize = 2; - const ACCOUNT_SIZE: usize = get_base_len() - + TLV_SIZE - + EXTRA_SPACE - + get_base_len() - + TLV_SIZE - + get_base_len() - + size_of::(); - let mut buffer = vec![0; ACCOUNT_SIZE]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - // alloc both types, two for the first type and one for the second - let _ = state.alloc::(TLV_SIZE, true).unwrap(); - let _ = state.alloc::(TLV_SIZE, true).unwrap(); - let _ = state.init_value::(true).unwrap(); - - // realloc first entry to larger, all 0 - let data = state - .realloc_with_repetition::(TLV_SIZE + EXTRA_SPACE, 0) - .unwrap(); - assert_eq!(data, [0; TLV_SIZE + EXTRA_SPACE]); - let value = state.get_bytes_with_repetition::(0).unwrap(); - assert_eq!(*value, [0; TLV_SIZE + EXTRA_SPACE]); - let value = state.get_bytes_with_repetition::(1).unwrap(); - assert_eq!(*value, [0; TLV_SIZE]); - let value = state.get_first_value::().unwrap(); - assert_eq!(*value, TestNonZeroDefault::default()); - - // realloc to smaller, still all 0 - let data = state - .realloc_with_repetition::(SMALL_SIZE, 0) - .unwrap(); - assert_eq!(data, [0; SMALL_SIZE]); - let value = state.get_bytes_with_repetition::(0).unwrap(); - assert_eq!(*value, [0; SMALL_SIZE]); - let value = state.get_bytes_with_repetition::(1).unwrap(); - assert_eq!(*value, [0; TLV_SIZE]); - let value = state.get_first_value::().unwrap(); - assert_eq!(*value, TestNonZeroDefault::default()); - let (_, end_index) = get_discriminators_and_end_index(&buffer).unwrap(); - assert_eq!( - &buffer[end_index..ACCOUNT_SIZE], - [0; TLV_SIZE + EXTRA_SPACE - SMALL_SIZE] - ); - - // unpack again since we dropped the last `state` - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - // realloc too much, fails - assert_eq!( - state - .realloc_with_repetition::(TLV_SIZE + EXTRA_SPACE + 1, 0) - .unwrap_err(), - ProgramError::InvalidAccountData, - ); - } - - #[derive(Clone, Debug, PartialEq)] - struct TestVariableLen { - data: String, // test with a variable length type - } - impl SplDiscriminate for TestVariableLen { - const SPL_DISCRIMINATOR: ArrayDiscriminator = - ArrayDiscriminator::new([5; ArrayDiscriminator::LENGTH]); - } - impl VariableLenPack for TestVariableLen { - fn pack_into_slice(&self, dst: &mut [u8]) -> Result<(), ProgramError> { - let bytes = self.data.as_bytes(); - let end = 8 + bytes.len(); - if dst.len() < end { - Err(ProgramError::InvalidAccountData) - } else { - dst[..8].copy_from_slice(&self.data.len().to_le_bytes()); - dst[8..end].copy_from_slice(bytes); - Ok(()) - } - } - fn unpack_from_slice(src: &[u8]) -> Result { - let length = u64::from_le_bytes(src[..8].try_into().unwrap()) as usize; - if src[8..8 + length].len() != length { - return Err(ProgramError::InvalidAccountData); - } - let data = std::str::from_utf8(&src[8..8 + length]) - .unwrap() - .to_string(); - Ok(Self { data }) - } - fn get_packed_len(&self) -> Result { - Ok(size_of::().saturating_add(self.data.len())) - } - } - - #[test] - fn first_variable_len_value() { - let initial_data = "This is a pretty cool test!"; - // exactly the right size - let tlv_size = 8 + initial_data.len(); - let account_size = get_base_len() + tlv_size; - let mut buffer = vec![0; account_size]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - // don't actually need to hold onto the data! - let _ = state.alloc::(tlv_size, false).unwrap(); - let test_variable_len = TestVariableLen { - data: initial_data.to_string(), - }; - state - .pack_first_variable_len_value(&test_variable_len) - .unwrap(); - let deser = state - .get_first_variable_len_value::() - .unwrap(); - assert_eq!(deser, test_variable_len); - - // writing too much data fails - let too_much_data = "This is a pretty cool test!?"; - assert_eq!( - state - .pack_first_variable_len_value(&TestVariableLen { - data: too_much_data.to_string(), - }) - .unwrap_err(), - ProgramError::InvalidAccountData - ); - } - - #[test] - fn variable_len_value_with_repetition() { - let variable_len_1 = TestVariableLen { - data: "Let's see if we can pack multiple variable length values".to_string(), - }; - let tlv_size_1 = 8 + variable_len_1.data.len(); - - let variable_len_2 = TestVariableLen { - data: "I think we can".to_string(), - }; - let tlv_size_2 = 8 + variable_len_2.data.len(); - - let variable_len_3 = TestVariableLen { - data: "In fact, I know we can!".to_string(), - }; - let tlv_size_3 = 8 + variable_len_3.data.len(); - - let variable_len_4 = TestVariableLen { - data: "How cool is this?".to_string(), - }; - let tlv_size_4 = 8 + variable_len_4.data.len(); - - let account_size = get_base_len() - + tlv_size_1 - + get_base_len() - + tlv_size_2 - + get_base_len() - + tlv_size_3 - + get_base_len() - + tlv_size_4; - let mut buffer = vec![0; account_size]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - let (_, repetition_number) = state.alloc::(tlv_size_1, true).unwrap(); - state - .pack_variable_len_value_with_repetition(&variable_len_1, repetition_number) - .unwrap(); - assert_eq!(repetition_number, 0); - assert_eq!( - state - .get_first_variable_len_value::() - .unwrap(), - variable_len_1, - ); - - let (_, repetition_number) = state.alloc::(tlv_size_2, true).unwrap(); - state - .pack_variable_len_value_with_repetition(&variable_len_2, repetition_number) - .unwrap(); - assert_eq!(repetition_number, 1); - assert_eq!( - state - .get_variable_len_value_with_repetition::(repetition_number) - .unwrap(), - variable_len_2, - ); - - let (_, repetition_number) = state.alloc::(tlv_size_3, true).unwrap(); - state - .pack_variable_len_value_with_repetition(&variable_len_3, repetition_number) - .unwrap(); - assert_eq!(repetition_number, 2); - assert_eq!( - state - .get_variable_len_value_with_repetition::(repetition_number) - .unwrap(), - variable_len_3, - ); - - let (_, repetition_number) = state.alloc::(tlv_size_4, true).unwrap(); - state - .pack_variable_len_value_with_repetition(&variable_len_4, repetition_number) - .unwrap(); - assert_eq!(repetition_number, 3); - assert_eq!( - state - .get_variable_len_value_with_repetition::(repetition_number) - .unwrap(), - variable_len_4, - ); - } - - #[test] - fn add_entry_mix_and_match() { - let mut buffer = vec![]; - - // Add an entry for a fixed length value - let fixed_data = TestValue { data: [1; 32] }; - let tlv_size = get_base_len() + size_of::(); - buffer.extend(vec![0; tlv_size]); - { - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let (value, repetition_number) = state.init_value::(true).unwrap(); - value.data = fixed_data.data; - assert_eq!(repetition_number, 0); - assert_eq!(*value, fixed_data); - } - - // Add an entry for a variable length value - let variable_data = TestVariableLen { - data: "This is my first variable length entry!".to_string(), - }; - let tlv_size = get_base_len() + 8 + variable_data.data.len(); - buffer.extend(vec![0; tlv_size]); - { - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let repetition_number = state - .alloc_and_pack_variable_len_entry(&variable_data, true) - .unwrap(); - let value = state - .get_variable_len_value_with_repetition::(repetition_number) - .unwrap(); - assert_eq!(repetition_number, 0); - assert_eq!(value, variable_data); - } - - // Add another entry for a variable length value - let variable_data = TestVariableLen { - data: "This is actually my second variable length entry!".to_string(), - }; - let tlv_size = get_base_len() + 8 + variable_data.data.len(); - buffer.extend(vec![0; tlv_size]); - { - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let repetition_number = state - .alloc_and_pack_variable_len_entry(&variable_data, true) - .unwrap(); - let value = state - .get_variable_len_value_with_repetition::(repetition_number) - .unwrap(); - assert_eq!(repetition_number, 1); - assert_eq!(value, variable_data); - } - - // Add another entry for a fixed length value - let fixed_data = TestValue { data: [2; 32] }; - let tlv_size = get_base_len() + size_of::(); - buffer.extend(vec![0; tlv_size]); - { - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let (value, repetition_number) = state.init_value::(true).unwrap(); - value.data = fixed_data.data; - assert_eq!(repetition_number, 1); - assert_eq!(*value, fixed_data); - } - - // Add another entry for a fixed length value - let fixed_data = TestValue { data: [3; 32] }; - let tlv_size = get_base_len() + size_of::(); - buffer.extend(vec![0; tlv_size]); - { - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let (value, repetition_number) = state.init_value::(true).unwrap(); - value.data = fixed_data.data; - assert_eq!(repetition_number, 2); - assert_eq!(*value, fixed_data); - } - - // Add another entry for a variable length value - let variable_data = TestVariableLen { - data: "Wow! My third variable length entry!".to_string(), - }; - let tlv_size = get_base_len() + 8 + variable_data.data.len(); - buffer.extend(vec![0; tlv_size]); - { - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - let repetition_number = state - .alloc_and_pack_variable_len_entry(&variable_data, true) - .unwrap(); - let value = state - .get_variable_len_value_with_repetition::(repetition_number) - .unwrap(); - assert_eq!(repetition_number, 2); - assert_eq!(value, variable_data); - } - } -} diff --git a/libraries/type-length-value/src/variable_len_pack.rs b/libraries/type-length-value/src/variable_len_pack.rs deleted file mode 100644 index b2750d259e5..00000000000 --- a/libraries/type-length-value/src/variable_len_pack.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! The [`VariableLenPack`] serialization trait. - -use solana_program_error::ProgramError; - -/// Trait that mimics a lot of the functionality of -/// `solana_program_pack::Pack` but specifically works for -/// variable-size types. -pub trait VariableLenPack { - /// Writes the serialized form of the instance into the given slice - fn pack_into_slice(&self, dst: &mut [u8]) -> Result<(), ProgramError>; - - /// Deserializes the type from the given slice - fn unpack_from_slice(src: &[u8]) -> Result - where - Self: Sized; - - /// Gets the packed length for a given instance of the type - fn get_packed_len(&self) -> Result; - - /// Safely write the contents to the type into the given slice - fn pack(&self, dst: &mut [u8]) -> Result<(), ProgramError> { - if dst.len() != self.get_packed_len()? { - return Err(ProgramError::InvalidAccountData); - } - self.pack_into_slice(dst) - } -} diff --git a/managed-token/program/Cargo.toml b/managed-token/program/Cargo.toml index a137ae59424..e7ce24bfad2 100644 --- a/managed-token/program/Cargo.toml +++ b/managed-token/program/Cargo.toml @@ -25,11 +25,11 @@ test = [] borsh = "1.5.3" shank = "^0.4.2" solana-program = "2.1.0" -spl-associated-token-account = { version = "6.0.0", path = "../../associated-token-account/program", features = [ +spl-associated-token-account = { version = "6.0.0", features = [ "no-entrypoint", ] } -spl-associated-token-account-client = { version = "2.0.0", path = "../../associated-token-account/client" } -spl-token = { version = "7.0", path = "../../token/program", features = [ +spl-associated-token-account-client = { version = "2.0.0" } +spl-token = { version = "7.0", features = [ "no-entrypoint", ] } thiserror = "^2.0.9" diff --git a/package.json b/package.json index 6cd43672c56..8369712d241 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,7 @@ "private": true, "workspaces": [ "account-compression/sdk", - "libraries/type-length-value/js", - "single-pool/js", - "stake-pool/js", - "token/js", - "token-group/js", "token-lending/js", - "token-metadata/js", "token-swap/js" ], "scripts": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fdeb62cfc1..b139175ded3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,58 +130,6 @@ importers: specifier: 5.7.2 version: 5.7.2 - libraries/type-length-value/js: - dependencies: - '@solana/assertions': - specifier: ^2.0.0 - version: 2.0.0(typescript@5.7.2) - buffer: - specifier: ^6.0.3 - version: 6.0.3 - devDependencies: - '@types/chai': - specifier: ^5.0.1 - version: 5.0.1 - '@types/mocha': - specifier: ^10.0.10 - version: 10.0.10 - '@types/node': - specifier: ^22.10.5 - version: 22.10.5 - '@typescript-eslint/eslint-plugin': - specifier: ^8.4.0 - version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.7.2) - '@typescript-eslint/parser': - specifier: ^8.4.0 - version: 8.4.0(eslint@8.57.0)(typescript@5.7.2) - chai: - specifier: ^5.1.2 - version: 5.1.2 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - eslint-plugin-require-extensions: - specifier: ^0.1.1 - version: 0.1.3(eslint@8.57.0) - gh-pages: - specifier: ^6.3.0 - version: 6.3.0 - mocha: - specifier: ^11.0.1 - version: 11.0.1 - shx: - specifier: ^0.3.4 - version: 0.3.4 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@22.10.5)(typescript@5.7.2) - typedoc: - specifier: ^0.27.6 - version: 0.27.6(typescript@5.7.2) - typescript: - specifier: ^5.7.2 - version: 5.7.2 - name-service/js: dependencies: '@solana/web3.js': @@ -243,214 +191,6 @@ importers: specifier: ^5.7.2 version: 5.7.2 - single-pool/js/packages/classic: - dependencies: - '@solana/addresses': - specifier: 2.0.0 - version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/spl-single-pool': - specifier: 1.0.0 - version: link:../modern - '@solana/web3.js': - specifier: ^1.95.5 - version: 1.95.5 - devDependencies: - '@ava/typescript': - specifier: ^5.0.0 - version: 5.0.0 - '@types/node': - specifier: ^22.10.5 - version: 22.10.5 - '@typescript-eslint/eslint-plugin': - specifier: ^8.4.0 - version: 8.4.0(@typescript-eslint/parser@8.12.2)(eslint@8.57.0)(typescript@5.7.2) - ava: - specifier: ^6.2.0 - version: 6.2.0(@ava/typescript@5.0.0) - eslint: - specifier: ^8.57.0 - version: 8.57.0 - solana-bankrun: - specifier: ^0.2.0 - version: 0.2.0 - tsx: - specifier: ^4.19.2 - version: 4.19.2 - typescript: - specifier: ^5.7.2 - version: 5.7.2 - - single-pool/js/packages/modern: - dependencies: - '@solana/addresses': - specifier: 2.0.0 - version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/instructions': - specifier: 2.0.0 - version: 2.0.0(typescript@5.7.2) - '@solana/transaction-messages': - specifier: 2.0.0 - version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - devDependencies: - '@types/node': - specifier: ^22.10.5 - version: 22.10.5 - '@typescript-eslint/eslint-plugin': - specifier: ^8.4.0 - version: 8.4.0(@typescript-eslint/parser@8.12.2)(eslint@8.57.0)(typescript@5.7.2) - eslint: - specifier: ^8.57.0 - version: 8.57.0 - typescript: - specifier: ^5.7.2 - version: 5.7.2 - - stake-pool/js: - dependencies: - '@solana/buffer-layout': - specifier: ^4.0.1 - version: 4.0.1 - '@solana/spl-token': - specifier: 0.4.9 - version: link:../../token/js - '@solana/web3.js': - specifier: ^1.95.5 - version: 1.95.5 - bn.js: - specifier: ^5.2.0 - version: 5.2.1 - buffer: - specifier: ^6.0.3 - version: 6.0.3 - buffer-layout: - specifier: ^1.2.2 - version: 1.2.2 - superstruct: - specifier: ^2.0.2 - version: 2.0.2 - devDependencies: - '@rollup/plugin-alias': - specifier: ^5.1.1 - version: 5.1.1(rollup@4.30.1) - '@rollup/plugin-commonjs': - specifier: ^28.0.2 - version: 28.0.2(rollup@4.30.1) - '@rollup/plugin-json': - specifier: ^6.1.0 - version: 6.1.0(rollup@4.30.1) - '@rollup/plugin-multi-entry': - specifier: ^6.0.0 - version: 6.0.1(rollup@4.30.1) - '@rollup/plugin-node-resolve': - specifier: ^16.0.0 - version: 16.0.0(rollup@4.30.1) - '@rollup/plugin-terser': - specifier: ^0.4.4 - version: 0.4.4(rollup@4.30.1) - '@rollup/plugin-typescript': - specifier: ^12.1.2 - version: 12.1.2(rollup@4.30.1)(tslib@2.8.1)(typescript@5.7.2) - '@types/bn.js': - specifier: ^5.1.6 - version: 5.1.6 - '@types/jest': - specifier: ^29.5.14 - version: 29.5.14 - '@types/node': - specifier: ^22.10.5 - version: 22.10.5 - '@types/node-fetch': - specifier: ^2.6.12 - version: 2.6.12 - '@typescript-eslint/eslint-plugin': - specifier: ^8.4.0 - version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.7.2) - '@typescript-eslint/parser': - specifier: ^8.4.0 - version: 8.4.0(eslint@8.57.0)(typescript@5.7.2) - cross-env: - specifier: ^7.0.3 - version: 7.0.3 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - jest: - specifier: ^29.0.0 - version: 29.7.0(@types/node@22.10.5)(ts-node@10.9.2) - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - rollup: - specifier: ^4.30.1 - version: 4.30.1 - rollup-plugin-dts: - specifier: ^6.1.1 - version: 6.1.1(rollup@4.30.1)(typescript@5.7.2) - ts-jest: - specifier: ^29.2.5 - version: 29.2.5(@babel/core@7.26.0)(jest@29.7.0)(typescript@5.7.2) - typescript: - specifier: ^5.7.2 - version: 5.7.2 - - token-group/js: - dependencies: - '@solana/codecs': - specifier: 2.0.0 - version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - devDependencies: - '@solana/spl-type-length-value': - specifier: 0.2.0 - version: link:../../libraries/type-length-value/js - '@solana/web3.js': - specifier: ^1.95.5 - version: 1.95.5 - '@types/chai': - specifier: ^5.0.1 - version: 5.0.1 - '@types/mocha': - specifier: ^10.0.10 - version: 10.0.10 - '@types/node': - specifier: ^22.10.5 - version: 22.10.5 - '@typescript-eslint/eslint-plugin': - specifier: ^8.4.0 - version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.7.2) - '@typescript-eslint/parser': - specifier: ^8.4.0 - version: 8.4.0(eslint@8.57.0)(typescript@5.7.2) - chai: - specifier: ^5.1.2 - version: 5.1.2 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - eslint-plugin-require-extensions: - specifier: ^0.1.1 - version: 0.1.3(eslint@8.57.0) - gh-pages: - specifier: ^6.3.0 - version: 6.3.0 - mocha: - specifier: ^11.0.1 - version: 11.0.1 - shx: - specifier: ^0.3.4 - version: 0.3.4 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@22.10.5)(typescript@5.7.2) - tslib: - specifier: ^2.8.1 - version: 2.8.1 - typedoc: - specifier: ^0.27.6 - version: 0.27.6(typescript@5.7.2) - typescript: - specifier: ^5.7.2 - version: 5.7.2 - token-lending/js: dependencies: '@solana/buffer-layout': @@ -477,7 +217,7 @@ importers: version: 12.1.2(rollup@4.30.1)(tslib@2.8.1)(typescript@5.7.2) '@solana/spl-token': specifier: 0.4.9 - version: link:../../token/js + version: 0.4.9(@solana/web3.js@1.95.5)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) '@solana/web3.js': specifier: ^1.95.5 version: 1.95.5 @@ -515,64 +255,6 @@ importers: specifier: ^5.7.2 version: 5.7.2 - token-metadata/js: - dependencies: - '@solana/codecs': - specifier: 2.0.0 - version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - devDependencies: - '@solana/spl-type-length-value': - specifier: 0.2.0 - version: link:../../libraries/type-length-value/js - '@solana/web3.js': - specifier: ^1.95.5 - version: 1.95.5 - '@types/chai': - specifier: ^5.0.1 - version: 5.0.1 - '@types/mocha': - specifier: ^10.0.10 - version: 10.0.10 - '@types/node': - specifier: ^22.10.5 - version: 22.10.5 - '@typescript-eslint/eslint-plugin': - specifier: ^8.4.0 - version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.7.2) - '@typescript-eslint/parser': - specifier: ^8.4.0 - version: 8.4.0(eslint@8.57.0)(typescript@5.7.2) - chai: - specifier: ^5.1.2 - version: 5.1.2 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - eslint-plugin-require-extensions: - specifier: ^0.1.1 - version: 0.1.3(eslint@8.57.0) - gh-pages: - specifier: ^6.3.0 - version: 6.3.0 - mocha: - specifier: ^11.0.1 - version: 11.0.1 - shx: - specifier: ^0.3.4 - version: 0.3.4 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@22.10.5)(typescript@5.7.2) - tslib: - specifier: ^2.8.1 - version: 2.8.1 - typedoc: - specifier: ^0.27.6 - version: 0.27.6(typescript@5.7.2) - typescript: - specifier: ^5.7.2 - version: 5.7.2 - token-swap/js: dependencies: '@solana/buffer-layout': @@ -584,7 +266,7 @@ importers: devDependencies: '@solana/spl-token': specifier: 0.4.9 - version: link:../../token/js + version: 0.4.9(@solana/web3.js@1.95.5)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) '@solana/web3.js': specifier: ^1.95.5 version: 1.95.5 @@ -628,91 +310,6 @@ importers: specifier: ^5.7.2 version: 5.7.2 - token/js: - dependencies: - '@solana/buffer-layout': - specifier: ^4.0.0 - version: 4.0.1 - '@solana/buffer-layout-utils': - specifier: ^0.2.0 - version: 0.2.0 - '@solana/spl-token-group': - specifier: ^0.0.7 - version: link:../../token-group/js - '@solana/spl-token-metadata': - specifier: ^0.1.6 - version: link:../../token-metadata/js - buffer: - specifier: ^6.0.3 - version: 6.0.3 - devDependencies: - '@solana/codecs-strings': - specifier: 2.0.0 - version: 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/spl-memo': - specifier: 0.2.5 - version: 0.2.5(@solana/web3.js@1.95.5) - '@solana/web3.js': - specifier: ^1.95.5 - version: 1.95.5 - '@types/chai': - specifier: ^5.0.1 - version: 5.0.1 - '@types/chai-as-promised': - specifier: ^8.0.1 - version: 8.0.1 - '@types/mocha': - specifier: ^10.0.10 - version: 10.0.10 - '@types/node': - specifier: ^22.10.5 - version: 22.10.5 - '@types/node-fetch': - specifier: ^2.6.12 - version: 2.6.12 - '@typescript-eslint/eslint-plugin': - specifier: ^8.4.0 - version: 8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.7.2) - '@typescript-eslint/parser': - specifier: ^8.4.0 - version: 8.4.0(eslint@8.57.0)(typescript@5.7.2) - chai: - specifier: ^5.1.2 - version: 5.1.2 - chai-as-promised: - specifier: ^8.0.1 - version: 8.0.1(chai@5.1.2) - eslint: - specifier: ^8.57.0 - version: 8.57.0 - eslint-plugin-require-extensions: - specifier: ^0.1.1 - version: 0.1.3(eslint@8.57.0) - gh-pages: - specifier: ^6.3.0 - version: 6.3.0 - mocha: - specifier: ^11.0.1 - version: 11.0.1 - process: - specifier: ^0.11.10 - version: 0.11.10 - shx: - specifier: ^0.3.4 - version: 0.3.4 - start-server-and-test: - specifier: ^2.0.9 - version: 2.0.9 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@22.10.5)(typescript@5.7.2) - typedoc: - specifier: ^0.27.6 - version: 0.27.6(typescript@5.7.2) - typescript: - specifier: ^5.7.2 - version: 5.7.2 - packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -728,14 +325,6 @@ packages: '@jridgewell/trace-mapping': 0.3.25 dev: true - /@ava/typescript@5.0.0: - resolution: {integrity: sha512-2twsQz2fUd95QK1MtKuEnjkiN47SKHZfi/vWj040EN6Eo2ZW3SNcAwncJqXXoMTYZTWtBRXYp3Fg8z+JkFI9aQ==} - engines: {node: ^18.18 || ^20.8 || ^21 || ^22} - dependencies: - escape-string-regexp: 5.0.0 - execa: 8.0.1 - dev: true - /@babel/code-frame@7.26.2: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -1030,267 +619,51 @@ packages: '@babel/helper-validator-identifier': 7.25.9 dev: true - /@bcoe/v8-coverage@0.2.3: - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - dev: true - - /@coral-xyz/anchor@0.29.0: - resolution: {integrity: sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==} - engines: {node: '>=11'} - dependencies: - '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.95.4) - '@noble/hashes': 1.4.0 - '@solana/web3.js': 1.95.4 - bn.js: 5.2.1 - bs58: 4.0.1 - buffer-layout: 1.2.2 - camelcase: 6.3.0 - cross-fetch: 3.1.8 - crypto-hash: 1.3.0 - eventemitter3: 4.0.7 - pako: 2.1.0 - snake-case: 3.0.4 - superstruct: 0.15.5 - toml: 3.0.0 - transitivePeerDependencies: - - bufferutil - - encoding - - utf-8-validate - dev: true - - /@coral-xyz/borsh@0.29.0(@solana/web3.js@1.95.4): - resolution: {integrity: sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==} - engines: {node: '>=10'} - peerDependencies: - '@solana/web3.js': ^1.68.0 - dependencies: - '@solana/web3.js': 1.95.4 - bn.js: 5.2.1 - buffer-layout: 1.2.2 - dev: true - - /@cspotcode/source-map-support@0.8.1: - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - dev: true - - /@esbuild/aix-ppc64@0.23.0: - resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.23.0: - resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.23.0: - resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.23.0: - resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.23.0: - resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.23.0: - resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.23.0: - resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.23.0: - resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.23.0: - resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.23.0: - resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.23.0: - resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.23.0: - resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.23.0: - resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.23.0: - resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.23.0: - resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.23.0: - resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.23.0: - resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.23.0: - resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-arm64@0.23.0: - resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.23.0: - resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.23.0: - resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - requiresBuild: true + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - optional: true - /@esbuild/win32-arm64@0.23.0: - resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - requiresBuild: true + /@coral-xyz/anchor@0.29.0: + resolution: {integrity: sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==} + engines: {node: '>=11'} + dependencies: + '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.95.4) + '@noble/hashes': 1.4.0 + '@solana/web3.js': 1.95.4 + bn.js: 5.2.1 + bs58: 4.0.1 + buffer-layout: 1.2.2 + camelcase: 6.3.0 + cross-fetch: 3.1.8 + crypto-hash: 1.3.0 + eventemitter3: 4.0.7 + pako: 2.1.0 + snake-case: 3.0.4 + superstruct: 0.15.5 + toml: 3.0.0 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate dev: true - optional: true - /@esbuild/win32-ia32@0.23.0: - resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - requiresBuild: true + /@coral-xyz/borsh@0.29.0(@solana/web3.js@1.95.4): + resolution: {integrity: sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==} + engines: {node: '>=10'} + peerDependencies: + '@solana/web3.js': ^1.68.0 + dependencies: + '@solana/web3.js': 1.95.4 + bn.js: 5.2.1 + buffer-layout: 1.2.2 dev: true - optional: true - /@esbuild/win32-x64@0.23.0: - resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - requiresBuild: true + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 dev: true - optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} @@ -1927,15 +1300,6 @@ packages: chalk: 4.1.2 dev: true - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.20 - dev: true - /@jridgewell/gen-mapping@0.3.5: resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -1950,23 +1314,11 @@ packages: engines: {node: '>=6.0.0'} dev: true - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: true - /@jridgewell/set-array@1.2.1: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} dev: true - /@jridgewell/source-map@0.3.5: - resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} - dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.20 - dev: true - /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true @@ -1992,24 +1344,6 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@mapbox/node-pre-gyp@1.0.11: - resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} - hasBin: true - dependencies: - detect-libc: 2.0.2 - https-proxy-agent: 5.0.1 - make-dir: 3.1.0 - node-fetch: 2.7.0 - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.6.3 - tar: 6.2.0 - transitivePeerDependencies: - - encoding - - supports-color - dev: true - /@metaplex-foundation/beet-solana@0.3.1: resolution: {integrity: sha512-tgyEl6dvtLln8XX81JyBvWjIiEcjTkUwZbrM5dIobTmoqMuGewSyk9CClno8qsMsFdB5T3jC91Rjeqmu/6xk2g==} dependencies: @@ -2123,18 +1457,6 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: true - /@rollup/plugin-alias@5.1.1(rollup@4.30.1): - resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - rollup: 4.30.1 - dev: true - /@rollup/plugin-commonjs@28.0.2(rollup@4.30.1): resolution: {integrity: sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==} engines: {node: '>=16.0.0 || 14 >= 14.17'} @@ -2154,33 +1476,6 @@ packages: rollup: 4.30.1 dev: true - /@rollup/plugin-json@6.1.0(rollup@4.30.1): - resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.30.1) - rollup: 4.30.1 - dev: true - - /@rollup/plugin-multi-entry@6.0.1(rollup@4.30.1): - resolution: {integrity: sha512-AXm6toPyTSfbYZWghQGbom1Uh7dHXlrGa+HoiYNhQtDUE3Q7LqoUYdVQx9E1579QWS1uOiu+cZRSE4okO7ySgw==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@rollup/plugin-virtual': 3.0.2(rollup@4.30.1) - matched: 5.0.1 - rollup: 4.30.1 - dev: true - /@rollup/plugin-node-resolve@16.0.0(rollup@4.30.1): resolution: {integrity: sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==} engines: {node: '>=14.0.0'} @@ -2198,21 +1493,6 @@ packages: rollup: 4.30.1 dev: true - /@rollup/plugin-terser@0.4.4(rollup@4.30.1): - resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - rollup: 4.30.1 - serialize-javascript: 6.0.1 - smob: 1.4.1 - terser: 5.24.0 - dev: true - /@rollup/plugin-typescript@12.1.2(rollup@4.30.1)(tslib@2.8.1)(typescript@5.7.2): resolution: {integrity: sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==} engines: {node: '>=14.0.0'} @@ -2233,26 +1513,6 @@ packages: typescript: 5.7.2 dev: true - /@rollup/plugin-virtual@3.0.2(rollup@4.30.1): - resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - rollup: 4.30.1 - dev: true - - /@rollup/pluginutils@4.2.1: - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} - dependencies: - estree-walker: 2.0.2 - picomatch: 2.3.1 - dev: true - /@rollup/pluginutils@5.1.0(rollup@4.30.1): resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} @@ -2464,11 +1724,6 @@ packages: resolution: {integrity: sha512-75232GRx3wp3P7NP+yc4nRK3XUAnaQShxTAzapgmQrgs0QvSq0/mOJGoZXRpH15cFCKyys+4laCPbBselqJ5Ag==} dev: true - /@sindresorhus/merge-streams@2.3.0: - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - dev: true - /@sinonjs/commons@3.0.0: resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} dependencies: @@ -2493,31 +1748,6 @@ packages: '@sinonjs/commons': 3.0.1 dev: true - /@solana/addresses@2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): - resolution: {integrity: sha512-8n3c/mUlH1/z+pM8e7OJ6uDSXw26Be0dgYiokiqblO66DGQ0d+7pqFUFZ5pEGjJ9PU2lDTSfY8rHf4cemOqwzQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5' - dependencies: - '@solana/assertions': 2.0.0(typescript@5.7.2) - '@solana/codecs-core': 2.0.0(typescript@5.7.2) - '@solana/codecs-strings': 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/errors': 2.0.0(typescript@5.7.2) - typescript: 5.7.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - dev: false - - /@solana/assertions@2.0.0(typescript@5.7.2): - resolution: {integrity: sha512-NyPPqZRNGXs/GAjfgsw7YS6vCTXWt4ibXveS+ciy5sdmp/0v3pA6DlzYjleF9Sljrew0IiON15rjaXamhDxYfQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5' - dependencies: - '@solana/errors': 2.0.0(typescript@5.7.2) - typescript: 5.7.2 - dev: false - /@solana/buffer-layout-utils@0.2.0: resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} engines: {node: '>= 10'} @@ -2530,7 +1760,6 @@ packages: - bufferutil - encoding - utf-8-validate - dev: false /@solana/buffer-layout@4.0.1: resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} @@ -2538,69 +1767,66 @@ packages: dependencies: buffer: 6.0.3 - /@solana/codecs-core@2.0.0(typescript@5.7.2): - resolution: {integrity: sha512-qCG+3hDU5Pm8V6joJjR4j4Zv9md1z0RaecniNDIkEglnxmOUODnmPLWbtOjnDylfItyuZeDihK8hkewdj8cUtw==} - engines: {node: '>=20.18.0'} + /@solana/codecs-core@2.0.0-rc.1(typescript@5.7.2): + resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==} peerDependencies: typescript: '>=5' dependencies: - '@solana/errors': 2.0.0(typescript@5.7.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.7.2) typescript: 5.7.2 + dev: true - /@solana/codecs-data-structures@2.0.0(typescript@5.7.2): - resolution: {integrity: sha512-N98Y4jsrC/XeOgqrfsGqcOFIaOoMsKdAxOmy5oqVaEN67YoGSLNC9ROnqamOAOrsZdicTWx9/YLKFmQi9DPh1A==} - engines: {node: '>=20.18.0'} + /@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.7.2): + resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==} peerDependencies: typescript: '>=5' dependencies: - '@solana/codecs-core': 2.0.0(typescript@5.7.2) - '@solana/codecs-numbers': 2.0.0(typescript@5.7.2) - '@solana/errors': 2.0.0(typescript@5.7.2) + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.7.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.7.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.7.2) typescript: 5.7.2 - dev: false + dev: true - /@solana/codecs-numbers@2.0.0(typescript@5.7.2): - resolution: {integrity: sha512-r66i7VzJO1MZkQWZIAI6jjJOFVpnq0+FIabo2Z2ZDtrArFus/SbSEv543yCLeD2tdR/G/p+1+P5On10qF50Y1Q==} - engines: {node: '>=20.18.0'} + /@solana/codecs-numbers@2.0.0-rc.1(typescript@5.7.2): + resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==} peerDependencies: typescript: '>=5' dependencies: - '@solana/codecs-core': 2.0.0(typescript@5.7.2) - '@solana/errors': 2.0.0(typescript@5.7.2) + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.7.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.7.2) typescript: 5.7.2 + dev: true - /@solana/codecs-strings@2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): - resolution: {integrity: sha512-dNqeCypsvaHcjW86H0gYgAZGGkKVBeKVeh7WXlOZ9kno7PeQ2wNkpccyzDfuzaIsKv+HZUD3v/eo86GCvnKazQ==} - engines: {node: '>=20.18.0'} + /@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): + resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==} peerDependencies: fastestsmallesttextencoderdecoder: ^1.0.22 typescript: '>=5' dependencies: - '@solana/codecs-core': 2.0.0(typescript@5.7.2) - '@solana/codecs-numbers': 2.0.0(typescript@5.7.2) - '@solana/errors': 2.0.0(typescript@5.7.2) + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.7.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.7.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.7.2) fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.7.2 + dev: true - /@solana/codecs@2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): - resolution: {integrity: sha512-xneIG5ppE6WIGaZCK7JTys0uLhzlnEJUdBO8nRVIyerwH6aqCfb0fGe7q5WNNYAVDRSxC0Pc1TDe1hpdx3KWmQ==} - engines: {node: '>=20.18.0'} + /@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): + resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==} peerDependencies: typescript: '>=5' dependencies: - '@solana/codecs-core': 2.0.0(typescript@5.7.2) - '@solana/codecs-data-structures': 2.0.0(typescript@5.7.2) - '@solana/codecs-numbers': 2.0.0(typescript@5.7.2) - '@solana/codecs-strings': 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/options': 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.7.2) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.7.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.7.2) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) typescript: 5.7.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - dev: false + dev: true - /@solana/errors@2.0.0(typescript@5.7.2): - resolution: {integrity: sha512-IHlaPFSy4lvYco1oHJ3X8DbchWwAwJaL/4wZKnF1ugwZ0g0re8wbABrqNOe/jyZ84VU9Z14PYM8W9oDAebdJbw==} - engines: {node: '>=20.18.0'} + /@solana/errors@2.0.0-rc.1(typescript@5.7.2): + resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==} hasBin: true peerDependencies: typescript: '>=5' @@ -2608,6 +1834,7 @@ packages: chalk: 5.3.0 commander: 12.1.0 typescript: 5.7.2 + dev: true /@solana/eslint-config-solana@3.0.6(@typescript-eslint/eslint-plugin@8.12.2)(@typescript-eslint/parser@8.12.2)(eslint-plugin-jest@28.10.0)(eslint-plugin-react-hooks@4.6.2)(eslint-plugin-simple-import-sort@12.1.1)(eslint-plugin-sort-keys-fix@1.1.2)(eslint-plugin-typescript-sort-keys@3.3.0)(eslint@8.57.0)(typescript@5.7.2): resolution: {integrity: sha512-3u024DkukJCfzUfOgN1EmWzVZLaZtgRLJ52FEdQmIG8NYOzLpaIJFgQvjYXWQlnK6ycIcSn/MesHG6sbKkMtTQ==} @@ -2663,40 +1890,20 @@ packages: typescript-eslint: 8.12.2(eslint@9.13.0)(typescript@5.7.2) dev: true - /@solana/functional@2.0.0(typescript@5.7.2): - resolution: {integrity: sha512-Sj+sLiUTimnMEyGnSLGt0lbih2xPDUhxhonnrIkPwA+hjQ3ULGHAxeevHU06nqiVEgENQYUJ5rCtHs4xhUFAkQ==} - engines: {node: '>=20.18.0'} + /@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): + resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==} peerDependencies: typescript: '>=5' dependencies: - typescript: 5.7.2 - dev: false - - /@solana/instructions@2.0.0(typescript@5.7.2): - resolution: {integrity: sha512-MiTEiNF7Pzp+Y+x4yadl2VUcNHboaW5WP52psBuhHns3GpbbruRv5efMpM9OEQNe1OsN+Eg39vjEidX55+P+DQ==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5' - dependencies: - '@solana/errors': 2.0.0(typescript@5.7.2) - typescript: 5.7.2 - dev: false - - /@solana/options@2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): - resolution: {integrity: sha512-OVc4KnYosB8oAukQ/htgrxXSxlUP6gUu5Aau6d/BgEkPQzWd/Pr+w91VWw3i3zZuu2SGpedbyh05RoJBe/hSXA==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5' - dependencies: - '@solana/codecs-core': 2.0.0(typescript@5.7.2) - '@solana/codecs-data-structures': 2.0.0(typescript@5.7.2) - '@solana/codecs-numbers': 2.0.0(typescript@5.7.2) - '@solana/codecs-strings': 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/errors': 2.0.0(typescript@5.7.2) + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.7.2) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.7.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.7.2) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.7.2) typescript: 5.7.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - dev: false + dev: true /@solana/prettier-config-solana@0.0.5(prettier@3.4.2): resolution: {integrity: sha512-igtLH1QaX5xzSLlqteexRIg9X1QKA03xKYQc2qY1TrMDDhxKXoRZOStQPWdita2FVJzxTGz/tdMGC1vS0biRcg==} @@ -2706,50 +1913,51 @@ packages: prettier: 3.4.2 dev: true - /@solana/rpc-types@2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): - resolution: {integrity: sha512-o1ApB9PYR0A3XjVSOh//SOVWgjDcqMlR3UNmtqciuREIBmWqnvPirdOa5EJxD3iPhfA4gnNnhGzT+tMDeDW/Kw==} - engines: {node: '>=20.18.0'} + /@solana/spl-token-group@0.0.7(@solana/web3.js@1.95.5)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): + resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==} + engines: {node: '>=16'} peerDependencies: - typescript: '>=5' + '@solana/web3.js': ^1.95.3 dependencies: - '@solana/addresses': 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/codecs-core': 2.0.0(typescript@5.7.2) - '@solana/codecs-numbers': 2.0.0(typescript@5.7.2) - '@solana/codecs-strings': 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/errors': 2.0.0(typescript@5.7.2) - typescript: 5.7.2 + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/web3.js': 1.95.5 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - dev: false + - typescript + dev: true - /@solana/spl-memo@0.2.5(@solana/web3.js@1.95.5): - resolution: {integrity: sha512-0Zx5t3gAdcHlRTt2O3RgGlni1x7vV7Xq7j4z9q8kKOMgU03PyoTbFQ/BSYCcICHzkaqD7ZxAiaJ6dlXolg01oA==} + /@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.95.5)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): + resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==} engines: {node: '>=16'} peerDependencies: - '@solana/web3.js': ^1.91.6 + '@solana/web3.js': ^1.95.3 dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) '@solana/web3.js': 1.95.5 - buffer: 6.0.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript dev: true - /@solana/transaction-messages@2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): - resolution: {integrity: sha512-Uc6Fw1EJLBrmgS1lH2ZfLAAKFvprWPQQzOVwZS78Pv8Whsk7tweYTK6S0Upv0nHr50rGpnORJfmdBrXE6OfNGg==} - engines: {node: '>=20.18.0'} + /@solana/spl-token@0.4.9(@solana/web3.js@1.95.5)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2): + resolution: {integrity: sha512-g3wbj4F4gq82YQlwqhPB0gHFXfgsC6UmyGMxtSLf/BozT/oKd59465DbnlUK8L8EcimKMavxsVAMoLcEdeCicg==} + engines: {node: '>=16'} peerDependencies: - typescript: '>=5' + '@solana/web3.js': ^1.95.3 dependencies: - '@solana/addresses': 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - '@solana/codecs-core': 2.0.0(typescript@5.7.2) - '@solana/codecs-data-structures': 2.0.0(typescript@5.7.2) - '@solana/codecs-numbers': 2.0.0(typescript@5.7.2) - '@solana/errors': 2.0.0(typescript@5.7.2) - '@solana/functional': 2.0.0(typescript@5.7.2) - '@solana/instructions': 2.0.0(typescript@5.7.2) - '@solana/rpc-types': 2.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) - typescript: 5.7.2 + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.2.0 + '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.95.5)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.95.5)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2) + '@solana/web3.js': 1.95.5 + buffer: 6.0.3 transitivePeerDependencies: + - bufferutil + - encoding - fastestsmallesttextencoderdecoder - dev: false + - typescript + - utf-8-validate + dev: true /@solana/web3.js@1.95.4: resolution: {integrity: sha512-sdewnNEA42ZSMxqkzdwEWi6fDgzwtJHaQa5ndUGEJYtoOnM6X5cvPmjoTUp7/k7bRrVAxfBgDnvQQHD6yhlLYw==} @@ -3050,33 +2258,6 @@ packages: - supports-color dev: true - /@typescript-eslint/eslint-plugin@8.4.0(@typescript-eslint/parser@8.12.2)(eslint@8.57.0)(typescript@5.7.2): - resolution: {integrity: sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 8.12.2(eslint@8.57.0)(typescript@5.7.2) - '@typescript-eslint/scope-manager': 8.4.0 - '@typescript-eslint/type-utils': 8.4.0(eslint@8.57.0)(typescript@5.7.2) - '@typescript-eslint/utils': 8.4.0(eslint@8.57.0)(typescript@5.7.2) - '@typescript-eslint/visitor-keys': 8.4.0 - eslint: 8.57.0 - graphemer: 1.4.0 - ignore: 5.3.1 - natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.7.2) - typescript: 5.7.2 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/eslint-plugin@8.4.0(@typescript-eslint/parser@8.4.0)(eslint@8.57.0)(typescript@5.7.2): resolution: {integrity: sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3470,28 +2651,6 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vercel/nft@0.27.5: - resolution: {integrity: sha512-b2A7M+4yMHdWKY7xCC+kBEcnMrpaSE84CnuauTjhKKoCEeej0byJMAB8h/RBVnw/HdZOAFVcxR0Izr3LL24FwA==} - engines: {node: '>=16'} - hasBin: true - dependencies: - '@mapbox/node-pre-gyp': 1.0.11 - '@rollup/pluginutils': 4.2.1 - acorn: 8.14.0 - acorn-import-attributes: 1.9.5(acorn@8.14.0) - async-sema: 3.1.1 - bindings: 1.5.0 - estree-walker: 2.0.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - node-gyp-build: 4.6.1 - resolve-from: 5.0.0 - transitivePeerDependencies: - - encoding - - supports-color - dev: true - /JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -3499,18 +2658,6 @@ packages: jsonparse: 1.3.1 through: 2.3.8 - /abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: true - - /acorn-import-attributes@1.9.5(acorn@8.14.0): - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} - peerDependencies: - acorn: ^8 - dependencies: - acorn: 8.14.0 - dev: true - /acorn-jsx@5.3.2(acorn@7.4.1): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3554,19 +2701,10 @@ packages: hasBin: true dev: true - /acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - dependencies: - debug: 4.4.0 - transitivePeerDependencies: - - supports-color + /acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true dev: true /agentkeepalive@4.5.0: @@ -3634,19 +2772,6 @@ packages: picomatch: 2.3.1 dev: true - /aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - dev: true - - /are-we-there-yet@2.0.0: - resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - dev: true - /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true @@ -3673,11 +2798,6 @@ packages: is-array-buffer: 3.0.4 dev: true - /array-find-index@1.0.2: - resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} - engines: {node: '>=0.10.0'} - dev: true - /array-includes@3.1.8: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} @@ -3741,16 +2861,6 @@ packages: is-shared-array-buffer: 1.0.3 dev: true - /arrgv@1.0.2: - resolution: {integrity: sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==} - engines: {node: '>=8.0.0'} - dev: true - - /arrify@3.0.0: - resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} - engines: {node: '>=12'} - dev: true - /assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} dependencies: @@ -3760,15 +2870,6 @@ packages: object.assign: 4.1.5 util: 0.12.5 - /assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - dev: true - - /async-sema@3.1.1: - resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} - dev: true - /async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} dev: true @@ -3777,62 +2878,6 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true - /ava@6.2.0(@ava/typescript@5.0.0): - resolution: {integrity: sha512-+GZk5PbyepjiO/68hzCZCUepQOQauKfNnI7sA4JukBTg97jD7E+tDKEA7OhGOGr6EorNNMM9+jqvgHVOTOzG4w==} - engines: {node: ^18.18 || ^20.8 || ^22 || >=23} - hasBin: true - peerDependencies: - '@ava/typescript': '*' - peerDependenciesMeta: - '@ava/typescript': - optional: true - dependencies: - '@ava/typescript': 5.0.0 - '@vercel/nft': 0.27.5 - acorn: 8.14.0 - acorn-walk: 8.3.4 - ansi-styles: 6.2.1 - arrgv: 1.0.2 - arrify: 3.0.0 - callsites: 4.2.0 - cbor: 9.0.2 - chalk: 5.3.0 - chunkd: 2.0.1 - ci-info: 4.0.0 - ci-parallel-vars: 1.0.1 - cli-truncate: 4.0.0 - code-excerpt: 4.0.0 - common-path-prefix: 3.0.0 - concordance: 5.0.4 - currently-unhandled: 0.4.1 - debug: 4.3.7(supports-color@8.1.1) - emittery: 1.0.3 - figures: 6.1.0 - globby: 14.0.2 - ignore-by-default: 2.1.0 - indent-string: 5.0.0 - is-plain-object: 5.0.0 - is-promise: 4.0.0 - matcher: 5.0.0 - memoize: 10.0.0 - ms: 2.1.3 - p-map: 7.0.2 - package-config: 5.0.0 - picomatch: 4.0.2 - plur: 5.1.0 - pretty-ms: 9.1.0 - resolve-cwd: 3.0.0 - stack-utils: 2.0.6 - strip-ansi: 7.1.0 - supertap: 3.0.1 - temp-dir: 3.0.0 - write-file-atomic: 6.0.0 - yargs: 17.7.2 - transitivePeerDependencies: - - encoding - - supports-color - dev: true - /available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -3996,7 +3041,6 @@ packages: /bignumber.js@9.1.2: resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} - dev: false /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} @@ -4012,10 +3056,6 @@ packages: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} dev: true - /blueimp-md5@2.19.0: - resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} - dev: true - /bn.js@5.2.1: resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} @@ -4095,6 +3135,7 @@ packages: /buffer-layout@1.2.2: resolution: {integrity: sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==} engines: {node: '>=4.5'} + dev: true /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -4124,11 +3165,6 @@ packages: engines: {node: '>=6'} dev: true - /callsites@4.2.0: - resolution: {integrity: sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==} - engines: {node: '>=12.20'} - dev: true - /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -4143,33 +3179,6 @@ packages: resolution: {integrity: sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==} dev: true - /cbor@9.0.2: - resolution: {integrity: sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==} - engines: {node: '>=16'} - dependencies: - nofilter: 3.1.0 - dev: true - - /chai-as-promised@8.0.1(chai@5.1.2): - resolution: {integrity: sha512-OIEJtOL8xxJSH8JJWbIoRjybbzR52iFuDHuF8eb+nTPD6tgXLjRqsgnUGqQfFODxYvq5QdirT0pN9dZ0+Gz6rA==} - peerDependencies: - chai: '>= 2.1.2 < 6' - dependencies: - chai: 5.1.2 - check-error: 2.1.1 - dev: true - - /chai@5.1.2: - resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} - engines: {node: '>=12'} - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.1 - loupe: 3.1.0 - pathval: 2.0.0 - dev: true - /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -4181,17 +3190,13 @@ packages: /chalk@5.3.0: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: true /char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} dev: true - /check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - dev: true - /check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -4212,15 +3217,6 @@ packages: fsevents: 2.3.3 dev: true - /chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - dev: true - - /chunkd@2.0.1: - resolution: {integrity: sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==} - dev: true - /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -4231,22 +3227,10 @@ packages: engines: {node: '>=8'} dev: true - /ci-parallel-vars@1.0.1: - resolution: {integrity: sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==} - dev: true - /cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: true - /cli-truncate@4.0.0: - resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} - engines: {node: '>=18'} - dependencies: - slice-ansi: 5.0.0 - string-width: 7.0.0 - dev: true - /cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: @@ -4269,13 +3253,6 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: true - /code-excerpt@4.0.0: - resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - convert-to-spaces: 2.0.1 - dev: true - /collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} dev: true @@ -4291,11 +3268,6 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true - /color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - dev: true - /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -4306,6 +3278,7 @@ packages: /commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + dev: true /commander@13.0.0: resolution: {integrity: sha512-oPYleIY8wmTVzkvQq10AEok6YcTC4sRUBl8F9gVuwchGVUCTbl/vhLTaQqutuuySYOsu8YTgV+OxKc/8Yvx+mQ==} @@ -4320,10 +3293,6 @@ packages: engines: {node: '>= 6'} dev: true - /common-path-prefix@3.0.0: - resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} - dev: true - /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: true @@ -4332,33 +3301,10 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true - /concordance@5.0.4: - resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} - engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} - dependencies: - date-time: 3.1.0 - esutils: 2.0.3 - fast-diff: 1.3.0 - js-string-escape: 1.0.1 - lodash: 4.17.21 - md5-hex: 3.0.1 - semver: 7.6.3 - well-known-symbols: 2.0.0 - dev: true - - /console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - dev: true - /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true - /convert-to-spaces@2.0.1: - resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - /create-jest@29.7.0(@types/node@22.10.5)(ts-node@10.9.2): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4382,14 +3328,6 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true - /cross-env@7.0.3: - resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} - engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} - hasBin: true - dependencies: - cross-spawn: 7.0.3 - dev: true - /cross-fetch@3.1.8: resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} dependencies: @@ -4412,13 +3350,6 @@ packages: engines: {node: '>=8'} dev: true - /currently-unhandled@0.4.1: - resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} - engines: {node: '>=0.10.0'} - dependencies: - array-find-index: 1.0.2 - dev: true - /data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -4446,13 +3377,6 @@ packages: is-data-view: 1.0.1 dev: true - /date-time@3.1.0: - resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} - engines: {node: '>=6'} - dependencies: - time-zone: 1.0.0 - dev: true - /debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -4514,11 +3438,6 @@ packages: optional: true dev: true - /deep-eql@5.0.1: - resolution: {integrity: sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==} - engines: {node: '>=6'} - dev: true - /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -4558,15 +3477,6 @@ packages: engines: {node: '>=0.4.0'} dev: true - /delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - dev: true - - /detect-libc@2.0.2: - resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} - engines: {node: '>=8'} - dev: true - /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -4654,15 +3564,6 @@ packages: engines: {node: '>=12'} dev: true - /emittery@1.0.3: - resolution: {integrity: sha512-tJdCJitoy2lrC2ldJcqN4vkqJ00lT+tOWNT1hBJjO/3FDMJa5TTIiYGCKGkn/WfCyOzUMObeohbVTj00fhiLiA==} - engines: {node: '>=14.16'} - dev: true - - /emoji-regex@10.3.0: - resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} - dev: true - /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true @@ -4783,38 +3684,6 @@ packages: dependencies: es6-promise: 4.2.8 - /esbuild@0.23.0: - resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==} - engines: {node: '>=18'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.23.0 - '@esbuild/android-arm': 0.23.0 - '@esbuild/android-arm64': 0.23.0 - '@esbuild/android-x64': 0.23.0 - '@esbuild/darwin-arm64': 0.23.0 - '@esbuild/darwin-x64': 0.23.0 - '@esbuild/freebsd-arm64': 0.23.0 - '@esbuild/freebsd-x64': 0.23.0 - '@esbuild/linux-arm': 0.23.0 - '@esbuild/linux-arm64': 0.23.0 - '@esbuild/linux-ia32': 0.23.0 - '@esbuild/linux-loong64': 0.23.0 - '@esbuild/linux-mips64el': 0.23.0 - '@esbuild/linux-ppc64': 0.23.0 - '@esbuild/linux-riscv64': 0.23.0 - '@esbuild/linux-s390x': 0.23.0 - '@esbuild/linux-x64': 0.23.0 - '@esbuild/netbsd-x64': 0.23.0 - '@esbuild/openbsd-arm64': 0.23.0 - '@esbuild/openbsd-x64': 0.23.0 - '@esbuild/sunos-x64': 0.23.0 - '@esbuild/win32-arm64': 0.23.0 - '@esbuild/win32-ia32': 0.23.0 - '@esbuild/win32-x64': 0.23.0 - dev: true - /escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -5475,21 +4344,6 @@ packages: strip-final-newline: 2.0.0 dev: true - /execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.1.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - dev: true - /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} @@ -5554,6 +4408,7 @@ packages: /fastestsmallesttextencoderdecoder@1.0.22: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + dev: true /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} @@ -5578,13 +4433,6 @@ packages: picomatch: 4.0.2 dev: true - /figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} - dependencies: - is-unicode-supported: 2.0.0 - dev: true - /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -5649,11 +4497,6 @@ packages: - supports-color dev: true - /find-up-simple@1.0.0: - resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} - engines: {node: '>=18'} - dev: true - /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5743,13 +4586,6 @@ packages: universalify: 2.0.1 dev: true - /fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: true - /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -5779,22 +4615,6 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true - /gauge@3.0.2: - resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} - engines: {node: '>=10'} - deprecated: This package is no longer supported. - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - dev: true - /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -5805,15 +4625,6 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true - /get-east-asian-width@1.2.0: - resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} - engines: {node: '>=18'} - dev: true - - /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true - /get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -5834,11 +4645,6 @@ packages: engines: {node: '>=10'} dev: true - /get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - dev: true - /get-symbol-description@1.0.2: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} @@ -5848,12 +4654,6 @@ packages: get-intrinsic: 1.2.4 dev: true - /get-tsconfig@4.7.5: - resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} - dependencies: - resolve-pkg-maps: 1.0.0 - dev: true - /gh-pages@6.3.0: resolution: {integrity: sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==} engines: {node: '>=10'} @@ -5894,19 +4694,6 @@ packages: path-scurry: 1.11.1 dev: true - /glob@11.0.0: - resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} - engines: {node: 20 || >=22} - hasBin: true - dependencies: - foreground-child: 3.1.1 - jackspeak: 4.0.1 - minimatch: 10.0.1 - minipass: 7.1.2 - package-json-from-dist: 1.0.0 - path-scurry: 2.0.0 - dev: true - /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -5960,18 +4747,6 @@ packages: slash: 3.0.0 dev: true - /globby@14.0.2: - resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} - engines: {node: '>=18'} - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.2 - ignore: 5.3.1 - path-type: 5.0.0 - slash: 5.1.0 - unicorn-magic: 0.1.0 - dev: true - /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -6012,10 +4787,6 @@ packages: dependencies: has-symbols: 1.0.3 - /has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - dev: true - /hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -6031,26 +4802,11 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true - /https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - dependencies: - agent-base: 6.0.2 - debug: 4.4.0 - transitivePeerDependencies: - - supports-color - dev: true - /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} dev: true - /human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - dev: true - /humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} dependencies: @@ -6059,11 +4815,6 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - /ignore-by-default@2.1.0: - resolution: {integrity: sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==} - engines: {node: '>=10 <11 || >=12 <13 || >=14'} - dev: true - /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -6096,11 +4847,6 @@ packages: engines: {node: '>=0.8.19'} dev: true - /indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - dev: true - /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -6121,16 +4867,6 @@ packages: side-channel: 1.0.4 dev: true - /interpret@1.4.0: - resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} - engines: {node: '>= 0.10'} - dev: true - - /irregular-plurals@3.5.0: - resolution: {integrity: sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==} - engines: {node: '>=8'} - dev: true - /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -6206,11 +4942,6 @@ packages: engines: {node: '>=8'} dev: true - /is-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - dev: true - /is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -6282,15 +5013,6 @@ packages: engines: {node: '>=8'} dev: true - /is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - dev: true - - /is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - dev: true - /is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} dependencies: @@ -6317,11 +5039,6 @@ packages: engines: {node: '>=8'} dev: true - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -6347,11 +5064,6 @@ packages: engines: {node: '>=10'} dev: true - /is-unicode-supported@2.0.0: - resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} - engines: {node: '>=18'} - dev: true - /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: @@ -6464,15 +5176,6 @@ packages: '@pkgjs/parseargs': 0.11.0 dev: true - /jackspeak@4.0.1: - resolution: {integrity: sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==} - engines: {node: 20 || >=22} - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - dev: true - /jake@10.9.1: resolution: {integrity: sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==} engines: {node: '>=10'} @@ -7351,11 +6054,6 @@ packages: resolution: {integrity: sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==} dev: false - /js-string-escape@1.0.1: - resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} - engines: {node: '>= 0.8'} - dev: true - /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -7468,11 +6166,6 @@ packages: uc.micro: 2.1.0 dev: true - /load-json-file@7.0.1: - resolution: {integrity: sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -7507,12 +6200,6 @@ packages: is-unicode-supported: 0.1.0 dev: true - /loupe@3.1.0: - resolution: {integrity: sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==} - dependencies: - get-func-name: 2.0.2 - dev: true - /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: @@ -7523,11 +6210,6 @@ packages: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} dev: true - /lru-cache@11.0.0: - resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==} - engines: {node: 20 || >=22} - dev: true - /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -7573,50 +6255,21 @@ packages: dev: true /markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - dev: true - - /matched@5.0.1: - resolution: {integrity: sha512-E1fhSTPRyhAlNaNvGXAgZQlq1hL0bgYMTk/6bktVlIhzUnX/SZs7296ACdVeNJE8xFNGSuvd9IpI7vSnmcqLvw==} - engines: {node: '>=10'} - dependencies: - glob: 7.2.3 - picomatch: 2.3.1 - dev: true - - /matcher@5.0.0: - resolution: {integrity: sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - escape-string-regexp: 5.0.0 - dev: true - - /md5-hex@3.0.1: - resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} - engines: {node: '>=8'} + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true dependencies: - blueimp-md5: 2.19.0 + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 dev: true /mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} dev: true - /memoize@10.0.0: - resolution: {integrity: sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==} - engines: {node: '>=18'} - dependencies: - mimic-function: 5.0.0 - dev: true - /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -7659,16 +6312,6 @@ packages: engines: {node: '>=6'} dev: true - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true - - /mimic-function@5.0.0: - resolution: {integrity: sha512-RBfQ+9X9DpXdEoK7Bu+KeEU6vFhumEIiXKWECPzRBmDserEq4uR2b/VCm0LwpMSosoq2k+Zuxj/GzOr0Fn6h/g==} - engines: {node: '>=18'} - dev: true - /minimatch@10.0.1: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} @@ -7700,37 +6343,11 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true - /minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - dependencies: - yallist: 4.0.0 - dev: true - - /minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - dev: true - /minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} dev: true - /minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - dev: true - - /mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - dev: true - /mocha@11.0.1: resolution: {integrity: sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -7804,19 +6421,6 @@ packages: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} dev: true - /nofilter@3.1.0: - resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==} - engines: {node: '>=12.19'} - dev: true - - /nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} - hasBin: true - dependencies: - abbrev: 1.1.1 - dev: true - /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -7829,28 +6433,6 @@ packages: path-key: 3.1.1 dev: true - /npm-run-path@5.1.0: - resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - path-key: 4.0.0 - dev: true - - /npmlog@5.0.1: - resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} - deprecated: This package is no longer supported. - dependencies: - are-we-there-yet: 2.0.0 - console-control-strings: 1.1.0 - gauge: 3.0.2 - set-blocking: 2.0.0 - dev: true - - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: true - /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} dev: true @@ -7916,13 +6498,6 @@ packages: mimic-fn: 2.1.0 dev: true - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - dependencies: - mimic-fn: 4.0.0 - dev: true - /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} @@ -7963,24 +6538,11 @@ packages: p-limit: 3.1.0 dev: true - /p-map@7.0.2: - resolution: {integrity: sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==} - engines: {node: '>=18'} - dev: true - /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} dev: true - /package-config@5.0.0: - resolution: {integrity: sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==} - engines: {node: '>=18'} - dependencies: - find-up-simple: 1.0.0 - load-json-file: 7.0.1 - dev: true - /package-json-from-dist@1.0.0: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} dev: true @@ -8006,11 +6568,6 @@ packages: lines-and-columns: 1.2.4 dev: true - /parse-ms@4.0.0: - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} - engines: {node: '>=18'} - dev: true - /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -8026,11 +6583,6 @@ packages: engines: {node: '>=8'} dev: true - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true - /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true @@ -8043,29 +6595,11 @@ packages: minipass: 7.1.2 dev: true - /path-scurry@2.0.0: - resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} - engines: {node: 20 || >=22} - dependencies: - lru-cache: 11.0.0 - minipass: 7.1.2 - dev: true - /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} dev: true - /path-type@5.0.0: - resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} - engines: {node: '>=12'} - dev: true - - /pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} - dev: true - /pause-stream@0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} dependencies: @@ -8102,13 +6636,6 @@ packages: find-up: 4.1.0 dev: true - /plur@5.1.0: - resolution: {integrity: sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - irregular-plurals: 3.5.0 - dev: true - /possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -8155,18 +6682,6 @@ packages: react-is: 18.2.0 dev: true - /pretty-ms@9.1.0: - resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} - engines: {node: '>=18'} - dependencies: - parse-ms: 4.0.0 - dev: true - - /process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - dev: true - /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -8219,15 +6734,6 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - dev: true - /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -8235,13 +6741,6 @@ packages: picomatch: 2.3.1 dev: true - /rechoir@0.6.2: - resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} - engines: {node: '>= 0.10'} - dependencies: - resolve: 1.22.8 - dev: true - /regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} @@ -8282,10 +6781,6 @@ packages: engines: {node: '>=8'} dev: true - /resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - dev: true - /resolve.exports@2.0.2: resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} engines: {node: '>=10'} @@ -8313,29 +6808,6 @@ packages: glob: 7.2.3 dev: true - /rimraf@6.0.1: - resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} - engines: {node: 20 || >=22} - hasBin: true - dependencies: - glob: 11.0.0 - package-json-from-dist: 1.0.0 - dev: true - - /rollup-plugin-dts@6.1.1(rollup@4.30.1)(typescript@5.7.2): - resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==} - engines: {node: '>=16'} - peerDependencies: - rollup: ^3.29.4 || ^4 - typescript: ^4.5 || ^5.0 - dependencies: - magic-string: 0.30.10 - rollup: 4.30.1 - typescript: 5.7.2 - optionalDependencies: - '@babel/code-frame': 7.26.2 - dev: true - /rollup@4.30.1: resolution: {integrity: sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -8424,29 +6896,12 @@ packages: hasBin: true dev: true - /serialize-error@7.0.1: - resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} - engines: {node: '>=10'} - dependencies: - type-fest: 0.13.1 - dev: true - - /serialize-javascript@6.0.1: - resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} - dependencies: - randombytes: 2.1.0 - dev: true - /serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} dependencies: randombytes: 2.1.0 dev: true - /set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: true - /set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -8479,25 +6934,6 @@ packages: engines: {node: '>=8'} dev: true - /shelljs@0.8.5: - resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} - engines: {node: '>=4'} - hasBin: true - dependencies: - glob: 7.2.3 - interpret: 1.4.0 - rechoir: 0.6.2 - dev: true - - /shx@0.3.4: - resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==} - engines: {node: '>=6'} - hasBin: true - dependencies: - minimist: 1.2.8 - shelljs: 0.8.5 - dev: true - /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -8524,23 +6960,6 @@ packages: engines: {node: '>=8'} dev: true - /slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - dev: true - - /slice-ansi@5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 4.0.0 - dev: true - - /smob@1.4.1: - resolution: {integrity: sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==} - dev: true - /snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: @@ -8548,68 +6967,6 @@ packages: tslib: 2.8.1 dev: true - /solana-bankrun-darwin-arm64@0.2.0: - resolution: {integrity: sha512-ENQ5Z/CYeY8ZVWIc2VutY/gMlBaHi93/kDw9w0iVwewoV+/YpQmP2irwrshIKu6ggRPTF3Ehlh2V6fGVIYWcXw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /solana-bankrun-darwin-universal@0.2.0: - resolution: {integrity: sha512-HE45TvZXzBipm1fMn87+fkHeIuQ/KFAi5G/S29y/TLuBYt4RDI935RkWiT0rEQ7KwnwO6Y1aTsOaQXldY5R7uQ==} - engines: {node: '>= 10'} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /solana-bankrun-darwin-x64@0.2.0: - resolution: {integrity: sha512-42UsVrnac2Oo4UaIDo60zfI3Xn1i8W6fmcc9ixJQZNTtdO8o2/sY4mFxcJx9lhLMhda5FPHrQbGYgYdIs0kK0g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /solana-bankrun-linux-x64-gnu@0.2.0: - resolution: {integrity: sha512-WnqQjfBBdcI0ZLysjvRStI8gX7vm1c3CI6CC03lgkUztH+Chcq9C4LI9m2M8mXza8Xkn9ryeKAmX36Bx/yoVzg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /solana-bankrun-linux-x64-musl@0.2.0: - resolution: {integrity: sha512-8mtf14ZBoah30+MIJBUwb5BlGLRZyK5cZhCkYnC/ROqaIDN8RxMM44NL63gTUIaNHsFwWGA9xR0KSeljeh3PKQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /solana-bankrun@0.2.0: - resolution: {integrity: sha512-TS6vYoO/9YJZng7oiLOVyuz8V7yLow5Hp4SLYWW71XM3702v+z9f1fvUBKudRfa4dfpta4tRNufApSiBIALxJQ==} - engines: {node: '>= 10'} - dependencies: - '@solana/web3.js': 1.95.5 - bs58: 4.0.1 - optionalDependencies: - solana-bankrun-darwin-arm64: 0.2.0 - solana-bankrun-darwin-universal: 0.2.0 - solana-bankrun-darwin-x64: 0.2.0 - solana-bankrun-linux-x64-gnu: 0.2.0 - solana-bankrun-linux-x64-musl: 0.2.0 - transitivePeerDependencies: - - bufferutil - - encoding - - utf-8-validate - dev: true - /source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} dependencies: @@ -8617,13 +6974,6 @@ packages: source-map: 0.6.1 dev: true - /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: true - /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -8704,15 +7054,6 @@ packages: strip-ansi: 7.1.0 dev: true - /string-width@7.0.0: - resolution: {integrity: sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==} - engines: {node: '>=18'} - dependencies: - emoji-regex: 10.3.0 - get-east-asian-width: 1.2.0 - strip-ansi: 7.1.0 - dev: true - /string.prototype.trim@1.2.9: resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} engines: {node: '>= 0.4'} @@ -8740,12 +7081,6 @@ packages: es-object-atoms: 1.0.0 dev: true - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - dependencies: - safe-buffer: 5.2.1 - dev: true - /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -8775,11 +7110,6 @@ packages: engines: {node: '>=6'} dev: true - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true - /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -8800,16 +7130,6 @@ packages: resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} engines: {node: '>=14.0.0'} - /supertap@3.0.1: - resolution: {integrity: sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - indent-string: 5.0.0 - js-yaml: 3.14.1 - serialize-error: 7.0.1 - strip-ansi: 7.1.0 - dev: true - /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -8836,34 +7156,6 @@ packages: tslib: 2.8.1 dev: true - /tar@6.2.0: - resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} - engines: {node: '>=10'} - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - dev: true - - /temp-dir@3.0.0: - resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} - engines: {node: '>=14.16'} - dev: true - - /terser@5.24.0: - resolution: {integrity: sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==} - engines: {node: '>=10'} - hasBin: true - dependencies: - '@jridgewell/source-map': 0.3.5 - acorn: 8.14.0 - commander: 2.20.3 - source-map-support: 0.5.21 - dev: true - /test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -8883,11 +7175,6 @@ packages: /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - /time-zone@1.0.0: - resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} - engines: {node: '>=4'} - dev: true - /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true @@ -9041,17 +7328,6 @@ packages: typescript: 5.7.2 dev: true - /tsx@4.19.2: - resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} - engines: {node: '>=18.0.0'} - hasBin: true - dependencies: - esbuild: 0.23.0 - get-tsconfig: 4.7.5 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /turbo-darwin-64@2.3.3: resolution: {integrity: sha512-bxX82xe6du/3rPmm4aCC5RdEilIN99VUld4HkFQuw+mvFg6darNBuQxyWSHZTtc25XgYjQrjsV05888w1grpaA==} cpu: [x64] @@ -9124,11 +7400,6 @@ packages: engines: {node: '>=4'} dev: true - /type-fest@0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} - engines: {node: '>=10'} - dev: true - /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -9224,6 +7495,7 @@ packages: resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} hasBin: true + dev: true /uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -9241,11 +7513,6 @@ packages: /undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - /unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - dev: true - /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -9275,10 +7542,6 @@ packages: dependencies: node-gyp-build: 4.6.1 - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true - /util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} dependencies: @@ -9328,11 +7591,6 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - /well-known-symbols@2.0.0: - resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} - engines: {node: '>=6'} - dev: true - /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: @@ -9367,12 +7625,6 @@ packages: isexe: 2.0.0 dev: true - /wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - dependencies: - string-width: 4.2.3 - dev: true - /workerpool@6.5.1: resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} dev: true @@ -9415,14 +7667,6 @@ packages: signal-exit: 4.1.0 dev: true - /write-file-atomic@6.0.0: - resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} - engines: {node: ^18.17.0 || >=20.5.0} - dependencies: - imurmurhash: 0.1.4 - signal-exit: 4.1.0 - dev: true - /ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -9459,10 +7703,6 @@ packages: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} dev: true - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - /yaml@2.6.1: resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} engines: {node: '>= 14'} diff --git a/record/README.md b/record/README.md new file mode 100644 index 00000000000..8a80596b395 --- /dev/null +++ b/record/README.md @@ -0,0 +1,2 @@ +NOTE: The record program and clients are now maintained at +[solana-program/record](https://github.com/solana-program/record). diff --git a/record/program/Cargo.toml b/record/program/Cargo.toml deleted file mode 100644 index 5d9ebe6fc79..00000000000 --- a/record/program/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "spl-record" -version = "0.3.0" -description = "Solana Program Library Record Program" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -bytemuck = { version = "1.21.0", features = ["derive"] } -num-derive = "0.4" -num-traits = "0.2" -solana-account-info = "2.1.0" -solana-decode-error = "2.1.0" -solana-instruction = { version = "2.1.0", features = ["std"] } -solana-msg = "2.1.0" -solana-program-entrypoint = "2.1.0" -solana-program-error = "2.1.0" -solana-program-pack = "2.1.0" -solana-pubkey = { version = "2.1.0", features = ["bytemuck"] } -solana-rent = "2.1.0" -thiserror = "2.0" - -[dev-dependencies] -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[lints] -workspace = true diff --git a/record/program/README.md b/record/program/README.md deleted file mode 100644 index 188614925a6..00000000000 --- a/record/program/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Record - -On-chain program for writing arbitrary data to an account, authorized by an -owner of the account. - -## Audit - -The repository [README](https://github.com/solana-labs/solana-program-library#audits) -contains information about program audits. diff --git a/record/program/program-id.md b/record/program/program-id.md deleted file mode 100644 index 09f28307584..00000000000 --- a/record/program/program-id.md +++ /dev/null @@ -1 +0,0 @@ -ReciQBw6sQKH9TVVJQDnbnJ5W7FP539tPHjZhRF4E9r diff --git a/record/program/src/entrypoint.rs b/record/program/src/entrypoint.rs deleted file mode 100644 index f52b183fb1c..00000000000 --- a/record/program/src/entrypoint.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Program entrypoint - -#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] - -use { - solana_account_info::AccountInfo, solana_program_error::ProgramResult, solana_pubkey::Pubkey, -}; - -solana_program_entrypoint::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - crate::processor::process_instruction(program_id, accounts, instruction_data) -} diff --git a/record/program/src/error.rs b/record/program/src/error.rs deleted file mode 100644 index f8780435836..00000000000 --- a/record/program/src/error.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Error types - -use { - num_derive::FromPrimitive, solana_decode_error::DecodeError, - solana_program_error::ProgramError, thiserror::Error, -}; - -/// Errors that may be returned by the program. -#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] -pub enum RecordError { - /// Incorrect authority provided on update or delete - #[error("Incorrect authority provided on update or delete")] - IncorrectAuthority, - - /// Calculation overflow - #[error("Calculation overflow")] - Overflow, -} -impl From for ProgramError { - fn from(e: RecordError) -> Self { - ProgramError::Custom(e as u32) - } -} -impl DecodeError for RecordError { - fn type_of() -> &'static str { - "Record Error" - } -} diff --git a/record/program/src/instruction.rs b/record/program/src/instruction.rs deleted file mode 100644 index a8de8c82a76..00000000000 --- a/record/program/src/instruction.rs +++ /dev/null @@ -1,260 +0,0 @@ -//! Program instructions - -use { - crate::id, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - std::mem::size_of, -}; - -/// Instructions supported by the program -#[derive(Clone, Debug, PartialEq)] -pub enum RecordInstruction<'a> { - /// Create a new record - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Record account, must be uninitialized - /// 1. `[]` Record authority - Initialize, - - /// Write to the provided record account - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Record account, must be previously initialized - /// 1. `[signer]` Current record authority - Write { - /// Offset to start writing record, expressed as `u64`. - offset: u64, - /// Data to replace the existing record data - data: &'a [u8], - }, - - /// Update the authority of the provided record account - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Record account, must be previously initialized - /// 1. `[signer]` Current record authority - /// 2. `[]` New record authority - SetAuthority, - - /// Close the provided record account, draining lamports to recipient - /// account - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` Record account, must be previously initialized - /// 1. `[signer]` Record authority - /// 2. `[]` Receiver of account lamports - CloseAccount, - - /// Reallocate additional space in a record account - /// - /// If the record account already has enough space to hold the specified - /// data length, then the instruction does nothing. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The record account to reallocate - /// 1. `[signer]` The account's owner - Reallocate { - /// The length of the data to hold in the record account excluding meta - /// data - data_length: u64, - }, -} - -impl<'a> RecordInstruction<'a> { - /// Unpacks a byte buffer into a [RecordInstruction]. - pub fn unpack(input: &'a [u8]) -> Result { - const U32_BYTES: usize = 4; - const U64_BYTES: usize = 8; - - let (&tag, rest) = input - .split_first() - .ok_or(ProgramError::InvalidInstructionData)?; - Ok(match tag { - 0 => Self::Initialize, - 1 => { - let offset = rest - .get(..U64_BYTES) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(ProgramError::InvalidInstructionData)?; - let (length, data) = rest[U64_BYTES..].split_at(U32_BYTES); - let length = u32::from_le_bytes( - length - .try_into() - .map_err(|_| ProgramError::InvalidInstructionData)?, - ) as usize; - - Self::Write { - offset, - data: &data[..length], - } - } - 2 => Self::SetAuthority, - 3 => Self::CloseAccount, - 4 => { - let data_length = rest - .get(..U64_BYTES) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(ProgramError::InvalidInstructionData)?; - - Self::Reallocate { data_length } - } - _ => return Err(ProgramError::InvalidInstructionData), - }) - } - - /// Packs a [RecordInstruction] into a byte buffer. - pub fn pack(&self) -> Vec { - let mut buf = Vec::with_capacity(size_of::()); - match self { - Self::Initialize => buf.push(0), - Self::Write { offset, data } => { - buf.push(1); - buf.extend_from_slice(&offset.to_le_bytes()); - buf.extend_from_slice(&(data.len() as u32).to_le_bytes()); - buf.extend_from_slice(data); - } - Self::SetAuthority => buf.push(2), - Self::CloseAccount => buf.push(3), - Self::Reallocate { data_length } => { - buf.push(4); - buf.extend_from_slice(&data_length.to_le_bytes()); - } - }; - buf - } -} - -/// Create a `RecordInstruction::Initialize` instruction -pub fn initialize(record_account: &Pubkey, authority: &Pubkey) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(*record_account, false), - AccountMeta::new_readonly(*authority, false), - ], - data: RecordInstruction::Initialize.pack(), - } -} - -/// Create a `RecordInstruction::Write` instruction -pub fn write(record_account: &Pubkey, signer: &Pubkey, offset: u64, data: &[u8]) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(*record_account, false), - AccountMeta::new_readonly(*signer, true), - ], - data: RecordInstruction::Write { offset, data }.pack(), - } -} - -/// Create a `RecordInstruction::SetAuthority` instruction -pub fn set_authority( - record_account: &Pubkey, - signer: &Pubkey, - new_authority: &Pubkey, -) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(*record_account, false), - AccountMeta::new_readonly(*signer, true), - AccountMeta::new_readonly(*new_authority, false), - ], - data: RecordInstruction::SetAuthority.pack(), - } -} - -/// Create a `RecordInstruction::CloseAccount` instruction -pub fn close_account(record_account: &Pubkey, signer: &Pubkey, receiver: &Pubkey) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(*record_account, false), - AccountMeta::new_readonly(*signer, true), - AccountMeta::new(*receiver, false), - ], - data: RecordInstruction::CloseAccount.pack(), - } -} - -/// Create a `RecordInstruction::Reallocate` instruction -pub fn reallocate(record_account: &Pubkey, signer: &Pubkey, data_length: u64) -> Instruction { - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(*record_account, false), - AccountMeta::new_readonly(*signer, true), - ], - data: RecordInstruction::Reallocate { data_length }.pack(), - } -} - -#[cfg(test)] -mod tests { - use {super::*, crate::state::tests::TEST_BYTES, solana_program_error::ProgramError}; - - #[test] - fn serialize_initialize() { - let instruction = RecordInstruction::Initialize; - let expected = vec![0]; - assert_eq!(instruction.pack(), expected); - assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); - } - - #[test] - fn serialize_write() { - let data = &TEST_BYTES; - let offset = 0u64; - let instruction = RecordInstruction::Write { offset: 0, data }; - let mut expected = vec![1]; - expected.extend_from_slice(&offset.to_le_bytes()); - expected.extend_from_slice(&(data.len() as u32).to_le_bytes()); - expected.extend_from_slice(data); - assert_eq!(instruction.pack(), expected); - assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); - } - - #[test] - fn serialize_set_authority() { - let instruction = RecordInstruction::SetAuthority; - let expected = vec![2]; - assert_eq!(instruction.pack(), expected); - assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); - } - - #[test] - fn serialize_close_account() { - let instruction = RecordInstruction::CloseAccount; - let expected = vec![3]; - assert_eq!(instruction.pack(), expected); - assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); - } - - #[test] - fn serialize_reallocate() { - let data_length = 16u64; - let instruction = RecordInstruction::Reallocate { data_length }; - let mut expected = vec![4]; - expected.extend_from_slice(&data_length.to_le_bytes()); - assert_eq!(instruction.pack(), expected); - assert_eq!(RecordInstruction::unpack(&expected).unwrap(), instruction); - } - - #[test] - fn deserialize_invalid_instruction() { - let mut expected = vec![12]; - expected.extend_from_slice(&TEST_BYTES); - let err: ProgramError = RecordInstruction::unpack(&expected).unwrap_err(); - assert_eq!(err, ProgramError::InvalidInstructionData); - } -} diff --git a/record/program/src/lib.rs b/record/program/src/lib.rs deleted file mode 100644 index 7646e07c210..00000000000 --- a/record/program/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Record program -#![deny(missing_docs)] - -mod entrypoint; -pub mod error; -pub mod instruction; -pub mod processor; -pub mod state; - -// Export current SDK types for downstream users building with a different SDK -// version -pub use { - solana_account_info, solana_decode_error, solana_instruction, solana_msg, - solana_program_entrypoint, solana_program_error, solana_program_pack, solana_pubkey, -}; - -solana_pubkey::declare_id!("recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5"); diff --git a/record/program/src/processor.rs b/record/program/src/processor.rs deleted file mode 100644 index 2def5c1e960..00000000000 --- a/record/program/src/processor.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Program state processor - -use { - crate::{error::RecordError, instruction::RecordInstruction, state::RecordData}, - solana_account_info::{next_account_info, AccountInfo}, - solana_msg::msg, - solana_program_error::{ProgramError, ProgramResult}, - solana_program_pack::IsInitialized, - solana_pubkey::Pubkey, -}; - -fn check_authority(authority_info: &AccountInfo, expected_authority: &Pubkey) -> ProgramResult { - if expected_authority != authority_info.key { - msg!("Incorrect record authority provided"); - return Err(RecordError::IncorrectAuthority.into()); - } - if !authority_info.is_signer { - msg!("Record authority signature missing"); - return Err(ProgramError::MissingRequiredSignature); - } - Ok(()) -} - -/// Instruction processor -pub fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - let instruction = RecordInstruction::unpack(input)?; - let account_info_iter = &mut accounts.iter(); - - match instruction { - RecordInstruction::Initialize => { - msg!("RecordInstruction::Initialize"); - - let data_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - let raw_data = &mut data_info.data.borrow_mut(); - if raw_data.len() < RecordData::WRITABLE_START_INDEX { - return Err(ProgramError::InvalidAccountData); - } - - let account_data = bytemuck::try_from_bytes_mut::( - &mut raw_data[..RecordData::WRITABLE_START_INDEX], - ) - .map_err(|_| ProgramError::InvalidArgument)?; - if account_data.is_initialized() { - msg!("Record account already initialized"); - return Err(ProgramError::AccountAlreadyInitialized); - } - - account_data.authority = *authority_info.key; - account_data.version = RecordData::CURRENT_VERSION; - Ok(()) - } - - RecordInstruction::Write { offset, data } => { - msg!("RecordInstruction::Write"); - let data_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - { - let raw_data = &data_info.data.borrow(); - if raw_data.len() < RecordData::WRITABLE_START_INDEX { - return Err(ProgramError::InvalidAccountData); - } - let account_data = bytemuck::try_from_bytes::( - &raw_data[..RecordData::WRITABLE_START_INDEX], - ) - .map_err(|_| ProgramError::InvalidArgument)?; - if !account_data.is_initialized() { - msg!("Record account not initialized"); - return Err(ProgramError::UninitializedAccount); - } - check_authority(authority_info, &account_data.authority)?; - } - let start = RecordData::WRITABLE_START_INDEX.saturating_add(offset as usize); - let end = start.saturating_add(data.len()); - if end > data_info.data.borrow().len() { - Err(ProgramError::AccountDataTooSmall) - } else { - data_info.data.borrow_mut()[start..end].copy_from_slice(data); - Ok(()) - } - } - - RecordInstruction::SetAuthority => { - msg!("RecordInstruction::SetAuthority"); - let data_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let new_authority_info = next_account_info(account_info_iter)?; - let raw_data = &mut data_info.data.borrow_mut(); - if raw_data.len() < RecordData::WRITABLE_START_INDEX { - return Err(ProgramError::InvalidAccountData); - } - let account_data = bytemuck::try_from_bytes_mut::( - &mut raw_data[..RecordData::WRITABLE_START_INDEX], - ) - .map_err(|_| ProgramError::InvalidArgument)?; - if !account_data.is_initialized() { - msg!("Record account not initialized"); - return Err(ProgramError::UninitializedAccount); - } - check_authority(authority_info, &account_data.authority)?; - account_data.authority = *new_authority_info.key; - Ok(()) - } - - RecordInstruction::CloseAccount => { - msg!("RecordInstruction::CloseAccount"); - let data_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let destination_info = next_account_info(account_info_iter)?; - let raw_data = &mut data_info.data.borrow_mut(); - if raw_data.len() < RecordData::WRITABLE_START_INDEX { - return Err(ProgramError::InvalidAccountData); - } - let account_data = bytemuck::try_from_bytes_mut::( - &mut raw_data[..RecordData::WRITABLE_START_INDEX], - ) - .map_err(|_| ProgramError::InvalidArgument)?; - if !account_data.is_initialized() { - msg!("Record not initialized"); - return Err(ProgramError::UninitializedAccount); - } - check_authority(authority_info, &account_data.authority)?; - let destination_starting_lamports = destination_info.lamports(); - let data_lamports = data_info.lamports(); - **data_info.lamports.borrow_mut() = 0; - **destination_info.lamports.borrow_mut() = destination_starting_lamports - .checked_add(data_lamports) - .ok_or(RecordError::Overflow)?; - Ok(()) - } - - RecordInstruction::Reallocate { data_length } => { - msg!("RecordInstruction::Reallocate"); - let data_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - { - let raw_data = &mut data_info.data.borrow_mut(); - if raw_data.len() < RecordData::WRITABLE_START_INDEX { - return Err(ProgramError::InvalidAccountData); - } - let account_data = bytemuck::try_from_bytes_mut::( - &mut raw_data[..RecordData::WRITABLE_START_INDEX], - ) - .map_err(|_| ProgramError::InvalidArgument)?; - if !account_data.is_initialized() { - msg!("Record not initialized"); - return Err(ProgramError::UninitializedAccount); - } - check_authority(authority_info, &account_data.authority)?; - } - - // needed account length is the sum of the meta data length and the specified - // data length - let needed_account_length = std::mem::size_of::() - .checked_add( - usize::try_from(data_length).map_err(|_| ProgramError::InvalidArgument)?, - ) - .unwrap(); - - // reallocate - if data_info.data_len() >= needed_account_length { - msg!("no additional reallocation needed"); - return Ok(()); - } - msg!( - "reallocating +{:?} bytes", - needed_account_length - .checked_sub(data_info.data_len()) - .unwrap(), - ); - data_info.realloc(needed_account_length, false)?; - Ok(()) - } - } -} diff --git a/record/program/src/state.rs b/record/program/src/state.rs deleted file mode 100644 index 0f8e78849a0..00000000000 --- a/record/program/src/state.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Program state -use { - bytemuck::{Pod, Zeroable}, - solana_program_pack::IsInitialized, - solana_pubkey::Pubkey, -}; - -/// Header type for recorded account data -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct RecordData { - /// Struct version, allows for upgrades to the program - pub version: u8, - - /// The account allowed to update the data - pub authority: Pubkey, -} - -impl RecordData { - /// Version to fill in on new created accounts - pub const CURRENT_VERSION: u8 = 1; - - /// Start of writable account data, after version and authority - pub const WRITABLE_START_INDEX: usize = 33; -} - -impl IsInitialized for RecordData { - /// Is initialized - fn is_initialized(&self) -> bool { - self.version == Self::CURRENT_VERSION - } -} - -#[cfg(test)] -pub mod tests { - use {super::*, solana_program_error::ProgramError}; - - /// Version for tests - pub const TEST_VERSION: u8 = 1; - /// Pubkey for tests - pub const TEST_PUBKEY: Pubkey = Pubkey::new_from_array([100; 32]); - /// Bytes for tests - pub const TEST_BYTES: [u8; 8] = [42; 8]; - /// RecordData for tests - pub const TEST_RECORD_DATA: RecordData = RecordData { - version: TEST_VERSION, - authority: TEST_PUBKEY, - }; - - #[test] - fn serialize_data() { - let mut expected = vec![TEST_VERSION]; - expected.extend_from_slice(&TEST_PUBKEY.to_bytes()); - assert_eq!(bytemuck::bytes_of(&TEST_RECORD_DATA), expected); - assert_eq!( - *bytemuck::try_from_bytes::(&expected).unwrap(), - TEST_RECORD_DATA, - ); - } - - #[test] - fn deserialize_invalid_slice() { - let mut expected = vec![TEST_VERSION]; - expected.extend_from_slice(&TEST_PUBKEY.to_bytes()); - expected.extend_from_slice(&TEST_BYTES); - let err = bytemuck::try_from_bytes::(&expected) - .map_err(|_| ProgramError::InvalidArgument) - .unwrap_err(); - assert_eq!(err, ProgramError::InvalidArgument); - } -} diff --git a/record/program/tests/functional.rs b/record/program/tests/functional.rs deleted file mode 100644 index 3fe0ef9de43..00000000000 --- a/record/program/tests/functional.rs +++ /dev/null @@ -1,698 +0,0 @@ -#![cfg(feature = "test-sbf")] - -use { - solana_instruction::{error::InstructionError, AccountMeta, Instruction}, - solana_program_test::*, - solana_pubkey::Pubkey, - solana_rent::Rent, - solana_sdk::{ - signature::{Keypair, Signer}, - system_instruction, - transaction::{Transaction, TransactionError}, - }, - spl_record::{ - error::RecordError, id, instruction, processor::process_instruction, state::RecordData, - }, -}; - -fn program_test() -> ProgramTest { - ProgramTest::new("spl_record", id(), processor!(process_instruction)) -} - -async fn initialize_storage_account( - context: &mut ProgramTestContext, - authority: &Keypair, - account: &Keypair, - data: &[u8], -) { - let account_length = std::mem::size_of::() - .checked_add(data.len()) - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &account.pubkey(), - 1.max(Rent::default().minimum_balance(account_length)), - account_length as u64, - &id(), - ), - instruction::initialize(&account.pubkey(), &authority.pubkey()), - instruction::write(&account.pubkey(), &authority.pubkey(), 0, data), - ], - Some(&context.payer.pubkey()), - &[&context.payer, account, authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} - -#[tokio::test] -async fn initialize_success() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[111u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let account = context - .banks_client - .get_account(account.pubkey()) - .await - .unwrap() - .unwrap(); - let account_data = - bytemuck::try_from_bytes::(&account.data[..RecordData::WRITABLE_START_INDEX]) - .unwrap(); - assert_eq!(account_data.authority, authority.pubkey()); - assert_eq!(account_data.version, RecordData::CURRENT_VERSION); - assert_eq!(&account.data[RecordData::WRITABLE_START_INDEX..], data); -} - -#[tokio::test] -async fn initialize_with_seed_success() { - let context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let seed = "storage"; - let account = Pubkey::create_with_seed(&authority.pubkey(), seed, &id()).unwrap(); - let data = &[111u8; 8]; - let account_length = std::mem::size_of::() - .checked_add(data.len()) - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account_with_seed( - &context.payer.pubkey(), - &account, - &authority.pubkey(), - seed, - 1.max(Rent::default().minimum_balance(account_length)), - account_length as u64, - &id(), - ), - instruction::initialize(&account, &authority.pubkey()), - instruction::write(&account, &authority.pubkey(), 0, data), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - let account = context - .banks_client - .get_account(account) - .await - .unwrap() - .unwrap(); - let account_data = - bytemuck::try_from_bytes::(&account.data[..RecordData::WRITABLE_START_INDEX]) - .unwrap(); - assert_eq!(account_data.authority, authority.pubkey()); - assert_eq!(account_data.version, RecordData::CURRENT_VERSION); - assert_eq!(&account.data[RecordData::WRITABLE_START_INDEX..], data); -} - -#[tokio::test] -async fn initialize_twice_fail() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[111u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - let transaction = Transaction::new_signed_with_payer( - &[instruction::initialize( - &account.pubkey(), - &authority.pubkey(), - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized) - ); -} - -#[tokio::test] -async fn write_success() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let new_data = &[200u8; 8]; - let transaction = Transaction::new_signed_with_payer( - &[instruction::write( - &account.pubkey(), - &authority.pubkey(), - 0, - new_data, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let account = context - .banks_client - .get_account(account.pubkey()) - .await - .unwrap() - .unwrap(); - let account_data = - bytemuck::try_from_bytes::(&account.data[..RecordData::WRITABLE_START_INDEX]) - .unwrap(); - assert_eq!(account_data.authority, authority.pubkey()); - assert_eq!(account_data.version, RecordData::CURRENT_VERSION); - assert_eq!(&account.data[RecordData::WRITABLE_START_INDEX..], new_data); -} - -#[tokio::test] -async fn write_fail_wrong_authority() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let new_data = &[200u8; 8]; - let wrong_authority = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::write( - &account.pubkey(), - &wrong_authority.pubkey(), - 0, - new_data, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_authority], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(RecordError::IncorrectAuthority as u32) - ) - ); -} - -#[tokio::test] -async fn write_fail_unsigned() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let data = &[200u8; 8]; - - let transaction = Transaction::new_signed_with_payer( - &[Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(account.pubkey(), false), - AccountMeta::new_readonly(authority.pubkey(), false), - ], - data: instruction::RecordInstruction::Write { offset: 0, data }.pack(), - }], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ); -} - -#[tokio::test] -async fn close_account_success() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - let account_length = std::mem::size_of::() - .checked_add(data.len()) - .unwrap(); - initialize_storage_account(&mut context, &authority, &account, data).await; - let recipient = Pubkey::new_unique(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::close_account( - &account.pubkey(), - &authority.pubkey(), - &recipient, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let account = context - .banks_client - .get_account(recipient) - .await - .unwrap() - .unwrap(); - assert_eq!( - account.lamports, - 1.max(Rent::default().minimum_balance(account_length)) - ); -} - -#[tokio::test] -async fn close_account_fail_wrong_authority() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let wrong_authority = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(account.pubkey(), false), - AccountMeta::new_readonly(wrong_authority.pubkey(), true), - AccountMeta::new(Pubkey::new_unique(), false), - ], - data: instruction::RecordInstruction::CloseAccount.pack(), - }], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_authority], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(RecordError::IncorrectAuthority as u32) - ) - ); -} - -#[tokio::test] -async fn close_account_fail_unsigned() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8, 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let transaction = Transaction::new_signed_with_payer( - &[Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(account.pubkey(), false), - AccountMeta::new_readonly(authority.pubkey(), false), - AccountMeta::new(Pubkey::new_unique(), false), - ], - data: instruction::RecordInstruction::CloseAccount.pack(), - }], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ); -} - -#[tokio::test] -async fn set_authority_success() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - let new_authority = Keypair::new(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_authority( - &account.pubkey(), - &authority.pubkey(), - &new_authority.pubkey(), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let account_handle = context - .banks_client - .get_account(account.pubkey()) - .await - .unwrap() - .unwrap(); - let account_data = bytemuck::try_from_bytes::( - &account_handle.data[..RecordData::WRITABLE_START_INDEX], - ) - .unwrap(); - assert_eq!(account_data.authority, new_authority.pubkey()); - - let new_data = &[200u8; 8]; - let transaction = Transaction::new_signed_with_payer( - &[instruction::write( - &account.pubkey(), - &new_authority.pubkey(), - 0, - new_data, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &new_authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let account_handle = context - .banks_client - .get_account(account.pubkey()) - .await - .unwrap() - .unwrap(); - let account_data = bytemuck::try_from_bytes::( - &account_handle.data[..RecordData::WRITABLE_START_INDEX], - ) - .unwrap(); - assert_eq!(account_data.authority, new_authority.pubkey()); - assert_eq!(account_data.version, RecordData::CURRENT_VERSION); - assert_eq!( - &account_handle.data[RecordData::WRITABLE_START_INDEX..], - new_data, - ); -} - -#[tokio::test] -async fn set_authority_fail_wrong_authority() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let wrong_authority = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(account.pubkey(), false), - AccountMeta::new_readonly(wrong_authority.pubkey(), true), - AccountMeta::new(Pubkey::new_unique(), false), - ], - data: instruction::RecordInstruction::SetAuthority.pack(), - }], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_authority], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(RecordError::IncorrectAuthority as u32) - ) - ); -} - -#[tokio::test] -async fn set_authority_fail_unsigned() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let transaction = Transaction::new_signed_with_payer( - &[Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(account.pubkey(), false), - AccountMeta::new_readonly(authority.pubkey(), false), - AccountMeta::new(Pubkey::new_unique(), false), - ], - data: instruction::RecordInstruction::SetAuthority.pack(), - }], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ); -} - -#[tokio::test] -async fn reallocate_success() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let new_data_length = 16u64; - let expected_account_data_length = RecordData::WRITABLE_START_INDEX - .checked_add(new_data_length as usize) - .unwrap(); - - let delta_account_data_length = new_data_length.saturating_sub(data.len() as u64); - let additional_lamports_needed = - Rent::default().minimum_balance(delta_account_data_length as usize); - - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::reallocate(&account.pubkey(), &authority.pubkey(), new_data_length), - system_instruction::transfer( - &context.payer.pubkey(), - &account.pubkey(), - additional_lamports_needed, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let account_handle = context - .banks_client - .get_account(account.pubkey()) - .await - .unwrap() - .unwrap(); - - assert_eq!(account_handle.data.len(), expected_account_data_length); - - // reallocate to a smaller length - let old_data_length = 8u64; - let transaction = Transaction::new_signed_with_payer( - &[instruction::reallocate( - &account.pubkey(), - &authority.pubkey(), - old_data_length, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let account = context - .banks_client - .get_account(account.pubkey()) - .await - .unwrap() - .unwrap(); - - assert_eq!(account.data.len(), expected_account_data_length); -} - -#[tokio::test] -async fn reallocate_fail_wrong_authority() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let new_data_length = 16u64; - let delta_account_data_length = new_data_length.saturating_sub(data.len() as u64); - let additional_lamports_needed = - Rent::default().minimum_balance(delta_account_data_length as usize); - - let wrong_authority = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[ - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(account.pubkey(), false), - AccountMeta::new(wrong_authority.pubkey(), true), - ], - data: instruction::RecordInstruction::Reallocate { - data_length: new_data_length, - } - .pack(), - }, - system_instruction::transfer( - &context.payer.pubkey(), - &account.pubkey(), - additional_lamports_needed, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_authority], - context.last_blockhash, - ); - - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(RecordError::IncorrectAuthority as u32) - ) - ); -} - -#[tokio::test] -async fn reallocate_fail_unsigned() { - let mut context = program_test().start_with_context().await; - - let authority = Keypair::new(); - let account = Keypair::new(); - let data = &[222u8; 8]; - initialize_storage_account(&mut context, &authority, &account, data).await; - - let new_data_length = 16u64; - let delta_account_data_length = new_data_length.saturating_sub(data.len() as u64); - let additional_lamports_needed = - Rent::default().minimum_balance(delta_account_data_length as usize); - - let transaction = Transaction::new_signed_with_payer( - &[ - Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(account.pubkey(), false), - AccountMeta::new(authority.pubkey(), false), - ], - data: instruction::RecordInstruction::Reallocate { - data_length: new_data_length, - } - .pack(), - }, - system_instruction::transfer( - &context.payer.pubkey(), - &account.pubkey(), - additional_lamports_needed, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ); -} diff --git a/single-pool/README.md b/single-pool/README.md index 9b8bba716d2..4c2a2cfad01 100644 --- a/single-pool/README.md +++ b/single-pool/README.md @@ -1,9 +1,2 @@ -## Solana Program Library Single-Validator Stake Pool - -The single-validator stake pool program is an upcoming SPL program that enables liquid staking with zero fees, no counterparty, and 100% capital efficiency. - -The program defines a canonical pool for every vote account, which can be initialized permissionlessly, and mints tokens in exchange for stake delegated to its designated validator. - -The program is a stripped-down adaptation of the existing multi-validator stake pool program, with approximately 80% less code, to minimize execution risk. - -On launch, users will only be able to deposit and withdraw active stake, but pending future stake program development, we hope to support instant sol deposits and withdrawals as well. +NOTE: The single-pool program and clients are now maintained at +[solana-program/single-pool](https://github.com/solana-program/single-pool). diff --git a/single-pool/cli/Cargo.toml b/single-pool/cli/Cargo.toml deleted file mode 100644 index c601d934e13..00000000000 --- a/single-pool/cli/Cargo.toml +++ /dev/null @@ -1,46 +0,0 @@ -[package] -name = "spl-single-pool-cli" -version = "1.0.0" -description = "Solana Program Library Single-Validator Stake Pool Command-line Utility" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -tokio = "1.42" -clap = { version = "3.2.23", features = ["derive"] } -console = "0.15.10" -borsh = "1.5.3" -bincode = "1.3.1" -serde = "1.0.217" -serde_derive = "1.0.103" -serde_json = "1.0.135" -serde_with = "3.12.0" -solana-account-decoder = "2.1.0" -solana-clap-v3-utils = "2.1.0" -solana-cli-config = "2.1.0" -solana-cli-output = "2.1.0" -solana-client = "2.1.0" -solana-logger = "2.1.0" -solana-remote-wallet = "2.1.0" -solana-sdk = "2.1.0" -solana-transaction-status = "2.1.0" -solana-vote-program = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ - "no-entrypoint", -] } -spl-token-client = { version = "0.13.0", path = "../../token/client" } -spl-single-pool = { version = "1.0.0", path = "../program", features = [ - "no-entrypoint", -] } - -[dev-dependencies] -solana-test-validator = "2.1.0" -serial_test = "3.2.0" -test-case = "3.3" -tempfile = "3.14.0" - -[[bin]] -name = "spl-single-pool" -path = "src/main.rs" diff --git a/single-pool/cli/src/cli.rs b/single-pool/cli/src/cli.rs deleted file mode 100644 index ae6351fc02d..00000000000 --- a/single-pool/cli/src/cli.rs +++ /dev/null @@ -1,473 +0,0 @@ -use { - crate::config::Error, - clap::{ - builder::{PossibleValuesParser, TypedValueParser}, - ArgGroup, ArgMatches, Args, Parser, Subcommand, - }, - solana_clap_v3_utils::{ - input_parsers::{parse_url_or_moniker, Amount}, - input_validators::{is_valid_pubkey, is_valid_signer}, - keypair::{pubkey_from_path, signer_from_path}, - }, - solana_cli_output::OutputFormat, - solana_remote_wallet::remote_wallet::RemoteWalletManager, - solana_sdk::{pubkey::Pubkey, signer::Signer}, - spl_single_pool::{self, find_pool_address}, - std::{rc::Rc, str::FromStr, sync::Arc}, -}; - -#[derive(Clone, Debug, Parser)] -#[clap(author, version, about, long_about = None)] -pub struct Cli { - /// Configuration file to use - #[clap(global(true), short = 'C', long = "config", id = "PATH")] - pub config_file: Option, - - /// Show additional information - #[clap(global(true), short, long)] - pub verbose: bool, - - /// Simulate transaction instead of executing - #[clap(global(true), long, alias = "dryrun")] - pub dry_run: bool, - - /// URL for Solana's JSON RPC or moniker (or their first letter): - /// [mainnet-beta, testnet, devnet, localhost]. - /// Default from the configuration file. - #[clap( - global(true), - short = 'u', - long = "url", - id = "URL_OR_MONIKER", - value_parser = parse_url_or_moniker, - )] - pub json_rpc_url: Option, - - /// Specify the fee-payer account. This may be a keypair file, the ASK - /// keyword or the pubkey of an offline signer, provided an appropriate - /// --signer argument is also passed. Defaults to the client keypair. - #[clap( - global(true), - long, - id = "PAYER_KEYPAIR", - validator = |s| is_valid_signer(s), - )] - pub fee_payer: Option, - - /// Return information in specified output format - #[clap( - global(true), - long = "output", - id = "FORMAT", - conflicts_with = "verbose", - value_parser = PossibleValuesParser::new(["json", "json-compact"]).map(|o| parse_output_format(&o)), - )] - pub output_format: Option, - - #[clap(subcommand)] - pub command: Command, -} - -#[derive(Clone, Debug, Subcommand)] -pub enum Command { - /// Commands used to initialize or manage existing single-validator stake - /// pools. Other than initializing new pools, most users should never - /// need to use these. - Manage(ManageCli), - - /// Deposit delegated stake into a pool in exchange for pool tokens, closing - /// out the original stake account. Provide either a stake account - /// address, or a pool or vote account address along with the - /// --default-stake-account flag to use an account created with - /// create-stake. - Deposit(DepositCli), - - /// Withdraw stake into a new stake account, burning tokens in exchange. - /// Provide either pool or vote account address, plus either an amount of - /// tokens to burn or the ALL keyword to burn all. - Withdraw(WithdrawCli), - - /// Create and delegate a new stake account to a given validator, using a - /// default address linked to the intended depository pool - CreateDefaultStake(CreateStakeCli), - - /// Display info for one or all single-validator stake pool(s) - Display(DisplayCli), -} - -#[derive(Clone, Debug, Parser)] -pub struct ManageCli { - #[clap(subcommand)] - pub manage: ManageCommand, -} - -#[derive(Clone, Debug, Subcommand)] -pub enum ManageCommand { - /// Permissionlessly create the single-validator stake pool for a given - /// validator vote account if one does not already exist. The fee payer - /// also pays rent-exemption for accounts, along with the - /// cluster-configured minimum stake delegation - Initialize(InitializeCli), - - /// Permissionlessly re-stake the pool stake account in the case when it has - /// been deactivated. This may happen if the validator is - /// force-deactivated, and then later reactivated using the same address - /// for its vote account. - ReactivatePoolStake(ReactivateCli), - - /// Permissionlessly create default MPL token metadata for the pool mint. - /// Normally this is done automatically upon initialization, so this - /// does not need to be called. - CreateTokenMetadata(CreateMetadataCli), - - /// Modify the MPL token metadata associated with the pool mint. This action - /// can only be performed by the validator vote account's withdraw - /// authority - UpdateTokenMetadata(UpdateMetadataCli), -} - -#[derive(Clone, Debug, Args)] -pub struct InitializeCli { - /// The vote account to create the pool for - #[clap(value_parser = |p: &str| parse_address(p, "vote_account_address"))] - pub vote_account_address: Pubkey, - - /// Do not create MPL metadata for the pool mint - #[clap(long)] - pub skip_metadata: bool, -} - -#[derive(Clone, Debug, Args)] -#[clap(group(pool_source_group()))] -pub struct ReactivateCli { - /// The pool to reactivate - #[clap(short, long = "pool", value_parser = |p: &str| parse_address(p, "pool_address"))] - pub pool_address: Option, - - /// The vote account corresponding to the pool to reactivate - #[clap(long = "vote-account", value_parser = |p: &str| parse_address(p, "vote_account_address"))] - pub vote_account_address: Option, - - // backdoor for testing, theres no reason to ever use this - #[clap(long, hide = true)] - pub skip_deactivation_check: bool, -} - -#[derive(Clone, Debug, Args)] -#[clap(group(ArgGroup::new("stake-source").required(true).args(&["stake-account-address", "default-stake-account"])))] -#[clap(group(pool_source_group().required(false)))] -pub struct DepositCli { - /// The stake account to deposit from. Must be in the same activation state - /// as the pool's stake account - #[clap(value_parser = |p: &str| parse_address(p, "stake_account_address"))] - pub stake_account_address: Option, - - /// Instead of using a stake account by address, use the user's default - /// account for a specified pool - #[clap( - short, - long, - conflicts_with = "stake-account-address", - requires = "pool-source" - )] - pub default_stake_account: bool, - - /// The pool to deposit into. Optional when stake account is provided - #[clap(short, long = "pool", value_parser = |p: &str| parse_address(p, "pool_address"))] - pub pool_address: Option, - - /// The vote account corresponding to the pool to deposit into. Optional - /// when stake account or pool is provided - #[clap(long = "vote-account", value_parser = |p: &str| parse_address(p, "vote_account_address"))] - pub vote_account_address: Option, - - /// Signing authority on the stake account to be deposited. Defaults to the - /// client keypair - #[clap(long = "withdraw-authority", id = "STAKE_WITHDRAW_AUTHORITY_KEYPAIR", validator = |s| is_valid_signer(s))] - pub stake_withdraw_authority: Option, - - /// The token account to mint to. Defaults to the client keypair's - /// associated token account - #[clap(long = "token-account", value_parser = |p: &str| parse_address(p, "token_account_address"))] - pub token_account_address: Option, - - /// The wallet to refund stake account rent to. Defaults to the client - /// keypair's pubkey - #[clap(long = "recipient", value_parser = |p: &str| parse_address(p, "lamport_recipient_address"))] - pub lamport_recipient_address: Option, -} - -#[derive(Clone, Debug, Args)] -#[clap(group(pool_source_group()))] -pub struct WithdrawCli { - /// Amount of tokens to burn for withdrawal - #[clap(value_parser = Amount::parse_decimal_or_all)] - pub token_amount: Amount, - - /// The token account to withdraw from. Defaults to the associated token - /// account for the pool mint - #[clap(long = "token-account", value_parser = |p: &str| parse_address(p, "token_account_address"))] - pub token_account_address: Option, - - /// The pool to withdraw from - #[clap(short, long = "pool", value_parser = |p: &str| parse_address(p, "pool_address"))] - pub pool_address: Option, - - /// The vote account corresponding to the pool to withdraw from - #[clap(long = "vote-account", value_parser = |p: &str| parse_address(p, "vote_account_address"))] - pub vote_account_address: Option, - - /// Signing authority on the token account. Defaults to the client keypair - #[clap(long = "token-authority", id = "TOKEN_AUTHORITY_KEYPAIR", validator = |s| is_valid_signer(s))] - pub token_authority: Option, - - /// Authority to assign to the new stake account. Defaults to the pubkey of - /// the client keypair - #[clap(long = "stake-authority", value_parser = |p: &str| parse_address(p, "stake_authority_address"))] - pub stake_authority_address: Option, - - /// Deactivate stake account after withdrawal - #[clap(long)] - pub deactivate: bool, -} - -#[derive(Clone, Debug, Args)] -#[clap(group(pool_source_group()))] -pub struct CreateMetadataCli { - /// The pool to create default MPL token metadata for - #[clap(short, long = "pool", value_parser = |p: &str| parse_address(p, "pool_address"))] - pub pool_address: Option, - - /// The vote account corresponding to the pool to create metadata for - #[clap(long = "vote-account", value_parser = |p: &str| parse_address(p, "vote_account_address"))] - pub vote_account_address: Option, -} - -#[derive(Clone, Debug, Args)] -#[clap(group(pool_source_group()))] -pub struct UpdateMetadataCli { - /// New name for the pool token - #[clap(validator = is_valid_token_name)] - pub token_name: String, - - /// New ticker symbol for the pool token - #[clap(validator = is_valid_token_symbol)] - pub token_symbol: String, - - /// Optional external URI for the pool token. Leaving this argument blank - /// will clear any existing value - #[clap(validator = is_valid_token_uri)] - pub token_uri: Option, - - /// The pool to change MPL token metadata for - #[clap(short, long = "pool", value_parser = |p: &str| parse_address(p, "pool_address"))] - pub pool_address: Option, - - /// The vote account corresponding to the pool to create metadata for - #[clap(long = "vote-account", value_parser = |p: &str| parse_address(p, "vote_account_address"))] - pub vote_account_address: Option, - - /// Authorized withdrawer for the vote account, to prove validator - /// ownership. Defaults to the client keypair - #[clap(long, id = "AUTHORIZED_WITHDRAWER_KEYPAIR", validator = |s| is_valid_signer(s))] - pub authorized_withdrawer: Option, -} - -#[derive(Clone, Debug, Args)] -#[clap(group(pool_source_group()))] -pub struct CreateStakeCli { - /// Number of lamports to stake - pub lamports: u64, - - /// The pool to create a stake account for - #[clap(short, long = "pool", value_parser = |p: &str| parse_address(p, "pool_address"))] - pub pool_address: Option, - - /// The vote account corresponding to the pool to create stake for - #[clap(long = "vote-account", value_parser = |p: &str| parse_address(p, "vote_account_address"))] - pub vote_account_address: Option, - - /// Authority to assign to the new stake account. Defaults to the pubkey of - /// the client keypair - #[clap(long = "stake-authority", value_parser = |p: &str| parse_address(p, "stake_authority_address"))] - pub stake_authority_address: Option, -} - -#[derive(Clone, Debug, Args)] -#[clap(group(pool_source_group().arg("all")))] -pub struct DisplayCli { - /// The pool to display - #[clap(value_parser = |p: &str| parse_address(p, "pool_address"))] - pub pool_address: Option, - - /// The vote account corresponding to the pool to display - #[clap(long = "vote-account", value_parser = |p: &str| parse_address(p, "vote_account_address"))] - pub vote_account_address: Option, - - /// Display all pools - #[clap(long)] - pub all: bool, -} - -fn pool_source_group() -> ArgGroup<'static> { - ArgGroup::new("pool-source") - .required(true) - .args(&["pool-address", "vote-account-address"]) -} - -pub fn parse_address(path: &str, name: &str) -> Result { - if is_valid_pubkey(path).is_ok() { - // this all is ugly but safe - // wallet_manager doesn't need to be shared, it just saves cycles to cache it - // and the only way argmatches default fails with an unchecked lookup is in the - // prompt branch which seems unlikely to ever be used for pubkeys - // the usb lookup in signer_from_path_with_config is safe - // and the pubkey lookups are unreachable because pubkey_from_path short - // circuits that case - let mut wallet_manager = None; - pubkey_from_path(&ArgMatches::default(), path, name, &mut wallet_manager) - .map_err(|_| format!("Failed to load pubkey {} at {}", name, path)) - } else { - Err(format!("Failed to parse pubkey {} at {}", name, path)) - } -} - -pub fn parse_output_format(output_format: &str) -> OutputFormat { - match output_format { - "json" => OutputFormat::Json, - "json-compact" => OutputFormat::JsonCompact, - _ => unreachable!(), - } -} - -pub fn is_valid_token_name(s: &str) -> Result<(), String> { - if s.len() > 32 { - Err("Maximum token name length is 32 characters".to_string()) - } else { - Ok(()) - } -} - -pub fn is_valid_token_symbol(s: &str) -> Result<(), String> { - if s.len() > 10 { - Err("Maximum token symbol length is 10 characters".to_string()) - } else { - Ok(()) - } -} - -pub fn is_valid_token_uri(s: &str) -> Result<(), String> { - if s.len() > 200 { - Err("Maximum token URI length is 200 characters".to_string()) - } else { - Ok(()) - } -} - -pub fn pool_address_from_args(maybe_pool: Option, maybe_vote: Option) -> Pubkey { - if let Some(pool_address) = maybe_pool { - pool_address - } else if let Some(vote_account_address) = maybe_vote { - find_pool_address(&spl_single_pool::id(), &vote_account_address) - } else { - unreachable!() - } -} - -// all this is because solana clap v3 utils signer handlers dont work with -// derive syntax which means its impossible to parse keypairs or addresses in -// value_parser instead, we take the input into a string wrapper from the cli -// and then once the first pass is over, we do a second manual pass converting -// to signer wrappers -#[derive(Clone, Debug)] -pub enum SignerArg { - Source(String), - Signer(Arc), -} -impl FromStr for SignerArg { - type Err = std::convert::Infallible; - - fn from_str(s: &str) -> Result { - Ok(Self::Source(s.to_string())) - } -} -impl PartialEq for SignerArg { - fn eq(&self, other: &SignerArg) -> bool { - match (self, other) { - (SignerArg::Source(ref a), SignerArg::Source(ref b)) => a == b, - (SignerArg::Signer(ref a), SignerArg::Signer(ref b)) => a == b, - (_, _) => false, - } - } -} - -pub fn signer_from_arg( - signer_arg: Option, - default_signer: &Arc, -) -> Result, Error> { - match signer_arg { - Some(SignerArg::Signer(signer)) => Ok(signer), - Some(SignerArg::Source(_)) => Err("Signer arg string must be converted to signer".into()), - None => Ok(default_signer.clone()), - } -} - -impl Command { - pub fn with_signers( - mut self, - matches: &ArgMatches, - wallet_manager: &mut Option>, - ) -> Result { - match self { - Command::Deposit(ref mut config) => { - config.stake_withdraw_authority = with_signer( - matches, - wallet_manager, - config.stake_withdraw_authority.clone(), - "stake_authority", - )?; - } - Command::Withdraw(ref mut config) => { - config.token_authority = with_signer( - matches, - wallet_manager, - config.token_authority.clone(), - "token_authority", - )?; - } - Command::Manage(ManageCli { - manage: ManageCommand::UpdateTokenMetadata(ref mut config), - }) => { - config.authorized_withdrawer = with_signer( - matches, - wallet_manager, - config.authorized_withdrawer.clone(), - "authorized_withdrawer", - )?; - } - _ => (), - } - - Ok(self) - } -} - -pub fn with_signer( - matches: &ArgMatches, - wallet_manager: &mut Option>, - arg: Option, - name: &str, -) -> Result, Error> { - Ok(match arg { - Some(SignerArg::Source(path)) => { - let signer = if let Ok(signer) = signer_from_path(matches, &path, name, wallet_manager) - { - signer - } else { - return Err(format!("Cannot parse signer {} / {}", name, path).into()); - }; - Some(SignerArg::Signer(Arc::from(signer))) - } - a => a, - }) -} diff --git a/single-pool/cli/src/config.rs b/single-pool/cli/src/config.rs deleted file mode 100644 index 649c2c7ea17..00000000000 --- a/single-pool/cli/src/config.rs +++ /dev/null @@ -1,129 +0,0 @@ -use { - crate::cli::*, - clap::ArgMatches, - solana_clap_v3_utils::keypair::signer_from_path, - solana_cli_output::OutputFormat, - solana_client::nonblocking::rpc_client::RpcClient, - solana_remote_wallet::remote_wallet::RemoteWalletManager, - solana_sdk::{commitment_config::CommitmentConfig, signature::Signer}, - spl_token_client::client::{ProgramClient, ProgramRpcClient, ProgramRpcClientSendTransaction}, - std::{process::exit, rc::Rc, sync::Arc}, -}; - -pub type Error = Box; - -pub fn println_display(config: &Config, message: String) { - match config.output_format { - OutputFormat::Display | OutputFormat::DisplayVerbose => { - println!("{}", message); - } - _ => {} - } -} - -pub fn eprintln_display(config: &Config, message: String) { - match config.output_format { - OutputFormat::Display | OutputFormat::DisplayVerbose => { - eprintln!("{}", message); - } - _ => {} - } -} - -pub struct Config { - pub rpc_client: Arc, - pub program_client: Arc>, - pub default_signer: Option>, - pub fee_payer: Option>, - pub output_format: OutputFormat, - pub dry_run: bool, -} -impl Config { - pub fn new( - cli: Cli, - matches: ArgMatches, - wallet_manager: &mut Option>, - ) -> Self { - // get the generic cli config struct - let cli_config = if let Some(config_file) = &cli.config_file { - solana_cli_config::Config::load(config_file).unwrap_or_else(|_| { - eprintln!("error: Could not load config file `{}`", config_file); - exit(1); - }) - } else if let Some(config_file) = &*solana_cli_config::CONFIG_FILE { - solana_cli_config::Config::load(config_file).unwrap_or_default() - } else { - solana_cli_config::Config::default() - }; - - // create rpc client - let rpc_client = Arc::new(RpcClient::new_with_commitment( - cli.json_rpc_url.unwrap_or(cli_config.json_rpc_url), - CommitmentConfig::confirmed(), - )); - - // and program client - let program_client = Arc::new(ProgramRpcClient::new( - rpc_client.clone(), - ProgramRpcClientSendTransaction, - )); - - // resolve default signer - let default_keypair = cli_config.keypair_path; - let default_signer = - signer_from_path(&matches, &default_keypair, "default", wallet_manager) - .ok() - .map(Arc::from); - - // resolve fee-payer - let fee_payer_arg = - with_signer(&matches, wallet_manager, cli.fee_payer, "fee_payer").unwrap(); - let fee_payer = default_signer - .clone() - .map(|default_signer| signer_from_arg(fee_payer_arg, &default_signer).unwrap()); - - // determine output format - let output_format = match (cli.output_format, cli.verbose) { - (Some(json_format), _) => json_format, - (None, true) => OutputFormat::DisplayVerbose, - (None, false) => OutputFormat::Display, - }; - - Self { - rpc_client, - program_client, - default_signer, - fee_payer, - output_format, - dry_run: cli.dry_run, - } - } - - // Returns Ok(default signer), or Err if there is no default signer configured - pub fn default_signer(&self) -> Result, Error> { - if let Some(default_signer) = &self.default_signer { - Ok(default_signer.clone()) - } else { - Err("default signer is required, please specify a valid default signer by identifying a \ - valid configuration file using the --config argument, or by creating a valid config \ - at the default location of ~/.config/solana/cli/config.yml using the solana config \ - command".to_string().into()) - } - } - - // Returns Ok(fee payer), or Err if there is no fee payer configured - pub fn fee_payer(&self) -> Result, Error> { - if let Some(fee_payer) = &self.fee_payer { - Ok(fee_payer.clone()) - } else { - Err("fee payer is required, please specify a valid fee payer using the --payer argument, or \ - by identifying a valid configuration file using the --config argument, or by creating a \ - valid config at the default location of ~/.config/solana/cli/config.yml using the solana \ - config command".to_string().into()) - } - } - - pub fn verbose(&self) -> bool { - self.output_format == OutputFormat::DisplayVerbose - } -} diff --git a/single-pool/cli/src/main.rs b/single-pool/cli/src/main.rs deleted file mode 100644 index b469edc37bb..00000000000 --- a/single-pool/cli/src/main.rs +++ /dev/null @@ -1,876 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![allow(deprecated)] - -use { - clap::{CommandFactory, Parser}, - solana_clap_v3_utils::input_parsers::Amount, - solana_client::{ - rpc_config::RpcProgramAccountsConfig, - rpc_filter::{Memcmp, RpcFilterType}, - }, - solana_sdk::{ - borsh1::try_from_slice_unchecked, - pubkey::Pubkey, - signature::{Keypair, Signature, Signer}, - stake, - transaction::Transaction, - }, - solana_vote_program::{self as vote_program, vote_state::VoteState}, - spl_single_pool::{ - self, find_default_deposit_account_address, find_pool_address, find_pool_mint_address, - find_pool_stake_address, instruction::SinglePoolInstruction, state::SinglePool, - }, - spl_token_client::token::Token, -}; - -mod config; -use config::*; - -mod cli; -use cli::*; - -mod output; -use output::*; - -mod quarantine; - -#[tokio::main] -async fn main() -> Result<(), Error> { - let cli = Cli::parse(); - let matches = Cli::command().get_matches(); - let mut wallet_manager = None; - - let command = cli - .command - .clone() - .with_signers(&matches, &mut wallet_manager)?; - let config = Config::new(cli, matches, &mut wallet_manager); - - solana_logger::setup_with_default("solana=info"); - - let res = command.execute(&config).await?; - println!("{}", res); - - Ok(()) -} - -pub type CommandResult = Result; - -impl Command { - pub async fn execute(self, config: &Config) -> CommandResult { - match self { - Command::Manage(command) => match command.manage { - ManageCommand::Initialize(command_config) => { - command_initialize(config, command_config).await - } - ManageCommand::ReactivatePoolStake(command_config) => { - command_reactivate_pool_stake(config, command_config).await - } - ManageCommand::CreateTokenMetadata(command_config) => { - command_create_metadata(config, command_config).await - } - ManageCommand::UpdateTokenMetadata(command_config) => { - command_update_metadata(config, command_config).await - } - }, - Command::Deposit(command_config) => command_deposit(config, command_config).await, - Command::Withdraw(command_config) => command_withdraw(config, command_config).await, - Command::CreateDefaultStake(command_config) => { - command_create_stake(config, command_config).await - } - Command::Display(command_config) => command_display(config, command_config).await, - } - } -} - -// initialize a new stake pool for a vote account -async fn command_initialize(config: &Config, command_config: InitializeCli) -> CommandResult { - let payer = config.fee_payer()?; - let vote_account_address = command_config.vote_account_address; - - println_display( - config, - format!( - "Initializing single-validator stake pool for vote account {}\n", - vote_account_address, - ), - ); - - // check if the vote account is valid - let vote_account = config - .program_client - .get_account(vote_account_address) - .await?; - if vote_account.is_none() || vote_account.unwrap().owner != vote_program::id() { - return Err(format!("{} is not a valid vote account", vote_account_address,).into()); - } - - let pool_address = find_pool_address(&spl_single_pool::id(), &vote_account_address); - - // check if the pool has already been initialized - if config - .program_client - .get_account(pool_address) - .await? - .is_some() - { - return Err(format!( - "Pool {} for vote account {} already exists", - pool_address, vote_account_address - ) - .into()); - } - - let mut instructions = spl_single_pool::instruction::initialize( - &spl_single_pool::id(), - &vote_account_address, - &payer.pubkey(), - &quarantine::get_rent(config).await?, - quarantine::get_minimum_delegation(config).await?, - ); - - // get rid of the CreateMetadata instruction if desired, eg if mpl breaks compat - if command_config.skip_metadata { - assert_eq!( - instructions.last().unwrap().data, - borsh::to_vec(&SinglePoolInstruction::CreateTokenMetadata).unwrap() - ); - - instructions.pop(); - } - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &vec![payer], - config.program_client.get_latest_blockhash().await?, - ); - - let signature = process_transaction(config, transaction).await?; - - Ok(format_output( - config, - "Initialize".to_string(), - StakePoolOutput { - pool_address, - vote_account_address, - available_stake: 0, - token_supply: 0, - signature, - }, - )) -} - -// reactivate pool stake account -async fn command_reactivate_pool_stake( - config: &Config, - command_config: ReactivateCli, -) -> CommandResult { - let payer = config.fee_payer()?; - let pool_address = pool_address_from_args( - command_config.pool_address, - command_config.vote_account_address, - ); - - println_display( - config, - format!("Reactivating stake account for pool {}\n", pool_address), - ); - - let vote_account_address = - if let Some(pool_data) = config.program_client.get_account(pool_address).await? { - try_from_slice_unchecked::(&pool_data.data)?.vote_account_address - } else { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - }; - - // the only reason this check is skippable is for testing, otherwise theres no - // reason - if !command_config.skip_deactivation_check { - let current_epoch = config.rpc_client.get_epoch_info().await?.epoch; - let pool_stake_address = find_pool_stake_address(&spl_single_pool::id(), &pool_address); - let pool_stake_deactivated = quarantine::get_stake_info(config, &pool_stake_address) - .await? - .unwrap() - .1 - .delegation - .deactivation_epoch - <= current_epoch; - - if !pool_stake_deactivated { - return Err("Pool stake account is neither deactivating nor deactivated".into()); - } - } - - let instruction = spl_single_pool::instruction::reactivate_pool_stake( - &spl_single_pool::id(), - &vote_account_address, - ); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &vec![payer], - config.program_client.get_latest_blockhash().await?, - ); - - let signature = process_transaction(config, transaction).await?; - - Ok(format_output( - config, - "ReactivatePoolStake".to_string(), - SignatureOutput { signature }, - )) -} - -// deposit stake -async fn command_deposit(config: &Config, command_config: DepositCli) -> CommandResult { - let payer = config.fee_payer()?; - let owner = config.default_signer()?; - let stake_authority = signer_from_arg(command_config.stake_withdraw_authority, &owner)?; - let lamport_recipient = command_config - .lamport_recipient_address - .unwrap_or_else(|| owner.pubkey()); - - let current_epoch = config.rpc_client.get_epoch_info().await?.epoch; - - // the cli invocation for this is conceptually simple, but a bit tricky - // the user can provide pool or vote and let the cli infer the stake account - // address but they can also provide pool or vote with the stake account, as - // a safety check first we want to get the pool address if they provided a - // pool or vote address - let provided_pool_address = command_config.pool_address.or_else(|| { - command_config - .vote_account_address - .map(|address| find_pool_address(&spl_single_pool::id(), &address)) - }); - - // from there we can determine the stake account address - let stake_account_address = - if let Some(stake_account_address) = command_config.stake_account_address { - stake_account_address - } else if let Some(pool_address) = provided_pool_address { - assert!(command_config.default_stake_account); - find_default_deposit_account_address(&pool_address, &stake_authority.pubkey()) - } else { - unreachable!() - }; - - // now we validate the stake account and definitively resolve the pool address - let (pool_address, user_stake_active) = if let Some((meta, stake)) = - quarantine::get_stake_info(config, &stake_account_address).await? - { - let derived_pool_address = - find_pool_address(&spl_single_pool::id(), &stake.delegation.voter_pubkey); - - if let Some(provided_pool_address) = provided_pool_address { - if provided_pool_address != derived_pool_address { - return Err(format!( - "Provided pool address {} does not match stake account-derived address {}", - provided_pool_address, derived_pool_address, - ) - .into()); - } - } - - if meta.authorized.withdrawer != stake_authority.pubkey() { - return Err(format!( - "Incorrect withdraw authority for stake account {}: got {}, expected {}", - stake_account_address, - meta.authorized.withdrawer, - stake_authority.pubkey(), - ) - .into()); - } - - if stake.delegation.deactivation_epoch < u64::MAX { - return Err(format!( - "Stake account {} is deactivating or deactivated", - stake_account_address - ) - .into()); - } - - ( - derived_pool_address, - stake.delegation.activation_epoch <= current_epoch, - ) - } else { - return Err(format!("Could not find stake account {}", stake_account_address).into()); - }; - - println_display( - config, - format!( - "Depositing stake from account {} into pool {}\n", - stake_account_address, pool_address - ), - ); - - if config - .program_client - .get_account(pool_address) - .await? - .is_none() - { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - } - - let pool_stake_address = find_pool_stake_address(&spl_single_pool::id(), &pool_address); - let pool_stake_active = quarantine::get_stake_info(config, &pool_stake_address) - .await? - .unwrap() - .1 - .delegation - .activation_epoch - <= current_epoch; - - if user_stake_active != pool_stake_active { - return Err("Activation status mismatch; try again next epoch".into()); - } - - let pool_mint_address = find_pool_mint_address(&spl_single_pool::id(), &pool_address); - let token = Token::new( - config.program_client.clone(), - &spl_token::id(), - &pool_mint_address, - None, - payer.clone(), - ); - - // use token account provided, or get/create the associated account for the - // client keypair - let token_account_address = if let Some(account) = command_config.token_account_address { - account - } else { - token - .get_or_create_associated_account_info(&owner.pubkey()) - .await?; - token.get_associated_token_address(&owner.pubkey()) - }; - - let previous_token_amount = token - .get_account_info(&token_account_address) - .await? - .base - .amount; - - let instructions = spl_single_pool::instruction::deposit( - &spl_single_pool::id(), - &pool_address, - &stake_account_address, - &token_account_address, - &lamport_recipient, - &stake_authority.pubkey(), - ); - - let mut signers = vec![]; - for signer in [payer.clone(), stake_authority] { - if !signers.contains(&signer) { - signers.push(signer); - } - } - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &signers, - config.program_client.get_latest_blockhash().await?, - ); - - let signature = process_transaction(config, transaction).await?; - let token_amount = token - .get_account_info(&token_account_address) - .await? - .base - .amount - - previous_token_amount; - - Ok(format_output( - config, - "Deposit".to_string(), - DepositOutput { - pool_address, - token_amount, - signature, - }, - )) -} - -// withdraw stake -async fn command_withdraw(config: &Config, command_config: WithdrawCli) -> CommandResult { - let payer = config.fee_payer()?; - let owner = config.default_signer()?; - let token_authority = signer_from_arg(command_config.token_authority, &owner)?; - let stake_authority_address = command_config - .stake_authority_address - .unwrap_or_else(|| owner.pubkey()); - - let stake_account = Keypair::new(); - let stake_account_address = stake_account.pubkey(); - - // since we can't infer pool from token account, the withdraw invocation is - // rather simpler first get the pool address - let pool_address = pool_address_from_args( - command_config.pool_address, - command_config.vote_account_address, - ); - - if config - .program_client - .get_account(pool_address) - .await? - .is_none() - { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - } - - // now all the mint and token info - let pool_mint_address = find_pool_mint_address(&spl_single_pool::id(), &pool_address); - let token = Token::new( - config.program_client.clone(), - &spl_token::id(), - &pool_mint_address, - None, - payer.clone(), - ); - - let token_account_address = command_config - .token_account_address - .unwrap_or_else(|| token.get_associated_token_address(&owner.pubkey())); - - let token_account = token.get_account_info(&token_account_address).await?; - - let token_amount = match command_config.token_amount.sol_to_lamport() { - Amount::All => token_account.base.amount, - Amount::Raw(amount) => amount, - Amount::Decimal(_) => unreachable!(), - }; - - println_display( - config, - format!( - "Withdrawing from pool {} into new stake account {}; burning {} tokens from {}\n", - pool_address, stake_account_address, token_amount, token_account_address, - ), - ); - - if token_amount == 0 { - return Err("Cannot withdraw zero tokens".into()); - } - - if token_amount > token_account.base.amount { - return Err(format!( - "Withdraw amount {} exceeds tokens in account ({})", - token_amount, token_account.base.amount - ) - .into()); - } - - // note a delegate authority is not allowed here because we must authorize the - // pool authority - if token_account.base.owner != token_authority.pubkey() { - return Err(format!( - "Invalid token authority: got {}, actual {}", - token_account.base.owner, - token_authority.pubkey() - ) - .into()); - } - - // create a blank stake account to withdraw into - let mut instructions = vec![ - quarantine::create_uninitialized_stake_account_instruction( - config, - &payer.pubkey(), - &stake_account_address, - ) - .await?, - ]; - - // perform the withdrawal - instructions.extend(spl_single_pool::instruction::withdraw( - &spl_single_pool::id(), - &pool_address, - &stake_account_address, - &stake_authority_address, - &token_account_address, - &token_authority.pubkey(), - token_amount, - )); - - // possibly deactivate the new stake account - if command_config.deactivate { - instructions.push(stake::instruction::deactivate_stake( - &stake_account_address, - &stake_authority_address, - )); - } - - let mut signers = vec![]; - for signer in [payer.as_ref(), token_authority.as_ref(), &stake_account] { - if !signers.contains(&signer) { - signers.push(signer); - } - } - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &signers, - config.program_client.get_latest_blockhash().await?, - ); - - let signature = process_transaction(config, transaction).await?; - let stake_amount = if let Some((_, stake)) = - quarantine::get_stake_info(config, &stake_account_address).await? - { - stake.delegation.stake - } else { - 0 - }; - - Ok(format_output( - config, - "Withdraw".to_string(), - WithdrawOutput { - pool_address, - stake_account_address, - stake_amount, - signature, - }, - )) -} - -// create token metadata -async fn command_create_metadata( - config: &Config, - command_config: CreateMetadataCli, -) -> CommandResult { - let payer = config.fee_payer()?; - - // first get the pool address - // i dont check metadata because i dont want to get entangled with mpl - let pool_address = pool_address_from_args( - command_config.pool_address, - command_config.vote_account_address, - ); - - println_display( - config, - format!( - "Creating default token metadata for pool {}\n", - pool_address - ), - ); - - if config - .program_client - .get_account(pool_address) - .await? - .is_none() - { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - } - - // and... i guess thats it? - - let instruction = spl_single_pool::instruction::create_token_metadata( - &spl_single_pool::id(), - &pool_address, - &payer.pubkey(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &vec![payer], - config.program_client.get_latest_blockhash().await?, - ); - - let signature = process_transaction(config, transaction).await?; - - Ok(format_output( - config, - "CreateTokenMetadata".to_string(), - SignatureOutput { signature }, - )) -} - -// update token metadata -async fn command_update_metadata( - config: &Config, - command_config: UpdateMetadataCli, -) -> CommandResult { - let payer = config.fee_payer()?; - let owner = config.default_signer()?; - let authorized_withdrawer = signer_from_arg(command_config.authorized_withdrawer, &owner)?; - - // first get the pool address - // i dont check metadata because i dont want to get entangled with mpl - let pool_address = pool_address_from_args( - command_config.pool_address, - command_config.vote_account_address, - ); - - println_display( - config, - format!("Updating token metadata for pool {}\n", pool_address), - ); - - // we always need the vote account - let vote_account_address = - if let Some(pool_data) = config.program_client.get_account(pool_address).await? { - try_from_slice_unchecked::(&pool_data.data)?.vote_account_address - } else { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - }; - - if let Some(vote_account_data) = config - .program_client - .get_account(vote_account_address) - .await? - { - let vote_account = VoteState::deserialize(&vote_account_data.data)?; - - if authorized_withdrawer.pubkey() != vote_account.authorized_withdrawer { - return Err(format!( - "Invalid authorized withdrawer: got {}, actual {}", - authorized_withdrawer.pubkey(), - vote_account.authorized_withdrawer, - ) - .into()); - } - } else { - // we know the pool exists so the vote account must exist - unreachable!(); - } - - let instruction = spl_single_pool::instruction::update_token_metadata( - &spl_single_pool::id(), - &vote_account_address, - &authorized_withdrawer.pubkey(), - command_config.token_name, - command_config.token_symbol, - command_config.token_uri.unwrap_or_default(), - ); - - let mut signers = vec![]; - for signer in [payer.clone(), authorized_withdrawer] { - if !signers.contains(&signer) { - signers.push(signer); - } - } - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &signers, - config.program_client.get_latest_blockhash().await?, - ); - - let signature = process_transaction(config, transaction).await?; - - Ok(format_output( - config, - "UpdateTokenMetadata".to_string(), - SignatureOutput { signature }, - )) -} - -// create default stake account -async fn command_create_stake(config: &Config, command_config: CreateStakeCli) -> CommandResult { - let payer = config.fee_payer()?; - let owner = config.default_signer()?; - let stake_authority_address = command_config - .stake_authority_address - .unwrap_or_else(|| owner.pubkey()); - - let pool_address = pool_address_from_args( - command_config.pool_address, - command_config.vote_account_address, - ); - - println_display( - config, - format!("Creating default stake account for pool {}\n", pool_address), - ); - - let vote_account_address = - if let Some(vote_account_address) = command_config.vote_account_address { - vote_account_address - } else if let Some(pool_data) = config.program_client.get_account(pool_address).await? { - try_from_slice_unchecked::(&pool_data.data)?.vote_account_address - } else { - return Err(format!( - "Cannot determine vote account address from uninitialized pool {}", - pool_address, - ) - .into()); - }; - - if command_config.vote_account_address.is_some() - && config - .program_client - .get_account(pool_address) - .await? - .is_none() - { - eprintln_display( - config, - format!("warning: Pool {} has not been initialized", pool_address), - ); - } - - let instructions = spl_single_pool::instruction::create_and_delegate_user_stake( - &spl_single_pool::id(), - &vote_account_address, - &stake_authority_address, - &quarantine::get_rent(config).await?, - command_config.lamports, - ); - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &vec![payer], - config.program_client.get_latest_blockhash().await?, - ); - - let signature = process_transaction(config, transaction).await?; - - Ok(format_output( - config, - "CreateDefaultStake".to_string(), - CreateStakeOutput { - pool_address, - stake_account_address: find_default_deposit_account_address( - &pool_address, - &stake_authority_address, - ), - signature, - }, - )) -} - -// display stake pool(s) -async fn command_display(config: &Config, command_config: DisplayCli) -> CommandResult { - if command_config.all { - // the filter isn't necessary now but makes the cli forward-compatible - let pools = config - .rpc_client - .get_program_accounts_with_config( - &spl_single_pool::id(), - RpcProgramAccountsConfig { - filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes( - 0, - vec![1], - ))]), - ..RpcProgramAccountsConfig::default() - }, - ) - .await?; - - let mut displays = vec![]; - for pool in pools { - let vote_account_address = - try_from_slice_unchecked::(&pool.1.data)?.vote_account_address; - displays.push(get_pool_display(config, pool.0, Some(vote_account_address)).await?); - } - - Ok(format_output( - config, - "DisplayAll".to_string(), - StakePoolListOutput(displays), - )) - } else { - let pool_address = pool_address_from_args( - command_config.pool_address, - command_config.vote_account_address, - ); - - Ok(format_output( - config, - "Display".to_string(), - get_pool_display(config, pool_address, None).await?, - )) - } -} - -async fn get_pool_display( - config: &Config, - pool_address: Pubkey, - maybe_vote_account: Option, -) -> Result { - let vote_account_address = if let Some(address) = maybe_vote_account { - address - } else if let Some(pool_data) = config.program_client.get_account(pool_address).await? { - if let Ok(data) = try_from_slice_unchecked::(&pool_data.data) { - data.vote_account_address - } else { - return Err(format!( - "Failed to parse account at {}; is this a pool?", - pool_address - ) - .into()); - } - } else { - return Err(format!("Pool {} does not exist", pool_address).into()); - }; - - let pool_stake_address = find_pool_stake_address(&spl_single_pool::id(), &pool_address); - let available_stake = - if let Some((_, stake)) = quarantine::get_stake_info(config, &pool_stake_address).await? { - stake.delegation.stake - quarantine::get_minimum_delegation(config).await? - } else { - unreachable!() - }; - - let pool_mint_address = find_pool_mint_address(&spl_single_pool::id(), &pool_address); - let token_supply = config - .rpc_client - .get_token_supply(&pool_mint_address) - .await? - .amount - .parse::()?; - - Ok(StakePoolOutput { - pool_address, - vote_account_address, - available_stake, - token_supply, - signature: None, - }) -} - -async fn process_transaction( - config: &Config, - transaction: Transaction, -) -> Result, Error> { - if config.dry_run { - let simulation_data = config.rpc_client.simulate_transaction(&transaction).await?; - - if config.verbose() { - if let Some(logs) = simulation_data.value.logs { - for log in logs { - println!(" {}", log); - } - } - - println!( - "\nSimulation succeeded, consumed {} compute units", - simulation_data.value.units_consumed.unwrap() - ); - } else { - println_display(config, "Simulation succeeded".to_string()); - } - - Ok(None) - } else { - Ok(Some( - config - .rpc_client - .send_and_confirm_transaction_with_spinner(&transaction) - .await?, - )) - } -} diff --git a/single-pool/cli/src/output.rs b/single-pool/cli/src/output.rs deleted file mode 100644 index 060986542a5..00000000000 --- a/single-pool/cli/src/output.rs +++ /dev/null @@ -1,307 +0,0 @@ -use { - crate::config::Config, - console::style, - serde::{Deserialize, Serialize}, - serde_with::{serde_as, DisplayFromStr}, - solana_cli_output::{display::writeln_name_value, QuietDisplay, VerboseDisplay}, - solana_sdk::{pubkey::Pubkey, signature::Signature}, - spl_single_pool::{ - self, find_pool_mint_address, find_pool_mint_authority_address, - find_pool_mpl_authority_address, find_pool_stake_address, - find_pool_stake_authority_address, - }, - std::fmt::{Display, Formatter, Result, Write}, -}; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CommandOutput -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - pub(crate) command_name: String, - pub(crate) command_output: T, -} - -impl Display for CommandOutput -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(&self.command_output, f) - } -} - -impl QuietDisplay for CommandOutput -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result { - QuietDisplay::write_str(&self.command_output, w) - } -} - -impl VerboseDisplay for CommandOutput -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result { - writeln_name_value(w, "Command:", &self.command_name)?; - VerboseDisplay::write_str(&self.command_output, w) - } -} - -pub fn format_output(config: &Config, command_name: String, command_output: T) -> String -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - config.output_format.formatted_string(&CommandOutput { - command_name, - command_output, - }) -} - -#[serde_as] -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SignatureOutput { - #[serde_as(as = "Option")] - pub signature: Option, -} - -impl QuietDisplay for SignatureOutput {} -impl VerboseDisplay for SignatureOutput {} - -impl Display for SignatureOutput { - fn fmt(&self, f: &mut Formatter) -> Result { - writeln!(f)?; - - if let Some(signature) = self.signature { - writeln_name_value(f, "Signature:", &signature.to_string())?; - } - - Ok(()) - } -} - -#[serde_as] -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StakePoolOutput { - #[serde_as(as = "DisplayFromStr")] - pub pool_address: Pubkey, - #[serde_as(as = "DisplayFromStr")] - pub vote_account_address: Pubkey, - pub available_stake: u64, - pub token_supply: u64, - #[serde_as(as = "Option")] - pub signature: Option, -} - -impl QuietDisplay for StakePoolOutput {} -impl VerboseDisplay for StakePoolOutput { - fn write_str(&self, w: &mut dyn Write) -> Result { - writeln!(w)?; - writeln!(w, "{}", style("SPL Single-Validator Stake Pool").bold())?; - writeln_name_value(w, " Pool address:", &self.pool_address.to_string())?; - writeln_name_value( - w, - " Vote account address:", - &self.vote_account_address.to_string(), - )?; - - writeln_name_value( - w, - " Pool stake address:", - &find_pool_stake_address(&spl_single_pool::id(), &self.pool_address).to_string(), - )?; - writeln_name_value( - w, - " Pool mint address:", - &find_pool_mint_address(&spl_single_pool::id(), &self.pool_address).to_string(), - )?; - writeln_name_value( - w, - " Pool stake authority address:", - &find_pool_stake_authority_address(&spl_single_pool::id(), &self.pool_address) - .to_string(), - )?; - writeln_name_value( - w, - " Pool mint authority address:", - &find_pool_mint_authority_address(&spl_single_pool::id(), &self.pool_address) - .to_string(), - )?; - writeln_name_value( - w, - " Pool MPL authority address:", - &find_pool_mpl_authority_address(&spl_single_pool::id(), &self.pool_address) - .to_string(), - )?; - - writeln_name_value(w, " Available stake:", &self.available_stake.to_string())?; - writeln_name_value(w, " Token supply:", &self.token_supply.to_string())?; - - if let Some(signature) = self.signature { - writeln!(w)?; - writeln_name_value(w, "Signature:", &signature.to_string())?; - } - - Ok(()) - } -} - -impl Display for StakePoolOutput { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - writeln!(f)?; - writeln!(f, "{}", style("SPL Single-Validator Stake Pool").bold())?; - writeln_name_value(f, " Pool address:", &self.pool_address.to_string())?; - writeln_name_value( - f, - " Vote account address:", - &self.vote_account_address.to_string(), - )?; - writeln_name_value(f, " Available stake:", &self.available_stake.to_string())?; - writeln_name_value(f, " Token supply:", &self.token_supply.to_string())?; - - if let Some(signature) = self.signature { - writeln!(f)?; - writeln_name_value(f, "Signature:", &signature.to_string())?; - } - - Ok(()) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct StakePoolListOutput(pub Vec); - -impl QuietDisplay for StakePoolListOutput {} -impl VerboseDisplay for StakePoolListOutput { - fn write_str(&self, w: &mut dyn Write) -> Result { - let mut stake = 0; - for svsp in &self.0 { - VerboseDisplay::write_str(svsp, w)?; - stake += svsp.available_stake; - } - - writeln!(w)?; - writeln_name_value(w, "Total stake:", &stake.to_string())?; - - Ok(()) - } -} - -impl Display for StakePoolListOutput { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - let mut stake = 0; - for svsp in &self.0 { - svsp.fmt(f)?; - stake += svsp.available_stake; - } - - writeln!(f)?; - writeln_name_value(f, "Total stake:", &stake.to_string())?; - - Ok(()) - } -} - -#[serde_as] -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DepositOutput { - #[serde_as(as = "DisplayFromStr")] - pub pool_address: Pubkey, - pub token_amount: u64, - #[serde_as(as = "Option")] - pub signature: Option, -} - -impl QuietDisplay for DepositOutput {} -impl VerboseDisplay for DepositOutput {} - -impl Display for DepositOutput { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - writeln!(f)?; - writeln_name_value(f, "Pool address:", &self.pool_address.to_string())?; - writeln_name_value(f, "Token amount:", &self.token_amount.to_string())?; - - if let Some(signature) = self.signature { - writeln!(f)?; - writeln_name_value(f, "Signature:", &signature.to_string())?; - } - - Ok(()) - } -} - -#[serde_as] -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WithdrawOutput { - #[serde_as(as = "DisplayFromStr")] - pub pool_address: Pubkey, - #[serde_as(as = "DisplayFromStr")] - pub stake_account_address: Pubkey, - pub stake_amount: u64, - #[serde_as(as = "Option")] - pub signature: Option, -} - -impl QuietDisplay for WithdrawOutput {} -impl VerboseDisplay for WithdrawOutput {} - -impl Display for WithdrawOutput { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - writeln!(f)?; - writeln_name_value(f, "Pool address:", &self.pool_address.to_string())?; - writeln_name_value( - f, - "Stake account address:", - &self.stake_account_address.to_string(), - )?; - writeln_name_value(f, "Stake amount:", &self.stake_amount.to_string())?; - - if let Some(signature) = self.signature { - writeln!(f)?; - writeln_name_value(f, "Signature:", &signature.to_string())?; - } - - Ok(()) - } -} - -#[serde_as] -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateStakeOutput { - #[serde_as(as = "DisplayFromStr")] - pub pool_address: Pubkey, - #[serde_as(as = "DisplayFromStr")] - pub stake_account_address: Pubkey, - #[serde_as(as = "Option")] - pub signature: Option, -} - -impl QuietDisplay for CreateStakeOutput {} -impl VerboseDisplay for CreateStakeOutput {} - -impl Display for CreateStakeOutput { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - writeln!(f)?; - writeln_name_value(f, "Pool address:", &self.pool_address.to_string())?; - writeln_name_value( - f, - "Stake account address:", - &self.stake_account_address.to_string(), - )?; - - if let Some(signature) = self.signature { - writeln!(f)?; - writeln_name_value(f, "Signature:", &signature.to_string())?; - } - - Ok(()) - } -} diff --git a/single-pool/cli/src/quarantine.rs b/single-pool/cli/src/quarantine.rs deleted file mode 100644 index 530f865b8b2..00000000000 --- a/single-pool/cli/src/quarantine.rs +++ /dev/null @@ -1,78 +0,0 @@ -// XXX this file will be deleted and replaced with a stake program client once i -// write one - -use { - crate::config::*, - solana_sdk::{ - instruction::Instruction, - native_token::LAMPORTS_PER_SOL, - pubkey::Pubkey, - stake::{ - self, - state::{Meta, Stake, StakeStateV2}, - }, - system_instruction, - sysvar::{self, rent::Rent}, - }, -}; - -pub async fn get_rent(config: &Config) -> Result { - let rent_data = config - .program_client - .get_account(sysvar::rent::id()) - .await? - .unwrap(); - let rent = bincode::deserialize::(&rent_data.data)?; - - Ok(rent) -} - -pub async fn get_minimum_delegation(config: &Config) -> Result { - Ok(std::cmp::max( - config.rpc_client.get_stake_minimum_delegation().await?, - LAMPORTS_PER_SOL, - )) -} - -pub async fn get_stake_info( - config: &Config, - stake_account_address: &Pubkey, -) -> Result, Error> { - if let Some(stake_account) = config - .program_client - .get_account(*stake_account_address) - .await? - { - match bincode::deserialize::(&stake_account.data)? { - StakeStateV2::Stake(meta, stake, _) => Ok(Some((meta, stake))), - StakeStateV2::Initialized(_) => { - Err(format!("Stake account {} is undelegated", stake_account_address).into()) - } - StakeStateV2::Uninitialized => { - Err(format!("Stake account {} is uninitialized", stake_account_address).into()) - } - StakeStateV2::RewardsPool => unimplemented!(), - } - } else { - Ok(None) - } -} - -pub async fn create_uninitialized_stake_account_instruction( - config: &Config, - payer: &Pubkey, - stake_account: &Pubkey, -) -> Result { - let rent_amount = config - .program_client - .get_minimum_balance_for_rent_exemption(std::mem::size_of::()) - .await?; - - Ok(system_instruction::create_account( - payer, - stake_account, - rent_amount, - std::mem::size_of::() as u64, - &stake::program::id(), - )) -} diff --git a/single-pool/cli/tests/test.rs b/single-pool/cli/tests/test.rs deleted file mode 100644 index bceafc59d44..00000000000 --- a/single-pool/cli/tests/test.rs +++ /dev/null @@ -1,429 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] - -use { - serial_test::serial, - solana_cli_config::Config as SolanaConfig, - solana_client::nonblocking::rpc_client::RpcClient, - solana_sdk::{ - bpf_loader_upgradeable, - clock::Epoch, - epoch_schedule::{EpochSchedule, MINIMUM_SLOTS_PER_EPOCH}, - native_token::LAMPORTS_PER_SOL, - pubkey::Pubkey, - signature::{write_keypair_file, Keypair, Signer}, - stake::{ - self, - state::{Authorized, Lockup, StakeStateV2}, - }, - system_instruction, system_program, - transaction::Transaction, - }, - solana_test_validator::{TestValidator, TestValidatorGenesis, UpgradeableProgramInfo}, - solana_vote_program::{ - vote_instruction::{self, CreateVoteAccountConfig}, - vote_state::{VoteInit, VoteState}, - }, - spl_token_client::client::{ProgramClient, ProgramRpcClient, ProgramRpcClientSendTransaction}, - std::{path::PathBuf, process::Command, str::FromStr, sync::Arc, time::Duration}, - tempfile::NamedTempFile, - test_case::test_case, - tokio::time::sleep, -}; - -type PClient = Arc>; -const SVSP_CLI: &str = "../../target/debug/spl-single-pool"; - -#[allow(dead_code)] -pub struct Env { - pub rpc_client: Arc, - pub program_client: PClient, - pub payer: Keypair, - pub keypair_file_path: String, - pub config_file_path: String, - pub vote_account: Pubkey, - - // persist in struct so they dont scope out but callers dont need to make them - validator: TestValidator, - keypair_file: NamedTempFile, - config_file: NamedTempFile, -} - -async fn setup(initialize: bool) -> Env { - // start test validator - let (validator, payer) = start_validator().await; - - // make clients - let rpc_client = Arc::new(validator.get_async_rpc_client()); - let program_client: PClient = Arc::new(ProgramRpcClient::new( - rpc_client.clone(), - ProgramRpcClientSendTransaction, - )); - - // write the payer to disk - let keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&payer, &keypair_file).unwrap(); - - // write a full config file with our rpc and payer to disk - let config_file = NamedTempFile::new().unwrap(); - let config_file_path = config_file.path().to_str().unwrap(); - let solana_config = SolanaConfig { - json_rpc_url: validator.rpc_url(), - websocket_url: validator.rpc_pubsub_url(), - keypair_path: keypair_file.path().to_str().unwrap().to_string(), - ..SolanaConfig::default() - }; - solana_config.save(config_file_path).unwrap(); - - // make vote and stake accounts - let vote_account = create_vote_account(&program_client, &payer, &payer.pubkey()).await; - if initialize { - let status = Command::new(SVSP_CLI) - .args([ - "manage", - "initialize", - "-C", - config_file_path, - &vote_account.to_string(), - ]) - .status() - .unwrap(); - assert!(status.success()); - } - - Env { - rpc_client, - program_client, - payer, - keypair_file_path: keypair_file.path().to_str().unwrap().to_string(), - config_file_path: config_file_path.to_string(), - vote_account, - validator, - keypair_file, - config_file, - } -} - -async fn start_validator() -> (TestValidator, Keypair) { - solana_logger::setup(); - let mut test_validator_genesis = TestValidatorGenesis::default(); - - test_validator_genesis.epoch_schedule(EpochSchedule::custom( - MINIMUM_SLOTS_PER_EPOCH, - MINIMUM_SLOTS_PER_EPOCH, - false, - )); - - test_validator_genesis.add_upgradeable_programs_with_path(&[ - UpgradeableProgramInfo { - program_id: Pubkey::from_str("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s").unwrap(), - loader: bpf_loader_upgradeable::id(), - program_path: PathBuf::from("../program/tests/fixtures/mpl_token_metadata.so"), - upgrade_authority: Pubkey::default(), - }, - UpgradeableProgramInfo { - program_id: spl_single_pool::id(), - loader: bpf_loader_upgradeable::id(), - program_path: PathBuf::from("../../target/deploy/spl_single_pool.so"), - upgrade_authority: Pubkey::default(), - }, - ]); - test_validator_genesis.start_async().await -} - -async fn wait_for_next_epoch(rpc_client: &RpcClient) -> Epoch { - let current_epoch = rpc_client.get_epoch_info().await.unwrap().epoch; - println!("current epoch {}, advancing to next...", current_epoch); - loop { - let epoch_info = rpc_client.get_epoch_info().await.unwrap(); - if epoch_info.epoch > current_epoch { - return epoch_info.epoch; - } - - sleep(Duration::from_millis(200)).await; - } -} - -async fn create_vote_account( - program_client: &PClient, - payer: &Keypair, - withdrawer: &Pubkey, -) -> Pubkey { - let validator = Keypair::new(); - let vote_account = Keypair::new(); - let voter = Keypair::new(); - - let zero_rent = program_client - .get_minimum_balance_for_rent_exemption(0) - .await - .unwrap(); - - let vote_rent = program_client - .get_minimum_balance_for_rent_exemption(VoteState::size_of() * 2) - .await - .unwrap(); - - let blockhash = program_client.get_latest_blockhash().await.unwrap(); - - let mut instructions = vec![system_instruction::create_account( - &payer.pubkey(), - &validator.pubkey(), - zero_rent, - 0, - &system_program::id(), - )]; - instructions.append(&mut vote_instruction::create_account_with_config( - &payer.pubkey(), - &vote_account.pubkey(), - &VoteInit { - node_pubkey: validator.pubkey(), - authorized_voter: voter.pubkey(), - authorized_withdrawer: *withdrawer, - ..VoteInit::default() - }, - vote_rent, - CreateVoteAccountConfig { - space: VoteState::size_of() as u64, - ..Default::default() - }, - )); - - let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey())); - - transaction - .try_partial_sign(&vec![payer], blockhash) - .unwrap(); - transaction - .try_partial_sign(&vec![&validator, &vote_account], blockhash) - .unwrap(); - - program_client.send_transaction(&transaction).await.unwrap(); - - vote_account.pubkey() -} - -async fn create_and_delegate_stake_account( - program_client: &PClient, - payer: &Keypair, - vote_account: &Pubkey, -) -> Pubkey { - let stake_account = Keypair::new(); - - let stake_rent = program_client - .get_minimum_balance_for_rent_exemption(StakeStateV2::size_of()) - .await - .unwrap(); - let blockhash = program_client.get_latest_blockhash().await.unwrap(); - - let mut transaction = Transaction::new_with_payer( - &stake::instruction::create_account( - &payer.pubkey(), - &stake_account.pubkey(), - &Authorized::auto(&payer.pubkey()), - &Lockup::default(), - stake_rent + LAMPORTS_PER_SOL, - ), - Some(&payer.pubkey()), - ); - - transaction - .try_partial_sign(&vec![payer], blockhash) - .unwrap(); - transaction - .try_partial_sign(&vec![&stake_account], blockhash) - .unwrap(); - - program_client.send_transaction(&transaction).await.unwrap(); - - let mut transaction = Transaction::new_with_payer( - &[stake::instruction::delegate_stake( - &stake_account.pubkey(), - &payer.pubkey(), - vote_account, - )], - Some(&payer.pubkey()), - ); - - transaction.sign(&vec![payer], blockhash); - - program_client.send_transaction(&transaction).await.unwrap(); - - stake_account.pubkey() -} - -#[tokio::test] -#[serial] -async fn reactivate_pool_stake() { - let env = setup(true).await; - - // setting up a test validator for this to succeed is hell, and success is - // tested in program tests so we just make sure the cli can send a - // well-formed instruction - let output = Command::new(SVSP_CLI) - .args([ - "manage", - "reactivate-pool-stake", - "-C", - &env.config_file_path, - "--vote-account", - &env.vote_account.to_string(), - "--skip-deactivation-check", - ]) - .output() - .unwrap(); - assert!(String::from_utf8(output.stderr) - .unwrap() - .contains("custom program error: 0xc")); -} - -#[test_case(true; "default_stake")] -#[test_case(false; "normal_stake")] -#[tokio::test] -#[serial] -async fn deposit(use_default: bool) { - let env = setup(true).await; - - let stake_account = if use_default { - let status = Command::new(SVSP_CLI) - .args([ - "create-default-stake", - "-C", - &env.config_file_path, - "--vote-account", - &env.vote_account.to_string(), - &LAMPORTS_PER_SOL.to_string(), - ]) - .status() - .unwrap(); - assert!(status.success()); - - Pubkey::default() - } else { - create_and_delegate_stake_account(&env.program_client, &env.payer, &env.vote_account).await - }; - - wait_for_next_epoch(&env.rpc_client).await; - - let mut args = vec![ - "deposit".to_string(), - "-C".to_string(), - env.config_file_path, - ]; - - if use_default { - args.extend([ - "--vote-account".to_string(), - env.vote_account.to_string(), - "--default-stake-account".to_string(), - ]); - } else { - args.push(stake_account.to_string()); - }; - - let status = Command::new(SVSP_CLI).args(&args).status().unwrap(); - assert!(status.success()); -} - -#[tokio::test] -#[serial] -async fn withdraw() { - let env = setup(true).await; - let stake_account = - create_and_delegate_stake_account(&env.program_client, &env.payer, &env.vote_account).await; - - wait_for_next_epoch(&env.rpc_client).await; - - let status = Command::new(SVSP_CLI) - .args([ - "deposit", - "-C", - &env.config_file_path, - &stake_account.to_string(), - ]) - .status() - .unwrap(); - assert!(status.success()); - - let status = Command::new(SVSP_CLI) - .args([ - "withdraw", - "-C", - &env.config_file_path, - "--vote-account", - &env.vote_account.to_string(), - "ALL", - ]) - .status() - .unwrap(); - assert!(status.success()); -} - -#[tokio::test] -#[serial] -async fn create_metadata() { - let env = setup(false).await; - - let status = Command::new(SVSP_CLI) - .args([ - "manage", - "initialize", - "-C", - &env.config_file_path, - "--skip-metadata", - &env.vote_account.to_string(), - ]) - .status() - .unwrap(); - assert!(status.success()); - - let status = Command::new(SVSP_CLI) - .args([ - "manage", - "create-token-metadata", - "-C", - &env.config_file_path, - "--vote-account", - &env.vote_account.to_string(), - ]) - .status() - .unwrap(); - assert!(status.success()); -} - -#[tokio::test] -#[serial] -async fn update_metadata() { - let env = setup(true).await; - - let status = Command::new(SVSP_CLI) - .args([ - "manage", - "update-token-metadata", - "-C", - &env.config_file_path, - "--vote-account", - &env.vote_account.to_string(), - "whatever", - "idk", - ]) - .status() - .unwrap(); - assert!(status.success()); - - // testing this flag because the match is rather torturous - let status = Command::new(SVSP_CLI) - .args([ - "manage", - "update-token-metadata", - "-C", - &env.config_file_path, - "--vote-account", - &env.vote_account.to_string(), - "--authorized-withdrawer", - &env.keypair_file_path, - "something", - "new", - ]) - .status() - .unwrap(); - assert!(status.success()); -} diff --git a/single-pool/js/LICENSE b/single-pool/js/LICENSE deleted file mode 100644 index d6456956733..00000000000 --- a/single-pool/js/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/single-pool/js/packages/classic/.eslintignore b/single-pool/js/packages/classic/.eslintignore deleted file mode 100644 index 58542507948..00000000000 --- a/single-pool/js/packages/classic/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -dist -node_modules -.vscode -.idea diff --git a/single-pool/js/packages/classic/.eslintrc.cjs b/single-pool/js/packages/classic/.eslintrc.cjs deleted file mode 100644 index 63db7b8405f..00000000000 --- a/single-pool/js/packages/classic/.eslintrc.cjs +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = { - root: true, - env: { - es6: true, - node: true, - jest: true, - }, - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], - plugins: ['@typescript-eslint/eslint-plugin'], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - }, - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - }, -}; diff --git a/single-pool/js/packages/classic/.gitignore b/single-pool/js/packages/classic/.gitignore deleted file mode 100644 index 77738287f0e..00000000000 --- a/single-pool/js/packages/classic/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ \ No newline at end of file diff --git a/single-pool/js/packages/classic/.prettierrc.cjs b/single-pool/js/packages/classic/.prettierrc.cjs deleted file mode 100644 index 8446d684477..00000000000 --- a/single-pool/js/packages/classic/.prettierrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - singleQuote: true, - trailingComma: 'all', - printWidth: 100, - endOfLine: 'lf', - semi: true, -}; diff --git a/single-pool/js/packages/classic/README.md b/single-pool/js/packages/classic/README.md deleted file mode 100644 index f5416d3e10c..00000000000 --- a/single-pool/js/packages/classic/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# `@solana/spl-single-pool-classic` - -A TypeScript library for interacting with the SPL Single-Validator Stake Pool program, targeting `@solana/web3.js` 1.x. -**If you are working on the new, bleeding-edge web3.js, you want `@solana/spl-single-pool`.** - -For information on installation and usage, see [SPL docs](https://spl.solana.com/single-pool). - -For support, please ask questions on the [Solana Stack Exchange](https://solana.stackexchange.com). - -If you've found a bug or you'd like to request a feature, please -[open an issue](https://github.com/solana-labs/solana-program-library/issues/new). diff --git a/single-pool/js/packages/classic/package.json b/single-pool/js/packages/classic/package.json deleted file mode 100644 index 6d2c40dd6c9..00000000000 --- a/single-pool/js/packages/classic/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@solana/spl-single-pool-classic", - "version": "1.0.2", - "main": "dist/cjs/index.js", - "module": "dist/mjs/index.js", - "exports": { - ".": { - "import": "./dist/mjs/index.js", - "require": "./dist/cjs/index.js" - } - }, - "scripts": { - "clean": "rm -rf dist/*", - "build": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && ./ts-fixup.sh", - "build:program": "cargo build-sbf --manifest-path=../../../program/Cargo.toml", - "lint": "eslint --max-warnings 0 .", - "lint:fix": "eslint . --fix", - "test": "sed -i '1s/.*/{ \"type\": \"module\",/' package.json && NODE_OPTIONS='--loader=tsx' ava ; ret=$?; sed -i '1s/.*/{/' package.json && exit $ret" - }, - "devDependencies": { - "@types/node": "^22.10.5", - "@ava/typescript": "^5.0.0", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "ava": "^6.2.0", - "eslint": "^8.57.0", - "solana-bankrun": "^0.2.0", - "tsx": "^4.19.2", - "typescript": "^5.7.2" - }, - "dependencies": { - "@solana/web3.js": "^1.95.5", - "@solana/addresses": "2.0.0", - "@solana/spl-single-pool": "1.0.0" - }, - "ava": { - "extensions": { - "ts": "module" - }, - "nodeArguments": [ - "--import=tsx" - ] - } -} diff --git a/single-pool/js/packages/classic/src/addresses.ts b/single-pool/js/packages/classic/src/addresses.ts deleted file mode 100644 index e9b6a22b2ff..00000000000 --- a/single-pool/js/packages/classic/src/addresses.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { Address } from '@solana/addresses'; -import { PublicKey } from '@solana/web3.js'; -import type { PoolAddress, VoteAccountAddress } from '@solana/spl-single-pool'; -import { - findPoolAddress as findPoolModern, - findPoolStakeAddress as findStakeModern, - findPoolMintAddress as findMintModern, - findPoolStakeAuthorityAddress as findStakeAuthorityModern, - findPoolMintAuthorityAddress as findMintAuthorityModern, - findPoolMplAuthorityAddress as findMplAuthorityModern, - findDefaultDepositAccountAddress as findDefaultDepositModern, -} from '@solana/spl-single-pool'; - -export async function findPoolAddress(programId: PublicKey, voteAccountAddress: PublicKey) { - return new PublicKey( - await findPoolModern( - programId.toBase58() as Address, - voteAccountAddress.toBase58() as VoteAccountAddress, - ), - ); -} - -export async function findPoolStakeAddress(programId: PublicKey, poolAddress: PublicKey) { - return new PublicKey( - await findStakeModern(programId.toBase58() as Address, poolAddress.toBase58() as PoolAddress), - ); -} - -export async function findPoolMintAddress(programId: PublicKey, poolAddress: PublicKey) { - return new PublicKey( - await findMintModern(programId.toBase58() as Address, poolAddress.toBase58() as PoolAddress), - ); -} - -export async function findPoolStakeAuthorityAddress(programId: PublicKey, poolAddress: PublicKey) { - return new PublicKey( - await findStakeAuthorityModern( - programId.toBase58() as Address, - poolAddress.toBase58() as PoolAddress, - ), - ); -} - -export async function findPoolMintAuthorityAddress(programId: PublicKey, poolAddress: PublicKey) { - return new PublicKey( - await findMintAuthorityModern( - programId.toBase58() as Address, - poolAddress.toBase58() as PoolAddress, - ), - ); -} - -export async function findPoolMplAuthorityAddress(programId: PublicKey, poolAddress: PublicKey) { - return new PublicKey( - await findMplAuthorityModern( - programId.toBase58() as Address, - poolAddress.toBase58() as PoolAddress, - ), - ); -} - -export async function findDefaultDepositAccountAddress( - poolAddress: PublicKey, - userWallet: PublicKey, -) { - return new PublicKey( - await findDefaultDepositModern( - poolAddress.toBase58() as PoolAddress, - userWallet.toBase58() as Address, - ), - ); -} diff --git a/single-pool/js/packages/classic/src/index.ts b/single-pool/js/packages/classic/src/index.ts deleted file mode 100644 index dc21826ad64..00000000000 --- a/single-pool/js/packages/classic/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Connection, PublicKey } from '@solana/web3.js'; -import { getVoteAccountAddressForPool as getVoteModern } from '@solana/spl-single-pool'; -import type { PoolAddress } from '@solana/spl-single-pool'; - -import { rpc } from './internal.js'; - -export * from './mpl_metadata.js'; -export * from './addresses.js'; -export * from './instructions.js'; -export * from './transactions.js'; - -export async function getVoteAccountAddressForPool(connection: Connection, poolAddress: PublicKey) { - const voteAccountModern = await getVoteModern( - rpc(connection), - poolAddress.toBase58() as PoolAddress, - ); - - return new PublicKey(voteAccountModern); -} diff --git a/single-pool/js/packages/classic/src/instructions.ts b/single-pool/js/packages/classic/src/instructions.ts deleted file mode 100644 index ae24d33829d..00000000000 --- a/single-pool/js/packages/classic/src/instructions.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { Address } from '@solana/addresses'; -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import type { PoolAddress, VoteAccountAddress } from '@solana/spl-single-pool'; -import { SinglePoolInstruction as PoolInstructionModern } from '@solana/spl-single-pool'; - -import { modernInstructionToLegacy } from './internal.js'; - -export class SinglePoolInstruction { - static async initializePool(voteAccount: PublicKey): Promise { - const instruction = await PoolInstructionModern.initializePool( - voteAccount.toBase58() as VoteAccountAddress, - ); - return modernInstructionToLegacy(instruction); - } - - static async reactivatePoolStake(voteAccount: PublicKey): Promise { - const instruction = await PoolInstructionModern.reactivatePoolStake( - voteAccount.toBase58() as VoteAccountAddress, - ); - return modernInstructionToLegacy(instruction); - } - - static async depositStake( - pool: PublicKey, - userStakeAccount: PublicKey, - userTokenAccount: PublicKey, - userLamportAccount: PublicKey, - ): Promise { - const instruction = await PoolInstructionModern.depositStake( - pool.toBase58() as PoolAddress, - userStakeAccount.toBase58() as Address, - userTokenAccount.toBase58() as Address, - userLamportAccount.toBase58() as Address, - ); - return modernInstructionToLegacy(instruction); - } - - static async withdrawStake( - pool: PublicKey, - userStakeAccount: PublicKey, - userStakeAuthority: PublicKey, - userTokenAccount: PublicKey, - tokenAmount: number | bigint, - ): Promise { - const instruction = await PoolInstructionModern.withdrawStake( - pool.toBase58() as PoolAddress, - userStakeAccount.toBase58() as Address, - userStakeAuthority.toBase58() as Address, - userTokenAccount.toBase58() as Address, - BigInt(tokenAmount), - ); - return modernInstructionToLegacy(instruction); - } - - static async createTokenMetadata( - pool: PublicKey, - payer: PublicKey, - ): Promise { - const instruction = await PoolInstructionModern.createTokenMetadata( - pool.toBase58() as PoolAddress, - payer.toBase58() as Address, - ); - return modernInstructionToLegacy(instruction); - } - - static async updateTokenMetadata( - voteAccount: PublicKey, - authorizedWithdrawer: PublicKey, - tokenName: string, - tokenSymbol: string, - tokenUri?: string, - ): Promise { - const instruction = await PoolInstructionModern.updateTokenMetadata( - voteAccount.toBase58() as VoteAccountAddress, - authorizedWithdrawer.toBase58() as Address, - tokenName, - tokenSymbol, - tokenUri, - ); - return modernInstructionToLegacy(instruction); - } -} diff --git a/single-pool/js/packages/classic/src/internal.ts b/single-pool/js/packages/classic/src/internal.ts deleted file mode 100644 index e1628ee76a8..00000000000 --- a/single-pool/js/packages/classic/src/internal.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Connection, Transaction, TransactionInstruction, PublicKey } from '@solana/web3.js'; -import { Buffer } from 'buffer'; - -export function rpc(connection: Connection) { - return { - getAccountInfo(address: string) { - return { - async send() { - const pubkey = new PublicKey(address); - return await connection.getAccountInfo(pubkey); - }, - }; - }, - getMinimumBalanceForRentExemption(size: bigint) { - return { - async send() { - return BigInt(await connection.getMinimumBalanceForRentExemption(Number(size))); - }, - }; - }, - getStakeMinimumDelegation() { - return { - async send() { - const minimumDelegation = await connection.getStakeMinimumDelegation(); - return { value: BigInt(minimumDelegation.value) }; - }, - }; - }, - }; -} - -export function modernInstructionToLegacy(modernInstruction: any): TransactionInstruction { - const keys = []; - for (const account of modernInstruction.accounts) { - keys.push({ - pubkey: new PublicKey(account.address), - isSigner: !!(account.role & 2), - isWritable: !!(account.role & 1), - }); - } - - return new TransactionInstruction({ - programId: new PublicKey(modernInstruction.programAddress), - keys, - data: Buffer.from(modernInstruction.data), - }); -} - -export function modernTransactionToLegacy(modernTransaction: any): Transaction { - const legacyTransaction = new Transaction(); - legacyTransaction.add(...modernTransaction.instructions.map(modernInstructionToLegacy)); - - return legacyTransaction; -} - -export function paramsToModern(params: any) { - const modernParams = {} as any; - for (const k of Object.keys(params)) { - if (k == 'connection') { - modernParams.rpc = rpc(params[k]); - } else if (params[k] instanceof PublicKey || params[k].constructor.name == 'PublicKey') { - modernParams[k] = params[k].toBase58(); - } else if (typeof params[k] == 'number') { - modernParams[k] = BigInt(params[k]); - } else { - modernParams[k] = params[k]; - } - } - - return modernParams; -} diff --git a/single-pool/js/packages/classic/src/mpl_metadata.ts b/single-pool/js/packages/classic/src/mpl_metadata.ts deleted file mode 100644 index 31c52ae2a50..00000000000 --- a/single-pool/js/packages/classic/src/mpl_metadata.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import { Buffer } from 'buffer'; - -export const MPL_METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'); - -export function findMplMetadataAddress(poolMintAddress: PublicKey) { - const [publicKey] = PublicKey.findProgramAddressSync( - [Buffer.from('metadata'), MPL_METADATA_PROGRAM_ID.toBuffer(), poolMintAddress.toBuffer()], - MPL_METADATA_PROGRAM_ID, - ); - return publicKey; -} diff --git a/single-pool/js/packages/classic/src/transactions.ts b/single-pool/js/packages/classic/src/transactions.ts deleted file mode 100644 index aac785ab48c..00000000000 --- a/single-pool/js/packages/classic/src/transactions.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Address } from '@solana/addresses'; -import { PublicKey, Connection } from '@solana/web3.js'; -import type { PoolAddress, VoteAccountAddress } from '@solana/spl-single-pool'; -import { SinglePoolProgram as PoolProgramModern } from '@solana/spl-single-pool'; - -import { paramsToModern, modernTransactionToLegacy, rpc } from './internal.js'; - -interface DepositParams { - connection: Connection; - pool: PublicKey; - userWallet: PublicKey; - userStakeAccount?: PublicKey; - depositFromDefaultAccount?: boolean; - userTokenAccount?: PublicKey; - userLamportAccount?: PublicKey; - userWithdrawAuthority?: PublicKey; -} - -interface WithdrawParams { - connection: Connection; - pool: PublicKey; - userWallet: PublicKey; - userStakeAccount: PublicKey; - tokenAmount: number | bigint; - createStakeAccount?: boolean; - userStakeAuthority?: PublicKey; - userTokenAccount?: PublicKey; - userTokenAuthority?: PublicKey; -} - -export class SinglePoolProgram { - static programId: PublicKey = new PublicKey(PoolProgramModern.programAddress); - static space: number = Number(PoolProgramModern.space); - - static async initialize( - connection: Connection, - voteAccount: PublicKey, - payer: PublicKey, - skipMetadata = false, - ) { - const modernTransaction = await PoolProgramModern.initialize( - rpc(connection), - voteAccount.toBase58() as VoteAccountAddress, - payer.toBase58() as Address, - skipMetadata, - ); - - return modernTransactionToLegacy(modernTransaction); - } - - static async reactivatePoolStake(connection: Connection, voteAccount: PublicKey) { - const modernTransaction = await PoolProgramModern.reactivatePoolStake( - voteAccount.toBase58() as VoteAccountAddress, - ); - - return modernTransactionToLegacy(modernTransaction); - } - - static async deposit(params: DepositParams) { - const modernParams = paramsToModern(params); - const modernTransaction = await PoolProgramModern.deposit(modernParams); - - return modernTransactionToLegacy(modernTransaction); - } - - static async withdraw(params: WithdrawParams) { - const modernParams = paramsToModern(params); - const modernTransaction = await PoolProgramModern.withdraw(modernParams); - - return modernTransactionToLegacy(modernTransaction); - } - - static async createTokenMetadata(pool: PublicKey, payer: PublicKey) { - const modernTransaction = await PoolProgramModern.createTokenMetadata( - pool.toBase58() as PoolAddress, - payer.toBase58() as Address, - ); - - return modernTransactionToLegacy(modernTransaction); - } - - static async updateTokenMetadata( - voteAccount: PublicKey, - authorizedWithdrawer: PublicKey, - name: string, - symbol: string, - uri?: string, - ) { - const modernTransaction = await PoolProgramModern.updateTokenMetadata( - voteAccount.toBase58() as VoteAccountAddress, - authorizedWithdrawer.toBase58() as Address, - name, - symbol, - uri, - ); - - return modernTransactionToLegacy(modernTransaction); - } - - static async createAndDelegateUserStake( - connection: Connection, - voteAccount: PublicKey, - userWallet: PublicKey, - stakeAmount: number | bigint, - ) { - const modernTransaction = await PoolProgramModern.createAndDelegateUserStake( - rpc(connection), - voteAccount.toBase58() as VoteAccountAddress, - userWallet.toBase58() as Address, - BigInt(stakeAmount), - ); - - return modernTransactionToLegacy(modernTransaction); - } -} diff --git a/single-pool/js/packages/classic/tests/fixtures/mpl_token_metadata.so b/single-pool/js/packages/classic/tests/fixtures/mpl_token_metadata.so deleted file mode 120000 index df4d35160ee..00000000000 --- a/single-pool/js/packages/classic/tests/fixtures/mpl_token_metadata.so +++ /dev/null @@ -1 +0,0 @@ -../../../../../../stake-pool/program/tests/fixtures/mpl_token_metadata.so \ No newline at end of file diff --git a/single-pool/js/packages/classic/tests/fixtures/spl_single_pool.so b/single-pool/js/packages/classic/tests/fixtures/spl_single_pool.so deleted file mode 120000 index 5fe6f8bd60e..00000000000 --- a/single-pool/js/packages/classic/tests/fixtures/spl_single_pool.so +++ /dev/null @@ -1 +0,0 @@ -../../../../../../target/deploy/spl_single_pool.so \ No newline at end of file diff --git a/single-pool/js/packages/classic/tests/transactions.test.ts b/single-pool/js/packages/classic/tests/transactions.test.ts deleted file mode 100644 index 38dcc41d867..00000000000 --- a/single-pool/js/packages/classic/tests/transactions.test.ts +++ /dev/null @@ -1,432 +0,0 @@ -import test from 'ava'; -import { start, BanksClient, ProgramTestContext } from 'solana-bankrun'; -import { - Keypair, - PublicKey, - Transaction, - Authorized, - TransactionInstruction, - StakeProgram, - VoteProgram, -} from '@solana/web3.js'; -import { Buffer } from 'buffer'; -import { - getVoteAccountAddressForPool, - findDefaultDepositAccountAddress, - MPL_METADATA_PROGRAM_ID, - findPoolAddress, - findPoolStakeAddress, - findPoolMintAddress, - SinglePoolProgram, - findMplMetadataAddress, -} from '../src/index.ts'; -import * as voteAccount from './vote_account.json'; - -const SLOTS_PER_EPOCH: bigint = 432000n; - -class BanksConnection { - constructor(client: BanksClient, payer: Keypair) { - this.client = client; - this.payer = payer; - } - - async getMinimumBalanceForRentExemption(dataLen: number): Promise { - const rent = await this.client.getRent(); - return Number(rent.minimumBalance(BigInt(dataLen))); - } - - async getStakeMinimumDelegation() { - const transaction = new Transaction(); - transaction.add( - new TransactionInstruction({ - programId: StakeProgram.programId, - keys: [], - data: Buffer.from([13, 0, 0, 0]), - }), - ); - transaction.recentBlockhash = (await this.client.getLatestBlockhash())[0]; - transaction.feePayer = this.payer.publicKey; - transaction.sign(this.payer); - - const res = await this.client.simulateTransaction(transaction); - const data = Array.from(res.inner.meta.returnData.data); - const minimumDelegation = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24); - - return { value: minimumDelegation }; - } - - async getAccountInfo(address: PublicKey, commitment?: string): Promise> { - const account = await this.client.getAccount(address, commitment); - if (account) { - account.data = Buffer.from(account.data); - } - return account; - } -} - -async function startWithContext(authorizedWithdrawer?: PublicKey) { - const voteAccountData = Uint8Array.from(atob(voteAccount.account.data[0]), (c) => - c.charCodeAt(0), - ); - - if (authorizedWithdrawer != null) { - voteAccountData.set(authorizedWithdrawer.toBytes(), 36); - } - - return await start( - [ - { name: 'spl_single_pool', programId: SinglePoolProgram.programId }, - { name: 'mpl_token_metadata', programId: MPL_METADATA_PROGRAM_ID }, - ], - [ - { - address: new PublicKey(voteAccount.pubkey), - info: { - lamports: voteAccount.account.lamports, - data: voteAccountData, - owner: VoteProgram.programId, - executable: false, - }, - }, - ], - ); -} - -async function processTransaction( - context: ProgramTestContext, - transaction: Transaction, - signers = [], -) { - transaction.recentBlockhash = context.lastBlockhash; - transaction.feePayer = context.payer.publicKey; - transaction.sign(...[context.payer].concat(signers)); - return context.banksClient.processTransaction(transaction); -} - -async function createAndDelegateStakeAccount( - context: ProgramTestContext, - voteAccountAddress: PublicKey, -): Promise { - const connection = new BanksConnection(context.banksClient, context.payer); - let userStakeAccount = new Keypair(); - - const stakeRent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space); - const minimumDelegation = (await connection.getStakeMinimumDelegation()).value; - let transaction = StakeProgram.createAccount({ - authorized: new Authorized(context.payer.publicKey, context.payer.publicKey), - fromPubkey: context.payer.publicKey, - lamports: stakeRent + minimumDelegation, - stakePubkey: userStakeAccount.publicKey, - }); - await processTransaction(context, transaction, [userStakeAccount]); - userStakeAccount = userStakeAccount.publicKey; - - transaction = StakeProgram.delegate({ - authorizedPubkey: context.payer.publicKey, - stakePubkey: userStakeAccount, - votePubkey: voteAccountAddress, - }); - await processTransaction(context, transaction); - - return userStakeAccount; -} - -test('initialize', async (t) => { - const context = await startWithContext(); - const client = context.banksClient; - const payer = context.payer; - const connection = new BanksConnection(client, payer); - - const voteAccountAddress = new PublicKey(voteAccount.pubkey); - const poolAddress = await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress); - - // initialize pool - const transaction = await SinglePoolProgram.initialize( - connection, - voteAccountAddress, - payer.publicKey, - ); - await processTransaction(context, transaction); - - t.truthy(await client.getAccount(poolAddress), 'pool has been created'); - t.truthy( - await client.getAccount( - findMplMetadataAddress(await findPoolMintAddress(SinglePoolProgram.programId, poolAddress)), - ), - 'metadata has been created', - ); -}); - -test('reactivate pool stake', async (t) => { - const context = await startWithContext(); - const client = context.banksClient; - const payer = context.payer; - const connection = new BanksConnection(client, payer); - - const voteAccountAddress = new PublicKey(voteAccount.pubkey); - - // initialize pool - let transaction = await SinglePoolProgram.initialize( - connection, - voteAccountAddress, - payer.publicKey, - ); - await processTransaction(context, transaction); - - const slot = await client.getSlot(); - context.warpToSlot(slot + SLOTS_PER_EPOCH); - - // reactivate pool stake - transaction = await SinglePoolProgram.reactivatePoolStake(connection, voteAccountAddress); - - // setting up the validator state for this to succeed is very annoying - // we test success in program tests; here we just confirm we submit a well-formed transaction - let message = ''; - try { - await processTransaction(context, transaction); - } catch (e) { - message = e.message; - } finally { - t.true(message.includes('custom program error: 0xc'), 'got expected stake mismatch error'); - } -}); - -test('deposit', async (t) => { - const context = await startWithContext(); - const client = context.banksClient; - const payer = context.payer; - const connection = new BanksConnection(client, payer); - - const voteAccountAddress = new PublicKey(voteAccount.pubkey); - const poolAddress = await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress); - const poolStakeAddress = await findPoolStakeAddress(SinglePoolProgram.programId, poolAddress); - const userStakeAccount = await createAndDelegateStakeAccount(context, voteAccountAddress); - - // initialize pool - let transaction = await SinglePoolProgram.initialize( - connection, - voteAccountAddress, - payer.publicKey, - ); - await processTransaction(context, transaction); - - const slot = await client.getSlot(); - context.warpToSlot(slot + SLOTS_PER_EPOCH); - - // deposit - transaction = await SinglePoolProgram.deposit({ - connection, - pool: poolAddress, - userWallet: payer.publicKey, - userStakeAccount, - }); - await processTransaction(context, transaction); - - const stakeRent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space); - const minimumDelegation = (await connection.getStakeMinimumDelegation()).value; - const poolStakeAccount = await client.getAccount(poolStakeAddress); - t.is(poolStakeAccount.lamports, minimumDelegation * 2 + stakeRent, 'stake has been deposited'); -}); - -test('deposit from default', async (t) => { - const context = await startWithContext(); - const client = context.banksClient; - const payer = context.payer; - const connection = new BanksConnection(client, payer); - - const voteAccountAddress = new PublicKey(voteAccount.pubkey); - const poolAddress = await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress); - const poolStakeAddress = await findPoolStakeAddress(SinglePoolProgram.programId, poolAddress); - - // create default account - const minimumDelegation = (await connection.getStakeMinimumDelegation()).value; - let transaction = await SinglePoolProgram.createAndDelegateUserStake( - connection, - voteAccountAddress, - payer.publicKey, - minimumDelegation, - ); - await processTransaction(context, transaction); - - // initialize pool - transaction = await SinglePoolProgram.initialize(connection, voteAccountAddress, payer.publicKey); - await processTransaction(context, transaction); - - const slot = await client.getSlot(); - context.warpToSlot(slot + SLOTS_PER_EPOCH); - - // deposit - transaction = await SinglePoolProgram.deposit({ - connection, - pool: poolAddress, - userWallet: payer.publicKey, - depositFromDefaultAccount: true, - }); - await processTransaction(context, transaction); - - const stakeRent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space); - const poolStakeAccount = await client.getAccount(poolStakeAddress); - t.is(poolStakeAccount.lamports, minimumDelegation * 2 + stakeRent, 'stake has been deposited'); -}); - -test('withdraw', async (t) => { - const context = await startWithContext(); - const client = context.banksClient; - const payer = context.payer; - const connection = new BanksConnection(client, payer); - - const voteAccountAddress = new PublicKey(voteAccount.pubkey); - const poolAddress = await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress); - const poolStakeAddress = await findPoolStakeAddress(SinglePoolProgram.programId, poolAddress); - const depositAccount = await createAndDelegateStakeAccount(context, voteAccountAddress); - - // initialize pool - let transaction = await SinglePoolProgram.initialize( - connection, - voteAccountAddress, - payer.publicKey, - ); - await processTransaction(context, transaction); - - const slot = await client.getSlot(); - context.warpToSlot(slot + SLOTS_PER_EPOCH); - - // deposit - transaction = await SinglePoolProgram.deposit({ - connection, - pool: poolAddress, - userWallet: payer.publicKey, - userStakeAccount: depositAccount, - }); - await processTransaction(context, transaction); - - const minimumDelegation = (await connection.getStakeMinimumDelegation()).value; - const poolStakeAccount = await client.getAccount(poolStakeAddress); - t.true(poolStakeAccount.lamports > minimumDelegation * 2, 'stake has been deposited'); - - // withdraw - const withdrawAccount = new Keypair(); - transaction = await SinglePoolProgram.withdraw({ - connection, - pool: poolAddress, - userWallet: payer.publicKey, - userStakeAccount: withdrawAccount.publicKey, - tokenAmount: minimumDelegation, - createStakeAccount: true, - }); - await processTransaction(context, transaction, [withdrawAccount]); - - const stakeRent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space); - const userStakeAccount = await client.getAccount(withdrawAccount.publicKey); - t.is(userStakeAccount.lamports, minimumDelegation + stakeRent, 'stake has been withdrawn'); -}); - -test('create metadata', async (t) => { - const context = await startWithContext(); - const client = context.banksClient; - const payer = context.payer; - const connection = new BanksConnection(client, payer); - - const voteAccountAddress = new PublicKey(voteAccount.pubkey); - const poolAddress = await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress); - - // initialize pool without metadata - let transaction = await SinglePoolProgram.initialize( - connection, - voteAccountAddress, - payer.publicKey, - true, - ); - await processTransaction(context, transaction); - - t.truthy(await client.getAccount(poolAddress), 'pool has been created'); - t.falsy( - await client.getAccount( - findMplMetadataAddress(await findPoolMintAddress(SinglePoolProgram.programId, poolAddress)), - ), - 'metadata has not been created', - ); - - // create metadata - transaction = await SinglePoolProgram.createTokenMetadata(poolAddress, payer.publicKey); - await processTransaction(context, transaction); - - t.truthy( - await client.getAccount( - findMplMetadataAddress(await findPoolMintAddress(SinglePoolProgram.programId, poolAddress)), - ), - 'metadata has been created', - ); -}); - -test('update metadata', async (t) => { - const authorizedWithdrawer = new Keypair(); - - const context = await startWithContext(authorizedWithdrawer.publicKey); - const client = context.banksClient; - const payer = context.payer; - const connection = new BanksConnection(client, payer); - - const voteAccountAddress = new PublicKey(voteAccount.pubkey); - const poolAddress = await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress); - const poolMintAddress = await findPoolMintAddress(SinglePoolProgram.programId, poolAddress); - const poolMetadataAddress = findMplMetadataAddress(poolMintAddress); - - // initialize pool - let transaction = await SinglePoolProgram.initialize( - connection, - voteAccountAddress, - payer.publicKey, - ); - await processTransaction(context, transaction); - - // update metadata - const newName = 'hana wuz here'; - transaction = await SinglePoolProgram.updateTokenMetadata( - voteAccountAddress, - authorizedWithdrawer.publicKey, - newName, - '', - ); - await processTransaction(context, transaction, [authorizedWithdrawer]); - - const metadataAccount = await client.getAccount(poolMetadataAddress); - t.true( - new TextDecoder('ascii').decode(metadataAccount.data).indexOf(newName) > -1, - 'metadata name has been updated', - ); -}); - -test('get vote account address', async (t) => { - const context = await startWithContext(); - const client = context.banksClient; - const payer = context.payer; - const connection = new BanksConnection(client, payer); - - const voteAccountAddress = new PublicKey(voteAccount.pubkey); - const poolAddress = await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress); - - // initialize pool - const transaction = await SinglePoolProgram.initialize( - connection, - voteAccountAddress, - payer.publicKey, - ); - await processTransaction(context, transaction); - - const chainVoteAccount = await getVoteAccountAddressForPool(connection, poolAddress); - t.true(chainVoteAccount.equals(voteAccountAddress), 'got correct vote account'); -}); - -test('default account address', async (t) => { - const voteAccountAddress = new PublicKey(voteAccount.pubkey); - const owner = new PublicKey('GtaYCtXWCrciizttN5mx9P38niTQPGWpfu6DnSgAr3Cj'); - const expectedDefault = new PublicKey('BbfrNeJrd82cSFsULXT9zG8SvLLB8WsTc1gQsDFy3Sed'); - - const actualDefault = await findDefaultDepositAccountAddress( - await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress), - owner, - ); - - t.true(actualDefault.equals(expectedDefault), 'got correct default account address'); -}); diff --git a/single-pool/js/packages/classic/tests/vote_account.json b/single-pool/js/packages/classic/tests/vote_account.json deleted file mode 100644 index 44a5efd12d8..00000000000 --- a/single-pool/js/packages/classic/tests/vote_account.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pubkey": "KRAKEnMdmT4EfM8ykTFH6yLoCd5vNLcQvJwF66Y2dag", - "account": { - "lamports": 1300578700922, - "data": [ - "AQAAAAs8CYpjxAGc9BKIFsvo43erJeAPq9FLBOZuVf7zcXQwDtalO9ClDHolg+JcQCSa0sIFkdUQpQh5ufXK07iakuhkHwAAAAAAAAACxGIMAAAAAB8AAAADxGIMAAAAAB4AAAAExGIMAAAAAB0AAAAFxGIMAAAAABwAAAAGxGIMAAAAABsAAAAHxGIMAAAAABoAAAAIxGIMAAAAABkAAAAJxGIMAAAAABgAAAAKxGIMAAAAABcAAAALxGIMAAAAABYAAAAMxGIMAAAAABUAAAANxGIMAAAAABQAAAAOxGIMAAAAABMAAAAPxGIMAAAAABIAAAAQxGIMAAAAABEAAAARxGIMAAAAABAAAAASxGIMAAAAAA8AAAATxGIMAAAAAA4AAAAUxGIMAAAAAA0AAAAVxGIMAAAAAAwAAAAWxGIMAAAAAAsAAAAXxGIMAAAAAAoAAAAYxGIMAAAAAAkAAAAZxGIMAAAAAAgAAAAaxGIMAAAAAAcAAAAbxGIMAAAAAAYAAAAcxGIMAAAAAAUAAAAdxGIMAAAAAAQAAAAexGIMAAAAAAMAAAAfxGIMAAAAAAIAAAAgxGIMAAAAAAEAAAABAcRiDAAAAAABAAAAAAAAAOEBAAAAAAAACzwJimPEAZz0EogWy+jjd6sl4A+r0UsE5m5V/vNxdDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAFAAAAAAAAAAKIBAAAAAAAA1diYBAAAAAAvw5IEAAAAAKMBAAAAAAAA++WeBAAAAADV2JgEAAAAAKQBAAAAAAAA5dukBAAAAAD75Z4EAAAAAKUBAAAAAAAAbf6qBAAAAADl26QEAAAAAKYBAAAAAAAA1hGxBAAAAABt/qoEAAAAAKcBAAAAAAAAaiS3BAAAAADWEbEEAAAAAKgBAAAAAAAARHK9BAAAAABqJLcEAAAAAKkBAAAAAAAAacPDBAAAAABEcr0EAAAAAKoBAAAAAAAAWQ3KBAAAAABpw8MEAAAAAKsBAAAAAAAAUHLQBAAAAABZDcoEAAAAAKwBAAAAAAAAk9nWBAAAAABQctAEAAAAAK0BAAAAAAAAxTHdBAAAAACT2dYEAAAAAK4BAAAAAAAA34bjBAAAAADFMd0EAAAAAK8BAAAAAAAA0+vpBAAAAADfhuMEAAAAALABAAAAAAAAnFLwBAAAAADT6+kEAAAAALEBAAAAAAAAt7z2BAAAAACcUvAEAAAAALIBAAAAAAAAoyT9BAAAAAC3vPYEAAAAALMBAAAAAAAAXX0DBQAAAACjJP0EAAAAALQBAAAAAAAA6NcJBQAAAABdfQMFAAAAALUBAAAAAAAA5wQQBQAAAADo1wkFAAAAALYBAAAAAAAAvAMWBQAAAADnBBAFAAAAALcBAAAAAAAA6DkcBQAAAAC8AxYFAAAAALgBAAAAAAAAx34iBQAAAADoORwFAAAAALkBAAAAAAAAm80oBQAAAADHfiIFAAAAALoBAAAAAAAAriQvBQAAAACbzSgFAAAAALsBAAAAAAAAsHE1BQAAAACuJC8FAAAAALwBAAAAAAAADpM7BQAAAACwcTUFAAAAAL0BAAAAAAAANsdBBQAAAAAOkzsFAAAAAL4BAAAAAAAAXgNIBQAAAAA2x0EFAAAAAL8BAAAAAAAAnBJOBQAAAABeA0gFAAAAAMABAAAAAAAAukpUBQAAAACcEk4FAAAAAMEBAAAAAAAALIxaBQAAAAC6SlQFAAAAAMIBAAAAAAAAzddgBQAAAAAsjFoFAAAAAMMBAAAAAAAAaS9nBQAAAADN12AFAAAAAMQBAAAAAAAATG1tBQAAAABpL2cFAAAAAMUBAAAAAAAAqptzBQAAAABMbW0FAAAAAMYBAAAAAAAACvJ5BQAAAACqm3MFAAAAAMcBAAAAAAAARUmABQAAAAAK8nkFAAAAAMgBAAAAAAAATJGGBQAAAABFSYAFAAAAAMkBAAAAAAAAZ+CMBQAAAABMkYYFAAAAAMoBAAAAAAAAsyGTBQAAAABn4IwFAAAAAMsBAAAAAAAAT2GZBQAAAACzIZMFAAAAAMwBAAAAAAAAEHKfBQAAAABPYZkFAAAAAM0BAAAAAAAAzbClBQAAAAAQcp8FAAAAAM4BAAAAAAAA0gWsBQAAAADNsKUFAAAAAM8BAAAAAAAAP2eyBQAAAADSBawFAAAAANABAAAAAAAAOLu4BQAAAAA/Z7IFAAAAANEBAAAAAAAAVQC/BQAAAAA4u7gFAAAAANIBAAAAAAAAilLFBQAAAABVAL8FAAAAANMBAAAAAAAAfaLLBQAAAACKUsUFAAAAANQBAAAAAAAAGfrRBQAAAAB9ossFAAAAANUBAAAAAAAA/1DYBQAAAAAZ+tEFAAAAANYBAAAAAAAA06reBQAAAAD/UNgFAAAAANcBAAAAAAAAwwXlBQAAAADTqt4FAAAAANgBAAAAAAAAnVvrBQAAAADDBeUFAAAAANkBAAAAAAAAvbXxBQAAAACdW+sFAAAAANoBAAAAAAAAMQH4BQAAAAC9tfEFAAAAANsBAAAAAAAANT/+BQAAAAAxAfgFAAAAANwBAAAAAAAA04wEBgAAAAA1P/4FAAAAAN0BAAAAAAAAhNIKBgAAAADTjAQGAAAAAN4BAAAAAAAADCkRBgAAAACE0goGAAAAAN8BAAAAAAAAL4MXBgAAAAAMKREGAAAAAOABAAAAAAAAF9odBgAAAAAvgxcGAAAAAOEBAAAAAAAAjfQdBgAAAAAX2h0GAAAAACDEYgwAAAAAzUXCZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "base64" - ], - "owner": "Vote111111111111111111111111111111111111111", - "executable": false, - "rentEpoch": 361, - "space": 3731 - } -} \ No newline at end of file diff --git a/single-pool/js/packages/classic/ts-fixup.sh b/single-pool/js/packages/classic/ts-fixup.sh deleted file mode 120000 index 2b79c262866..00000000000 --- a/single-pool/js/packages/classic/ts-fixup.sh +++ /dev/null @@ -1 +0,0 @@ -../../ts-fixup.sh \ No newline at end of file diff --git a/single-pool/js/packages/classic/tsconfig-base.json b/single-pool/js/packages/classic/tsconfig-base.json deleted file mode 120000 index 8cdeff689fc..00000000000 --- a/single-pool/js/packages/classic/tsconfig-base.json +++ /dev/null @@ -1 +0,0 @@ -../../tsconfig-base.json \ No newline at end of file diff --git a/single-pool/js/packages/classic/tsconfig-cjs.json b/single-pool/js/packages/classic/tsconfig-cjs.json deleted file mode 120000 index eb5b6778868..00000000000 --- a/single-pool/js/packages/classic/tsconfig-cjs.json +++ /dev/null @@ -1 +0,0 @@ -../../tsconfig-cjs.json \ No newline at end of file diff --git a/single-pool/js/packages/classic/tsconfig.json b/single-pool/js/packages/classic/tsconfig.json deleted file mode 120000 index fd0e4743dda..00000000000 --- a/single-pool/js/packages/classic/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -../../tsconfig.json \ No newline at end of file diff --git a/single-pool/js/packages/modern/.eslintignore b/single-pool/js/packages/modern/.eslintignore deleted file mode 100644 index 58542507948..00000000000 --- a/single-pool/js/packages/modern/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -dist -node_modules -.vscode -.idea diff --git a/single-pool/js/packages/modern/.eslintrc.cjs b/single-pool/js/packages/modern/.eslintrc.cjs deleted file mode 100644 index 63db7b8405f..00000000000 --- a/single-pool/js/packages/modern/.eslintrc.cjs +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = { - root: true, - env: { - es6: true, - node: true, - jest: true, - }, - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], - plugins: ['@typescript-eslint/eslint-plugin'], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - }, - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - }, -}; diff --git a/single-pool/js/packages/modern/.gitignore b/single-pool/js/packages/modern/.gitignore deleted file mode 100644 index 77738287f0e..00000000000 --- a/single-pool/js/packages/modern/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ \ No newline at end of file diff --git a/single-pool/js/packages/modern/.prettierrc.cjs b/single-pool/js/packages/modern/.prettierrc.cjs deleted file mode 100644 index 8446d684477..00000000000 --- a/single-pool/js/packages/modern/.prettierrc.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - singleQuote: true, - trailingComma: 'all', - printWidth: 100, - endOfLine: 'lf', - semi: true, -}; diff --git a/single-pool/js/packages/modern/README.md b/single-pool/js/packages/modern/README.md deleted file mode 100644 index 2cd1ff2c225..00000000000 --- a/single-pool/js/packages/modern/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# `@solana/spl-single-pool` - -A TypeScript library for interacting with the SPL Single-Validator Stake Pool program, targeting `@solana/web3.js` 2.0. -**If you are working on the legacy web3.js (if you're not sure, you probably are!), you want `@solana/spl-single-pool-classic`.** - -For information on installation and usage, see [SPL docs](https://spl.solana.com/single-pool). - -For support, please ask questions on the [Solana Stack Exchange](https://solana.stackexchange.com). - -If you've found a bug or you'd like to request a feature, please -[open an issue](https://github.com/solana-labs/solana-program-library/issues/new). diff --git a/single-pool/js/packages/modern/package.json b/single-pool/js/packages/modern/package.json deleted file mode 100644 index 2c763c66de9..00000000000 --- a/single-pool/js/packages/modern/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@solana/spl-single-pool", - "version": "1.0.0", - "main": "dist/cjs/index.js", - "module": "dist/mjs/index.js", - "exports": { - ".": { - "import": "./dist/mjs/index.js", - "require": "./dist/cjs/index.js" - } - }, - "scripts": { - "clean": "rm -fr dist/*", - "build": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && ./ts-fixup.sh", - "lint": "eslint --max-warnings 0 .", - "lint:fix": "eslint . --fix" - }, - "devDependencies": { - "@types/node": "^22.10.5", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "eslint": "^8.57.0", - "typescript": "^5.7.2" - }, - "dependencies": { - "@solana/addresses": "2.0.0", - "@solana/instructions": "2.0.0", - "@solana/transaction-messages": "2.0.0" - } -} diff --git a/single-pool/js/packages/modern/src/addresses.ts b/single-pool/js/packages/modern/src/addresses.ts deleted file mode 100644 index 473b7237e3e..00000000000 --- a/single-pool/js/packages/modern/src/addresses.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { - address, - getAddressCodec, - getProgramDerivedAddress, - createAddressWithSeed, - Address, -} from '@solana/addresses'; - -import { MPL_METADATA_PROGRAM_ID } from './internal.js'; -import { STAKE_PROGRAM_ID } from './quarantine.js'; - -export const SINGLE_POOL_PROGRAM_ID = address('SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE'); - -export type VoteAccountAddress = Address & { - readonly __voteAccountAddress: unique symbol; -}; - -export type PoolAddress = Address & { - readonly __poolAddress: unique symbol; -}; - -export type PoolStakeAddress = Address & { - readonly __poolStakeAddress: unique symbol; -}; - -export type PoolMintAddress = Address & { - readonly __poolMintAddress: unique symbol; -}; - -export type PoolStakeAuthorityAddress = Address & { - readonly __poolStakeAuthorityAddress: unique symbol; -}; - -export type PoolMintAuthorityAddress = Address & { - readonly __poolMintAuthorityAddress: unique symbol; -}; - -export type PoolMplAuthorityAddress = Address & { - readonly __poolMplAuthorityAddress: unique symbol; -}; - -export async function findPoolAddress( - programId: Address, - voteAccountAddress: VoteAccountAddress, -): Promise { - return (await findPda(programId, voteAccountAddress, 'pool')) as PoolAddress; -} - -export async function findPoolStakeAddress( - programId: Address, - poolAddress: PoolAddress, -): Promise { - return (await findPda(programId, poolAddress, 'stake')) as PoolStakeAddress; -} - -export async function findPoolMintAddress( - programId: Address, - poolAddress: PoolAddress, -): Promise { - return (await findPda(programId, poolAddress, 'mint')) as PoolMintAddress; -} - -export async function findPoolStakeAuthorityAddress( - programId: Address, - poolAddress: PoolAddress, -): Promise { - return (await findPda(programId, poolAddress, 'stake_authority')) as PoolStakeAuthorityAddress; -} - -export async function findPoolMintAuthorityAddress( - programId: Address, - poolAddress: PoolAddress, -): Promise { - return (await findPda(programId, poolAddress, 'mint_authority')) as PoolMintAuthorityAddress; -} - -export async function findPoolMplAuthorityAddress( - programId: Address, - poolAddress: PoolAddress, -): Promise { - return (await findPda(programId, poolAddress, 'mpl_authority')) as PoolMplAuthorityAddress; -} - -async function findPda(programId: Address, baseAddress: Address, prefix: string) { - const { encode } = getAddressCodec(); - const [pda] = await getProgramDerivedAddress({ - programAddress: programId, - seeds: [prefix, encode(baseAddress)], - }); - - return pda; -} - -export async function findDefaultDepositAccountAddress( - poolAddress: PoolAddress, - userWallet: Address, -) { - return createAddressWithSeed({ - baseAddress: userWallet, - seed: defaultDepositAccountSeed(poolAddress), - programAddress: STAKE_PROGRAM_ID, - }); -} - -export function defaultDepositAccountSeed(poolAddress: PoolAddress): string { - return 'svsp' + poolAddress.slice(0, 28); -} - -export async function findMplMetadataAddress(poolMintAddress: PoolMintAddress) { - const { encode } = getAddressCodec(); - const [pda] = await getProgramDerivedAddress({ - programAddress: MPL_METADATA_PROGRAM_ID, - seeds: ['metadata', encode(MPL_METADATA_PROGRAM_ID), encode(poolMintAddress)], - }); - - return pda; -} diff --git a/single-pool/js/packages/modern/src/index.ts b/single-pool/js/packages/modern/src/index.ts deleted file mode 100644 index f739b8ae9a0..00000000000 --- a/single-pool/js/packages/modern/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { getAddressCodec } from '@solana/addresses'; - -import { PoolAddress, VoteAccountAddress } from './addresses.js'; - -export * from './addresses.js'; -export * from './instructions.js'; -export * from './transactions.js'; - -export async function getVoteAccountAddressForPool( - rpc: any, // XXX not exported: Rpc, - poolAddress: PoolAddress, - abortSignal?: AbortSignal, -): Promise { - const poolAccount = await rpc.getAccountInfo(poolAddress).send(abortSignal); - if (!(poolAccount && poolAccount.data[0] === 1)) { - throw 'invalid pool address'; - } - return getAddressCodec().decode(poolAccount.data.slice(1)) as VoteAccountAddress; -} diff --git a/single-pool/js/packages/modern/src/instructions.ts b/single-pool/js/packages/modern/src/instructions.ts deleted file mode 100644 index 5fcaf618faa..00000000000 --- a/single-pool/js/packages/modern/src/instructions.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { getAddressCodec, Address } from '@solana/addresses'; -import { - ReadonlySignerAccount, - ReadonlyAccount, - IInstructionWithAccounts, - IInstructionWithData, - WritableAccount, - WritableSignerAccount, - IInstruction, - AccountRole, -} from '@solana/instructions'; - -import { - PoolMintAuthorityAddress, - PoolMintAddress, - PoolMplAuthorityAddress, - PoolStakeAuthorityAddress, - PoolStakeAddress, - findMplMetadataAddress, - findPoolMplAuthorityAddress, - findPoolAddress, - VoteAccountAddress, - PoolAddress, - findPoolStakeAddress, - findPoolMintAddress, - findPoolMintAuthorityAddress, - findPoolStakeAuthorityAddress, - SINGLE_POOL_PROGRAM_ID, -} from './addresses.js'; -import { MPL_METADATA_PROGRAM_ID } from './internal.js'; -import { - SYSTEM_PROGRAM_ID, - SYSVAR_RENT_ID, - SYSVAR_CLOCK_ID, - STAKE_PROGRAM_ID, - SYSVAR_STAKE_HISTORY_ID, - STAKE_CONFIG_ID, - TOKEN_PROGRAM_ID, - u32, - u64, -} from './quarantine.js'; - -type InitializePoolInstruction = IInstruction & - IInstructionWithAccounts< - [ - ReadonlyAccount, - WritableAccount, - WritableAccount, - WritableAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ] - > & - IInstructionWithData; - -type ReactivatePoolStakeInstruction = IInstruction & - IInstructionWithAccounts< - [ - ReadonlyAccount, - ReadonlyAccount, - WritableAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ] - > & - IInstructionWithData; - -type DepositStakeInstruction = IInstruction & - IInstructionWithAccounts< - [ - ReadonlyAccount, - WritableAccount, - WritableAccount, - ReadonlyAccount, - ReadonlyAccount, - WritableAccount
, // user stake - WritableAccount
, // user token - WritableAccount
, // user lamport - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ] - > & - IInstructionWithData; - -type WithdrawStakeInstruction = IInstruction & - IInstructionWithAccounts< - [ - ReadonlyAccount, - WritableAccount, - WritableAccount, - ReadonlyAccount, - ReadonlyAccount, - WritableAccount
, // user stake - WritableAccount
, // user token - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ] - > & - IInstructionWithData; - -type CreateTokenMetadataInstruction = IInstruction & - IInstructionWithAccounts< - [ - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - WritableSignerAccount
, // mpl payer - WritableAccount
, // mpl account - ReadonlyAccount, - ReadonlyAccount, - ] - > & - IInstructionWithData; - -type UpdateTokenMetadataInstruction = IInstruction & - IInstructionWithAccounts< - [ - ReadonlyAccount, - ReadonlyAccount, - ReadonlyAccount, - ReadonlySignerAccount
, // authorized withdrawer - WritableAccount
, // mpl account - ReadonlyAccount, - ] - > & - IInstructionWithData; - -const enum SinglePoolInstructionType { - InitializePool = 0, - ReactivatePoolStake, - DepositStake, - WithdrawStake, - CreateTokenMetadata, - UpdateTokenMetadata, -} - -export const SinglePoolInstruction = { - initializePool: initializePoolInstruction, - reactivatePoolStake: reactivatePoolStakeInstruction, - depositStake: depositStakeInstruction, - withdrawStake: withdrawStakeInstruction, - createTokenMetadata: createTokenMetadataInstruction, - updateTokenMetadata: updateTokenMetadataInstruction, -}; - -export async function initializePoolInstruction( - voteAccount: VoteAccountAddress, -): Promise { - const programAddress = SINGLE_POOL_PROGRAM_ID; - const pool = await findPoolAddress(programAddress, voteAccount); - const [stake, mint, stakeAuthority, mintAuthority] = await Promise.all([ - findPoolStakeAddress(programAddress, pool), - findPoolMintAddress(programAddress, pool), - findPoolStakeAuthorityAddress(programAddress, pool), - findPoolMintAuthorityAddress(programAddress, pool), - ]); - - const data = new Uint8Array([SinglePoolInstructionType.InitializePool]); - - return { - data, - accounts: [ - { address: voteAccount, role: AccountRole.READONLY }, - { address: pool, role: AccountRole.WRITABLE }, - { address: stake, role: AccountRole.WRITABLE }, - { address: mint, role: AccountRole.WRITABLE }, - { address: stakeAuthority, role: AccountRole.READONLY }, - { address: mintAuthority, role: AccountRole.READONLY }, - { address: SYSVAR_RENT_ID, role: AccountRole.READONLY }, - { address: SYSVAR_CLOCK_ID, role: AccountRole.READONLY }, - { address: SYSVAR_STAKE_HISTORY_ID, role: AccountRole.READONLY }, - { address: STAKE_CONFIG_ID, role: AccountRole.READONLY }, - { address: SYSTEM_PROGRAM_ID, role: AccountRole.READONLY }, - { address: TOKEN_PROGRAM_ID, role: AccountRole.READONLY }, - { address: STAKE_PROGRAM_ID, role: AccountRole.READONLY }, - ], - programAddress, - }; -} - -export async function reactivatePoolStakeInstruction( - voteAccount: VoteAccountAddress, -): Promise { - const programAddress = SINGLE_POOL_PROGRAM_ID; - const pool = await findPoolAddress(programAddress, voteAccount); - const [stake, stakeAuthority] = await Promise.all([ - findPoolStakeAddress(programAddress, pool), - findPoolStakeAuthorityAddress(programAddress, pool), - ]); - - const data = new Uint8Array([SinglePoolInstructionType.ReactivatePoolStake]); - - return { - data, - accounts: [ - { address: voteAccount, role: AccountRole.READONLY }, - { address: pool, role: AccountRole.READONLY }, - { address: stake, role: AccountRole.WRITABLE }, - { address: stakeAuthority, role: AccountRole.READONLY }, - { address: SYSVAR_CLOCK_ID, role: AccountRole.READONLY }, - { address: SYSVAR_STAKE_HISTORY_ID, role: AccountRole.READONLY }, - { address: STAKE_CONFIG_ID, role: AccountRole.READONLY }, - { address: STAKE_PROGRAM_ID, role: AccountRole.READONLY }, - ], - programAddress, - }; -} - -export async function depositStakeInstruction( - pool: PoolAddress, - userStakeAccount: Address, - userTokenAccount: Address, - userLamportAccount: Address, -): Promise { - const programAddress = SINGLE_POOL_PROGRAM_ID; - const [stake, mint, stakeAuthority, mintAuthority] = await Promise.all([ - findPoolStakeAddress(programAddress, pool), - findPoolMintAddress(programAddress, pool), - findPoolStakeAuthorityAddress(programAddress, pool), - findPoolMintAuthorityAddress(programAddress, pool), - ]); - - const data = new Uint8Array([SinglePoolInstructionType.DepositStake]); - - return { - data, - accounts: [ - { address: pool, role: AccountRole.READONLY }, - { address: stake, role: AccountRole.WRITABLE }, - { address: mint, role: AccountRole.WRITABLE }, - { address: stakeAuthority, role: AccountRole.READONLY }, - { address: mintAuthority, role: AccountRole.READONLY }, - { address: userStakeAccount, role: AccountRole.WRITABLE }, - { address: userTokenAccount, role: AccountRole.WRITABLE }, - { address: userLamportAccount, role: AccountRole.WRITABLE }, - { address: SYSVAR_CLOCK_ID, role: AccountRole.READONLY }, - { address: SYSVAR_STAKE_HISTORY_ID, role: AccountRole.READONLY }, - { address: TOKEN_PROGRAM_ID, role: AccountRole.READONLY }, - { address: STAKE_PROGRAM_ID, role: AccountRole.READONLY }, - ], - programAddress, - }; -} - -export async function withdrawStakeInstruction( - pool: PoolAddress, - userStakeAccount: Address, - userStakeAuthority: Address, - userTokenAccount: Address, - tokenAmount: bigint, -): Promise { - const programAddress = SINGLE_POOL_PROGRAM_ID; - const [stake, mint, stakeAuthority, mintAuthority] = await Promise.all([ - findPoolStakeAddress(programAddress, pool), - findPoolMintAddress(programAddress, pool), - findPoolStakeAuthorityAddress(programAddress, pool), - findPoolMintAuthorityAddress(programAddress, pool), - ]); - - const { encode } = getAddressCodec(); - const data = new Uint8Array([ - SinglePoolInstructionType.WithdrawStake, - ...encode(userStakeAuthority), - ...u64(tokenAmount), - ]); - - return { - data, - accounts: [ - { address: pool, role: AccountRole.READONLY }, - { address: stake, role: AccountRole.WRITABLE }, - { address: mint, role: AccountRole.WRITABLE }, - { address: stakeAuthority, role: AccountRole.READONLY }, - { address: mintAuthority, role: AccountRole.READONLY }, - { address: userStakeAccount, role: AccountRole.WRITABLE }, - { address: userTokenAccount, role: AccountRole.WRITABLE }, - { address: SYSVAR_CLOCK_ID, role: AccountRole.READONLY }, - { address: TOKEN_PROGRAM_ID, role: AccountRole.READONLY }, - { address: STAKE_PROGRAM_ID, role: AccountRole.READONLY }, - ], - programAddress, - }; -} - -export async function createTokenMetadataInstruction( - pool: PoolAddress, - payer: Address, -): Promise { - const programAddress = SINGLE_POOL_PROGRAM_ID; - const mint = await findPoolMintAddress(programAddress, pool); - const [mintAuthority, mplAuthority, mplMetadata] = await Promise.all([ - findPoolMintAuthorityAddress(programAddress, pool), - findPoolMplAuthorityAddress(programAddress, pool), - findMplMetadataAddress(mint), - ]); - - const data = new Uint8Array([SinglePoolInstructionType.CreateTokenMetadata]); - - return { - data, - accounts: [ - { address: pool, role: AccountRole.READONLY }, - { address: mint, role: AccountRole.READONLY }, - { address: mintAuthority, role: AccountRole.READONLY }, - { address: mplAuthority, role: AccountRole.READONLY }, - { address: payer, role: AccountRole.WRITABLE_SIGNER }, - { address: mplMetadata, role: AccountRole.WRITABLE }, - { address: MPL_METADATA_PROGRAM_ID, role: AccountRole.READONLY }, - { address: SYSTEM_PROGRAM_ID, role: AccountRole.READONLY }, - ], - programAddress, - }; -} - -export async function updateTokenMetadataInstruction( - voteAccount: VoteAccountAddress, - authorizedWithdrawer: Address, - tokenName: string, - tokenSymbol: string, - tokenUri?: string, -): Promise { - const programAddress = SINGLE_POOL_PROGRAM_ID; - tokenUri = tokenUri || ''; - - if (tokenName.length > 32) { - throw 'maximum token name length is 32 characters'; - } - - if (tokenSymbol.length > 10) { - throw 'maximum token symbol length is 10 characters'; - } - - if (tokenUri.length > 200) { - throw 'maximum token uri length is 200 characters'; - } - - const pool = await findPoolAddress(programAddress, voteAccount); - const [mint, mplAuthority] = await Promise.all([ - findPoolMintAddress(programAddress, pool), - findPoolMplAuthorityAddress(programAddress, pool), - ]); - const mplMetadata = await findMplMetadataAddress(mint); - - const text = new TextEncoder(); - const data = new Uint8Array([ - SinglePoolInstructionType.UpdateTokenMetadata, - ...u32(tokenName.length), - ...text.encode(tokenName), - ...u32(tokenSymbol.length), - ...text.encode(tokenSymbol), - ...u32(tokenUri.length), - ...text.encode(tokenUri), - ]); - - return { - data, - accounts: [ - { address: voteAccount, role: AccountRole.READONLY }, - { address: pool, role: AccountRole.READONLY }, - { address: mplAuthority, role: AccountRole.READONLY }, - { address: authorizedWithdrawer, role: AccountRole.READONLY_SIGNER }, - { address: mplMetadata, role: AccountRole.WRITABLE }, - { address: MPL_METADATA_PROGRAM_ID, role: AccountRole.READONLY }, - ], - programAddress, - }; -} diff --git a/single-pool/js/packages/modern/src/internal.ts b/single-pool/js/packages/modern/src/internal.ts deleted file mode 100644 index 6ade8ed221b..00000000000 --- a/single-pool/js/packages/modern/src/internal.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { address } from '@solana/addresses'; - -export const MPL_METADATA_PROGRAM_ID = address('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'); diff --git a/single-pool/js/packages/modern/src/quarantine.ts b/single-pool/js/packages/modern/src/quarantine.ts deleted file mode 100644 index b3f93a74818..00000000000 --- a/single-pool/js/packages/modern/src/quarantine.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { address, getAddressCodec, getProgramDerivedAddress, Address } from '@solana/addresses'; -import { AccountRole } from '@solana/instructions'; - -// HERE BE DRAGONS -// this is all the stuff that shouldn't be in our library once we can import from elsewhere - -export const SYSTEM_PROGRAM_ID = address('11111111111111111111111111111111'); -export const STAKE_PROGRAM_ID = address('Stake11111111111111111111111111111111111111'); -export const SYSVAR_RENT_ID = address('SysvarRent111111111111111111111111111111111'); -export const SYSVAR_CLOCK_ID = address('SysvarC1ock11111111111111111111111111111111'); -export const SYSVAR_STAKE_HISTORY_ID = address('SysvarStakeHistory1111111111111111111111111'); -export const STAKE_CONFIG_ID = address('StakeConfig11111111111111111111111111111111'); -export const STAKE_ACCOUNT_SIZE = 200n; - -export const TOKEN_PROGRAM_ID = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); -export const ATOKEN_PROGRAM_ID = address('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); -export const MINT_SIZE = 82n; - -export function u32(n: number): Uint8Array { - const bns = Uint32Array.from([n]); - return new Uint8Array(bns.buffer); -} - -export function u64(n: bigint): Uint8Array { - const bns = BigUint64Array.from([n]); - return new Uint8Array(bns.buffer); -} - -export class SystemInstruction { - static createAccount(params: { - from: Address; - newAccount: Address; - lamports: bigint; - space: bigint; - programAddress: Address; - }) { - const { encode } = getAddressCodec(); - const data = new Uint8Array([ - ...u32(0), - ...u64(params.lamports), - ...u64(params.space), - ...encode(params.programAddress), - ]); - - const accounts = [ - { address: params.from, role: AccountRole.WRITABLE_SIGNER }, - { address: params.newAccount, role: AccountRole.WRITABLE_SIGNER }, - ]; - - return { - data, - accounts, - programAddress: SYSTEM_PROGRAM_ID, - }; - } - - static transfer(params: { from: Address; to: Address; lamports: bigint }) { - const data = new Uint8Array([...u32(2), ...u64(params.lamports)]); - - const accounts = [ - { address: params.from, role: AccountRole.WRITABLE_SIGNER }, - { address: params.to, role: AccountRole.WRITABLE }, - ]; - - return { - data, - accounts, - programAddress: SYSTEM_PROGRAM_ID, - }; - } - - static createAccountWithSeed(params: { - from: Address; - newAccount: Address; - base: Address; - seed: string; - lamports: bigint; - space: bigint; - programAddress: Address; - }) { - const { encode } = getAddressCodec(); - const data = new Uint8Array([ - ...u32(3), - ...encode(params.base), - ...u64(BigInt(params.seed.length)), - ...new TextEncoder().encode(params.seed), - ...u64(params.lamports), - ...u64(params.space), - ...encode(params.programAddress), - ]); - - const accounts = [ - { address: params.from, role: AccountRole.WRITABLE_SIGNER }, - { address: params.newAccount, role: AccountRole.WRITABLE }, - ]; - if (params.base != params.from) { - accounts.push({ address: params.base, role: AccountRole.READONLY_SIGNER }); - } - - return { - data, - accounts, - programAddress: SYSTEM_PROGRAM_ID, - }; - } -} - -export class TokenInstruction { - static approve(params: { account: Address; delegate: Address; owner: Address; amount: bigint }) { - const data = new Uint8Array([...u32(4), ...u64(params.amount)]); - - const accounts = [ - { address: params.account, role: AccountRole.WRITABLE }, - { address: params.delegate, role: AccountRole.READONLY }, - { address: params.owner, role: AccountRole.READONLY_SIGNER }, - ]; - - return { - data, - accounts, - programAddress: TOKEN_PROGRAM_ID, - }; - } - - static createAssociatedTokenAccount(params: { - payer: Address; - associatedAccount: Address; - owner: Address; - mint: Address; - }) { - const data = new Uint8Array([0]); - - const accounts = [ - { address: params.payer, role: AccountRole.WRITABLE_SIGNER }, - { address: params.associatedAccount, role: AccountRole.WRITABLE }, - { address: params.owner, role: AccountRole.READONLY }, - { address: params.mint, role: AccountRole.READONLY }, - { address: SYSTEM_PROGRAM_ID, role: AccountRole.READONLY }, - { address: TOKEN_PROGRAM_ID, role: AccountRole.READONLY }, - ]; - - return { - data, - accounts, - programAddress: ATOKEN_PROGRAM_ID, - }; - } -} - -export enum StakeAuthorizationType { - Staker, - Withdrawer, -} - -export class StakeInstruction { - // idc about doing it right unless this goes in a lib - static initialize(params: { stakeAccount: Address; staker: Address; withdrawer: Address }) { - const { encode } = getAddressCodec(); - const data = new Uint8Array([ - ...u32(0), - ...encode(params.staker), - ...encode(params.withdrawer), - ...Array(48).fill(0), - ]); - - const accounts = [ - { address: params.stakeAccount, role: AccountRole.WRITABLE }, - { address: SYSVAR_RENT_ID, role: AccountRole.READONLY }, - ]; - - return { - data, - accounts, - programAddress: STAKE_PROGRAM_ID, - }; - } - - static authorize(params: { - stakeAccount: Address; - authorized: Address; - newAuthorized: Address; - authorizationType: StakeAuthorizationType; - custodian?: Address; - }) { - const { encode } = getAddressCodec(); - const data = new Uint8Array([ - ...u32(1), - ...encode(params.newAuthorized), - ...u32(params.authorizationType), - ]); - - const accounts = [ - { address: params.stakeAccount, role: AccountRole.WRITABLE }, - { address: SYSVAR_CLOCK_ID, role: AccountRole.READONLY }, - { address: params.authorized, role: AccountRole.READONLY_SIGNER }, - ]; - if (params.custodian) { - accounts.push({ address: params.custodian, role: AccountRole.READONLY }); - } - - return { - data, - accounts, - programAddress: STAKE_PROGRAM_ID, - }; - } - - static delegate(params: { stakeAccount: Address; authorized: Address; voteAccount: Address }) { - const data = new Uint8Array(u32(2)); - - const accounts = [ - { address: params.stakeAccount, role: AccountRole.WRITABLE }, - { address: params.voteAccount, role: AccountRole.READONLY }, - { address: SYSVAR_CLOCK_ID, role: AccountRole.READONLY }, - { address: SYSVAR_STAKE_HISTORY_ID, role: AccountRole.READONLY }, - { address: STAKE_CONFIG_ID, role: AccountRole.READONLY }, - { address: params.authorized, role: AccountRole.READONLY_SIGNER }, - ]; - - return { - data, - accounts, - programAddress: STAKE_PROGRAM_ID, - }; - } -} - -export async function getAssociatedTokenAddress(mint: Address, owner: Address) { - const { encode } = getAddressCodec(); - const [pda] = await getProgramDerivedAddress({ - programAddress: ATOKEN_PROGRAM_ID, - seeds: [encode(owner), encode(TOKEN_PROGRAM_ID), encode(mint)], - }); - - return pda; -} diff --git a/single-pool/js/packages/modern/src/transactions.ts b/single-pool/js/packages/modern/src/transactions.ts deleted file mode 100644 index 7ad7b2579f6..00000000000 --- a/single-pool/js/packages/modern/src/transactions.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { Address } from '@solana/addresses'; -import { - appendTransactionMessageInstruction, - createTransactionMessage, - TransactionVersion, - TransactionMessage, -} from '@solana/transaction-messages'; - -import { - findPoolAddress, - VoteAccountAddress, - PoolAddress, - findPoolStakeAddress, - findPoolMintAddress, - defaultDepositAccountSeed, - findDefaultDepositAccountAddress, - findPoolMintAuthorityAddress, - findPoolStakeAuthorityAddress, - SINGLE_POOL_PROGRAM_ID, -} from './addresses.js'; -import { - initializePoolInstruction, - reactivatePoolStakeInstruction, - depositStakeInstruction, - withdrawStakeInstruction, - createTokenMetadataInstruction, - updateTokenMetadataInstruction, -} from './instructions.js'; -import { - STAKE_PROGRAM_ID, - STAKE_ACCOUNT_SIZE, - MINT_SIZE, - StakeInstruction, - SystemInstruction, - TokenInstruction, - StakeAuthorizationType, - getAssociatedTokenAddress, -} from './quarantine.js'; - -interface DepositParams { - rpc: any; // XXX Rpc - pool: PoolAddress; - userWallet: Address; - userStakeAccount?: Address; - depositFromDefaultAccount?: boolean; - userTokenAccount?: Address; - userLamportAccount?: Address; - userWithdrawAuthority?: Address; -} - -interface WithdrawParams { - rpc: any; // XXX Rpc - pool: PoolAddress; - userWallet: Address; - userStakeAccount: Address; - tokenAmount: bigint; - createStakeAccount?: boolean; - userStakeAuthority?: Address; - userTokenAccount?: Address; - userTokenAuthority?: Address; -} - -export const SINGLE_POOL_ACCOUNT_SIZE = 33n; - -export const SinglePoolProgram = { - programAddress: SINGLE_POOL_PROGRAM_ID, - space: SINGLE_POOL_ACCOUNT_SIZE, - initialize: initializeTransaction, - reactivatePoolStake: reactivatePoolStakeTransaction, - deposit: depositTransaction, - withdraw: withdrawTransaction, - createTokenMetadata: createTokenMetadataTransaction, - updateTokenMetadata: updateTokenMetadataTransaction, - createAndDelegateUserStake: createAndDelegateUserStakeTransaction, -}; - -export async function initializeTransaction( - rpc: any, // XXX not exported: Rpc, - voteAccount: VoteAccountAddress, - payer: Address, - skipMetadata = false, -): Promise { - let transaction = createTransactionMessage({ version: 0 }); - - const pool = await findPoolAddress(SINGLE_POOL_PROGRAM_ID, voteAccount); - const [stake, mint, poolRent, stakeRent, mintRent, minimumDelegationObj] = await Promise.all([ - findPoolStakeAddress(SINGLE_POOL_PROGRAM_ID, pool), - findPoolMintAddress(SINGLE_POOL_PROGRAM_ID, pool), - rpc.getMinimumBalanceForRentExemption(SINGLE_POOL_ACCOUNT_SIZE).send(), - rpc.getMinimumBalanceForRentExemption(STAKE_ACCOUNT_SIZE).send(), - rpc.getMinimumBalanceForRentExemption(MINT_SIZE).send(), - rpc.getStakeMinimumDelegation().send(), - ]); - const minimumDelegation = minimumDelegationObj.value; - - transaction = appendTransactionMessageInstruction( - SystemInstruction.transfer({ - from: payer, - to: pool, - lamports: poolRent, - }), - transaction, - ); - - transaction = appendTransactionMessageInstruction( - SystemInstruction.transfer({ - from: payer, - to: stake, - lamports: stakeRent + minimumDelegation, - }), - transaction, - ); - - transaction = appendTransactionMessageInstruction( - SystemInstruction.transfer({ - from: payer, - to: mint, - lamports: mintRent, - }), - transaction, - ); - - transaction = appendTransactionMessageInstruction( - await initializePoolInstruction(voteAccount), - transaction, - ); - - if (!skipMetadata) { - transaction = appendTransactionMessageInstruction( - await createTokenMetadataInstruction(pool, payer), - transaction, - ); - } - - return transaction; -} - -export async function reactivatePoolStakeTransaction( - voteAccount: VoteAccountAddress, -): Promise { - let transaction = { instructions: [] as any, version: 'legacy' as TransactionVersion }; - transaction = appendTransactionMessageInstruction( - await reactivatePoolStakeInstruction(voteAccount), - transaction, - ); - - return transaction; -} - -export async function depositTransaction(params: DepositParams) { - const { rpc, pool, userWallet } = params; - - // note this is just xnor - if (!params.userStakeAccount == !params.depositFromDefaultAccount) { - throw 'must either provide userStakeAccount or true depositFromDefaultAccount'; - } - - const userStakeAccount = ( - params.depositFromDefaultAccount - ? await findDefaultDepositAccountAddress(pool, userWallet) - : params.userStakeAccount - ) as Address; - - let transaction = { instructions: [] as any, version: 'legacy' as TransactionVersion }; - - const [mint, poolStakeAuthority] = await Promise.all([ - findPoolMintAddress(SINGLE_POOL_PROGRAM_ID, pool), - findPoolStakeAuthorityAddress(SINGLE_POOL_PROGRAM_ID, pool), - ]); - - const userAssociatedTokenAccount = await getAssociatedTokenAddress(mint, userWallet); - const userTokenAccount = params.userTokenAccount || userAssociatedTokenAccount; - const userLamportAccount = params.userLamportAccount || userWallet; - const userWithdrawAuthority = params.userWithdrawAuthority || userWallet; - - if ( - userTokenAccount == userAssociatedTokenAccount && - (await rpc.getAccountInfo(userAssociatedTokenAccount).send()) == null - ) { - transaction = appendTransactionMessageInstruction( - TokenInstruction.createAssociatedTokenAccount({ - payer: userWallet, - associatedAccount: userAssociatedTokenAccount, - owner: userWallet, - mint, - }), - transaction, - ); - } - - transaction = appendTransactionMessageInstruction( - StakeInstruction.authorize({ - stakeAccount: userStakeAccount, - authorized: userWithdrawAuthority, - newAuthorized: poolStakeAuthority, - authorizationType: StakeAuthorizationType.Staker, - }), - transaction, - ); - - transaction = appendTransactionMessageInstruction( - StakeInstruction.authorize({ - stakeAccount: userStakeAccount, - authorized: userWithdrawAuthority, - newAuthorized: poolStakeAuthority, - authorizationType: StakeAuthorizationType.Withdrawer, - }), - transaction, - ); - - transaction = appendTransactionMessageInstruction( - await depositStakeInstruction(pool, userStakeAccount, userTokenAccount, userLamportAccount), - transaction, - ); - - return transaction; -} - -export async function withdrawTransaction(params: WithdrawParams) { - const { rpc, pool, userWallet, userStakeAccount, tokenAmount, createStakeAccount } = params; - - let transaction = { instructions: [] as any, version: 'legacy' as TransactionVersion }; - - const poolMintAuthority = await findPoolMintAuthorityAddress(SINGLE_POOL_PROGRAM_ID, pool); - - const userStakeAuthority = params.userStakeAuthority || userWallet; - const userTokenAccount = - params.userTokenAccount || - (await getAssociatedTokenAddress( - await findPoolMintAddress(SINGLE_POOL_PROGRAM_ID, pool), - userWallet, - )); - const userTokenAuthority = params.userTokenAuthority || userWallet; - - if (createStakeAccount) { - transaction = appendTransactionMessageInstruction( - SystemInstruction.createAccount({ - from: userWallet, - lamports: await rpc.getMinimumBalanceForRentExemption(STAKE_ACCOUNT_SIZE).send(), - newAccount: userStakeAccount, - programAddress: STAKE_PROGRAM_ID, - space: STAKE_ACCOUNT_SIZE, - }), - transaction, - ); - } - - transaction = appendTransactionMessageInstruction( - TokenInstruction.approve({ - account: userTokenAccount, - delegate: poolMintAuthority, - owner: userTokenAuthority, - amount: tokenAmount, - }), - transaction, - ); - - transaction = appendTransactionMessageInstruction( - await withdrawStakeInstruction( - pool, - userStakeAccount, - userStakeAuthority, - userTokenAccount, - tokenAmount, - ), - transaction, - ); - - return transaction; -} - -export async function createTokenMetadataTransaction( - pool: PoolAddress, - payer: Address, -): Promise { - let transaction = { instructions: [] as any, version: 'legacy' as TransactionVersion }; - transaction = appendTransactionMessageInstruction( - await createTokenMetadataInstruction(pool, payer), - transaction, - ); - - return transaction; -} - -export async function updateTokenMetadataTransaction( - voteAccount: VoteAccountAddress, - authorizedWithdrawer: Address, - name: string, - symbol: string, - uri?: string, -): Promise { - let transaction = { instructions: [] as any, version: 'legacy' as TransactionVersion }; - transaction = appendTransactionMessageInstruction( - await updateTokenMetadataInstruction(voteAccount, authorizedWithdrawer, name, symbol, uri), - transaction, - ); - - return transaction; -} - -export async function createAndDelegateUserStakeTransaction( - rpc: any, // XXX not exported: Rpc, - voteAccount: VoteAccountAddress, - userWallet: Address, - stakeAmount: bigint, -): Promise { - let transaction = { instructions: [] as any, version: 'legacy' as TransactionVersion }; - - const pool = await findPoolAddress(SINGLE_POOL_PROGRAM_ID, voteAccount); - const [stakeAccount, stakeRent] = await Promise.all([ - findDefaultDepositAccountAddress(pool, userWallet), - await rpc.getMinimumBalanceForRentExemption(STAKE_ACCOUNT_SIZE).send(), - ]); - - transaction = appendTransactionMessageInstruction( - SystemInstruction.createAccountWithSeed({ - base: userWallet, - from: userWallet, - lamports: stakeAmount + stakeRent, - newAccount: stakeAccount, - programAddress: STAKE_PROGRAM_ID, - seed: defaultDepositAccountSeed(pool), - space: STAKE_ACCOUNT_SIZE, - }), - transaction, - ); - - transaction = appendTransactionMessageInstruction( - StakeInstruction.initialize({ - stakeAccount, - staker: userWallet, - withdrawer: userWallet, - }), - transaction, - ); - - transaction = appendTransactionMessageInstruction( - StakeInstruction.delegate({ - stakeAccount, - authorized: userWallet, - voteAccount, - }), - transaction, - ); - - return transaction; -} diff --git a/single-pool/js/packages/modern/ts-fixup.sh b/single-pool/js/packages/modern/ts-fixup.sh deleted file mode 120000 index 2b79c262866..00000000000 --- a/single-pool/js/packages/modern/ts-fixup.sh +++ /dev/null @@ -1 +0,0 @@ -../../ts-fixup.sh \ No newline at end of file diff --git a/single-pool/js/packages/modern/tsconfig-base.json b/single-pool/js/packages/modern/tsconfig-base.json deleted file mode 120000 index 8cdeff689fc..00000000000 --- a/single-pool/js/packages/modern/tsconfig-base.json +++ /dev/null @@ -1 +0,0 @@ -../../tsconfig-base.json \ No newline at end of file diff --git a/single-pool/js/packages/modern/tsconfig-cjs.json b/single-pool/js/packages/modern/tsconfig-cjs.json deleted file mode 120000 index eb5b6778868..00000000000 --- a/single-pool/js/packages/modern/tsconfig-cjs.json +++ /dev/null @@ -1 +0,0 @@ -../../tsconfig-cjs.json \ No newline at end of file diff --git a/single-pool/js/packages/modern/tsconfig.json b/single-pool/js/packages/modern/tsconfig.json deleted file mode 120000 index fd0e4743dda..00000000000 --- a/single-pool/js/packages/modern/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -../../tsconfig.json \ No newline at end of file diff --git a/single-pool/js/ts-fixup.sh b/single-pool/js/ts-fixup.sh deleted file mode 100755 index 39177d08657..00000000000 --- a/single-pool/js/ts-fixup.sh +++ /dev/null @@ -1,11 +0,0 @@ -cat >dist/cjs/package.json <dist/mjs/package.json <"] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -arrayref = "0.3.9" -borsh = "1.5.3" -num-derive = "0.4" -num-traits = "0.2" -num_enum = "0.7.3" -solana-program = "2.1.0" -solana-security-txt = "1.1.1" -spl-token = { version = "7.0", path = "../../token/program", features = [ - "no-entrypoint", -] } -thiserror = "2.0" - -[dev-dependencies] -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" -solana-vote-program = "2.1.0" -spl-associated-token-account = { version = "6.0.0", path = "../../associated-token-account/program", features = [ - "no-entrypoint", -] } -spl-associated-token-account-client = { version = "2.0.0", path = "../../associated-token-account/client" } -test-case = "3.3" -bincode = "1.3.1" -rand = "0.8.5" -approx = "0.5.1" - -[lib] -crate-type = ["cdylib", "lib"] - -[lints] -workspace = true diff --git a/single-pool/program/program-id.md b/single-pool/program/program-id.md deleted file mode 100644 index e873b000f15..00000000000 --- a/single-pool/program/program-id.md +++ /dev/null @@ -1 +0,0 @@ -SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE diff --git a/single-pool/program/src/entrypoint.rs b/single-pool/program/src/entrypoint.rs deleted file mode 100644 index 07d7ae60f23..00000000000 --- a/single-pool/program/src/entrypoint.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Program entrypoint - -#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] - -use { - crate::{error::SinglePoolError, processor::Processor}, - solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError, - pubkey::Pubkey, - }, - solana_security_txt::security_txt, -}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - if let Err(error) = Processor::process(program_id, accounts, instruction_data) { - // catch the error so we can print it - error.print::(); - Err(error) - } else { - Ok(()) - } -} - -security_txt! { - // Required fields - name: "SPL Single-Validator Stake Pool", - project_url: "https://spl.solana.com/single-pool", - contacts: "link:https://github.com/solana-labs/solana-program-library/security/advisories/new,mailto:security@solana.com,discord:https://discord.gg/solana", - policy: "https://github.com/solana-labs/solana-program-library/blob/master/SECURITY.md", - - // Optional Fields - preferred_languages: "en", - source_code: "https://github.com/solana-labs/solana-program-library/tree/master/single-pool/program", - source_revision: "ef44df985e76a697ee9a8aabb3a223610e4cf1dc", - source_release: "single-pool-v1.0.0", - auditors: "https://github.com/solana-labs/security-audits#single-stake-pool" -} diff --git a/single-pool/program/src/error.rs b/single-pool/program/src/error.rs deleted file mode 100644 index 087768492da..00000000000 --- a/single-pool/program/src/error.rs +++ /dev/null @@ -1,157 +0,0 @@ -//! Error types - -use { - solana_program::{ - decode_error::DecodeError, - msg, - program_error::{PrintProgramError, ProgramError}, - }, - thiserror::Error, -}; - -/// Errors that may be returned by the SinglePool program. -#[derive(Clone, Debug, Eq, Error, num_derive::FromPrimitive, PartialEq)] -pub enum SinglePoolError { - // 0. - /// Provided pool account has the wrong address for its vote account, is - /// uninitialized, or otherwise invalid. - #[error("InvalidPoolAccount")] - InvalidPoolAccount, - /// Provided pool stake account does not match address derived from the pool - /// account. - #[error("InvalidPoolStakeAccount")] - InvalidPoolStakeAccount, - /// Provided pool mint does not match address derived from the pool account. - #[error("InvalidPoolMint")] - InvalidPoolMint, - /// Provided pool stake authority does not match address derived from the - /// pool account. - #[error("InvalidPoolStakeAuthority")] - InvalidPoolStakeAuthority, - /// Provided pool mint authority does not match address derived from the - /// pool account. - #[error("InvalidPoolMintAuthority")] - InvalidPoolMintAuthority, - - // 5. - /// Provided pool MPL authority does not match address derived from the pool - /// account. - #[error("InvalidPoolMplAuthority")] - InvalidPoolMplAuthority, - /// Provided metadata account does not match metadata account derived for - /// pool mint. - #[error("InvalidMetadataAccount")] - InvalidMetadataAccount, - /// Authorized withdrawer provided for metadata update does not match the - /// vote account. - #[error("InvalidMetadataSigner")] - InvalidMetadataSigner, - /// Not enough lamports provided for deposit to result in one pool token. - #[error("DepositTooSmall")] - DepositTooSmall, - /// Not enough pool tokens provided to withdraw stake worth one lamport. - #[error("WithdrawalTooSmall")] - WithdrawalTooSmall, - - // 10 - /// Not enough stake to cover the provided quantity of pool tokens. - /// (Generally this should not happen absent user error, but may if the - /// minimum delegation increases.) - #[error("WithdrawalTooLarge")] - WithdrawalTooLarge, - /// Required signature is missing. - #[error("SignatureMissing")] - SignatureMissing, - /// Stake account is not in the state expected by the program. - #[error("WrongStakeStake")] - WrongStakeStake, - /// Unsigned subtraction crossed the zero. - #[error("ArithmeticOverflow")] - ArithmeticOverflow, - /// A calculation failed unexpectedly. - /// (This error should never be surfaced; it stands in for failure - /// conditions that should never be reached.) - #[error("UnexpectedMathError")] - UnexpectedMathError, - - // 15 - /// The V0_23_5 vote account type is unsupported and should be upgraded via - /// `convert_to_current()`. - #[error("LegacyVoteAccount")] - LegacyVoteAccount, - /// Failed to parse vote account. - #[error("UnparseableVoteAccount")] - UnparseableVoteAccount, - /// Incorrect number of lamports provided for rent-exemption when - /// initializing. - #[error("WrongRentAmount")] - WrongRentAmount, - /// Attempted to deposit from or withdraw to pool stake account. - #[error("InvalidPoolStakeAccountUsage")] - InvalidPoolStakeAccountUsage, - /// Attempted to initialize a pool that is already initialized. - #[error("PoolAlreadyInitialized")] - PoolAlreadyInitialized, -} -impl From for ProgramError { - fn from(e: SinglePoolError) -> Self { - ProgramError::Custom(e as u32) - } -} -impl DecodeError for SinglePoolError { - fn type_of() -> &'static str { - "Single-Validator Stake Pool Error" - } -} -impl PrintProgramError for SinglePoolError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - SinglePoolError::InvalidPoolAccount => - msg!("Error: Provided pool account has the wrong address for its vote account, is uninitialized, \ - or is otherwise invalid."), - SinglePoolError::InvalidPoolStakeAccount => - msg!("Error: Provided pool stake account does not match address derived from the pool account."), - SinglePoolError::InvalidPoolMint => - msg!("Error: Provided pool mint does not match address derived from the pool account."), - SinglePoolError::InvalidPoolStakeAuthority => - msg!("Error: Provided pool stake authority does not match address derived from the pool account."), - SinglePoolError::InvalidPoolMintAuthority => - msg!("Error: Provided pool mint authority does not match address derived from the pool account."), - SinglePoolError::InvalidPoolMplAuthority => - msg!("Error: Provided pool MPL authority does not match address derived from the pool account."), - SinglePoolError::InvalidMetadataAccount => - msg!("Error: Provided metadata account does not match metadata account derived for pool mint."), - SinglePoolError::InvalidMetadataSigner => - msg!("Error: Authorized withdrawer provided for metadata update does not match the vote account."), - SinglePoolError::DepositTooSmall => - msg!("Error: Not enough lamports provided for deposit to result in one pool token."), - SinglePoolError::WithdrawalTooSmall => - msg!("Error: Not enough pool tokens provided to withdraw stake worth one lamport."), - SinglePoolError::WithdrawalTooLarge => - msg!("Error: Not enough stake to cover the provided quantity of pool tokens. \ - (Generally this should not happen absent user error, but may if the minimum delegation increases.)"), - SinglePoolError::SignatureMissing => msg!("Error: Required signature is missing."), - SinglePoolError::WrongStakeStake => msg!("Error: Stake account is not in the state expected by the program."), - SinglePoolError::ArithmeticOverflow => msg!("Error: Unsigned subtraction crossed the zero."), - SinglePoolError::UnexpectedMathError => - msg!("Error: A calculation failed unexpectedly. \ - (This error should never be surfaced; it stands in for failure conditions that should never be reached.)"), - SinglePoolError::UnparseableVoteAccount => msg!("Error: Failed to parse vote account."), - SinglePoolError::LegacyVoteAccount => - msg!("Error: The V0_23_5 vote account type is unsupported and should be upgraded via `convert_to_current()`."), - SinglePoolError::WrongRentAmount => - msg!("Error: Incorrect number of lamports provided for rent-exemption when initializing."), - SinglePoolError::InvalidPoolStakeAccountUsage => - msg!("Error: Attempted to deposit from or withdraw to pool stake account."), - SinglePoolError::PoolAlreadyInitialized => - msg!("Error: Attempted to initialize a pool that is already initialized."), - } - } -} diff --git a/single-pool/program/src/inline_mpl_token_metadata.rs b/single-pool/program/src/inline_mpl_token_metadata.rs deleted file mode 120000 index 144225f4bfc..00000000000 --- a/single-pool/program/src/inline_mpl_token_metadata.rs +++ /dev/null @@ -1 +0,0 @@ -../../../stake-pool/program/src/inline_mpl_token_metadata.rs \ No newline at end of file diff --git a/single-pool/program/src/instruction.rs b/single-pool/program/src/instruction.rs deleted file mode 100644 index fda54691a3c..00000000000 --- a/single-pool/program/src/instruction.rs +++ /dev/null @@ -1,473 +0,0 @@ -//! Instruction types - -#![allow(clippy::too_many_arguments)] - -use { - crate::{ - find_default_deposit_account_address_and_seed, find_pool_address, find_pool_mint_address, - find_pool_mint_authority_address, find_pool_mpl_authority_address, find_pool_stake_address, - find_pool_stake_authority_address, - inline_mpl_token_metadata::{self, pda::find_metadata_account}, - state::SinglePool, - }, - borsh::{BorshDeserialize, BorshSerialize}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_pack::Pack, - pubkey::Pubkey, - rent::Rent, - stake, system_instruction, system_program, sysvar, - }, -}; - -/// Instructions supported by the SinglePool program. -#[repr(C)] -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)] -pub enum SinglePoolInstruction { - /// Initialize the mint and stake account for a new single-validator - /// stake pool. The pool stake account must contain the rent-exempt - /// minimum plus the minimum delegation. No tokens will be minted: to - /// deposit more, use `Deposit` after `InitializeStake`. - /// - /// 0. `[]` Validator vote account - /// 1. `[w]` Pool account - /// 2. `[w]` Pool stake account - /// 3. `[w]` Pool token mint - /// 4. `[]` Pool stake authority - /// 5. `[]` Pool mint authority - /// 6. `[]` Rent sysvar - /// 7. `[]` Clock sysvar - /// 8. `[]` Stake history sysvar - /// 9. `[]` Stake config sysvar - /// 10. `[]` System program - /// 11. `[]` Token program - /// 12. `[]` Stake program - InitializePool, - - /// Restake the pool stake account if it was deactivated. This can - /// happen through the stake program's `DeactivateDelinquent` - /// instruction, or during a cluster restart. - /// - /// 0. `[]` Validator vote account - /// 1. `[]` Pool account - /// 2. `[w]` Pool stake account - /// 3. `[]` Pool stake authority - /// 4. `[]` Clock sysvar - /// 5. `[]` Stake history sysvar - /// 6. `[]` Stake config sysvar - /// 7. `[]` Stake program - ReactivatePoolStake, - - /// Deposit stake into the pool. The output is a "pool" token - /// representing fractional ownership of the pool stake. Inputs are - /// converted to the current ratio. - /// - /// 0. `[]` Pool account - /// 1. `[w]` Pool stake account - /// 2. `[w]` Pool token mint - /// 3. `[]` Pool stake authority - /// 4. `[]` Pool mint authority - /// 5. `[w]` User stake account to join to the pool - /// 6. `[w]` User account to receive pool tokens - /// 7. `[w]` User account to receive lamports - /// 8. `[]` Clock sysvar - /// 9. `[]` Stake history sysvar - /// 10. `[]` Token program - /// 11. `[]` Stake program - DepositStake, - - /// Redeem tokens issued by this pool for stake at the current ratio. - /// - /// 0. `[]` Pool account - /// 1. `[w]` Pool stake account - /// 2. `[w]` Pool token mint - /// 3. `[]` Pool stake authority - /// 4. `[]` Pool mint authority - /// 5. `[w]` User stake account to receive stake at - /// 6. `[w]` User account to take pool tokens from - /// 7. `[]` Clock sysvar - /// 8. `[]` Token program - /// 9. `[]` Stake program - WithdrawStake { - /// User authority for the new stake account - user_stake_authority: Pubkey, - /// Amount of tokens to redeem for stake - token_amount: u64, - }, - - /// Create token metadata for the stake-pool token in the metaplex-token - /// program. Step three of the permissionless three-stage initialization - /// flow. - /// Note this instruction is not necessary for the pool to operate, to - /// ensure we cannot be broken by upstream. - /// - /// 0. `[]` Pool account - /// 1. `[]` Pool token mint - /// 2. `[]` Pool mint authority - /// 3. `[]` Pool MPL authority - /// 4. `[s, w]` Payer for creation of token metadata account - /// 5. `[w]` Token metadata account - /// 6. `[]` Metadata program id - /// 7. `[]` System program id - CreateTokenMetadata, - - /// Update token metadata for the stake-pool token in the metaplex-token - /// program. - /// - /// 0. `[]` Validator vote account - /// 1. `[]` Pool account - /// 2. `[]` Pool MPL authority - /// 3. `[s]` Vote account authorized withdrawer - /// 4. `[w]` Token metadata account - /// 5. `[]` Metadata program id - UpdateTokenMetadata { - /// Token name - name: String, - /// Token symbol e.g. stkSOL - symbol: String, - /// URI of the uploaded metadata of the spl-token - uri: String, - }, -} - -/// Creates all necessary instructions to initialize the stake pool. -pub fn initialize( - program_id: &Pubkey, - vote_account_address: &Pubkey, - payer: &Pubkey, - rent: &Rent, - minimum_delegation: u64, -) -> Vec { - let pool_address = find_pool_address(program_id, vote_account_address); - let pool_rent = rent.minimum_balance(std::mem::size_of::()); - - let stake_address = find_pool_stake_address(program_id, &pool_address); - let stake_space = std::mem::size_of::(); - let stake_rent_plus_minimum = rent - .minimum_balance(stake_space) - .saturating_add(minimum_delegation); - - let mint_address = find_pool_mint_address(program_id, &pool_address); - let mint_rent = rent.minimum_balance(spl_token::state::Mint::LEN); - - vec![ - system_instruction::transfer(payer, &pool_address, pool_rent), - system_instruction::transfer(payer, &stake_address, stake_rent_plus_minimum), - system_instruction::transfer(payer, &mint_address, mint_rent), - initialize_pool(program_id, vote_account_address), - create_token_metadata(program_id, &pool_address, payer), - ] -} - -/// Creates an `InitializePool` instruction. -pub fn initialize_pool(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction { - let pool_address = find_pool_address(program_id, vote_account_address); - let mint_address = find_pool_mint_address(program_id, &pool_address); - - let data = borsh::to_vec(&SinglePoolInstruction::InitializePool).unwrap(); - let accounts = vec![ - AccountMeta::new_readonly(*vote_account_address, false), - AccountMeta::new(pool_address, false), - AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false), - AccountMeta::new(mint_address, false), - AccountMeta::new_readonly( - find_pool_stake_authority_address(program_id, &pool_address), - false, - ), - AccountMeta::new_readonly( - find_pool_mint_authority_address(program_id, &pool_address), - false, - ), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - #[allow(deprecated)] - AccountMeta::new_readonly(stake::config::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -/// Creates a `ReactivatePoolStake` instruction. -pub fn reactivate_pool_stake(program_id: &Pubkey, vote_account_address: &Pubkey) -> Instruction { - let pool_address = find_pool_address(program_id, vote_account_address); - - let data = borsh::to_vec(&SinglePoolInstruction::ReactivatePoolStake).unwrap(); - let accounts = vec![ - AccountMeta::new_readonly(*vote_account_address, false), - AccountMeta::new_readonly(pool_address, false), - AccountMeta::new(find_pool_stake_address(program_id, &pool_address), false), - AccountMeta::new_readonly( - find_pool_stake_authority_address(program_id, &pool_address), - false, - ), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - #[allow(deprecated)] - AccountMeta::new_readonly(stake::config::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -/// Creates all necessary instructions to deposit stake. -pub fn deposit( - program_id: &Pubkey, - pool_address: &Pubkey, - user_stake_account: &Pubkey, - user_token_account: &Pubkey, - user_lamport_account: &Pubkey, - user_withdraw_authority: &Pubkey, -) -> Vec { - let pool_stake_authority = find_pool_stake_authority_address(program_id, pool_address); - - vec![ - stake::instruction::authorize( - user_stake_account, - user_withdraw_authority, - &pool_stake_authority, - stake::state::StakeAuthorize::Staker, - None, - ), - stake::instruction::authorize( - user_stake_account, - user_withdraw_authority, - &pool_stake_authority, - stake::state::StakeAuthorize::Withdrawer, - None, - ), - deposit_stake( - program_id, - pool_address, - user_stake_account, - user_token_account, - user_lamport_account, - ), - ] -} - -/// Creates a `DepositStake` instruction. -pub fn deposit_stake( - program_id: &Pubkey, - pool_address: &Pubkey, - user_stake_account: &Pubkey, - user_token_account: &Pubkey, - user_lamport_account: &Pubkey, -) -> Instruction { - let data = borsh::to_vec(&SinglePoolInstruction::DepositStake).unwrap(); - - let accounts = vec![ - AccountMeta::new_readonly(*pool_address, false), - AccountMeta::new(find_pool_stake_address(program_id, pool_address), false), - AccountMeta::new(find_pool_mint_address(program_id, pool_address), false), - AccountMeta::new_readonly( - find_pool_stake_authority_address(program_id, pool_address), - false, - ), - AccountMeta::new_readonly( - find_pool_mint_authority_address(program_id, pool_address), - false, - ), - AccountMeta::new(*user_stake_account, false), - AccountMeta::new(*user_token_account, false), - AccountMeta::new(*user_lamport_account, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -/// Creates all necessary instructions to withdraw stake into a given stake -/// account. If a new stake account is required, the user should first include -/// `system_instruction::create_account` with account size -/// `std::mem::size_of::()` and owner -/// `stake::program::id()`. -pub fn withdraw( - program_id: &Pubkey, - pool_address: &Pubkey, - user_stake_account: &Pubkey, - user_stake_authority: &Pubkey, - user_token_account: &Pubkey, - user_token_authority: &Pubkey, - token_amount: u64, -) -> Vec { - vec![ - spl_token::instruction::approve( - &spl_token::id(), - user_token_account, - &find_pool_mint_authority_address(program_id, pool_address), - user_token_authority, - &[], - token_amount, - ) - .unwrap(), - withdraw_stake( - program_id, - pool_address, - user_stake_account, - user_stake_authority, - user_token_account, - token_amount, - ), - ] -} - -/// Creates a `WithdrawStake` instruction. -pub fn withdraw_stake( - program_id: &Pubkey, - pool_address: &Pubkey, - user_stake_account: &Pubkey, - user_stake_authority: &Pubkey, - user_token_account: &Pubkey, - token_amount: u64, -) -> Instruction { - let data = borsh::to_vec(&SinglePoolInstruction::WithdrawStake { - user_stake_authority: *user_stake_authority, - token_amount, - }) - .unwrap(); - - let accounts = vec![ - AccountMeta::new_readonly(*pool_address, false), - AccountMeta::new(find_pool_stake_address(program_id, pool_address), false), - AccountMeta::new(find_pool_mint_address(program_id, pool_address), false), - AccountMeta::new_readonly( - find_pool_stake_authority_address(program_id, pool_address), - false, - ), - AccountMeta::new_readonly( - find_pool_mint_authority_address(program_id, pool_address), - false, - ), - AccountMeta::new(*user_stake_account, false), - AccountMeta::new(*user_token_account, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -/// Creates necessary instructions to create and delegate a new stake account to -/// a given validator. Uses a fixed address for each wallet and vote account -/// combination to make it easier to find for deposits. This is an optional -/// helper function; deposits can come from any owned stake account without -/// lockup. -pub fn create_and_delegate_user_stake( - program_id: &Pubkey, - vote_account_address: &Pubkey, - user_wallet: &Pubkey, - rent: &Rent, - stake_amount: u64, -) -> Vec { - let pool_address = find_pool_address(program_id, vote_account_address); - let stake_space = std::mem::size_of::(); - let lamports = rent - .minimum_balance(stake_space) - .saturating_add(stake_amount); - let (deposit_address, deposit_seed) = - find_default_deposit_account_address_and_seed(&pool_address, user_wallet); - - stake::instruction::create_account_with_seed_and_delegate_stake( - user_wallet, - &deposit_address, - user_wallet, - &deposit_seed, - vote_account_address, - &stake::state::Authorized::auto(user_wallet), - &stake::state::Lockup::default(), - lamports, - ) -} - -/// Creates a `CreateTokenMetadata` instruction. -pub fn create_token_metadata( - program_id: &Pubkey, - pool_address: &Pubkey, - payer: &Pubkey, -) -> Instruction { - let pool_mint = find_pool_mint_address(program_id, pool_address); - let (token_metadata, _) = find_metadata_account(&pool_mint); - let data = borsh::to_vec(&SinglePoolInstruction::CreateTokenMetadata).unwrap(); - - let accounts = vec![ - AccountMeta::new_readonly(*pool_address, false), - AccountMeta::new_readonly(pool_mint, false), - AccountMeta::new_readonly( - find_pool_mint_authority_address(program_id, pool_address), - false, - ), - AccountMeta::new_readonly( - find_pool_mpl_authority_address(program_id, pool_address), - false, - ), - AccountMeta::new(*payer, true), - AccountMeta::new(token_metadata, false), - AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - ]; - - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -/// Creates an `UpdateTokenMetadata` instruction. -pub fn update_token_metadata( - program_id: &Pubkey, - vote_account_address: &Pubkey, - authorized_withdrawer: &Pubkey, - name: String, - symbol: String, - uri: String, -) -> Instruction { - let pool_address = find_pool_address(program_id, vote_account_address); - let pool_mint = find_pool_mint_address(program_id, &pool_address); - let (token_metadata, _) = find_metadata_account(&pool_mint); - let data = - borsh::to_vec(&SinglePoolInstruction::UpdateTokenMetadata { name, symbol, uri }).unwrap(); - - let accounts = vec![ - AccountMeta::new_readonly(*vote_account_address, false), - AccountMeta::new_readonly(pool_address, false), - AccountMeta::new_readonly( - find_pool_mpl_authority_address(program_id, &pool_address), - false, - ), - AccountMeta::new_readonly(*authorized_withdrawer, true), - AccountMeta::new(token_metadata, false), - AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false), - ]; - - Instruction { - program_id: *program_id, - accounts, - data, - } -} diff --git a/single-pool/program/src/lib.rs b/single-pool/program/src/lib.rs deleted file mode 100644 index 62ed4376647..00000000000 --- a/single-pool/program/src/lib.rs +++ /dev/null @@ -1,125 +0,0 @@ -#![deny(missing_docs)] - -//! A program for liquid staking with a single validator - -pub mod error; -pub mod inline_mpl_token_metadata; -pub mod instruction; -pub mod processor; -pub mod state; - -#[cfg(not(feature = "no-entrypoint"))] -pub mod entrypoint; - -// export current sdk types for downstream users building with a different sdk -// version -pub use solana_program; -use solana_program::{pubkey::Pubkey, stake}; - -solana_program::declare_id!("SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE"); - -const POOL_PREFIX: &[u8] = b"pool"; -const POOL_STAKE_PREFIX: &[u8] = b"stake"; -const POOL_MINT_PREFIX: &[u8] = b"mint"; -const POOL_MINT_AUTHORITY_PREFIX: &[u8] = b"mint_authority"; -const POOL_STAKE_AUTHORITY_PREFIX: &[u8] = b"stake_authority"; -const POOL_MPL_AUTHORITY_PREFIX: &[u8] = b"mpl_authority"; - -const MINT_DECIMALS: u8 = 9; - -const VOTE_STATE_DISCRIMINATOR_END: usize = 4; -const VOTE_STATE_AUTHORIZED_WITHDRAWER_START: usize = 36; -const VOTE_STATE_AUTHORIZED_WITHDRAWER_END: usize = 68; - -fn find_pool_address_and_bump(program_id: &Pubkey, vote_account_address: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address(&[POOL_PREFIX, vote_account_address.as_ref()], program_id) -} - -fn find_pool_stake_address_and_bump(program_id: &Pubkey, pool_address: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address(&[POOL_STAKE_PREFIX, pool_address.as_ref()], program_id) -} - -fn find_pool_mint_address_and_bump(program_id: &Pubkey, pool_address: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address(&[POOL_MINT_PREFIX, pool_address.as_ref()], program_id) -} - -fn find_pool_stake_authority_address_and_bump( - program_id: &Pubkey, - pool_address: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[POOL_STAKE_AUTHORITY_PREFIX, pool_address.as_ref()], - program_id, - ) -} - -fn find_pool_mint_authority_address_and_bump( - program_id: &Pubkey, - pool_address: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[POOL_MINT_AUTHORITY_PREFIX, pool_address.as_ref()], - program_id, - ) -} - -fn find_pool_mpl_authority_address_and_bump( - program_id: &Pubkey, - pool_address: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[POOL_MPL_AUTHORITY_PREFIX, pool_address.as_ref()], - program_id, - ) -} - -fn find_default_deposit_account_address_and_seed( - pool_address: &Pubkey, - user_wallet_address: &Pubkey, -) -> (Pubkey, String) { - let pool_address_str = pool_address.to_string(); - let seed = format!("svsp{}", &pool_address_str[0..28]); - let address = - Pubkey::create_with_seed(user_wallet_address, &seed, &stake::program::id()).unwrap(); - - (address, seed) -} - -/// Find the canonical pool address for a given vote account. -pub fn find_pool_address(program_id: &Pubkey, vote_account_address: &Pubkey) -> Pubkey { - find_pool_address_and_bump(program_id, vote_account_address).0 -} - -/// Find the canonical stake account address for a given pool account. -pub fn find_pool_stake_address(program_id: &Pubkey, pool_address: &Pubkey) -> Pubkey { - find_pool_stake_address_and_bump(program_id, pool_address).0 -} - -/// Find the canonical token mint address for a given pool account. -pub fn find_pool_mint_address(program_id: &Pubkey, pool_address: &Pubkey) -> Pubkey { - find_pool_mint_address_and_bump(program_id, pool_address).0 -} - -/// Find the canonical stake authority address for a given pool account. -pub fn find_pool_stake_authority_address(program_id: &Pubkey, pool_address: &Pubkey) -> Pubkey { - find_pool_stake_authority_address_and_bump(program_id, pool_address).0 -} - -/// Find the canonical mint authority address for a given pool account. -pub fn find_pool_mint_authority_address(program_id: &Pubkey, pool_address: &Pubkey) -> Pubkey { - find_pool_mint_authority_address_and_bump(program_id, pool_address).0 -} - -/// Find the canonical MPL authority address for a given pool account. -pub fn find_pool_mpl_authority_address(program_id: &Pubkey, pool_address: &Pubkey) -> Pubkey { - find_pool_mpl_authority_address_and_bump(program_id, pool_address).0 -} - -/// Find the address of the default intermediate account that holds activating -/// user stake before deposit. -pub fn find_default_deposit_account_address( - pool_address: &Pubkey, - user_wallet_address: &Pubkey, -) -> Pubkey { - find_default_deposit_account_address_and_seed(pool_address, user_wallet_address).0 -} diff --git a/single-pool/program/src/processor.rs b/single-pool/program/src/processor.rs deleted file mode 100644 index 55379d637d4..00000000000 --- a/single-pool/program/src/processor.rs +++ /dev/null @@ -1,1539 +0,0 @@ -//! program state processor - -use { - crate::{ - error::SinglePoolError, - inline_mpl_token_metadata::{ - self, - instruction::{create_metadata_accounts_v3, update_metadata_accounts_v2}, - pda::find_metadata_account, - state::DataV2, - }, - instruction::SinglePoolInstruction, - state::{SinglePool, SinglePoolAccountType}, - MINT_DECIMALS, POOL_MINT_AUTHORITY_PREFIX, POOL_MINT_PREFIX, POOL_MPL_AUTHORITY_PREFIX, - POOL_PREFIX, POOL_STAKE_AUTHORITY_PREFIX, POOL_STAKE_PREFIX, - VOTE_STATE_AUTHORIZED_WITHDRAWER_END, VOTE_STATE_AUTHORIZED_WITHDRAWER_START, - VOTE_STATE_DISCRIMINATOR_END, - }, - borsh::BorshDeserialize, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - borsh1::{get_packed_len, try_from_slice_unchecked}, - entrypoint::ProgramResult, - msg, - native_token::LAMPORTS_PER_SOL, - program::invoke_signed, - program_error::ProgramError, - program_pack::Pack, - pubkey::Pubkey, - rent::Rent, - stake::{ - self, - state::{Meta, Stake, StakeStateV2}, - }, - stake_history::Epoch, - system_instruction, system_program, - sysvar::{clock::Clock, Sysvar}, - vote::program as vote_program, - }, - spl_token::state::Mint, -}; - -/// Calculate pool tokens to mint, given outstanding token supply, pool active -/// stake, and deposit active stake -fn calculate_deposit_amount( - pre_token_supply: u64, - pre_pool_stake: u64, - user_stake_to_deposit: u64, -) -> Option { - if pre_pool_stake == 0 || pre_token_supply == 0 { - Some(user_stake_to_deposit) - } else { - u64::try_from( - (user_stake_to_deposit as u128) - .checked_mul(pre_token_supply as u128)? - .checked_div(pre_pool_stake as u128)?, - ) - .ok() - } -} - -/// Calculate pool stake to return, given outstanding token supply, pool active -/// stake, and tokens to redeem -fn calculate_withdraw_amount( - pre_token_supply: u64, - pre_pool_stake: u64, - user_tokens_to_burn: u64, -) -> Option { - let numerator = (user_tokens_to_burn as u128).checked_mul(pre_pool_stake as u128)?; - let denominator = pre_token_supply as u128; - if numerator < denominator || denominator == 0 { - Some(0) - } else { - u64::try_from(numerator.checked_div(denominator)?).ok() - } -} - -/// Deserialize the stake state from AccountInfo -fn get_stake_state(stake_account_info: &AccountInfo) -> Result<(Meta, Stake), ProgramError> { - let stake_state = try_from_slice_unchecked::(&stake_account_info.data.borrow())?; - - match stake_state { - StakeStateV2::Stake(meta, stake, _) => Ok((meta, stake)), - _ => Err(SinglePoolError::WrongStakeStake.into()), - } -} - -/// Deserialize the stake amount from AccountInfo -fn get_stake_amount(stake_account_info: &AccountInfo) -> Result { - Ok(get_stake_state(stake_account_info)?.1.delegation.stake) -} - -/// Determine if stake is active -fn is_stake_active_without_history(stake: &Stake, current_epoch: Epoch) -> bool { - stake.delegation.activation_epoch < current_epoch - && stake.delegation.deactivation_epoch == Epoch::MAX -} - -/// Check pool account address for the validator vote account -fn check_pool_address( - program_id: &Pubkey, - vote_account_address: &Pubkey, - check_address: &Pubkey, -) -> Result { - check_pool_pda( - program_id, - vote_account_address, - check_address, - &crate::find_pool_address_and_bump, - "pool", - SinglePoolError::InvalidPoolAccount, - ) -} - -/// Check pool stake account address for the pool account -fn check_pool_stake_address( - program_id: &Pubkey, - pool_address: &Pubkey, - check_address: &Pubkey, -) -> Result { - check_pool_pda( - program_id, - pool_address, - check_address, - &crate::find_pool_stake_address_and_bump, - "stake account", - SinglePoolError::InvalidPoolStakeAccount, - ) -} - -/// Check pool mint address for the pool account -fn check_pool_mint_address( - program_id: &Pubkey, - pool_address: &Pubkey, - check_address: &Pubkey, -) -> Result { - check_pool_pda( - program_id, - pool_address, - check_address, - &crate::find_pool_mint_address_and_bump, - "mint", - SinglePoolError::InvalidPoolMint, - ) -} - -/// Check pool stake authority address for the pool account -fn check_pool_stake_authority_address( - program_id: &Pubkey, - pool_address: &Pubkey, - check_address: &Pubkey, -) -> Result { - check_pool_pda( - program_id, - pool_address, - check_address, - &crate::find_pool_stake_authority_address_and_bump, - "stake authority", - SinglePoolError::InvalidPoolStakeAuthority, - ) -} - -/// Check pool mint authority address for the pool account -fn check_pool_mint_authority_address( - program_id: &Pubkey, - pool_address: &Pubkey, - check_address: &Pubkey, -) -> Result { - check_pool_pda( - program_id, - pool_address, - check_address, - &crate::find_pool_mint_authority_address_and_bump, - "mint authority", - SinglePoolError::InvalidPoolMintAuthority, - ) -} - -/// Check pool MPL authority address for the pool account -fn check_pool_mpl_authority_address( - program_id: &Pubkey, - pool_address: &Pubkey, - check_address: &Pubkey, -) -> Result { - check_pool_pda( - program_id, - pool_address, - check_address, - &crate::find_pool_mpl_authority_address_and_bump, - "MPL authority", - SinglePoolError::InvalidPoolMplAuthority, - ) -} - -fn check_pool_pda( - program_id: &Pubkey, - base_address: &Pubkey, - check_address: &Pubkey, - pda_lookup_fn: &dyn Fn(&Pubkey, &Pubkey) -> (Pubkey, u8), - pda_name: &str, - pool_error: SinglePoolError, -) -> Result { - let (derived_address, bump_seed) = pda_lookup_fn(program_id, base_address); - if *check_address != derived_address { - msg!( - "Incorrect {} address for base {}: expected {}, received {}", - pda_name, - base_address, - derived_address, - check_address, - ); - Err(pool_error.into()) - } else { - Ok(bump_seed) - } -} - -/// Check vote account is owned by the vote program and not a legacy variant -fn check_vote_account(vote_account_info: &AccountInfo) -> Result<(), ProgramError> { - check_account_owner(vote_account_info, &vote_program::id())?; - - let vote_account_data = &vote_account_info.try_borrow_data()?; - let state_variant = vote_account_data - .get(..VOTE_STATE_DISCRIMINATOR_END) - .and_then(|s| s.try_into().ok()) - .ok_or(SinglePoolError::UnparseableVoteAccount)?; - - match u32::from_le_bytes(state_variant) { - 1 | 2 => Ok(()), - 0 => Err(SinglePoolError::LegacyVoteAccount.into()), - _ => Err(SinglePoolError::UnparseableVoteAccount.into()), - } -} - -/// Check MPL metadata account address for the pool mint -fn check_mpl_metadata_account_address( - metadata_address: &Pubkey, - pool_mint: &Pubkey, -) -> Result<(), ProgramError> { - let (metadata_account_pubkey, _) = find_metadata_account(pool_mint); - if metadata_account_pubkey != *metadata_address { - Err(SinglePoolError::InvalidMetadataAccount.into()) - } else { - Ok(()) - } -} - -/// Check system program address -fn check_system_program(program_id: &Pubkey) -> Result<(), ProgramError> { - if *program_id != system_program::id() { - msg!( - "Expected system program {}, received {}", - system_program::id(), - program_id - ); - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } -} - -/// Check token program address -fn check_token_program(address: &Pubkey) -> Result<(), ProgramError> { - if *address != spl_token::id() { - msg!( - "Incorrect token program, expected {}, received {}", - spl_token::id(), - address - ); - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } -} - -/// Check stake program address -fn check_stake_program(program_id: &Pubkey) -> Result<(), ProgramError> { - if *program_id != stake::program::id() { - msg!( - "Expected stake program {}, received {}", - stake::program::id(), - program_id - ); - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } -} - -/// Check MPL metadata program -fn check_mpl_metadata_program(program_id: &Pubkey) -> Result<(), ProgramError> { - if *program_id != inline_mpl_token_metadata::id() { - msg!( - "Expected MPL metadata program {}, received {}", - inline_mpl_token_metadata::id(), - program_id - ); - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } -} - -/// Check account owner is the given program -fn check_account_owner( - account_info: &AccountInfo, - program_id: &Pubkey, -) -> Result<(), ProgramError> { - if *program_id != *account_info.owner { - msg!( - "Expected account to be owned by program {}, received {}", - program_id, - account_info.owner - ); - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } -} - -/// Minimum delegation to create a pool -/// We floor at 1sol to avoid over-minting tokens before the relevant feature is -/// active -fn minimum_delegation() -> Result { - Ok(std::cmp::max( - stake::tools::get_minimum_delegation()?, - LAMPORTS_PER_SOL, - )) -} - -/// Program state handler. -pub struct Processor {} -impl Processor { - #[allow(clippy::too_many_arguments)] - fn stake_merge<'a>( - pool_account_key: &Pubkey, - source_account: AccountInfo<'a>, - authority: AccountInfo<'a>, - bump_seed: u8, - destination_account: AccountInfo<'a>, - clock: AccountInfo<'a>, - stake_history: AccountInfo<'a>, - ) -> Result<(), ProgramError> { - let authority_seeds = &[ - POOL_STAKE_AUTHORITY_PREFIX, - pool_account_key.as_ref(), - &[bump_seed], - ]; - let signers = &[&authority_seeds[..]]; - - invoke_signed( - &stake::instruction::merge(destination_account.key, source_account.key, authority.key) - [0], - &[ - destination_account, - source_account, - clock, - stake_history, - authority, - ], - signers, - ) - } - - fn stake_split<'a>( - pool_account_key: &Pubkey, - stake_account: AccountInfo<'a>, - authority: AccountInfo<'a>, - bump_seed: u8, - amount: u64, - split_stake: AccountInfo<'a>, - ) -> Result<(), ProgramError> { - let authority_seeds = &[ - POOL_STAKE_AUTHORITY_PREFIX, - pool_account_key.as_ref(), - &[bump_seed], - ]; - let signers = &[&authority_seeds[..]]; - - let split_instruction = - stake::instruction::split(stake_account.key, authority.key, amount, split_stake.key); - - invoke_signed( - split_instruction.last().unwrap(), - &[stake_account, split_stake, authority], - signers, - ) - } - - #[allow(clippy::too_many_arguments)] - fn stake_authorize<'a>( - pool_account_key: &Pubkey, - stake_account: AccountInfo<'a>, - stake_authority: AccountInfo<'a>, - bump_seed: u8, - new_stake_authority: &Pubkey, - clock: AccountInfo<'a>, - ) -> Result<(), ProgramError> { - let authority_seeds = &[ - POOL_STAKE_AUTHORITY_PREFIX, - pool_account_key.as_ref(), - &[bump_seed], - ]; - let signers = &[&authority_seeds[..]]; - - let authorize_instruction = stake::instruction::authorize( - stake_account.key, - stake_authority.key, - new_stake_authority, - stake::state::StakeAuthorize::Staker, - None, - ); - - invoke_signed( - &authorize_instruction, - &[ - stake_account.clone(), - clock.clone(), - stake_authority.clone(), - ], - signers, - )?; - - let authorize_instruction = stake::instruction::authorize( - stake_account.key, - stake_authority.key, - new_stake_authority, - stake::state::StakeAuthorize::Withdrawer, - None, - ); - invoke_signed( - &authorize_instruction, - &[stake_account, clock, stake_authority], - signers, - ) - } - - #[allow(clippy::too_many_arguments)] - fn stake_withdraw<'a>( - pool_account_key: &Pubkey, - stake_account: AccountInfo<'a>, - stake_authority: AccountInfo<'a>, - bump_seed: u8, - destination_account: AccountInfo<'a>, - clock: AccountInfo<'a>, - stake_history: AccountInfo<'a>, - lamports: u64, - ) -> Result<(), ProgramError> { - let authority_seeds = &[ - POOL_STAKE_AUTHORITY_PREFIX, - pool_account_key.as_ref(), - &[bump_seed], - ]; - let signers = &[&authority_seeds[..]]; - - let withdraw_instruction = stake::instruction::withdraw( - stake_account.key, - stake_authority.key, - destination_account.key, - lamports, - None, - ); - - invoke_signed( - &withdraw_instruction, - &[ - stake_account, - destination_account, - clock, - stake_history, - stake_authority, - ], - signers, - ) - } - - #[allow(clippy::too_many_arguments)] - fn token_mint_to<'a>( - pool_account_key: &Pubkey, - token_program: AccountInfo<'a>, - mint: AccountInfo<'a>, - destination: AccountInfo<'a>, - authority: AccountInfo<'a>, - bump_seed: u8, - amount: u64, - ) -> Result<(), ProgramError> { - let authority_seeds = &[ - POOL_MINT_AUTHORITY_PREFIX, - pool_account_key.as_ref(), - &[bump_seed], - ]; - let signers = &[&authority_seeds[..]]; - - let ix = spl_token::instruction::mint_to( - token_program.key, - mint.key, - destination.key, - authority.key, - &[], - amount, - )?; - - invoke_signed(&ix, &[mint, destination, authority], signers) - } - - #[allow(clippy::too_many_arguments)] - fn token_burn<'a>( - pool_account_key: &Pubkey, - token_program: AccountInfo<'a>, - burn_account: AccountInfo<'a>, - mint: AccountInfo<'a>, - authority: AccountInfo<'a>, - bump_seed: u8, - amount: u64, - ) -> Result<(), ProgramError> { - let authority_seeds = &[ - POOL_MINT_AUTHORITY_PREFIX, - pool_account_key.as_ref(), - &[bump_seed], - ]; - let signers = &[&authority_seeds[..]]; - - let ix = spl_token::instruction::burn( - token_program.key, - burn_account.key, - mint.key, - authority.key, - &[], - amount, - )?; - - invoke_signed(&ix, &[burn_account, mint, authority], signers) - } - - fn process_initialize_pool(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let vote_account_info = next_account_info(account_info_iter)?; - let pool_info = next_account_info(account_info_iter)?; - let pool_stake_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let pool_stake_authority_info = next_account_info(account_info_iter)?; - let pool_mint_authority_info = next_account_info(account_info_iter)?; - let rent_info = next_account_info(account_info_iter)?; - let rent = &Rent::from_account_info(rent_info)?; - let clock_info = next_account_info(account_info_iter)?; - let stake_history_info = next_account_info(account_info_iter)?; - let stake_config_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - let token_program_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - - check_vote_account(vote_account_info)?; - let pool_bump_seed = check_pool_address(program_id, vote_account_info.key, pool_info.key)?; - let stake_bump_seed = - check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?; - let mint_bump_seed = - check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?; - let stake_authority_bump_seed = check_pool_stake_authority_address( - program_id, - pool_info.key, - pool_stake_authority_info.key, - )?; - let mint_authority_bump_seed = check_pool_mint_authority_address( - program_id, - pool_info.key, - pool_mint_authority_info.key, - )?; - check_system_program(system_program_info.key)?; - check_token_program(token_program_info.key)?; - check_stake_program(stake_program_info.key)?; - - let pool_seeds = &[ - POOL_PREFIX, - vote_account_info.key.as_ref(), - &[pool_bump_seed], - ]; - let pool_signers = &[&pool_seeds[..]]; - - let stake_seeds = &[ - POOL_STAKE_PREFIX, - pool_info.key.as_ref(), - &[stake_bump_seed], - ]; - let stake_signers = &[&stake_seeds[..]]; - - let mint_seeds = &[POOL_MINT_PREFIX, pool_info.key.as_ref(), &[mint_bump_seed]]; - let mint_signers = &[&mint_seeds[..]]; - - let stake_authority_seeds = &[ - POOL_STAKE_AUTHORITY_PREFIX, - pool_info.key.as_ref(), - &[stake_authority_bump_seed], - ]; - let stake_authority_signers = &[&stake_authority_seeds[..]]; - - let mint_authority_seeds = &[ - POOL_MINT_AUTHORITY_PREFIX, - pool_info.key.as_ref(), - &[mint_authority_bump_seed], - ]; - let mint_authority_signers = &[&mint_authority_seeds[..]]; - - // create the pool. user has already transferred in rent - let pool_space = get_packed_len::(); - if !rent.is_exempt(pool_info.lamports(), pool_space) { - return Err(SinglePoolError::WrongRentAmount.into()); - } - if pool_info.data_len() != 0 { - return Err(SinglePoolError::PoolAlreadyInitialized.into()); - } - - invoke_signed( - &system_instruction::allocate(pool_info.key, pool_space as u64), - &[pool_info.clone()], - pool_signers, - )?; - - invoke_signed( - &system_instruction::assign(pool_info.key, program_id), - &[pool_info.clone()], - pool_signers, - )?; - - let mut pool = try_from_slice_unchecked::(&pool_info.data.borrow())?; - pool.account_type = SinglePoolAccountType::Pool; - pool.vote_account_address = *vote_account_info.key; - borsh::to_writer(&mut pool_info.data.borrow_mut()[..], &pool)?; - - // create the pool mint. user has already transferred in rent - let mint_space = spl_token::state::Mint::LEN; - - invoke_signed( - &system_instruction::allocate(pool_mint_info.key, mint_space as u64), - &[pool_mint_info.clone()], - mint_signers, - )?; - - invoke_signed( - &system_instruction::assign(pool_mint_info.key, token_program_info.key), - &[pool_mint_info.clone()], - mint_signers, - )?; - - invoke_signed( - &spl_token::instruction::initialize_mint2( - token_program_info.key, - pool_mint_info.key, - pool_mint_authority_info.key, - None, - MINT_DECIMALS, - )?, - &[pool_mint_info.clone()], - mint_authority_signers, - )?; - - // create the pool stake account. user has already transferred in rent plus at - // least the minimum - let minimum_delegation = minimum_delegation()?; - let stake_space = std::mem::size_of::(); - let stake_rent_plus_initial = rent - .minimum_balance(stake_space) - .saturating_add(minimum_delegation); - - if pool_stake_info.lamports() < stake_rent_plus_initial { - return Err(SinglePoolError::WrongRentAmount.into()); - } - - let authorized = stake::state::Authorized::auto(pool_stake_authority_info.key); - - invoke_signed( - &system_instruction::allocate(pool_stake_info.key, stake_space as u64), - &[pool_stake_info.clone()], - stake_signers, - )?; - - invoke_signed( - &system_instruction::assign(pool_stake_info.key, stake_program_info.key), - &[pool_stake_info.clone()], - stake_signers, - )?; - - invoke_signed( - &stake::instruction::initialize_checked(pool_stake_info.key, &authorized), - &[ - pool_stake_info.clone(), - rent_info.clone(), - pool_stake_authority_info.clone(), - pool_stake_authority_info.clone(), - ], - stake_authority_signers, - )?; - - // delegate stake so it activates - invoke_signed( - &stake::instruction::delegate_stake( - pool_stake_info.key, - pool_stake_authority_info.key, - vote_account_info.key, - ), - &[ - pool_stake_info.clone(), - vote_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - stake_config_info.clone(), - pool_stake_authority_info.clone(), - ], - stake_authority_signers, - )?; - - Ok(()) - } - - fn process_reactivate_pool_stake( - program_id: &Pubkey, - accounts: &[AccountInfo], - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let vote_account_info = next_account_info(account_info_iter)?; - let pool_info = next_account_info(account_info_iter)?; - let pool_stake_info = next_account_info(account_info_iter)?; - let pool_stake_authority_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let stake_history_info = next_account_info(account_info_iter)?; - let stake_config_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - - check_vote_account(vote_account_info)?; - check_pool_address(program_id, vote_account_info.key, pool_info.key)?; - - SinglePool::from_account_info(pool_info, program_id)?; - - check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?; - let stake_authority_bump_seed = check_pool_stake_authority_address( - program_id, - pool_info.key, - pool_stake_authority_info.key, - )?; - check_stake_program(stake_program_info.key)?; - - let (_, pool_stake_state) = get_stake_state(pool_stake_info)?; - if pool_stake_state.delegation.deactivation_epoch > clock.epoch { - return Err(SinglePoolError::WrongStakeStake.into()); - } - - let stake_authority_seeds = &[ - POOL_STAKE_AUTHORITY_PREFIX, - pool_info.key.as_ref(), - &[stake_authority_bump_seed], - ]; - let stake_authority_signers = &[&stake_authority_seeds[..]]; - - // delegate stake so it activates - invoke_signed( - &stake::instruction::delegate_stake( - pool_stake_info.key, - pool_stake_authority_info.key, - vote_account_info.key, - ), - &[ - pool_stake_info.clone(), - vote_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - stake_config_info.clone(), - pool_stake_authority_info.clone(), - ], - stake_authority_signers, - )?; - - Ok(()) - } - - fn process_deposit_stake(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let pool_info = next_account_info(account_info_iter)?; - let pool_stake_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let pool_stake_authority_info = next_account_info(account_info_iter)?; - let pool_mint_authority_info = next_account_info(account_info_iter)?; - let user_stake_info = next_account_info(account_info_iter)?; - let user_token_account_info = next_account_info(account_info_iter)?; - let user_lamport_account_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let stake_history_info = next_account_info(account_info_iter)?; - let token_program_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - - SinglePool::from_account_info(pool_info, program_id)?; - - check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?; - let stake_authority_bump_seed = check_pool_stake_authority_address( - program_id, - pool_info.key, - pool_stake_authority_info.key, - )?; - let mint_authority_bump_seed = check_pool_mint_authority_address( - program_id, - pool_info.key, - pool_mint_authority_info.key, - )?; - check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?; - check_token_program(token_program_info.key)?; - check_stake_program(stake_program_info.key)?; - - if pool_stake_info.key == user_stake_info.key { - return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into()); - } - - let minimum_delegation = minimum_delegation()?; - - let (_, pool_stake_state) = get_stake_state(pool_stake_info)?; - let pre_pool_stake = pool_stake_state - .delegation - .stake - .saturating_sub(minimum_delegation); - msg!("Available stake pre merge {}", pre_pool_stake); - - // user can deposit active stake into an active pool or inactive stake into an - // activating pool - let (user_stake_meta, user_stake_state) = get_stake_state(user_stake_info)?; - if user_stake_meta.authorized - != stake::state::Authorized::auto(pool_stake_authority_info.key) - || is_stake_active_without_history(&pool_stake_state, clock.epoch) - != is_stake_active_without_history(&user_stake_state, clock.epoch) - { - return Err(SinglePoolError::WrongStakeStake.into()); - } - - // merge the user stake account, which is preauthed to us, into the pool stake - // account this merge succeeding implicitly validates authority/lockup - // of the user stake account - Self::stake_merge( - pool_info.key, - user_stake_info.clone(), - pool_stake_authority_info.clone(), - stake_authority_bump_seed, - pool_stake_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - )?; - - let (pool_stake_meta, pool_stake_state) = get_stake_state(pool_stake_info)?; - let post_pool_stake = pool_stake_state - .delegation - .stake - .saturating_sub(minimum_delegation); - let post_pool_lamports = pool_stake_info.lamports(); - msg!("Available stake post merge {}", post_pool_stake); - - // stake lamports added, as a stake difference - let stake_added = post_pool_stake - .checked_sub(pre_pool_stake) - .ok_or(SinglePoolError::ArithmeticOverflow)?; - - // we calculate absolute rather than relative to deposit amount to allow - // claiming lamports mistakenly transferred in - let excess_lamports = post_pool_lamports - .checked_sub(pool_stake_state.delegation.stake) - .and_then(|amount| amount.checked_sub(pool_stake_meta.rent_exempt_reserve)) - .ok_or(SinglePoolError::ArithmeticOverflow)?; - - // sanity check: the user stake account is empty - if user_stake_info.lamports() != 0 { - return Err(SinglePoolError::UnexpectedMathError.into()); - } - - let token_supply = { - let pool_mint_data = pool_mint_info.try_borrow_data()?; - let pool_mint = Mint::unpack_from_slice(&pool_mint_data)?; - pool_mint.supply - }; - - // deposit amount is determined off stake because we return excess rent - let new_pool_tokens = calculate_deposit_amount(token_supply, pre_pool_stake, stake_added) - .ok_or(SinglePoolError::UnexpectedMathError)?; - - if new_pool_tokens == 0 { - return Err(SinglePoolError::DepositTooSmall.into()); - } - - // mint tokens to the user corresponding to their stake deposit - Self::token_mint_to( - pool_info.key, - token_program_info.clone(), - pool_mint_info.clone(), - user_token_account_info.clone(), - pool_mint_authority_info.clone(), - mint_authority_bump_seed, - new_pool_tokens, - )?; - - // return the lamports their stake account previously held for rent-exemption - if excess_lamports > 0 { - Self::stake_withdraw( - pool_info.key, - pool_stake_info.clone(), - pool_stake_authority_info.clone(), - stake_authority_bump_seed, - user_lamport_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - excess_lamports, - )?; - } - - Ok(()) - } - - fn process_withdraw_stake( - program_id: &Pubkey, - accounts: &[AccountInfo], - user_stake_authority: &Pubkey, - token_amount: u64, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let pool_info = next_account_info(account_info_iter)?; - let pool_stake_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let pool_stake_authority_info = next_account_info(account_info_iter)?; - let pool_mint_authority_info = next_account_info(account_info_iter)?; - let user_stake_info = next_account_info(account_info_iter)?; - let user_token_account_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let token_program_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - - SinglePool::from_account_info(pool_info, program_id)?; - - check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?; - let stake_authority_bump_seed = check_pool_stake_authority_address( - program_id, - pool_info.key, - pool_stake_authority_info.key, - )?; - let mint_authority_bump_seed = check_pool_mint_authority_address( - program_id, - pool_info.key, - pool_mint_authority_info.key, - )?; - check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?; - check_token_program(token_program_info.key)?; - check_stake_program(stake_program_info.key)?; - - if pool_stake_info.key == user_stake_info.key { - return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into()); - } - - let minimum_delegation = minimum_delegation()?; - - let pre_pool_stake = get_stake_amount(pool_stake_info)?.saturating_sub(minimum_delegation); - msg!("Available stake pre split {}", pre_pool_stake); - - let token_supply = { - let pool_mint_data = pool_mint_info.try_borrow_data()?; - let pool_mint = Mint::unpack_from_slice(&pool_mint_data)?; - pool_mint.supply - }; - - // withdraw amount is determined off stake just like deposit amount - let withdraw_stake = calculate_withdraw_amount(token_supply, pre_pool_stake, token_amount) - .ok_or(SinglePoolError::UnexpectedMathError)?; - - if withdraw_stake == 0 { - return Err(SinglePoolError::WithdrawalTooSmall.into()); - } - - // the second case should never be true, but its best to be sure - if withdraw_stake > pre_pool_stake || withdraw_stake == pool_stake_info.lamports() { - return Err(SinglePoolError::WithdrawalTooLarge.into()); - } - - // burn user tokens corresponding to the amount of stake they wish to withdraw - Self::token_burn( - pool_info.key, - token_program_info.clone(), - user_token_account_info.clone(), - pool_mint_info.clone(), - pool_mint_authority_info.clone(), - mint_authority_bump_seed, - token_amount, - )?; - - // split stake into a blank stake account the user has created for this purpose - Self::stake_split( - pool_info.key, - pool_stake_info.clone(), - pool_stake_authority_info.clone(), - stake_authority_bump_seed, - withdraw_stake, - user_stake_info.clone(), - )?; - - // assign both authorities on the new stake account to the user - Self::stake_authorize( - pool_info.key, - user_stake_info.clone(), - pool_stake_authority_info.clone(), - stake_authority_bump_seed, - user_stake_authority, - clock_info.clone(), - )?; - - let post_pool_stake = get_stake_amount(pool_stake_info)?.saturating_sub(minimum_delegation); - msg!("Available stake post split {}", post_pool_stake); - - Ok(()) - } - - fn process_create_pool_token_metadata( - program_id: &Pubkey, - accounts: &[AccountInfo], - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let pool_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let pool_mint_authority_info = next_account_info(account_info_iter)?; - let pool_mpl_authority_info = next_account_info(account_info_iter)?; - let payer_info = next_account_info(account_info_iter)?; - let metadata_info = next_account_info(account_info_iter)?; - let mpl_token_metadata_program_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - - let pool = SinglePool::from_account_info(pool_info, program_id)?; - - let mint_authority_bump_seed = check_pool_mint_authority_address( - program_id, - pool_info.key, - pool_mint_authority_info.key, - )?; - let mpl_authority_bump_seed = check_pool_mpl_authority_address( - program_id, - pool_info.key, - pool_mpl_authority_info.key, - )?; - check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?; - check_system_program(system_program_info.key)?; - check_account_owner(payer_info, &system_program::id())?; - check_mpl_metadata_program(mpl_token_metadata_program_info.key)?; - check_mpl_metadata_account_address(metadata_info.key, pool_mint_info.key)?; - - if !payer_info.is_signer { - msg!("Payer did not sign metadata creation"); - return Err(SinglePoolError::SignatureMissing.into()); - } - - let vote_address_str = pool.vote_account_address.to_string(); - let token_name = format!("SPL Single Pool {}", &vote_address_str[0..15]); - let token_symbol = format!("st{}", &vote_address_str[0..7]); - - let new_metadata_instruction = create_metadata_accounts_v3( - *mpl_token_metadata_program_info.key, - *metadata_info.key, - *pool_mint_info.key, - *pool_mint_authority_info.key, - *payer_info.key, - *pool_mpl_authority_info.key, - token_name, - token_symbol, - "".to_string(), - ); - - let mint_authority_seeds = &[ - POOL_MINT_AUTHORITY_PREFIX, - pool_info.key.as_ref(), - &[mint_authority_bump_seed], - ]; - let mpl_authority_seeds = &[ - POOL_MPL_AUTHORITY_PREFIX, - pool_info.key.as_ref(), - &[mpl_authority_bump_seed], - ]; - let signers = &[&mint_authority_seeds[..], &mpl_authority_seeds[..]]; - - invoke_signed( - &new_metadata_instruction, - &[ - metadata_info.clone(), - pool_mint_info.clone(), - pool_mint_authority_info.clone(), - payer_info.clone(), - pool_mpl_authority_info.clone(), - system_program_info.clone(), - ], - signers, - )?; - - Ok(()) - } - - fn process_update_pool_token_metadata( - program_id: &Pubkey, - accounts: &[AccountInfo], - name: String, - symbol: String, - uri: String, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let vote_account_info = next_account_info(account_info_iter)?; - let pool_info = next_account_info(account_info_iter)?; - let pool_mpl_authority_info = next_account_info(account_info_iter)?; - let authorized_withdrawer_info = next_account_info(account_info_iter)?; - let metadata_info = next_account_info(account_info_iter)?; - let mpl_token_metadata_program_info = next_account_info(account_info_iter)?; - - check_vote_account(vote_account_info)?; - check_pool_address(program_id, vote_account_info.key, pool_info.key)?; - - let pool = SinglePool::from_account_info(pool_info, program_id)?; - if pool.vote_account_address != *vote_account_info.key { - return Err(SinglePoolError::InvalidPoolAccount.into()); - } - - let mpl_authority_bump_seed = check_pool_mpl_authority_address( - program_id, - pool_info.key, - pool_mpl_authority_info.key, - )?; - let pool_mint_address = crate::find_pool_mint_address(program_id, pool_info.key); - check_mpl_metadata_program(mpl_token_metadata_program_info.key)?; - check_mpl_metadata_account_address(metadata_info.key, &pool_mint_address)?; - - // we use authorized_withdrawer to authenticate the caller controls the vote - // account this is safer than using an authorized_voter since those keys - // live hot and validator-operators we spoke with indicated this would - // be their preference as well - let vote_account_data = &vote_account_info.try_borrow_data()?; - let vote_account_withdrawer = vote_account_data - .get(VOTE_STATE_AUTHORIZED_WITHDRAWER_START..VOTE_STATE_AUTHORIZED_WITHDRAWER_END) - .and_then(|x| Pubkey::try_from(x).ok()) - .ok_or(SinglePoolError::UnparseableVoteAccount)?; - - if *authorized_withdrawer_info.key != vote_account_withdrawer { - msg!("Vote account authorized withdrawer does not match the account provided."); - return Err(SinglePoolError::InvalidMetadataSigner.into()); - } - - if !authorized_withdrawer_info.is_signer { - msg!("Vote account authorized withdrawer did not sign metadata update."); - return Err(SinglePoolError::SignatureMissing.into()); - } - - let update_metadata_accounts_instruction = update_metadata_accounts_v2( - *mpl_token_metadata_program_info.key, - *metadata_info.key, - *pool_mpl_authority_info.key, - None, - Some(DataV2 { - name, - symbol, - uri, - seller_fee_basis_points: 0, - creators: None, - collection: None, - uses: None, - }), - None, - Some(true), - ); - - let mpl_authority_seeds = &[ - POOL_MPL_AUTHORITY_PREFIX, - pool_info.key.as_ref(), - &[mpl_authority_bump_seed], - ]; - let signers = &[&mpl_authority_seeds[..]]; - - invoke_signed( - &update_metadata_accounts_instruction, - &[metadata_info.clone(), pool_mpl_authority_info.clone()], - signers, - )?; - - Ok(()) - } - - /// Processes [Instruction](enum.Instruction.html). - pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { - let instruction = SinglePoolInstruction::try_from_slice(input)?; - match instruction { - SinglePoolInstruction::InitializePool => { - msg!("Instruction: InitializePool"); - Self::process_initialize_pool(program_id, accounts) - } - SinglePoolInstruction::ReactivatePoolStake => { - msg!("Instruction: ReactivatePoolStake"); - Self::process_reactivate_pool_stake(program_id, accounts) - } - SinglePoolInstruction::DepositStake => { - msg!("Instruction: DepositStake"); - Self::process_deposit_stake(program_id, accounts) - } - SinglePoolInstruction::WithdrawStake { - user_stake_authority, - token_amount, - } => { - msg!("Instruction: WithdrawStake"); - Self::process_withdraw_stake( - program_id, - accounts, - &user_stake_authority, - token_amount, - ) - } - SinglePoolInstruction::CreateTokenMetadata => { - msg!("Instruction: CreateTokenMetadata"); - Self::process_create_pool_token_metadata(program_id, accounts) - } - SinglePoolInstruction::UpdateTokenMetadata { name, symbol, uri } => { - msg!("Instruction: UpdateTokenMetadata"); - Self::process_update_pool_token_metadata(program_id, accounts, name, symbol, uri) - } - } - } -} - -#[cfg(test)] -#[allow(clippy::arithmetic_side_effects)] -mod tests { - use { - super::*, - approx::assert_relative_eq, - rand::{ - distributions::{Distribution, Uniform}, - rngs::StdRng, - seq::{IteratorRandom, SliceRandom}, - Rng, SeedableRng, - }, - std::collections::BTreeMap, - test_case::test_case, - }; - - // approximately 6%/yr assuking 146 epochs - const INFLATION_BASE_RATE: f64 = 0.0004; - - #[derive(Clone, Debug, Default)] - struct PoolState { - pub token_supply: u64, - pub total_stake: u64, - pub user_token_balances: BTreeMap, - } - impl PoolState { - // deposits a given amount of stake and returns the equivalent tokens on success - // note this is written as unsugared do-notation, so *any* failure returns None - // otherwise returns the value produced by its respective calculate function - #[rustfmt::skip] - pub fn deposit(&mut self, user_pubkey: &Pubkey, stake_to_deposit: u64) -> Option { - calculate_deposit_amount(self.token_supply, self.total_stake, stake_to_deposit) - .and_then(|tokens_to_mint| self.token_supply.checked_add(tokens_to_mint) - .and_then(|new_token_supply| self.total_stake.checked_add(stake_to_deposit) - .and_then(|new_total_stake| self.user_token_balances.remove(user_pubkey).or(Some(0)) - .and_then(|old_user_token_balance| old_user_token_balance.checked_add(tokens_to_mint) - .map(|new_user_token_balance| { - self.token_supply = new_token_supply; - self.total_stake = new_total_stake; - let _ = self.user_token_balances.insert(*user_pubkey, new_user_token_balance); - tokens_to_mint - }))))) - } - - // burns a given amount of tokens and returns the equivalent stake on success - // note this is written as unsugared do-notation, so *any* failure returns None - // otherwise returns the value produced by its respective calculate function - #[rustfmt::skip] - pub fn withdraw(&mut self, user_pubkey: &Pubkey, tokens_to_burn: u64) -> Option { - calculate_withdraw_amount(self.token_supply, self.total_stake, tokens_to_burn) - .and_then(|stake_to_withdraw| self.token_supply.checked_sub(tokens_to_burn) - .and_then(|new_token_supply| self.total_stake.checked_sub(stake_to_withdraw) - .and_then(|new_total_stake| self.user_token_balances.remove(user_pubkey) - .and_then(|old_user_token_balance| old_user_token_balance.checked_sub(tokens_to_burn) - .map(|new_user_token_balance| { - self.token_supply = new_token_supply; - self.total_stake = new_total_stake; - let _ = self.user_token_balances.insert(*user_pubkey, new_user_token_balance); - stake_to_withdraw - }))))) - } - - // adds an arbitrary amount of stake, as if inflation rewards were granted - pub fn reward(&mut self, reward_amount: u64) { - self.total_stake = self.total_stake.checked_add(reward_amount).unwrap(); - } - - // get the token balance for a user - pub fn tokens(&self, user_pubkey: &Pubkey) -> u64 { - *self.user_token_balances.get(user_pubkey).unwrap_or(&0) - } - - // get the amount of stake that belongs to a user - pub fn stake(&self, user_pubkey: &Pubkey) -> u64 { - let tokens = self.tokens(user_pubkey); - if tokens > 0 { - u64::try_from(tokens as u128 * self.total_stake as u128 / self.token_supply as u128) - .unwrap() - } else { - 0 - } - } - - // get the share of the pool that belongs to a user, as a float between 0 and 1 - pub fn share(&self, user_pubkey: &Pubkey) -> f64 { - let tokens = self.tokens(user_pubkey); - if tokens > 0 { - tokens as f64 / self.token_supply as f64 - } else { - 0.0 - } - } - } - - // this deterministically tests basic behavior of calculate_deposit_amount and - // calculate_withdraw_amount - #[test] - fn simple_deposit_withdraw() { - let mut pool = PoolState::default(); - let alice = Pubkey::new_unique(); - let bob = Pubkey::new_unique(); - let chad = Pubkey::new_unique(); - - // first deposit. alice now has 250 - pool.deposit(&alice, 250).unwrap(); - assert_eq!(pool.tokens(&alice), 250); - assert_eq!(pool.token_supply, 250); - assert_eq!(pool.total_stake, 250); - - // second deposit. bob now has 750 - pool.deposit(&bob, 750).unwrap(); - assert_eq!(pool.tokens(&bob), 750); - assert_eq!(pool.token_supply, 1000); - assert_eq!(pool.total_stake, 1000); - - // alice controls 25% of the pool and bob controls 75%. rewards should accrue - // likewise use nice even numbers, we can test fiddly stuff in the - // stochastic cases - assert_relative_eq!(pool.share(&alice), 0.25); - assert_relative_eq!(pool.share(&bob), 0.75); - pool.reward(1000); - assert_eq!(pool.stake(&alice), pool.tokens(&alice) * 2); - assert_eq!(pool.stake(&bob), pool.tokens(&bob) * 2); - assert_relative_eq!(pool.share(&alice), 0.25); - assert_relative_eq!(pool.share(&bob), 0.75); - - // alice harvests rewards, reducing her share of the *previous* pool size to - // 12.5% but because the pool itself has shrunk to 87.5%, its actually - // more like 14.3% luckily chad deposits immediately after to make our - // math easier - let stake_removed = pool.withdraw(&alice, 125).unwrap(); - pool.deposit(&chad, 250).unwrap(); - assert_eq!(stake_removed, 250); - assert_relative_eq!(pool.share(&alice), 0.125); - assert_relative_eq!(pool.share(&bob), 0.75); - - // bob and chad exit the pool - let stake_removed = pool.withdraw(&bob, 750).unwrap(); - assert_eq!(stake_removed, 1500); - assert_relative_eq!(pool.share(&bob), 0.0); - pool.withdraw(&chad, 125).unwrap(); - assert_relative_eq!(pool.share(&alice), 1.0); - } - - // this stochastically tests calculate_deposit_amount and - // calculate_withdraw_amount the objective is specifically to ensure that - // the math does not fail on any combination of state changes the no_minimum - // case is to account for a future where small deposits are possible through - // multistake - #[test_case(rand::random(), false, false; "no_rewards")] - #[test_case(rand::random(), true, false; "with_rewards")] - #[test_case(rand::random(), true, true; "no_minimum")] - fn random_deposit_withdraw(seed: u64, with_rewards: bool, no_minimum: bool) { - println!( - "TEST SEED: {}. edit the test case to pass this value if needed to debug failures", - seed - ); - let mut prng = rand::rngs::StdRng::seed_from_u64(seed); - - // deposit_range is the range of typical deposits within minimum_delegation - // minnow_range is under the minimum for cases where we test that - // op_range is how we roll whether to deposit, withdraw, or reward - // std_range is a standard probability - let deposit_range = Uniform::from(LAMPORTS_PER_SOL..LAMPORTS_PER_SOL * 1000); - let minnow_range = Uniform::from(1..LAMPORTS_PER_SOL); - let op_range = Uniform::from(if with_rewards { 0.0..1.0 } else { 0.0..0.65 }); - let std_range = Uniform::from(0.0..1.0); - - let deposit_amount = |prng: &mut StdRng| { - if no_minimum && prng.gen_bool(0.2) { - minnow_range.sample(prng) - } else { - deposit_range.sample(prng) - } - }; - - // run everything a number of times to get a good sample - for _ in 0..100 { - // PoolState tracks all outstanding tokens and the total combined stake - // there is no reasonable way to track "deposited stake" because reward accrual - // makes this concept incoherent a token corresponds to a - // percentage, not a stake value - let mut pool = PoolState::default(); - - // generate between 1 and 100 users and have ~half of them deposit - // note for most of these tests we adhere to the minimum delegation - // one of the thing we want to see is deposit size being many ooms larger than - // reward size - let mut users = vec![]; - let user_count: usize = prng.gen_range(1..=100); - for _ in 0..user_count { - let user = Pubkey::new_unique(); - - if prng.gen_bool(0.5) { - pool.deposit(&user, deposit_amount(&mut prng)).unwrap(); - } - - users.push(user); - } - - // now we do a set of arbitrary operations and confirm invariants hold - // we underweight withdraw a little bit to lessen the chances we random walk to - // an empty pool - for _ in 0..1000 { - match op_range.sample(&mut prng) { - // deposit a random amount of stake for tokens with a random user - // check their stake, tokens, and share increase by the expected amount - n if n <= 0.35 => { - let user = users.choose(&mut prng).unwrap(); - let prev_share = pool.share(user); - let prev_stake = pool.stake(user); - let prev_token_supply = pool.token_supply; - let prev_total_stake = pool.total_stake; - - let stake_deposited = deposit_amount(&mut prng); - let tokens_minted = pool.deposit(user, stake_deposited).unwrap(); - - // stake increased by exactly the deposit amount - assert_eq!(pool.total_stake - prev_total_stake, stake_deposited); - - // calculated stake fraction is within 2 lamps of deposit amount - assert!( - (pool.stake(user) as i64 - prev_stake as i64 - stake_deposited as i64) - .abs() - <= 2 - ); - - // tokens increased by exactly the mint amount - assert_eq!(pool.token_supply - prev_token_supply, tokens_minted); - - // tokens per supply increased with stake per total - if prev_total_stake > 0 { - assert_relative_eq!( - pool.share(user) - prev_share, - pool.stake(user) as f64 / pool.total_stake as f64 - - prev_stake as f64 / prev_total_stake as f64, - epsilon = 1e-6 - ); - } - } - - // burn a random amount of tokens from a random user with outstanding deposits - // check their stake, tokens, and share decrease by the expected amount - n if n > 0.35 && n <= 0.65 => { - if let Some(user) = users - .iter() - .filter(|user| pool.tokens(user) > 0) - .choose(&mut prng) - { - let prev_tokens = pool.tokens(user); - let prev_share = pool.share(user); - let prev_stake = pool.stake(user); - let prev_token_supply = pool.token_supply; - let prev_total_stake = pool.total_stake; - - let tokens_burned = if std_range.sample(&mut prng) <= 0.1 { - prev_tokens - } else { - prng.gen_range(0..prev_tokens) - }; - let stake_received = pool.withdraw(user, tokens_burned).unwrap(); - - // stake decreased by exactly the withdraw amount - assert_eq!(prev_total_stake - pool.total_stake, stake_received); - - // calculated stake fraction is within 2 lamps of withdraw amount - assert!( - (prev_stake as i64 - - pool.stake(user) as i64 - - stake_received as i64) - .abs() - <= 2 - ); - - // tokens decreased by the burn amount - assert_eq!(prev_token_supply - pool.token_supply, tokens_burned); - - // tokens per supply decreased with stake per total - if pool.total_stake > 0 { - assert_relative_eq!( - prev_share - pool.share(user), - prev_stake as f64 / prev_total_stake as f64 - - pool.stake(user) as f64 / pool.total_stake as f64, - epsilon = 1e-6 - ); - } - }; - } - - // run a single epoch worth of rewards - // check all user shares stay the same and stakes increase by the expected - // amount - _ => { - assert!(with_rewards); - - let prev_shares_stakes = users - .iter() - .map(|user| (user, pool.share(user), pool.stake(user))) - .filter(|(_, _, stake)| stake > &0) - .collect::>(); - - pool.reward((pool.total_stake as f64 * INFLATION_BASE_RATE) as u64); - - for (user, prev_share, prev_stake) in prev_shares_stakes { - // shares are the same before and after - assert_eq!(pool.share(user), prev_share); - - let curr_stake = pool.stake(user); - let stake_share = prev_stake as f64 * INFLATION_BASE_RATE; - let stake_diff = (curr_stake - prev_stake) as f64; - - // stake increase is within 2 lamps when calculated as a difference or a - // percentage - assert!((stake_share - stake_diff).abs() <= 2.0); - } - } - } - } - } - } -} diff --git a/single-pool/program/src/state.rs b/single-pool/program/src/state.rs deleted file mode 100644 index 0c58b87f6d0..00000000000 --- a/single-pool/program/src/state.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! State transition types - -use { - crate::{error::SinglePoolError, find_pool_address}, - borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, - solana_program::{ - account_info::AccountInfo, borsh1::try_from_slice_unchecked, program_error::ProgramError, - pubkey::Pubkey, - }, -}; - -/// Single-Validator Stake Pool account type -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub enum SinglePoolAccountType { - /// Uninitialized account - #[default] - Uninitialized, - /// Main pool account - Pool, -} - -/// Single-Validator Stake Pool account, used to derive all PDAs -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub struct SinglePool { - /// Pool account type, reserved for future compat - pub account_type: SinglePoolAccountType, - /// The vote account this pool is mapped to - pub vote_account_address: Pubkey, -} -impl SinglePool { - /// Create a SinglePool struct from its account info - pub fn from_account_info( - account_info: &AccountInfo, - program_id: &Pubkey, - ) -> Result { - // pool is allocated and owned by this program - if account_info.data_len() == 0 || account_info.owner != program_id { - return Err(SinglePoolError::InvalidPoolAccount.into()); - } - - let pool = try_from_slice_unchecked::(&account_info.data.borrow())?; - - // pool is well-typed - if pool.account_type != SinglePoolAccountType::Pool { - return Err(SinglePoolError::InvalidPoolAccount.into()); - } - - // pool vote account address is properly configured. in practice this is - // irrefutable because the pool is initialized from the address that - // derives it, and never modified - if *account_info.key != find_pool_address(program_id, &pool.vote_account_address) { - return Err(SinglePoolError::InvalidPoolAccount.into()); - } - - Ok(pool) - } -} diff --git a/single-pool/program/tests/accounts.rs b/single-pool/program/tests/accounts.rs deleted file mode 100644 index d6276be2639..00000000000 --- a/single-pool/program/tests/accounts.rs +++ /dev/null @@ -1,304 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![allow(clippy::items_after_test_module)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{ - instruction::Instruction, program_error::ProgramError, pubkey::Pubkey, signature::Signer, - stake, system_program, transaction::Transaction, - }, - spl_single_pool::{ - error::SinglePoolError, - id, - instruction::{self, SinglePoolInstruction}, - }, - test_case::test_case, -}; - -#[derive(Clone, Debug, PartialEq, Eq)] -enum TestMode { - Initialize, - Deposit, - Withdraw, -} - -// build a full transaction for initialize, deposit, and withdraw -// this is used to test knocking out individual accounts, for the sake of -// confirming the pubkeys are checked -async fn build_instructions( - context: &mut ProgramTestContext, - accounts: &SinglePoolAccounts, - test_mode: TestMode, -) -> (Vec, usize) { - let initialize_instructions = if test_mode == TestMode::Initialize { - let slot = context.genesis_config().epoch_schedule.first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - - create_vote( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &accounts.validator, - &accounts.voter.pubkey(), - &accounts.withdrawer.pubkey(), - &accounts.vote_account, - ) - .await; - - transfer( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &accounts.alice.pubkey(), - USER_STARTING_LAMPORTS, - ) - .await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let minimum_delegation = get_pool_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - instruction::initialize( - &id(), - &accounts.vote_account.pubkey(), - &accounts.alice.pubkey(), - &rent, - minimum_delegation, - ) - } else { - accounts - .initialize_for_deposit(context, TEST_STAKE_AMOUNT, None) - .await; - advance_epoch(context).await; - - vec![] - }; - - let deposit_instructions = instruction::deposit( - &id(), - &accounts.pool, - &accounts.alice_stake.pubkey(), - &accounts.alice_token, - &accounts.alice.pubkey(), - &accounts.alice.pubkey(), - ); - - let withdraw_instructions = if test_mode == TestMode::Withdraw { - let transaction = Transaction::new_signed_with_payer( - &deposit_instructions, - Some(&accounts.alice.pubkey()), - &[&accounts.alice], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - create_blank_stake_account( - &mut context.banks_client, - &context.payer, - &accounts.alice, - &context.last_blockhash, - &accounts.alice_stake, - ) - .await; - - instruction::withdraw( - &id(), - &accounts.pool, - &accounts.alice_stake.pubkey(), - &accounts.alice.pubkey(), - &accounts.alice_token, - &accounts.alice.pubkey(), - get_token_balance(&mut context.banks_client, &accounts.alice_token).await, - ) - } else { - vec![] - }; - - let (instructions, i) = match test_mode { - TestMode::Initialize => (initialize_instructions, 3), - TestMode::Deposit => (deposit_instructions, 2), - TestMode::Withdraw => (withdraw_instructions, 1), - }; - - // guard against instructions moving with code changes - assert_eq!(instructions[i].program_id, id()); - - (instructions, i) -} - -// test that account addresses are checked properly -#[test_case(TestMode::Initialize; "initialize")] -#[test_case(TestMode::Deposit; "deposit")] -#[test_case(TestMode::Withdraw; "withdraw")] -#[tokio::test] -async fn fail_account_checks(test_mode: TestMode) { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - let (instructions, i) = build_instructions(&mut context, &accounts, test_mode).await; - - for j in 0..instructions[i].accounts.len() { - let mut instructions = instructions.clone(); - let instruction_account = &mut instructions[i].accounts[j]; - - // wallet address can be arbitrary - if instruction_account.pubkey == accounts.alice.pubkey() { - continue; - } - - let prev_pubkey = instruction_account.pubkey; - instruction_account.pubkey = Pubkey::new_unique(); - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&accounts.alice.pubkey()), - &[&accounts.alice], - context.last_blockhash, - ); - - // random addresses should error always otherwise - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - - // these ones we can also make sure we hit the explicit check, before we use it - if prev_pubkey == accounts.pool { - check_error(e, SinglePoolError::InvalidPoolAccount) - } else if prev_pubkey == accounts.stake_account { - check_error(e, SinglePoolError::InvalidPoolStakeAccount) - } else if prev_pubkey == accounts.stake_authority { - check_error(e, SinglePoolError::InvalidPoolStakeAuthority) - } else if prev_pubkey == accounts.mint_authority { - check_error(e, SinglePoolError::InvalidPoolMintAuthority) - } else if prev_pubkey == accounts.mpl_authority { - check_error(e, SinglePoolError::InvalidPoolMplAuthority) - } else if prev_pubkey == accounts.mint { - check_error(e, SinglePoolError::InvalidPoolMint) - } else if [system_program::id(), spl_token::id(), stake::program::id()] - .contains(&prev_pubkey) - { - check_error(e, ProgramError::IncorrectProgramId) - } - } -} - -// make an individual instruction for all program instructions -// the match is just so this will error if new instructions are added -// if you are reading this because of that error, add the case to the -// `consistent_account_order` test!!! -fn make_basic_instruction( - accounts: &SinglePoolAccounts, - instruction_type: SinglePoolInstruction, -) -> Instruction { - match instruction_type { - SinglePoolInstruction::InitializePool => { - instruction::initialize_pool(&id(), &accounts.vote_account.pubkey()) - } - SinglePoolInstruction::ReactivatePoolStake => { - instruction::reactivate_pool_stake(&id(), &accounts.vote_account.pubkey()) - } - SinglePoolInstruction::DepositStake => instruction::deposit_stake( - &id(), - &accounts.pool, - &Pubkey::default(), - &Pubkey::default(), - &Pubkey::default(), - ), - SinglePoolInstruction::WithdrawStake { .. } => instruction::withdraw_stake( - &id(), - &accounts.pool, - &Pubkey::default(), - &Pubkey::default(), - &Pubkey::default(), - 0, - ), - SinglePoolInstruction::CreateTokenMetadata => { - instruction::create_token_metadata(&id(), &accounts.pool, &Pubkey::default()) - } - SinglePoolInstruction::UpdateTokenMetadata { .. } => instruction::update_token_metadata( - &id(), - &accounts.vote_account.pubkey(), - &accounts.withdrawer.pubkey(), - "".to_string(), - "".to_string(), - "".to_string(), - ), - } -} - -// advanced technology -fn is_sorted(data: &[T]) -> bool -where - T: Ord, -{ - data.windows(2).all(|w| w[0] <= w[1]) -} - -// check that major accounts always show up in the same order, to spare -// developer confusion -#[test] -fn consistent_account_order() { - let accounts = SinglePoolAccounts::default(); - - let ordering = vec![ - accounts.vote_account.pubkey(), - accounts.pool, - accounts.stake_account, - accounts.mint, - accounts.stake_authority, - accounts.mint_authority, - accounts.mpl_authority, - ]; - - let instructions = vec![ - make_basic_instruction(&accounts, SinglePoolInstruction::InitializePool), - make_basic_instruction(&accounts, SinglePoolInstruction::ReactivatePoolStake), - make_basic_instruction(&accounts, SinglePoolInstruction::DepositStake), - make_basic_instruction( - &accounts, - SinglePoolInstruction::WithdrawStake { - user_stake_authority: Pubkey::default(), - token_amount: 0, - }, - ), - make_basic_instruction(&accounts, SinglePoolInstruction::CreateTokenMetadata), - make_basic_instruction( - &accounts, - SinglePoolInstruction::UpdateTokenMetadata { - name: "".to_string(), - symbol: "".to_string(), - uri: "".to_string(), - }, - ), - ]; - - for instruction in instructions { - let mut indexes = vec![]; - - for target in &ordering { - if let Some(i) = instruction - .accounts - .iter() - .position(|meta| meta.pubkey == *target) - { - indexes.push(i); - } - } - - assert!(is_sorted(&indexes)); - } -} diff --git a/single-pool/program/tests/create_pool_token_metadata.rs b/single-pool/program/tests/create_pool_token_metadata.rs deleted file mode 100644 index 930c0f4f7cc..00000000000 --- a/single-pool/program/tests/create_pool_token_metadata.rs +++ /dev/null @@ -1,57 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, - system_instruction::SystemError, transaction::Transaction, - }, - spl_single_pool::{id, instruction}, -}; - -fn assert_metadata(vote_account: &Pubkey, metadata: &Metadata) { - let vote_address_str = vote_account.to_string(); - let name = format!("SPL Single Pool {}", &vote_address_str[0..15]); - let symbol = format!("st{}", &vote_address_str[0..7]); - - assert!(metadata.name.starts_with(&name)); - assert!(metadata.symbol.starts_with(&symbol)); -} - -#[tokio::test] -async fn success() { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - accounts.initialize(&mut context).await; - - let metadata = get_metadata_account(&mut context.banks_client, &accounts.mint).await; - assert_metadata(&accounts.vote_account.pubkey(), &metadata); -} - -#[tokio::test] -async fn fail_double_init() { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - accounts.initialize(&mut context).await; - refresh_blockhash(&mut context).await; - - let instruction = - instruction::create_token_metadata(&id(), &accounts.pool, &context.payer.pubkey()); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error::(e, SystemError::AccountAlreadyInUse.into()); -} diff --git a/single-pool/program/tests/deposit.rs b/single-pool/program/tests/deposit.rs deleted file mode 100644 index 1fcc0523d81..00000000000 --- a/single-pool/program/tests/deposit.rs +++ /dev/null @@ -1,477 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{ - signature::Signer, - signer::keypair::Keypair, - stake::state::{Authorized, Lockup}, - transaction::Transaction, - }, - spl_associated_token_account_client::address as atoken, - spl_single_pool::{ - error::SinglePoolError, find_default_deposit_account_address, id, instruction, - }, - test_case::test_case, -}; - -#[test_case(true, 0, false, false, false; "activated::minimum_disabled")] -#[test_case(true, 0, false, false, true; "activated::minimum_disabled::small")] -#[test_case(true, 0, false, true, false; "activated::minimum_enabled")] -#[test_case(false, 0, false, false, false; "activating::minimum_disabled")] -#[test_case(false, 0, false, false, true; "activating::minimum_disabled::small")] -#[test_case(false, 0, false, true, false; "activating::minimum_enabled")] -#[test_case(true, 100_000, false, false, false; "activated::extra")] -#[test_case(false, 100_000, false, false, false; "activating::extra")] -#[test_case(true, 0, true, false, false; "activated::second")] -#[test_case(false, 0, true, false, false; "activating::second")] -#[tokio::test] -async fn success( - activate: bool, - extra_lamports: u64, - prior_deposit: bool, - enable_minimum_delegation: bool, - small_deposit: bool, -) { - let mut context = program_test(enable_minimum_delegation) - .start_with_context() - .await; - let accounts = SinglePoolAccounts::default(); - accounts - .initialize_for_deposit( - &mut context, - if small_deposit { 1 } else { TEST_STAKE_AMOUNT }, - if prior_deposit { - Some(TEST_STAKE_AMOUNT * 10) - } else { - None - }, - ) - .await; - - if activate { - advance_epoch(&mut context).await; - } - - if prior_deposit { - let instructions = instruction::deposit( - &id(), - &accounts.pool, - &accounts.bob_stake.pubkey(), - &accounts.bob_token, - &accounts.bob.pubkey(), - &accounts.bob.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer, &accounts.bob], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - } - - let (_, alice_stake_before_deposit, stake_lamports) = - get_stake_account(&mut context.banks_client, &accounts.alice_stake.pubkey()).await; - let alice_stake_before_deposit = alice_stake_before_deposit.unwrap().delegation.stake; - - let (_, pool_stake_before, pool_lamports_before) = - get_stake_account(&mut context.banks_client, &accounts.stake_account).await; - let pool_stake_before = pool_stake_before.unwrap().delegation.stake; - - if extra_lamports > 0 { - transfer( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &accounts.stake_account, - extra_lamports, - ) - .await; - } - - let instructions = instruction::deposit( - &id(), - &accounts.pool, - &accounts.alice_stake.pubkey(), - &accounts.alice_token, - &accounts.alice.pubkey(), - &accounts.alice.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer, &accounts.alice], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let wallet_lamports_after_deposit = - get_account(&mut context.banks_client, &accounts.alice.pubkey()) - .await - .lamports; - - let (pool_meta_after, pool_stake_after, pool_lamports_after) = - get_stake_account(&mut context.banks_client, &accounts.stake_account).await; - let pool_stake_after = pool_stake_after.unwrap().delegation.stake; - - // when active, the depositor gets their rent back - // but when activating, its just added to stake - let expected_deposit = if activate { - alice_stake_before_deposit - } else { - stake_lamports - }; - - // deposit stake account is closed - assert!(context - .banks_client - .get_account(accounts.alice_stake.pubkey()) - .await - .expect("get_account") - .is_none()); - - // entire stake has moved to pool - assert_eq!(pool_stake_before + expected_deposit, pool_stake_after); - - // pool only gained stake - assert_eq!(pool_lamports_after, pool_lamports_before + expected_deposit); - assert_eq!( - pool_lamports_after, - pool_stake_before + expected_deposit + pool_meta_after.rent_exempt_reserve, - ); - - // alice got her rent back if active, or everything otherwise - // and if someone sent lamports to the stake account, the next depositor gets - // them - assert_eq!( - wallet_lamports_after_deposit, - USER_STARTING_LAMPORTS - expected_deposit + extra_lamports, - ); - - // alice got tokens. no rewards have been paid so tokens correspond to stake 1:1 - assert_eq!( - get_token_balance(&mut context.banks_client, &accounts.alice_token).await, - expected_deposit, - ); -} - -#[test_case(true, false, false; "activated::minimum_disabled")] -#[test_case(true, false, true; "activated::minimum_disabled::small")] -#[test_case(true, true, false; "activated::minimum_enabled")] -#[test_case(false, false, false; "activating::minimum_disabled")] -#[test_case(false, false, true; "activating::minimum_disabled::small")] -#[test_case(false, true, false; "activating::minimum_enabled")] -#[tokio::test] -async fn success_with_seed(activate: bool, enable_minimum_delegation: bool, small_deposit: bool) { - let mut context = program_test(enable_minimum_delegation) - .start_with_context() - .await; - let accounts = SinglePoolAccounts::default(); - let rent = context.banks_client.get_rent().await.unwrap(); - let minimum_stake = accounts.initialize(&mut context).await; - let alice_default_stake = - find_default_deposit_account_address(&accounts.pool, &accounts.alice.pubkey()); - - let instructions = instruction::create_and_delegate_user_stake( - &id(), - &accounts.vote_account.pubkey(), - &accounts.alice.pubkey(), - &rent, - if small_deposit { 1 } else { minimum_stake }, - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer, &accounts.alice], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - if activate { - advance_epoch(&mut context).await; - } - - let (_, alice_stake_before_deposit, stake_lamports) = - get_stake_account(&mut context.banks_client, &alice_default_stake).await; - let alice_stake_before_deposit = alice_stake_before_deposit.unwrap().delegation.stake; - - let instructions = instruction::deposit( - &id(), - &accounts.pool, - &alice_default_stake, - &accounts.alice_token, - &accounts.alice.pubkey(), - &accounts.alice.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer, &accounts.alice], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let wallet_lamports_after_deposit = - get_account(&mut context.banks_client, &accounts.alice.pubkey()) - .await - .lamports; - - let (_, pool_stake_after, _) = - get_stake_account(&mut context.banks_client, &accounts.stake_account).await; - let pool_stake_after = pool_stake_after.unwrap().delegation.stake; - - let expected_deposit = if activate { - alice_stake_before_deposit - } else { - stake_lamports - }; - - // deposit stake account is closed - assert!(context - .banks_client - .get_account(alice_default_stake) - .await - .expect("get_account") - .is_none()); - - // stake moved to pool - assert_eq!(minimum_stake + expected_deposit, pool_stake_after); - - // alice got her rent back if active, or everything otherwise - assert_eq!( - wallet_lamports_after_deposit, - USER_STARTING_LAMPORTS - expected_deposit - ); - - // alice got tokens. no rewards have been paid so tokens correspond to stake 1:1 - assert_eq!( - get_token_balance(&mut context.banks_client, &accounts.alice_token).await, - expected_deposit, - ); -} - -#[test_case(true; "activated")] -#[test_case(false; "activating")] -#[tokio::test] -async fn fail_uninitialized(activate: bool) { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - let stake_account = Keypair::new(); - - let slot = context.genesis_config().epoch_schedule.first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - - create_vote( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &accounts.validator, - &accounts.voter.pubkey(), - &accounts.withdrawer.pubkey(), - &accounts.vote_account, - ) - .await; - - let token_account = - atoken::get_associated_token_address(&context.payer.pubkey(), &accounts.mint); - - create_independent_stake_account( - &mut context.banks_client, - &context.payer, - &context.payer, - &context.last_blockhash, - &stake_account, - &Authorized::auto(&context.payer.pubkey()), - &Lockup::default(), - TEST_STAKE_AMOUNT, - ) - .await; - - delegate_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_account.pubkey(), - &context.payer, - &accounts.vote_account.pubkey(), - ) - .await; - - if activate { - advance_epoch(&mut context).await; - } - - let instructions = instruction::deposit( - &id(), - &accounts.pool, - &stake_account.pubkey(), - &token_account, - &context.payer.pubkey(), - &context.payer.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error(e, SinglePoolError::InvalidPoolAccount); -} - -#[test_case(true, true; "activated::automorph")] -#[test_case(false, true; "activating::automorph")] -#[test_case(true, false; "activated::unauth")] -#[test_case(false, false; "activating::unauth")] -#[tokio::test] -async fn fail_bad_account(activate: bool, automorph: bool) { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - accounts - .initialize_for_deposit(&mut context, TEST_STAKE_AMOUNT, None) - .await; - - let instruction = instruction::deposit_stake( - &id(), - &accounts.pool, - &if automorph { - accounts.stake_account - } else { - accounts.alice_stake.pubkey() - }, - &accounts.alice_token, - &accounts.alice.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&accounts.alice.pubkey()), - &[&accounts.alice], - context.last_blockhash, - ); - - if activate { - advance_epoch(&mut context).await; - } - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - - if automorph { - check_error(e, SinglePoolError::InvalidPoolStakeAccountUsage); - } else { - check_error(e, SinglePoolError::WrongStakeStake); - } -} - -#[test_case(true; "pool_active")] -#[test_case(false; "user_active")] -#[tokio::test] -async fn fail_activation_mismatch(pool_first: bool) { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - - let minimum_delegation = get_pool_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - create_vote( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &accounts.validator, - &accounts.voter.pubkey(), - &accounts.withdrawer.pubkey(), - &accounts.vote_account, - ) - .await; - - if pool_first { - accounts.initialize(&mut context).await; - advance_epoch(&mut context).await; - } - - create_independent_stake_account( - &mut context.banks_client, - &context.payer, - &context.payer, - &context.last_blockhash, - &accounts.alice_stake, - &Authorized::auto(&accounts.alice.pubkey()), - &Lockup::default(), - minimum_delegation, - ) - .await; - - delegate_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &accounts.alice_stake.pubkey(), - &accounts.alice, - &accounts.vote_account.pubkey(), - ) - .await; - - if !pool_first { - advance_epoch(&mut context).await; - accounts.initialize(&mut context).await; - } - - let instructions = instruction::deposit( - &id(), - &accounts.pool, - &accounts.alice_stake.pubkey(), - &accounts.alice_token, - &accounts.alice.pubkey(), - &accounts.alice.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&accounts.alice.pubkey()), - &[&accounts.alice], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error(e, SinglePoolError::WrongStakeStake); -} diff --git a/single-pool/program/tests/fixtures/mpl_token_metadata.so b/single-pool/program/tests/fixtures/mpl_token_metadata.so deleted file mode 120000 index 43816797329..00000000000 --- a/single-pool/program/tests/fixtures/mpl_token_metadata.so +++ /dev/null @@ -1 +0,0 @@ -../../../../stake-pool/program/tests/fixtures/mpl_token_metadata.so \ No newline at end of file diff --git a/single-pool/program/tests/helpers/mod.rs b/single-pool/program/tests/helpers/mod.rs deleted file mode 100644 index 29dd1e178fd..00000000000 --- a/single-pool/program/tests/helpers/mod.rs +++ /dev/null @@ -1,450 +0,0 @@ -#![allow(dead_code)] // needed because cargo doesn't understand test usage - -use { - solana_program_test::*, - solana_sdk::{ - account::Account as SolanaAccount, - feature_set::stake_raise_minimum_delegation_to_1_sol, - hash::Hash, - program_error::ProgramError, - pubkey::Pubkey, - signature::{Keypair, Signer}, - stake::state::{Authorized, Lockup}, - system_instruction, system_program, - transaction::{Transaction, TransactionError}, - }, - solana_vote_program::{ - self, vote_instruction, - vote_state::{VoteInit, VoteState}, - }, - spl_associated_token_account_client::address as atoken, - spl_single_pool::{ - find_pool_address, find_pool_mint_address, find_pool_mint_authority_address, - find_pool_mpl_authority_address, find_pool_stake_address, - find_pool_stake_authority_address, id, inline_mpl_token_metadata, instruction, - processor::Processor, - }, -}; - -pub mod token; -pub use token::*; - -pub mod stake; -pub use stake::*; - -pub const FIRST_NORMAL_EPOCH: u64 = 15; -pub const USER_STARTING_LAMPORTS: u64 = 10_000_000_000_000; // 10k sol - -pub fn program_test(enable_minimum_delegation: bool) -> ProgramTest { - let mut program_test = ProgramTest::default(); - - program_test.add_program("mpl_token_metadata", inline_mpl_token_metadata::id(), None); - program_test.add_program("spl_single_pool", id(), processor!(Processor::process)); - program_test.prefer_bpf(false); - - if !enable_minimum_delegation { - program_test.deactivate_feature(stake_raise_minimum_delegation_to_1_sol::id()); - } - - program_test -} - -#[derive(Debug, PartialEq)] -pub struct SinglePoolAccounts { - pub validator: Keypair, - pub voter: Keypair, - pub withdrawer: Keypair, - pub vote_account: Keypair, - pub pool: Pubkey, - pub stake_account: Pubkey, - pub mint: Pubkey, - pub stake_authority: Pubkey, - pub mint_authority: Pubkey, - pub mpl_authority: Pubkey, - pub alice: Keypair, - pub bob: Keypair, - pub alice_stake: Keypair, - pub bob_stake: Keypair, - pub alice_token: Pubkey, - pub bob_token: Pubkey, - pub token_program_id: Pubkey, -} -impl SinglePoolAccounts { - // does everything in initialize_for_deposit plus performs the deposit(s) and - // creates blank account(s) optionally advances to activation before the - // deposit - pub async fn initialize_for_withdraw( - &self, - context: &mut ProgramTestContext, - alice_amount: u64, - maybe_bob_amount: Option, - activate: bool, - ) -> u64 { - let minimum_delegation = self - .initialize_for_deposit(context, alice_amount, maybe_bob_amount) - .await; - - if activate { - advance_epoch(context).await; - } - - let instructions = instruction::deposit( - &id(), - &self.pool, - &self.alice_stake.pubkey(), - &self.alice_token, - &self.alice.pubkey(), - &self.alice.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer, &self.alice], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - create_blank_stake_account( - &mut context.banks_client, - &context.payer, - &self.alice, - &context.last_blockhash, - &self.alice_stake, - ) - .await; - - if maybe_bob_amount.is_some() { - let instructions = instruction::deposit( - &id(), - &self.pool, - &self.bob_stake.pubkey(), - &self.bob_token, - &self.bob.pubkey(), - &self.bob.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer, &self.bob], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - create_blank_stake_account( - &mut context.banks_client, - &context.payer, - &self.bob, - &context.last_blockhash, - &self.bob_stake, - ) - .await; - } - - minimum_delegation - } - - // does everything in initialize plus creates/delegates one or both stake - // accounts for our users note this does not advance time, so everything is - // in an activating state - pub async fn initialize_for_deposit( - &self, - context: &mut ProgramTestContext, - alice_amount: u64, - maybe_bob_amount: Option, - ) -> u64 { - let minimum_delegation = self.initialize(context).await; - - create_independent_stake_account( - &mut context.banks_client, - &context.payer, - &self.alice, - &context.last_blockhash, - &self.alice_stake, - &Authorized::auto(&self.alice.pubkey()), - &Lockup::default(), - alice_amount, - ) - .await; - - delegate_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &self.alice_stake.pubkey(), - &self.alice, - &self.vote_account.pubkey(), - ) - .await; - - if let Some(bob_amount) = maybe_bob_amount { - create_independent_stake_account( - &mut context.banks_client, - &context.payer, - &self.bob, - &context.last_blockhash, - &self.bob_stake, - &Authorized::auto(&self.bob.pubkey()), - &Lockup::default(), - bob_amount, - ) - .await; - - delegate_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &self.bob_stake.pubkey(), - &self.bob, - &self.vote_account.pubkey(), - ) - .await; - }; - - minimum_delegation - } - - // creates a vote account and stake pool for it. also sets up two users with sol - // and token accounts note this leaves the pool in an activating state. - // caller can advance to next epoch if they please - pub async fn initialize(&self, context: &mut ProgramTestContext) -> u64 { - let slot = context.genesis_config().epoch_schedule.first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - - create_vote( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &self.validator, - &self.voter.pubkey(), - &self.withdrawer.pubkey(), - &self.vote_account, - ) - .await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let minimum_delegation = get_pool_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - let instructions = instruction::initialize( - &id(), - &self.vote_account.pubkey(), - &context.payer.pubkey(), - &rent, - minimum_delegation, - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - transfer( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &self.alice.pubkey(), - USER_STARTING_LAMPORTS, - ) - .await; - - transfer( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &self.bob.pubkey(), - USER_STARTING_LAMPORTS, - ) - .await; - - create_ata( - &mut context.banks_client, - &context.payer, - &self.alice.pubkey(), - &context.last_blockhash, - &self.mint, - ) - .await; - - create_ata( - &mut context.banks_client, - &context.payer, - &self.bob.pubkey(), - &context.last_blockhash, - &self.mint, - ) - .await; - - minimum_delegation - } -} -impl Default for SinglePoolAccounts { - fn default() -> Self { - let vote_account = Keypair::new(); - let alice = Keypair::new(); - let bob = Keypair::new(); - let pool = find_pool_address(&id(), &vote_account.pubkey()); - let mint = find_pool_mint_address(&id(), &pool); - - Self { - validator: Keypair::new(), - voter: Keypair::new(), - withdrawer: Keypair::new(), - stake_account: find_pool_stake_address(&id(), &pool), - pool, - mint, - stake_authority: find_pool_stake_authority_address(&id(), &pool), - mint_authority: find_pool_mint_authority_address(&id(), &pool), - mpl_authority: find_pool_mpl_authority_address(&id(), &pool), - vote_account, - alice_stake: Keypair::new(), - bob_stake: Keypair::new(), - alice_token: atoken::get_associated_token_address(&alice.pubkey(), &mint), - bob_token: atoken::get_associated_token_address(&bob.pubkey(), &mint), - alice, - bob, - token_program_id: spl_token::id(), - } - } -} - -pub async fn refresh_blockhash(context: &mut ProgramTestContext) { - context.last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); -} - -pub async fn advance_epoch(context: &mut ProgramTestContext) { - let root_slot = context.banks_client.get_root_slot().await.unwrap(); - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - context.warp_to_slot(root_slot + slots_per_epoch).unwrap(); -} - -pub async fn get_account(banks_client: &mut BanksClient, pubkey: &Pubkey) -> SolanaAccount { - banks_client - .get_account(*pubkey) - .await - .expect("client error") - .expect("account not found") -} - -pub async fn create_vote( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - validator: &Keypair, - voter: &Pubkey, - withdrawer: &Pubkey, - vote_account: &Keypair, -) { - let rent = banks_client.get_rent().await.unwrap(); - let rent_voter = rent.minimum_balance(VoteState::size_of()); - - let mut instructions = vec![system_instruction::create_account( - &payer.pubkey(), - &validator.pubkey(), - rent.minimum_balance(0), - 0, - &system_program::id(), - )]; - instructions.append(&mut vote_instruction::create_account_with_config( - &payer.pubkey(), - &vote_account.pubkey(), - &VoteInit { - node_pubkey: validator.pubkey(), - authorized_voter: *voter, - authorized_withdrawer: *withdrawer, - ..VoteInit::default() - }, - rent_voter, - vote_instruction::CreateVoteAccountConfig { - space: VoteState::size_of() as u64, - ..Default::default() - }, - )); - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[validator, vote_account, payer], - *recent_blockhash, - ); - - // ignore errors for idempotency - let _ = banks_client.process_transaction(transaction).await; -} - -pub async fn transfer( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - recipient: &Pubkey, - amount: u64, -) { - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - recipient, - amount, - )], - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -pub fn check_error(got: BanksClientError, expected: T) -where - ProgramError: TryFrom, -{ - // banks error -> transaction error -> instruction error -> program error - let got_p: ProgramError = if let TransactionError::InstructionError(_, e) = got.unwrap() { - e.try_into().unwrap() - } else { - panic!( - "couldn't convert {:?} to ProgramError (expected {:?})", - got, expected - ); - }; - - // this silly thing is because we can guarantee From has a Debug for T - // but TryFrom produces Result and E may not have Debug. so we can't - // call unwrap also we use TryFrom because we have to go `instruction - // error-> program error` because StakeError impls the former but not the - // latter... and that conversion is merely surjective........ - // infomercial lady: "if only there were a better way!" - let expected_p = match expected.clone().try_into() { - Ok(v) => v, - Err(_) => panic!("could not unwrap {:?}", expected), - }; - - if got_p != expected_p { - panic!( - "error comparison failed!\n\nGOT: {:#?} / ({:?})\n\nEXPECTED: {:#?} / ({:?})\n\n", - got, got_p, expected, expected_p - ); - } -} diff --git a/single-pool/program/tests/helpers/stake.rs b/single-pool/program/tests/helpers/stake.rs deleted file mode 100644 index 73047eeb310..00000000000 --- a/single-pool/program/tests/helpers/stake.rs +++ /dev/null @@ -1,140 +0,0 @@ -#![allow(dead_code)] // needed because cargo doesn't understand test usage - -use { - crate::get_account, - bincode::deserialize, - solana_program_test::BanksClient, - solana_sdk::{ - hash::Hash, - native_token::LAMPORTS_PER_SOL, - pubkey::Pubkey, - signature::{Keypair, Signer}, - stake::{ - self, - state::{Meta, Stake, StakeStateV2}, - }, - system_instruction, - transaction::Transaction, - }, - std::convert::TryInto, -}; - -pub const TEST_STAKE_AMOUNT: u64 = 10_000_000_000; // 10 sol - -pub async fn get_stake_account( - banks_client: &mut BanksClient, - pubkey: &Pubkey, -) -> (Meta, Option, u64) { - let stake_account = get_account(banks_client, pubkey).await; - let lamports = stake_account.lamports; - match deserialize::(&stake_account.data).unwrap() { - StakeStateV2::Initialized(meta) => (meta, None, lamports), - StakeStateV2::Stake(meta, stake, _) => (meta, Some(stake), lamports), - _ => unimplemented!(), - } -} - -pub async fn get_stake_account_rent(banks_client: &mut BanksClient) -> u64 { - let rent = banks_client.get_rent().await.unwrap(); - rent.minimum_balance(std::mem::size_of::()) -} - -pub async fn get_pool_minimum_delegation( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, -) -> u64 { - let transaction = Transaction::new_signed_with_payer( - &[stake::instruction::get_minimum_delegation()], - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - let mut data = banks_client - .simulate_transaction(transaction) - .await - .unwrap() - .simulation_details - .unwrap() - .return_data - .unwrap() - .data; - data.resize(8, 0); - let stake_program_minimum = data.try_into().map(u64::from_le_bytes).unwrap(); - - std::cmp::max(stake_program_minimum, LAMPORTS_PER_SOL) -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_independent_stake_account( - banks_client: &mut BanksClient, - fee_payer: &Keypair, - rent_payer: &Keypair, - recent_blockhash: &Hash, - stake: &Keypair, - authorized: &stake::state::Authorized, - lockup: &stake::state::Lockup, - stake_amount: u64, -) -> u64 { - let lamports = get_stake_account_rent(banks_client).await + stake_amount; - let transaction = Transaction::new_signed_with_payer( - &stake::instruction::create_account( - &rent_payer.pubkey(), - &stake.pubkey(), - authorized, - lockup, - lamports, - ), - Some(&fee_payer.pubkey()), - &[fee_payer, rent_payer, stake], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - lamports -} - -pub async fn create_blank_stake_account( - banks_client: &mut BanksClient, - fee_payer: &Keypair, - rent_payer: &Keypair, - recent_blockhash: &Hash, - stake: &Keypair, -) -> u64 { - let lamports = get_stake_account_rent(banks_client).await; - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::create_account( - &rent_payer.pubkey(), - &stake.pubkey(), - lamports, - std::mem::size_of::() as u64, - &stake::program::id(), - )], - Some(&fee_payer.pubkey()), - &[fee_payer, rent_payer, stake], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - lamports -} - -pub async fn delegate_stake_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake: &Pubkey, - authorized: &Keypair, - vote: &Pubkey, -) { - let mut transaction = Transaction::new_with_payer( - &[stake::instruction::delegate_stake( - stake, - &authorized.pubkey(), - vote, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[payer, authorized], *recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); -} diff --git a/single-pool/program/tests/helpers/token.rs b/single-pool/program/tests/helpers/token.rs deleted file mode 100644 index 582bd7ab6cd..00000000000 --- a/single-pool/program/tests/helpers/token.rs +++ /dev/null @@ -1,76 +0,0 @@ -#![allow(dead_code)] - -use { - borsh::BorshDeserialize, - solana_program_test::BanksClient, - solana_sdk::{ - borsh1::try_from_slice_unchecked, - hash::Hash, - program_pack::Pack, - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::Transaction, - }, - spl_associated_token_account as atoken, - spl_single_pool::inline_mpl_token_metadata::pda::find_metadata_account, - spl_token::state::{Account, Mint}, -}; - -pub async fn create_ata( - banks_client: &mut BanksClient, - payer: &Keypair, - owner: &Pubkey, - recent_blockhash: &Hash, - pool_mint: &Pubkey, -) { - let instruction = atoken::instruction::create_associated_token_account( - &payer.pubkey(), - owner, - pool_mint, - &spl_token::id(), - ); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - - banks_client.process_transaction(transaction).await.unwrap(); -} - -pub async fn get_token_balance(banks_client: &mut BanksClient, token: &Pubkey) -> u64 { - let token_account = banks_client.get_account(*token).await.unwrap().unwrap(); - let account_info = Account::unpack_from_slice(&token_account.data).unwrap(); - account_info.amount -} - -pub async fn get_token_supply(banks_client: &mut BanksClient, mint: &Pubkey) -> u64 { - let mint_account = banks_client.get_account(*mint).await.unwrap().unwrap(); - let account_info = Mint::unpack_from_slice(&mint_account.data).unwrap(); - account_info.supply -} - -#[derive(Clone, BorshDeserialize, Debug, PartialEq, Eq)] -pub struct Metadata { - pub key: u8, - pub update_authority: Pubkey, - pub mint: Pubkey, - pub name: String, - pub symbol: String, - pub uri: String, - pub seller_fee_basis_points: u16, - pub creators: Option>, - pub primary_sale_happened: bool, - pub is_mutable: bool, -} - -pub async fn get_metadata_account(banks_client: &mut BanksClient, token_mint: &Pubkey) -> Metadata { - let (token_metadata, _) = find_metadata_account(token_mint); - let token_metadata_account = banks_client - .get_account(token_metadata) - .await - .unwrap() - .unwrap(); - try_from_slice_unchecked(token_metadata_account.data.as_slice()).unwrap() -} diff --git a/single-pool/program/tests/initialize.rs b/single-pool/program/tests/initialize.rs deleted file mode 100644 index 88fddd89282..00000000000 --- a/single-pool/program/tests/initialize.rs +++ /dev/null @@ -1,116 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{program_pack::Pack, signature::Signer, stake, transaction::Transaction}, - spl_single_pool::{error::SinglePoolError, id, instruction}, - spl_token::state::Mint, - test_case::test_case, -}; - -#[test_case(true; "minimum_enabled")] -#[test_case(false; "minimum_disabled")] -#[tokio::test] -async fn success(enable_minimum_delegation: bool) { - let mut context = program_test(enable_minimum_delegation) - .start_with_context() - .await; - let accounts = SinglePoolAccounts::default(); - accounts.initialize(&mut context).await; - - // mint exists - let mint_account = get_account(&mut context.banks_client, &accounts.mint).await; - Mint::unpack_from_slice(&mint_account.data).unwrap(); - - // stake account exists - let stake_account = get_account(&mut context.banks_client, &accounts.stake_account).await; - assert_eq!(stake_account.owner, stake::program::id()); -} - -#[tokio::test] -async fn fail_double_init() { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - let minimum_delegation = accounts.initialize(&mut context).await; - refresh_blockhash(&mut context).await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let instructions = instruction::initialize( - &id(), - &accounts.vote_account.pubkey(), - &context.payer.pubkey(), - &rent, - minimum_delegation, - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error(e, SinglePoolError::PoolAlreadyInitialized); -} - -#[test_case(true; "minimum_enabled")] -#[test_case(false; "minimum_disabled")] -#[tokio::test] -async fn fail_below_pool_minimum(enable_minimum_delegation: bool) { - let mut context = program_test(enable_minimum_delegation) - .start_with_context() - .await; - let accounts = SinglePoolAccounts::default(); - let slot = context.genesis_config().epoch_schedule.first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - - create_vote( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &accounts.validator, - &accounts.voter.pubkey(), - &accounts.withdrawer.pubkey(), - &accounts.vote_account, - ) - .await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let minimum_delegation = get_pool_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - let instructions = instruction::initialize( - &id(), - &accounts.vote_account.pubkey(), - &context.payer.pubkey(), - &rent, - minimum_delegation - 1, - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error(e, SinglePoolError::WrongRentAmount); -} - -// TODO test that init can succeed without mpl program diff --git a/single-pool/program/tests/reactivate.rs b/single-pool/program/tests/reactivate.rs deleted file mode 100644 index 2b43dfa3149..00000000000 --- a/single-pool/program/tests/reactivate.rs +++ /dev/null @@ -1,156 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{ - account::AccountSharedData, - signature::Signer, - stake::{ - stake_flags::StakeFlags, - state::{Delegation, Stake, StakeStateV2}, - }, - transaction::Transaction, - }, - spl_single_pool::{error::SinglePoolError, id, instruction}, - test_case::test_case, -}; - -#[tokio::test] -async fn success() { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - accounts - .initialize_for_deposit(&mut context, TEST_STAKE_AMOUNT, None) - .await; - advance_epoch(&mut context).await; - - // deactivate the pool stake account - let (meta, stake, _) = - get_stake_account(&mut context.banks_client, &accounts.stake_account).await; - let delegation = Delegation { - activation_epoch: 0, - deactivation_epoch: 0, - ..stake.unwrap().delegation - }; - let mut account_data = vec![0; std::mem::size_of::()]; - bincode::serialize_into( - &mut account_data[..], - &StakeStateV2::Stake( - meta, - Stake { - delegation, - ..stake.unwrap() - }, - StakeFlags::empty(), - ), - ) - .unwrap(); - - let mut stake_account = get_account(&mut context.banks_client, &accounts.stake_account).await; - stake_account.data = account_data; - context.set_account( - &accounts.stake_account, - &AccountSharedData::from(stake_account), - ); - - // make sure deposit fails - let instructions = instruction::deposit( - &id(), - &accounts.pool, - &accounts.alice_stake.pubkey(), - &accounts.alice_token, - &accounts.alice.pubkey(), - &accounts.alice.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer, &accounts.alice], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error(e, SinglePoolError::WrongStakeStake); - - // reactivate - let instruction = instruction::reactivate_pool_stake(&id(), &accounts.vote_account.pubkey()); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - advance_epoch(&mut context).await; - - // deposit works again - let instructions = instruction::deposit( - &id(), - &accounts.pool, - &accounts.alice_stake.pubkey(), - &accounts.alice_token, - &accounts.alice.pubkey(), - &accounts.alice.pubkey(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer, &accounts.alice], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - assert!(context - .banks_client - .get_account(accounts.alice_stake.pubkey()) - .await - .expect("get_account") - .is_none()); -} - -#[test_case(true; "activated")] -#[test_case(false; "activating")] -#[tokio::test] -async fn fail_not_deactivated(activate: bool) { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - accounts.initialize(&mut context).await; - - if activate { - advance_epoch(&mut context).await; - } - - let instruction = instruction::reactivate_pool_stake(&id(), &accounts.vote_account.pubkey()); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error(e, SinglePoolError::WrongStakeStake); -} diff --git a/single-pool/program/tests/update_pool_token_metadata.rs b/single-pool/program/tests/update_pool_token_metadata.rs deleted file mode 100644 index 6642af4b04e..00000000000 --- a/single-pool/program/tests/update_pool_token_metadata.rs +++ /dev/null @@ -1,127 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{signature::Signer, transaction::Transaction}, - spl_single_pool::{error::SinglePoolError, id, instruction}, - test_case::test_case, -}; - -const UPDATED_NAME: &str = "updated_name"; -const UPDATED_SYMBOL: &str = "USYM"; -const UPDATED_URI: &str = "updated_uri"; - -#[tokio::test] -async fn success_update_pool_token_metadata() { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - accounts.initialize(&mut context).await; - - let instruction = instruction::update_token_metadata( - &id(), - &accounts.vote_account.pubkey(), - &accounts.withdrawer.pubkey(), - UPDATED_NAME.to_string(), - UPDATED_SYMBOL.to_string(), - UPDATED_URI.to_string(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer, &accounts.withdrawer], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let metadata = get_metadata_account(&mut context.banks_client, &accounts.mint).await; - - assert!(metadata.name.starts_with(UPDATED_NAME)); - assert!(metadata.symbol.starts_with(UPDATED_SYMBOL)); - assert!(metadata.uri.starts_with(UPDATED_URI)); -} - -#[tokio::test] -async fn fail_no_signature() { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - accounts.initialize(&mut context).await; - - let mut instruction = instruction::update_token_metadata( - &id(), - &accounts.vote_account.pubkey(), - &accounts.withdrawer.pubkey(), - UPDATED_NAME.to_string(), - UPDATED_SYMBOL.to_string(), - UPDATED_URI.to_string(), - ); - assert_eq!(instruction.accounts[3].pubkey, accounts.withdrawer.pubkey()); - instruction.accounts[3].is_signer = false; - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error(e, SinglePoolError::SignatureMissing); -} - -enum BadWithdrawer { - Validator, - Voter, - VoteAccount, -} - -#[test_case(BadWithdrawer::Validator; "validator")] -#[test_case(BadWithdrawer::Voter; "voter")] -#[test_case(BadWithdrawer::VoteAccount; "vote_account")] -#[tokio::test] -async fn fail_bad_withdrawer(withdrawer_type: BadWithdrawer) { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - accounts.initialize(&mut context).await; - - let withdrawer = match withdrawer_type { - BadWithdrawer::Validator => &accounts.validator, - BadWithdrawer::Voter => &accounts.voter, - BadWithdrawer::VoteAccount => &accounts.vote_account, - }; - - let instruction = instruction::update_token_metadata( - &id(), - &accounts.vote_account.pubkey(), - &withdrawer.pubkey(), - UPDATED_NAME.to_string(), - UPDATED_SYMBOL.to_string(), - UPDATED_URI.to_string(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer, withdrawer], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error(e, SinglePoolError::InvalidMetadataSigner); -} diff --git a/single-pool/program/tests/withdraw.rs b/single-pool/program/tests/withdraw.rs deleted file mode 100644 index 4619613b668..00000000000 --- a/single-pool/program/tests/withdraw.rs +++ /dev/null @@ -1,257 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{signature::Signer, transaction::Transaction}, - spl_single_pool::{error::SinglePoolError, id, instruction}, - test_case::test_case, -}; - -#[test_case(true, 0, false, false, false; "activated::minimum_disabled")] -#[test_case(true, 0, false, false, true; "activated::minimum_disabled::small")] -#[test_case(true, 0, false, true, false; "activated::minimum_enabled")] -#[test_case(false, 0, false, false, false; "activating::minimum_disabled")] -#[test_case(false, 0, false, false, true; "activating::minimum_disabled::small")] -#[test_case(false, 0, false, true, false; "activating::minimum_enabled")] -#[test_case(true, 100_000, false, false, false; "activated::extra")] -#[test_case(false, 100_000, false, false, false; "activating::extra")] -#[test_case(true, 0, true, false, false; "activated::second")] -#[test_case(false, 0, true, false, false; "activating::second")] -#[tokio::test] -async fn success( - activate: bool, - extra_lamports: u64, - prior_deposit: bool, - enable_minimum_delegation: bool, - small_deposit: bool, -) { - let mut context = program_test(enable_minimum_delegation) - .start_with_context() - .await; - let accounts = SinglePoolAccounts::default(); - - let amount_deposited = if small_deposit { 1 } else { TEST_STAKE_AMOUNT }; - - let minimum_delegation = accounts - .initialize_for_withdraw( - &mut context, - amount_deposited, - if prior_deposit { - Some(TEST_STAKE_AMOUNT * 10) - } else { - None - }, - activate, - ) - .await; - - let (_, _, pool_lamports_before) = - get_stake_account(&mut context.banks_client, &accounts.stake_account).await; - - let wallet_lamports_before = get_account(&mut context.banks_client, &accounts.alice.pubkey()) - .await - .lamports; - - if extra_lamports > 0 { - transfer( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &accounts.stake_account, - extra_lamports, - ) - .await; - } - - let instructions = instruction::withdraw( - &id(), - &accounts.pool, - &accounts.alice_stake.pubkey(), - &accounts.alice.pubkey(), - &accounts.alice_token, - &accounts.alice.pubkey(), - get_token_balance(&mut context.banks_client, &accounts.alice_token).await, - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer, &accounts.alice], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let wallet_lamports_after = get_account(&mut context.banks_client, &accounts.alice.pubkey()) - .await - .lamports; - - let (_, alice_stake_after, _) = - get_stake_account(&mut context.banks_client, &accounts.alice_stake.pubkey()).await; - let alice_stake_after = alice_stake_after.unwrap().delegation.stake; - - let (_, pool_stake_after, pool_lamports_after) = - get_stake_account(&mut context.banks_client, &accounts.stake_account).await; - let pool_stake_after = pool_stake_after.unwrap().delegation.stake; - - // when active, the depositor gets their rent back, but when activating, its - // just added to stake - let expected_deposit = if activate { - amount_deposited - } else { - amount_deposited + get_stake_account_rent(&mut context.banks_client).await - }; - - let prior_deposits = if prior_deposit { - if activate { - TEST_STAKE_AMOUNT * 10 - } else { - TEST_STAKE_AMOUNT * 10 + get_stake_account_rent(&mut context.banks_client).await - } - } else { - 0 - }; - - // alice received her stake back - assert_eq!(alice_stake_after, expected_deposit); - - // alice nothing to withdraw - // (we create the blank account before getting wallet_lamports_before) - assert_eq!(wallet_lamports_after, wallet_lamports_before); - - // pool retains minstake - assert_eq!(pool_stake_after, prior_deposits + minimum_delegation); - - // pool lamports otherwise unchanged. unexpected transfers affect nothing - assert_eq!( - pool_lamports_after, - pool_lamports_before - expected_deposit + extra_lamports - ); - - // alice has no tokens - assert_eq!( - get_token_balance(&mut context.banks_client, &accounts.alice_token).await, - 0, - ); - - // tokens were burned - assert_eq!( - get_token_supply(&mut context.banks_client, &accounts.mint).await, - prior_deposits, - ); -} - -#[tokio::test] -async fn success_with_rewards() { - let alice_deposit = TEST_STAKE_AMOUNT; - let bob_deposit = TEST_STAKE_AMOUNT * 3; - - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - let minimum_delegation = accounts - .initialize_for_withdraw(&mut context, alice_deposit, Some(bob_deposit), true) - .await; - - context.increment_vote_account_credits(&accounts.vote_account.pubkey(), 1); - advance_epoch(&mut context).await; - - let alice_tokens = get_token_balance(&mut context.banks_client, &accounts.alice_token).await; - let bob_tokens = get_token_balance(&mut context.banks_client, &accounts.bob_token).await; - - // tokens correspond to deposit after rewards - assert_eq!(alice_tokens, alice_deposit); - assert_eq!(bob_tokens, bob_deposit); - - let (_, pool_stake, _) = - get_stake_account(&mut context.banks_client, &accounts.stake_account).await; - let pool_stake = pool_stake.unwrap().delegation.stake; - let total_rewards = pool_stake - alice_deposit - bob_deposit - minimum_delegation; - - let instructions = instruction::withdraw( - &id(), - &accounts.pool, - &accounts.alice_stake.pubkey(), - &accounts.alice.pubkey(), - &accounts.alice_token, - &accounts.alice.pubkey(), - alice_tokens, - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&accounts.alice.pubkey()), - &[&accounts.alice], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let alice_tokens = get_token_balance(&mut context.banks_client, &accounts.alice_token).await; - let bob_tokens = get_token_balance(&mut context.banks_client, &accounts.bob_token).await; - - let (_, alice_stake, _) = - get_stake_account(&mut context.banks_client, &accounts.alice_stake.pubkey()).await; - let alice_rewards = alice_stake.unwrap().delegation.stake - alice_deposit; - - let (_, bob_stake, _) = - get_stake_account(&mut context.banks_client, &accounts.stake_account).await; - let bob_rewards = bob_stake.unwrap().delegation.stake - minimum_delegation - bob_deposit; - - // alice tokens are fully burned, bob remains unchanged - assert_eq!(alice_tokens, 0); - assert_eq!(bob_tokens, bob_deposit); - - // reward amounts are proportional to deposits - assert_eq!( - (alice_rewards as f64 / total_rewards as f64 * 100.0).round(), - 25.0 - ); - assert_eq!( - (bob_rewards as f64 / total_rewards as f64 * 100.0).round(), - 75.0 - ); -} - -#[test_case(true; "activated")] -#[test_case(false; "activating")] -#[tokio::test] -async fn fail_automorphic(activate: bool) { - let mut context = program_test(false).start_with_context().await; - let accounts = SinglePoolAccounts::default(); - accounts - .initialize_for_withdraw(&mut context, TEST_STAKE_AMOUNT, None, activate) - .await; - - let instructions = instruction::withdraw( - &id(), - &accounts.pool, - &accounts.stake_account, - &accounts.stake_authority, - &accounts.alice_token, - &accounts.alice.pubkey(), - TEST_STAKE_AMOUNT, - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&accounts.alice.pubkey()), - &[&accounts.alice], - context.last_blockhash, - ); - - let e = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err(); - check_error(e, SinglePoolError::InvalidPoolStakeAccountUsage); -} diff --git a/slashing/README.md b/slashing/README.md index e69de29bb2d..93dcccf439d 100644 --- a/slashing/README.md +++ b/slashing/README.md @@ -0,0 +1,2 @@ +NOTE: The slashing program and clients are now maintained at +[solana-program/slashing](https://github.com/solana-program/slashing). diff --git a/slashing/program/Cargo.toml b/slashing/program/Cargo.toml deleted file mode 100644 index fd135217b0c..00000000000 --- a/slashing/program/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "spl-slashing" -version = "0.1.0" -description = "Solana Program Library Slashing" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -bitflags = { version = "2.6.0", features = ["serde"] } -bytemuck = { version = "1.21.0", features = ["derive"] } -num_enum = "0.7.3" -generic-array = { version = "0.14.7", features = ["serde"], default-features = false } -bincode = "1.3.3" -num-derive = "0.4" -num-traits = "0.2" -solana-program = "2.1.0" -serde = "1.0.217" # must match the serde_derive version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251 -serde_bytes = "0.11.15" -serde_derive = "1.0.210" # must match the serde version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251 -serde_with = { version = "3.12.0", default-features = false } - -thiserror = "2.0" -spl-pod = { version = "0.5.0", path = "../../libraries/pod" } - -[dev-dependencies] -lazy_static = "1.5.0" -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" -solana-ledger = "2.1.0" -solana-entry = "2.1.0" -solana-client = "2.1.0" -spl-record = { version = "0.3.0", path = "../../record/program" } -rand = "0.8.5" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[lints] -workspace = true diff --git a/slashing/program/program-id.md b/slashing/program/program-id.md deleted file mode 100644 index 116e8d6bcc1..00000000000 --- a/slashing/program/program-id.md +++ /dev/null @@ -1 +0,0 @@ -S1ashing11111111111111111111111111111111111 diff --git a/slashing/program/src/duplicate_block_proof.rs b/slashing/program/src/duplicate_block_proof.rs deleted file mode 100644 index 8a093f6ee51..00000000000 --- a/slashing/program/src/duplicate_block_proof.rs +++ /dev/null @@ -1,872 +0,0 @@ -//! Duplicate block proof data and verification -use { - crate::{ - error::SlashingError, - shred::{Shred, ShredType}, - state::{ProofType, SlashingProofData}, - }, - bytemuck::try_from_bytes, - solana_program::{clock::Slot, msg, pubkey::Pubkey}, - spl_pod::primitives::PodU32, -}; - -/// Proof of a duplicate block violation -pub struct DuplicateBlockProofData<'a> { - /// Shred signed by a leader - pub shred1: &'a [u8], - /// Conflicting shred signed by the same leader - pub shred2: &'a [u8], -} - -impl<'a> DuplicateBlockProofData<'a> { - const LENGTH_SIZE: usize = std::mem::size_of::(); - - /// Packs proof data to write in account for - /// `SlashingInstruction::DuplicateBlockProof` - pub fn pack(self) -> Vec { - let mut buf = vec![]; - buf.extend_from_slice(&(self.shred1.len() as u32).to_le_bytes()); - buf.extend_from_slice(self.shred1); - buf.extend_from_slice(&(self.shred2.len() as u32).to_le_bytes()); - buf.extend_from_slice(self.shred2); - buf - } - - /// Given the maximum size of a shred as `shred_size` this returns - /// the maximum size of the account needed to store a - /// `DuplicateBlockProofData` - pub const fn size_of(shred_size: usize) -> usize { - 2usize - .wrapping_mul(shred_size) - .saturating_add(2 * Self::LENGTH_SIZE) - } -} - -impl<'a> SlashingProofData<'a> for DuplicateBlockProofData<'a> { - const PROOF_TYPE: ProofType = ProofType::DuplicateBlockProof; - - fn verify_proof(self, slot: Slot, _node_pubkey: &Pubkey) -> Result<(), SlashingError> { - // TODO: verify through instruction inspection that the shreds were sigverified - // earlier in this transaction. - // Ed25519 Singature verification is performed on the merkle root: - // node_pubkey.verify_strict(merkle_root, signature). - // We will verify that the pubkey merkle root and signature match the shred and - // that the verification was successful. - let shred1 = Shred::new_from_payload(self.shred1)?; - let shred2 = Shred::new_from_payload(self.shred2)?; - check_shreds(slot, &shred1, &shred2) - } - - fn unpack(data: &'a [u8]) -> Result - where - Self: Sized, - { - if data.len() < Self::LENGTH_SIZE { - return Err(SlashingError::ProofBufferTooSmall); - } - let (length1, data) = data.split_at(Self::LENGTH_SIZE); - let shred1_length = try_from_bytes::(length1) - .map_err(|_| SlashingError::ProofBufferDeserializationError)?; - let shred1_length = u32::from(*shred1_length) as usize; - - if data.len() < shred1_length { - return Err(SlashingError::ProofBufferTooSmall); - } - let (shred1, data) = data.split_at(shred1_length); - - if data.len() < Self::LENGTH_SIZE { - return Err(SlashingError::ProofBufferTooSmall); - } - let (length2, shred2) = data.split_at(Self::LENGTH_SIZE); - let shred2_length = try_from_bytes::(length2) - .map_err(|_| SlashingError::ProofBufferDeserializationError)?; - let shred2_length = u32::from(*shred2_length) as usize; - - if shred2.len() < shred2_length { - return Err(SlashingError::ProofBufferTooSmall); - } - - Ok(Self { shred1, shred2 }) - } -} - -/// Check that `shred1` and `shred2` indicate a valid duplicate proof -/// - Must be for the same slot `slot` -/// - Must be for the same shred version -/// - Must have a merkle root conflict, otherwise `shred1` and `shred2` must -/// have the same `shred_type` -/// - If `shred1` and `shred2` share the same index they must be not have -/// equal payloads excluding the retransmitter signature -/// - If `shred1` and `shred2` do not share the same index and are data -/// shreds verify that they indicate an index conflict. One of them must -/// be the LAST_SHRED_IN_SLOT, however the other shred must have a higher -/// index. -/// - If `shred1` and `shred2` do not share the same index and are coding -/// shreds verify that they have conflicting erasure metas -fn check_shreds(slot: Slot, shred1: &Shred, shred2: &Shred) -> Result<(), SlashingError> { - if shred1.slot()? != slot { - msg!( - "Invalid proof for different slots {} vs {}", - shred1.slot()?, - slot, - ); - return Err(SlashingError::SlotMismatch); - } - - if shred2.slot()? != slot { - msg!( - "Invalid proof for different slots {} vs {}", - shred1.slot()?, - slot, - ); - return Err(SlashingError::SlotMismatch); - } - - if shred1.version()? != shred2.version()? { - msg!( - "Invalid proof for different shred versions {} vs {}", - shred1.version()?, - shred2.version()?, - ); - return Err(SlashingError::InvalidShredVersion); - } - - // Merkle root conflict check - if shred1.fec_set_index()? == shred2.fec_set_index()? - && shred1.merkle_root()? != shred2.merkle_root()? - { - // Legacy shreds are discarded by validators and already filtered out - // above during proof deserialization, so any valid proof should have - // merkle roots. - msg!( - "Valid merkle root conflict for fec set {}, {:?} vs {:?}", - shred1.fec_set_index()?, - shred1.merkle_root()?, - shred2.merkle_root()? - ); - return Ok(()); - } - - // Overlapping fec set check - if shred1.shred_type() == ShredType::Code && shred1.fec_set_index()? < shred2.fec_set_index()? { - let next_fec_set_index = shred1.next_fec_set_index()?; - if next_fec_set_index > shred2.fec_set_index()? { - msg!( - "Valid overlapping fec set conflict. fec set {}'s next set is {} \ - however we observed a shred with fec set index {}", - shred1.fec_set_index()?, - next_fec_set_index, - shred2.fec_set_index()? - ); - return Ok(()); - } - } - - if shred2.shred_type() == ShredType::Code && shred1.fec_set_index()? > shred2.fec_set_index()? { - let next_fec_set_index = shred2.next_fec_set_index()?; - if next_fec_set_index > shred1.fec_set_index()? { - msg!( - "Valid overlapping fec set conflict. fec set {}'s next set is {} \ - however we observed a shred with fec set index {}", - shred2.fec_set_index()?, - next_fec_set_index, - shred1.fec_set_index()? - ); - return Ok(()); - } - } - - if shred1.shred_type() != shred2.shred_type() { - msg!( - "Invalid proof for different shred types {:?} vs {:?}", - shred1.shred_type(), - shred2.shred_type() - ); - return Err(SlashingError::ShredTypeMismatch); - } - - if shred1.index()? == shred2.index()? { - if shred1.is_shred_duplicate(shred2) { - msg!("Valid payload mismatch for shred index {}", shred1.index()?); - return Ok(()); - } - msg!( - "Invalid proof, payload matches for index {}", - shred1.index()? - ); - return Err(SlashingError::InvalidPayloadProof); - } - - if shred1.shred_type() == ShredType::Data { - if shred1.last_in_slot()? && shred2.index()? > shred1.index()? { - msg!( - "Valid last in slot conflict last index {} but shred with index {} is present", - shred1.index()?, - shred2.index()? - ); - return Ok(()); - } - if shred2.last_in_slot()? && shred1.index()? > shred2.index()? { - msg!( - "Valid last in slot conflict last index {} but shred with index {} is present", - shred2.index()?, - shred1.index()? - ); - return Ok(()); - } - msg!( - "Invalid proof, no last in shred conflict for data shreds {} and {}", - shred1.index()?, - shred2.index()? - ); - return Err(SlashingError::InvalidLastIndexConflict); - } - - if shred1.fec_set_index() == shred2.fec_set_index() - && !shred1.check_erasure_consistency(shred2)? - { - msg!( - "Valid erasure meta conflict in fec set {}, config {:?} vs {:?}", - shred1.fec_set_index()?, - shred1.erasure_meta()?, - shred2.erasure_meta()?, - ); - return Ok(()); - } - msg!( - "Invalid proof, no erasure meta conflict for coding shreds set {} idx {} and set {} idx {}", - shred1.fec_set_index()?, - shred1.index()?, - shred2.fec_set_index()?, - shred2.index()?, - ); - Err(SlashingError::InvalidErasureMetaConflict) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::shred::{ - tests::{new_rand_coding_shreds, new_rand_data_shred, new_rand_shreds}, - SIZE_OF_SIGNATURE, - }, - rand::Rng, - solana_ledger::shred::{Shred as SolanaShred, Shredder}, - solana_sdk::signature::{Keypair, Signature, Signer}, - std::sync::Arc, - }; - - const SLOT: Slot = 53084024; - const PARENT_SLOT: Slot = SLOT - 1; - const REFERENCE_TICK: u8 = 0; - const VERSION: u16 = 0; - - fn generate_proof_data<'a>( - shred1: &'a SolanaShred, - shred2: &'a SolanaShred, - ) -> DuplicateBlockProofData<'a> { - DuplicateBlockProofData { - shred1: shred1.payload().as_slice(), - shred2: shred2.payload().as_slice(), - } - } - - #[test] - fn test_legacy_shreds_invalid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let legacy_data_shred = - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, false, false); - let legacy_coding_shred = - new_rand_coding_shreds(&mut rng, next_shred_index, 5, &shredder, &leader, false)[0] - .clone(); - let data_shred = - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, false); - let coding_shred = - new_rand_coding_shreds(&mut rng, next_shred_index, 5, &shredder, &leader, true)[0] - .clone(); - - let test_cases = [ - (legacy_data_shred.clone(), legacy_data_shred.clone()), - (legacy_coding_shred.clone(), legacy_coding_shred.clone()), - (legacy_data_shred.clone(), legacy_coding_shred.clone()), - // Mix of legacy and merkle - (legacy_data_shred.clone(), data_shred.clone()), - (legacy_coding_shred.clone(), coding_shred.clone()), - (legacy_data_shred.clone(), coding_shred.clone()), - (data_shred.clone(), legacy_coding_shred.clone()), - ]; - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), - SlashingError::LegacyShreds, - ); - } - } - - #[test] - fn test_slot_invalid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder_slot = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let shredder_bad_slot = - Shredder::new(SLOT + 1, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let data_shred = new_rand_data_shred( - &mut rng, - next_shred_index, - &shredder_slot, - &leader, - true, - false, - ); - let data_shred_bad_slot = new_rand_data_shred( - &mut rng, - next_shred_index, - &shredder_bad_slot, - &leader, - true, - false, - ); - let coding_shred = - new_rand_coding_shreds(&mut rng, next_shred_index, 5, &shredder_slot, &leader, true)[0] - .clone(); - - let coding_shred_bad_slot = new_rand_coding_shreds( - &mut rng, - next_shred_index, - 5, - &shredder_bad_slot, - &leader, - true, - )[0] - .clone(); - - let test_cases = vec![ - (data_shred_bad_slot.clone(), data_shred_bad_slot.clone()), - (coding_shred_bad_slot.clone(), coding_shred_bad_slot.clone()), - (data_shred_bad_slot.clone(), coding_shred_bad_slot.clone()), - (data_shred.clone(), data_shred_bad_slot.clone()), - (coding_shred.clone(), coding_shred_bad_slot.clone()), - (data_shred.clone(), coding_shred_bad_slot.clone()), - (data_shred_bad_slot.clone(), coding_shred.clone()), - ]; - - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), - SlashingError::SlotMismatch - ); - } - } - - #[test] - fn test_payload_proof_valid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let shred1 = - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); - let shred2 = - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); - let proof_data = generate_proof_data(&shred1, &shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); - } - - #[test] - fn test_payload_proof_invalid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let data_shred = - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); - let coding_shreds = - new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader, true); - let test_cases = vec![ - // Same data_shred - (data_shred.clone(), data_shred), - // Same coding_shred - (coding_shreds[0].clone(), coding_shreds[0].clone()), - ]; - - for (shred1, shred2) in test_cases.into_iter() { - let proof_data = generate_proof_data(&shred1, &shred2); - assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), - SlashingError::InvalidPayloadProof - ); - } - } - - #[test] - fn test_merkle_root_proof_valid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let (data_shreds, coding_shreds) = new_rand_shreds( - &mut rng, - next_shred_index, - next_shred_index, - 10, - true, /* merkle_variant */ - &shredder, - &leader, - false, - ); - - let (diff_data_shreds, diff_coding_shreds) = new_rand_shreds( - &mut rng, - next_shred_index, - next_shred_index, - 10, - true, /* merkle_variant */ - &shredder, - &leader, - false, - ); - - let test_cases = vec![ - (data_shreds[0].clone(), diff_data_shreds[1].clone()), - (coding_shreds[0].clone(), diff_coding_shreds[1].clone()), - (data_shreds[0].clone(), diff_coding_shreds[0].clone()), - (coding_shreds[0].clone(), diff_data_shreds[0].clone()), - ]; - - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); - } - } - - #[test] - fn test_merkle_root_proof_invalid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let (data_shreds, coding_shreds) = new_rand_shreds( - &mut rng, - next_shred_index, - next_shred_index, - 10, - true, - &shredder, - &leader, - true, - ); - - let (next_data_shreds, next_coding_shreds) = new_rand_shreds( - &mut rng, - next_shred_index + 33, - next_shred_index + 33, - 10, - true, - &shredder, - &leader, - true, - ); - - let test_cases = vec![ - // Same fec set same merkle root - (coding_shreds[0].clone(), data_shreds[0].clone()), - // Different FEC set different merkle root - (coding_shreds[0].clone(), next_data_shreds[0].clone()), - (next_coding_shreds[0].clone(), data_shreds[0].clone()), - ]; - - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), - SlashingError::ShredTypeMismatch - ); - } - } - - #[test] - fn test_last_index_conflict_valid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let test_cases = vec![ - ( - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true), - new_rand_data_shred( - &mut rng, - // With Merkle shreds, last erasure batch is padded with - // empty data shreds. - next_shred_index + 30, - &shredder, - &leader, - true, - false, - ), - ), - ( - new_rand_data_shred( - &mut rng, - next_shred_index + 100, - &shredder, - &leader, - true, - true, - ), - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true), - ), - ]; - - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); - } - } - - #[test] - fn test_last_index_conflict_invalid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let test_cases = vec![ - ( - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, false), - new_rand_data_shred( - &mut rng, - next_shred_index + 1, - &shredder, - &leader, - true, - true, - ), - ), - ( - new_rand_data_shred( - &mut rng, - next_shred_index + 1, - &shredder, - &leader, - true, - true, - ), - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, false), - ), - ( - new_rand_data_shred( - &mut rng, - next_shred_index + 100, - &shredder, - &leader, - true, - false, - ), - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, false), - ), - ( - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, false), - new_rand_data_shred( - &mut rng, - next_shred_index + 100, - &shredder, - &leader, - true, - false, - ), - ), - ]; - - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), - SlashingError::InvalidLastIndexConflict - ); - } - } - - #[test] - fn test_erasure_meta_conflict_valid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let coding_shreds = - new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader, true); - let coding_shreds_bigger = - new_rand_coding_shreds(&mut rng, next_shred_index, 13, &shredder, &leader, true); - let coding_shreds_smaller = - new_rand_coding_shreds(&mut rng, next_shred_index, 7, &shredder, &leader, true); - - // Same fec-set, different index, different erasure meta - let test_cases = vec![ - (coding_shreds[0].clone(), coding_shreds_bigger[1].clone()), - (coding_shreds[0].clone(), coding_shreds_smaller[1].clone()), - ]; - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); - } - } - - #[test] - fn test_erasure_meta_conflict_invalid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let coding_shreds = - new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader, true); - let coding_shreds_different_fec = new_rand_coding_shreds( - &mut rng, - next_shred_index + 100, - 10, - &shredder, - &leader, - true, - ); - let coding_shreds_different_fec_and_size = new_rand_coding_shreds( - &mut rng, - next_shred_index + 100, - 13, - &shredder, - &leader, - true, - ); - - let test_cases = vec![ - // Different index, different fec set, same erasure meta - ( - coding_shreds[0].clone(), - coding_shreds_different_fec[1].clone(), - ), - // Different index, different fec set, different erasure meta - ( - coding_shreds[0].clone(), - coding_shreds_different_fec_and_size[1].clone(), - ), - // Different index, same fec set, same erasure meta - (coding_shreds[0].clone(), coding_shreds[1].clone()), - ( - coding_shreds_different_fec[0].clone(), - coding_shreds_different_fec[1].clone(), - ), - ( - coding_shreds_different_fec_and_size[0].clone(), - coding_shreds_different_fec_and_size[1].clone(), - ), - ]; - - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), - SlashingError::InvalidErasureMetaConflict - ); - } - } - - #[test] - fn test_shred_version_invalid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let (data_shreds, coding_shreds) = new_rand_shreds( - &mut rng, - next_shred_index, - next_shred_index, - 10, - true, - &shredder, - &leader, - true, - ); - - // Wrong shred VERSION - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION + 1).unwrap(); - let (wrong_data_shreds, wrong_coding_shreds) = new_rand_shreds( - &mut rng, - next_shred_index, - next_shred_index, - 10, - true, - &shredder, - &leader, - true, - ); - let test_cases = vec![ - // One correct shred VERSION, one wrong - (coding_shreds[0].clone(), wrong_coding_shreds[0].clone()), - (coding_shreds[0].clone(), wrong_data_shreds[0].clone()), - (data_shreds[0].clone(), wrong_coding_shreds[0].clone()), - (data_shreds[0].clone(), wrong_data_shreds[0].clone()), - ]; - - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), - SlashingError::InvalidShredVersion - ); - } - } - - #[test] - fn test_retransmitter_signature_payload_proof_invalid() { - // TODO: change visbility of shred::layout::set_retransmitter_signature. - // Hardcode offsets for now; - const DATA_SHRED_OFFSET: usize = 1139; - const CODING_SHRED_OFFSET: usize = 1164; - - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let data_shred = - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); - let coding_shred = - new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader, true)[0] - .clone(); - - let mut data_shred_different_retransmitter_payload = data_shred.clone().into_payload(); - let buffer = data_shred_different_retransmitter_payload - .get_mut(DATA_SHRED_OFFSET..DATA_SHRED_OFFSET + SIZE_OF_SIGNATURE) - .unwrap(); - buffer.copy_from_slice(Signature::new_unique().as_ref()); - let data_shred_different_retransmitter = - SolanaShred::new_from_serialized_shred(data_shred_different_retransmitter_payload) - .unwrap(); - - let mut coding_shred_different_retransmitter_payload = coding_shred.clone().into_payload(); - let buffer = coding_shred_different_retransmitter_payload - .get_mut(CODING_SHRED_OFFSET..CODING_SHRED_OFFSET + SIZE_OF_SIGNATURE) - .unwrap(); - buffer.copy_from_slice(Signature::new_unique().as_ref()); - let coding_shred_different_retransmitter = - SolanaShred::new_from_serialized_shred(coding_shred_different_retransmitter_payload) - .unwrap(); - - let test_cases = vec![ - // Same data shred from different retransmitter - (data_shred, data_shred_different_retransmitter), - // Same coding shred from different retransmitter - (coding_shred, coding_shred_different_retransmitter), - ]; - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), - SlashingError::InvalidPayloadProof - ); - } - } - - #[test] - fn test_overlapping_erasure_meta_proof_valid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let coding_shreds = - new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader, true); - let (data_shred_next, coding_shred_next) = new_rand_shreds( - &mut rng, - next_shred_index + 1, - next_shred_index + 33, - 10, - true, - &shredder, - &leader, - true, - ); - - // Fec set is overlapping - let test_cases = vec![ - (coding_shreds[0].clone(), coding_shred_next[0].clone()), - (coding_shreds[0].clone(), data_shred_next[0].clone()), - ( - coding_shreds[2].clone(), - coding_shred_next.last().unwrap().clone(), - ), - ( - coding_shreds[2].clone(), - data_shred_next.last().unwrap().clone(), - ), - ]; - for (shred1, shred2) in test_cases.iter().flat_map(|(a, b)| [(a, b), (b, a)]) { - let proof_data = generate_proof_data(shred1, shred2); - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap(); - } - } - - #[test] - fn test_overlapping_erasure_meta_proof_invalid() { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let (data_shred, coding_shred) = new_rand_shreds( - &mut rng, - next_shred_index, - next_shred_index, - 10, - true, - &shredder, - &leader, - true, - ); - let next_shred_index = next_shred_index + data_shred.len() as u32; - let next_code_index = next_shred_index + coding_shred.len() as u32; - let (data_shred_next, coding_shred_next) = new_rand_shreds( - &mut rng, - next_shred_index, - next_code_index, - 10, - true, - &shredder, - &leader, - true, - ); - let test_cases = vec![ - ( - coding_shred[0].clone(), - data_shred_next[0].clone(), - SlashingError::ShredTypeMismatch, - ), - ( - coding_shred[0].clone(), - coding_shred_next[0].clone(), - SlashingError::InvalidErasureMetaConflict, - ), - ( - coding_shred[0].clone(), - data_shred_next.last().unwrap().clone(), - SlashingError::ShredTypeMismatch, - ), - ( - coding_shred[0].clone(), - coding_shred_next.last().unwrap().clone(), - SlashingError::InvalidErasureMetaConflict, - ), - ]; - - for (shred1, shred2, expected) in test_cases - .iter() - .flat_map(|(a, b, c)| [(a, b, c), (b, a, c)]) - { - let proof_data = generate_proof_data(shred1, shred2); - assert_eq!( - proof_data.verify_proof(SLOT, &leader.pubkey()).unwrap_err(), - *expected, - ); - } - } -} diff --git a/slashing/program/src/entrypoint.rs b/slashing/program/src/entrypoint.rs deleted file mode 100644 index 62a02f2a465..00000000000 --- a/slashing/program/src/entrypoint.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Program entrypoint - -#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] - -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - crate::processor::process_instruction(program_id, accounts, instruction_data) -} diff --git a/slashing/program/src/error.rs b/slashing/program/src/error.rs deleted file mode 100644 index 0c6d2b4ec3b..00000000000 --- a/slashing/program/src/error.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Error types - -use { - num_derive::FromPrimitive, - solana_program::{decode_error::DecodeError, program_error::ProgramError}, - thiserror::Error, -}; - -/// Errors that may be returned by the program. -#[derive(Clone, Copy, Debug, Eq, Error, FromPrimitive, PartialEq)] -pub enum SlashingError { - /// Violation is too old for statue of limitations - #[error("Exceeds statue of limitations")] - ExceedsStatueOfLimitations, - - /// Invalid shred variant - #[error("Invalid shred variant")] - InvalidShredVariant, - - /// Invalid merkle shred - #[error("Invalid Merkle shred")] - InvalidMerkleShred, - - /// Invalid duplicate block payload proof - #[error("Invalid payload proof")] - InvalidPayloadProof, - - /// Invalid duplicate block erasure meta proof - #[error("Invalid erasure meta conflict")] - InvalidErasureMetaConflict, - - /// Invalid instruction - #[error("Invalid instruction")] - InvalidInstruction, - - /// Invalid duplicate block last index proof - #[error("Invalid last index conflict")] - InvalidLastIndexConflict, - - /// Invalid shred version on duplicate block proof shreds - #[error("Invalid shred version")] - InvalidShredVersion, - - /// Invalid signature on duplicate block proof shreds - #[error("Invalid signature")] - InvalidSignature, - - /// Legacy shreds are not supported - #[error("Legacy shreds are not eligible for slashing")] - LegacyShreds, - - /// Unable to deserialize proof buffer - #[error("Proof buffer deserialization error")] - ProofBufferDeserializationError, - - /// Proof buffer is too small - #[error("Proof buffer too small")] - ProofBufferTooSmall, - - /// Shred deserialization error - #[error("Deserialization error")] - ShredDeserializationError, - - /// Invalid shred type on duplicate block proof shreds - #[error("Shred type mismatch")] - ShredTypeMismatch, - - /// Invalid slot on duplicate block proof shreds - #[error("Slot mismatch")] - SlotMismatch, -} - -impl From for ProgramError { - fn from(e: SlashingError) -> Self { - ProgramError::Custom(e as u32) - } -} - -impl DecodeError for SlashingError { - fn type_of() -> &'static str { - "Slashing Error" - } -} diff --git a/slashing/program/src/instruction.rs b/slashing/program/src/instruction.rs deleted file mode 100644 index d31eacf9a8e..00000000000 --- a/slashing/program/src/instruction.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Program instructions - -use { - crate::{error::SlashingError, id}, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - clock::Slot, - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::{ - bytemuck::{pod_from_bytes, pod_get_packed_len}, - primitives::PodU64, - }, -}; - -/// Instructions supported by the program -#[repr(u8)] -#[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] -pub enum SlashingInstruction { - /// Submit a slashable violation proof for `node_pubkey`, which indicates - /// that they submitted a duplicate block to the network - /// - /// - /// Accounts expected by this instruction: - /// 0. `[]` Proof account, must be previously initialized with the proof - /// data. - /// - /// We expect the proof account to be properly sized as to hold a duplicate - /// block proof. See [ProofType] for sizing requirements. - /// - /// Deserializing the proof account from `offset` should result in a - /// [DuplicateBlockProofData] - /// - /// Data expected by this instruction: - /// DuplicateBlockProofInstructionData - DuplicateBlockProof, -} - -/// Data expected by -/// `SlashingInstruction::DuplicateBlockProof` -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct DuplicateBlockProofInstructionData { - /// Offset into the proof account to begin reading, expressed as `u64` - pub(crate) offset: PodU64, - /// Slot for which the violation occured - pub(crate) slot: PodU64, - /// Identity pubkey of the Node that signed the duplicate block - pub(crate) node_pubkey: Pubkey, -} - -/// Utility function for encoding instruction data -pub(crate) fn encode_instruction( - accounts: Vec, - instruction: SlashingInstruction, - instruction_data: &D, -) -> Instruction { - let mut data = vec![u8::from(instruction)]; - data.extend_from_slice(bytemuck::bytes_of(instruction_data)); - Instruction { - program_id: id(), - accounts, - data, - } -} - -/// Utility function for decoding just the instruction type -pub(crate) fn decode_instruction_type(input: &[u8]) -> Result { - if input.is_empty() { - Err(ProgramError::InvalidInstructionData) - } else { - SlashingInstruction::try_from(input[0]) - .map_err(|_| SlashingError::InvalidInstruction.into()) - } -} - -/// Utility function for decoding instruction data -pub(crate) fn decode_instruction_data(input_with_type: &[u8]) -> Result<&T, ProgramError> { - if input_with_type.len() != pod_get_packed_len::().saturating_add(1) { - Err(ProgramError::InvalidInstructionData) - } else { - pod_from_bytes(&input_with_type[1..]) - } -} - -/// Create a `SlashingInstruction::DuplicateBlockProof` instruction -pub fn duplicate_block_proof( - proof_account: &Pubkey, - offset: u64, - slot: Slot, - node_pubkey: Pubkey, -) -> Instruction { - encode_instruction( - vec![AccountMeta::new_readonly(*proof_account, false)], - SlashingInstruction::DuplicateBlockProof, - &DuplicateBlockProofInstructionData { - offset: PodU64::from(offset), - slot: PodU64::from(slot), - node_pubkey, - }, - ) -} - -#[cfg(test)] -mod tests { - use {super::*, solana_program::program_error::ProgramError}; - - const TEST_BYTES: [u8; 8] = [42; 8]; - - #[test] - fn serialize_duplicate_block_proof() { - let offset = 34; - let slot = 42; - let node_pubkey = Pubkey::new_unique(); - let instruction = duplicate_block_proof(&Pubkey::new_unique(), offset, slot, node_pubkey); - let mut expected = vec![0]; - expected.extend_from_slice(&offset.to_le_bytes()); - expected.extend_from_slice(&slot.to_le_bytes()); - expected.extend_from_slice(&node_pubkey.to_bytes()); - assert_eq!(instruction.data, expected); - - assert_eq!( - SlashingInstruction::DuplicateBlockProof, - decode_instruction_type(&instruction.data).unwrap() - ); - let instruction_data: &DuplicateBlockProofInstructionData = - decode_instruction_data(&instruction.data).unwrap(); - - assert_eq!(instruction_data.offset, offset.into()); - assert_eq!(instruction_data.slot, slot.into()); - assert_eq!(instruction_data.node_pubkey, node_pubkey); - } - - #[test] - fn deserialize_invalid_instruction() { - let mut expected = vec![12]; - expected.extend_from_slice(&TEST_BYTES); - let err: ProgramError = decode_instruction_type(&expected).unwrap_err(); - assert_eq!(err, SlashingError::InvalidInstruction.into()); - } -} diff --git a/slashing/program/src/lib.rs b/slashing/program/src/lib.rs deleted file mode 100644 index 24b7343ab09..00000000000 --- a/slashing/program/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Slashing program -#![deny(missing_docs)] - -pub mod duplicate_block_proof; -mod entrypoint; -pub mod error; -pub mod instruction; -pub mod processor; -mod shred; -pub mod state; - -// Export current SDK types for downstream users building with a different SDK -// version -pub use solana_program; - -solana_program::declare_id!("S1ashing11111111111111111111111111111111111"); diff --git a/slashing/program/src/processor.rs b/slashing/program/src/processor.rs deleted file mode 100644 index 07efa42ccd9..00000000000 --- a/slashing/program/src/processor.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! Program state processor - -use { - crate::{ - duplicate_block_proof::DuplicateBlockProofData, - error::SlashingError, - instruction::{ - decode_instruction_data, decode_instruction_type, DuplicateBlockProofInstructionData, - SlashingInstruction, - }, - state::SlashingProofData, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - clock::Slot, - entrypoint::ProgramResult, - msg, - program_error::ProgramError, - pubkey::Pubkey, - sysvar::{clock::Clock, epoch_schedule::EpochSchedule, Sysvar}, - }, -}; - -fn verify_proof_data<'a, T>(slot: Slot, pubkey: &Pubkey, proof_data: &'a [u8]) -> ProgramResult -where - T: SlashingProofData<'a>, -{ - // Statue of limitations is 1 epoch - let clock = Clock::get()?; - let Some(elapsed) = clock.slot.checked_sub(slot) else { - return Err(ProgramError::ArithmeticOverflow); - }; - let epoch_schedule = EpochSchedule::get()?; - if elapsed > epoch_schedule.slots_per_epoch { - return Err(SlashingError::ExceedsStatueOfLimitations.into()); - } - - let proof_data: T = - T::unpack(proof_data).map_err(|_| SlashingError::ShredDeserializationError)?; - - SlashingProofData::verify_proof(proof_data, slot, pubkey)?; - - // TODO: follow up PR will record this violation in context state account. just - // log for now. - msg!( - "{} violation verified in slot {}. This incident will be recorded", - T::PROOF_TYPE.violation_str(), - slot - ); - Ok(()) -} - -/// Instruction processor -pub fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - let instruction_type = decode_instruction_type(input)?; - let account_info_iter = &mut accounts.iter(); - let proof_data_info = next_account_info(account_info_iter); - - match instruction_type { - SlashingInstruction::DuplicateBlockProof => { - let data = decode_instruction_data::(input)?; - let proof_data = &proof_data_info?.data.borrow()[u64::from(data.offset) as usize..]; - verify_proof_data::( - data.slot.into(), - &data.node_pubkey, - proof_data, - )?; - Ok(()) - } - } -} - -#[cfg(test)] -mod tests { - use { - super::verify_proof_data, - crate::{ - duplicate_block_proof::DuplicateBlockProofData, error::SlashingError, - shred::tests::new_rand_data_shred, - }, - rand::Rng, - solana_ledger::shred::Shredder, - solana_sdk::{ - clock::{Clock, Slot, DEFAULT_SLOTS_PER_EPOCH}, - epoch_schedule::EpochSchedule, - program_error::ProgramError, - signature::Keypair, - signer::Signer, - }, - std::sync::{Arc, RwLock}, - }; - - const SLOT: Slot = 53084024; - lazy_static::lazy_static! { - static ref CLOCK_SLOT: Arc> = Arc::new(RwLock::new(SLOT)); - } - - fn generate_proof_data(leader: Arc) -> Vec { - let mut rng = rand::thread_rng(); - let (slot, parent_slot, reference_tick, version) = (SLOT, SLOT - 1, 0, 0); - let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let shred1 = - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); - let shred2 = - new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true, true); - let proof = DuplicateBlockProofData { - shred1: shred1.payload().as_slice(), - shred2: shred2.payload().as_slice(), - }; - proof.pack() - } - - #[test] - fn test_statue_of_limitations() { - *CLOCK_SLOT.write().unwrap() = SLOT + 5; - verify_with_clock().unwrap(); - - *CLOCK_SLOT.write().unwrap() = SLOT - 1; - assert_eq!( - verify_with_clock().unwrap_err(), - ProgramError::ArithmeticOverflow - ); - - *CLOCK_SLOT.write().unwrap() = SLOT + DEFAULT_SLOTS_PER_EPOCH + 1; - assert_eq!( - verify_with_clock().unwrap_err(), - SlashingError::ExceedsStatueOfLimitations.into() - ); - } - - fn verify_with_clock() -> Result<(), ProgramError> { - struct SyscallStubs {} - impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { - fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 { - unsafe { - let clock = Clock { - slot: *CLOCK_SLOT.read().unwrap(), - ..Clock::default() - }; - *(var_addr as *mut _ as *mut Clock) = clock; - } - solana_program::entrypoint::SUCCESS - } - - fn sol_get_epoch_schedule_sysvar(&self, var_addr: *mut u8) -> u64 { - unsafe { - *(var_addr as *mut _ as *mut EpochSchedule) = EpochSchedule::default(); - } - solana_program::entrypoint::SUCCESS - } - } - - solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); - let leader = Arc::new(Keypair::new()); - verify_proof_data::( - SLOT, - &leader.pubkey(), - &generate_proof_data(leader), - ) - } -} diff --git a/slashing/program/src/shred.rs b/slashing/program/src/shred.rs deleted file mode 100644 index 47e4308e086..00000000000 --- a/slashing/program/src/shred.rs +++ /dev/null @@ -1,564 +0,0 @@ -//! Shred representation -use { - crate::error::SlashingError, - bitflags::bitflags, - bytemuck::Pod, - generic_array::{typenum::U64, GenericArray}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - serde_derive::Deserialize, - solana_program::{ - clock::Slot, - hash::{hashv, Hash}, - }, - spl_pod::primitives::{PodU16, PodU32, PodU64}, -}; - -pub(crate) const SIZE_OF_SIGNATURE: usize = 64; -const SIZE_OF_SHRED_VARIANT: usize = 1; -const SIZE_OF_SLOT: usize = 8; -const SIZE_OF_INDEX: usize = 4; -const SIZE_OF_VERSION: usize = 2; -const SIZE_OF_FEC_SET_INDEX: usize = 4; -const SIZE_OF_PARENT_OFFSET: usize = 2; -const SIZE_OF_NUM_DATA_SHREDS: usize = 2; -const SIZE_OF_NUM_CODING_SHREDS: usize = 2; -const SIZE_OF_POSITION: usize = 2; - -const SIZE_OF_MERKLE_ROOT: usize = 32; -const SIZE_OF_MERKLE_PROOF_ENTRY: usize = 20; - -const OFFSET_OF_SHRED_VARIANT: usize = SIZE_OF_SIGNATURE; -const OFFSET_OF_SLOT: usize = SIZE_OF_SIGNATURE + SIZE_OF_SHRED_VARIANT; -const OFFSET_OF_INDEX: usize = OFFSET_OF_SLOT + SIZE_OF_SLOT; -const OFFSET_OF_VERSION: usize = OFFSET_OF_INDEX + SIZE_OF_INDEX; -const OFFSET_OF_FEC_SET_INDEX: usize = OFFSET_OF_VERSION + SIZE_OF_VERSION; - -const OFFSET_OF_DATA_PARENT_OFFSET: usize = OFFSET_OF_FEC_SET_INDEX + SIZE_OF_FEC_SET_INDEX; -const OFFSET_OF_DATA_SHRED_FLAGS: usize = OFFSET_OF_DATA_PARENT_OFFSET + SIZE_OF_PARENT_OFFSET; - -const OFFSET_OF_CODING_NUM_DATA_SHREDS: usize = OFFSET_OF_FEC_SET_INDEX + SIZE_OF_FEC_SET_INDEX; -const OFFSET_OF_CODING_NUM_CODING_SHREDS: usize = - OFFSET_OF_CODING_NUM_DATA_SHREDS + SIZE_OF_NUM_DATA_SHREDS; -const OFFSET_OF_CODING_POSITION: usize = - OFFSET_OF_CODING_NUM_CODING_SHREDS + SIZE_OF_NUM_CODING_SHREDS; - -type MerkleProofEntry = [u8; 20]; -const MERKLE_HASH_PREFIX_LEAF: &[u8] = b"\x00SOLANA_MERKLE_SHREDS_LEAF"; -const MERKLE_HASH_PREFIX_NODE: &[u8] = b"\x01SOLANA_MERKLE_SHREDS_NODE"; - -#[repr(transparent)] -#[derive(Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize)] -pub(crate) struct Signature(GenericArray); - -bitflags! { - #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize)] - pub struct ShredFlags:u8 { - const SHRED_TICK_REFERENCE_MASK = 0b0011_1111; - const DATA_COMPLETE_SHRED = 0b0100_0000; - const LAST_SHRED_IN_SLOT = 0b1100_0000; - } -} - -#[repr(u8)] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, IntoPrimitive, TryFromPrimitive)] -pub(crate) enum ShredType { - Data = 0b1010_0101, - Code = 0b0101_1010, -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -enum ShredVariant { - LegacyCode, - LegacyData, - MerkleCode { - proof_size: u8, - chained: bool, - resigned: bool, - }, - MerkleData { - proof_size: u8, - chained: bool, - resigned: bool, - }, -} - -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub(crate) struct ErasureMeta { - num_data_shreds: usize, - num_coding_shreds: usize, - first_coding_index: u32, -} - -#[derive(Clone, PartialEq, Eq)] -pub(crate) struct Shred<'a> { - shred_type: ShredType, - proof_size: u8, - chained: bool, - resigned: bool, - payload: &'a [u8], -} - -impl<'a> Shred<'a> { - const SIZE_OF_CODING_PAYLOAD: usize = 1228; - const SIZE_OF_DATA_PAYLOAD: usize = - Self::SIZE_OF_CODING_PAYLOAD - Self::SIZE_OF_CODING_HEADERS + SIZE_OF_SIGNATURE; - const SIZE_OF_CODING_HEADERS: usize = 89; - const SIZE_OF_DATA_HEADERS: usize = 88; - - pub(crate) fn new_from_payload(payload: &'a [u8]) -> Result { - match Self::get_shred_variant(payload)? { - ShredVariant::LegacyCode | ShredVariant::LegacyData => Err(SlashingError::LegacyShreds), - ShredVariant::MerkleCode { - proof_size, - chained, - resigned, - } => Ok(Self { - shred_type: ShredType::Code, - proof_size, - chained, - resigned, - payload, - }), - ShredVariant::MerkleData { - proof_size, - chained, - resigned, - } => Ok(Self { - shred_type: ShredType::Data, - proof_size, - chained, - resigned, - payload, - }), - } - } - - fn pod_from_bytes( - &self, - ) -> Result<&T, SlashingError> { - let end_index: usize = OFFSET - .checked_add(SIZE) - .ok_or(SlashingError::ShredDeserializationError)?; - bytemuck::try_from_bytes( - self.payload - .get(OFFSET..end_index) - .ok_or(SlashingError::ShredDeserializationError)?, - ) - .map_err(|_| SlashingError::ShredDeserializationError) - } - - fn get_shred_variant(payload: &'a [u8]) -> Result { - let Some(&shred_variant) = payload.get(OFFSET_OF_SHRED_VARIANT) else { - return Err(SlashingError::ShredDeserializationError); - }; - ShredVariant::try_from(shred_variant).map_err(|_| SlashingError::InvalidShredVariant) - } - - pub(crate) fn slot(&self) -> Result { - self.pod_from_bytes::() - .map(|x| u64::from(*x)) - } - - pub(crate) fn index(&self) -> Result { - self.pod_from_bytes::() - .map(|x| u32::from(*x)) - } - - pub(crate) fn version(&self) -> Result { - self.pod_from_bytes::() - .map(|x| u16::from(*x)) - } - - pub(crate) fn fec_set_index(&self) -> Result { - self.pod_from_bytes::() - .map(|x| u32::from(*x)) - } - - pub(crate) fn shred_type(&self) -> ShredType { - self.shred_type - } - - pub(crate) fn last_in_slot(&self) -> Result { - debug_assert!(self.shred_type == ShredType::Data); - let Some(&flags) = self.payload.get(OFFSET_OF_DATA_SHRED_FLAGS) else { - return Err(SlashingError::ShredDeserializationError); - }; - - let flags: ShredFlags = - bincode::deserialize(&[flags]).map_err(|_| SlashingError::InvalidShredVariant)?; - Ok(flags.contains(ShredFlags::LAST_SHRED_IN_SLOT)) - } - - fn num_data_shreds(&self) -> Result { - debug_assert!(self.shred_type == ShredType::Code); - self.pod_from_bytes::() - .map(|x| u16::from(*x) as usize) - } - - fn num_coding_shreds(&self) -> Result { - debug_assert!(self.shred_type == ShredType::Code); - self.pod_from_bytes::() - .map(|x| u16::from(*x) as usize) - } - - fn position(&self) -> Result { - debug_assert!(self.shred_type == ShredType::Code); - self.pod_from_bytes::() - .map(|x| u16::from(*x) as usize) - } - - pub(crate) fn next_fec_set_index(&self) -> Result { - debug_assert!(self.shred_type == ShredType::Code); - let num_data = u32::try_from(self.num_data_shreds()?) - .map_err(|_| SlashingError::ShredDeserializationError)?; - self.fec_set_index()? - .checked_add(num_data) - .ok_or(SlashingError::ShredDeserializationError) - } - - pub(crate) fn erasure_meta(&self) -> Result { - debug_assert!(self.shred_type == ShredType::Code); - let num_data_shreds = self.num_data_shreds()?; - let num_coding_shreds = self.num_coding_shreds()?; - let first_coding_index = self - .index()? - .checked_sub( - u32::try_from(self.position()?) - .map_err(|_| SlashingError::ShredDeserializationError)?, - ) - .ok_or(SlashingError::ShredDeserializationError)?; - Ok(ErasureMeta { - num_data_shreds, - num_coding_shreds, - first_coding_index, - }) - } - - fn erasure_batch_index(&self) -> Result { - match self.shred_type { - ShredType::Data => self - .index()? - .checked_sub(self.fec_set_index()?) - .and_then(|x| usize::try_from(x).ok()) - .ok_or(SlashingError::ShredDeserializationError), - ShredType::Code => self - .num_data_shreds()? - .checked_add(self.position()?) - .ok_or(SlashingError::ShredDeserializationError), - } - } - - pub(crate) fn merkle_root(&self) -> Result { - let (proof_offset, proof_size) = self.get_proof_offset_and_size()?; - let proof_end = proof_offset - .checked_add(proof_size) - .ok_or(SlashingError::ShredDeserializationError)?; - let index = self.erasure_batch_index()?; - - let proof = self - .payload - .get(proof_offset..proof_end) - .ok_or(SlashingError::InvalidMerkleShred)? - .chunks(SIZE_OF_MERKLE_PROOF_ENTRY) - .map(<&MerkleProofEntry>::try_from) - .map(Result::unwrap); - let node = self - .payload - .get(SIZE_OF_SIGNATURE..proof_offset) - .ok_or(SlashingError::InvalidMerkleShred)?; - let node = hashv(&[MERKLE_HASH_PREFIX_LEAF, node]); - - Self::get_merkle_root(index, node, proof) - } - - fn get_proof_offset_and_size(&self) -> Result<(usize, usize), SlashingError> { - let (header_size, payload_size) = match self.shred_type { - ShredType::Code => (Self::SIZE_OF_CODING_HEADERS, Self::SIZE_OF_CODING_PAYLOAD), - ShredType::Data => (Self::SIZE_OF_DATA_HEADERS, Self::SIZE_OF_DATA_PAYLOAD), - }; - let proof_size = usize::from(self.proof_size) - .checked_mul(SIZE_OF_MERKLE_PROOF_ENTRY) - .ok_or(SlashingError::ShredDeserializationError)?; - let bytes_past_end = header_size - .checked_add(if self.chained { SIZE_OF_MERKLE_ROOT } else { 0 }) - .and_then(|x| x.checked_add(proof_size)) - .and_then(|x| x.checked_add(if self.resigned { SIZE_OF_SIGNATURE } else { 0 })) - .ok_or(SlashingError::ShredDeserializationError)?; - - let capacity = payload_size - .checked_sub(bytes_past_end) - .ok_or(SlashingError::ShredDeserializationError)?; - let proof_offset = header_size - .checked_add(capacity) - .and_then(|x| x.checked_add(if self.chained { SIZE_OF_MERKLE_ROOT } else { 0 })) - .ok_or(SlashingError::ShredDeserializationError)?; - Ok((proof_offset, proof_size)) - } - - // Obtains parent's hash by joining two sibiling nodes in merkle tree. - fn join_nodes, T: AsRef<[u8]>>(node: S, other: T) -> Hash { - let node = &node.as_ref()[..SIZE_OF_MERKLE_PROOF_ENTRY]; - let other = &other.as_ref()[..SIZE_OF_MERKLE_PROOF_ENTRY]; - hashv(&[MERKLE_HASH_PREFIX_NODE, node, other]) - } - - // Recovers root of the merkle tree from a leaf node - // at the given index and the respective proof. - fn get_merkle_root<'b, I>(index: usize, node: Hash, proof: I) -> Result - where - I: IntoIterator, - { - let (index, root) = proof - .into_iter() - .fold((index, node), |(index, node), other| { - let parent = if index % 2 == 0 { - Self::join_nodes(node, other) - } else { - Self::join_nodes(other, node) - }; - (index >> 1, parent) - }); - (index == 0) - .then_some(root) - .ok_or(SlashingError::InvalidMerkleShred) - } - - /// Returns true if the other shred has the same (slot, index, - /// shred-type), but different payload. - /// Retransmitter's signature is ignored when comparing payloads. - pub(crate) fn is_shred_duplicate(&self, other: &Shred) -> bool { - if (self.slot(), self.index(), self.shred_type()) - != (other.slot(), other.index(), other.shred_type()) - { - return false; - } - fn get_payload<'a>(shred: &Shred<'a>) -> &'a [u8] { - let Ok((proof_offset, proof_size)) = shred.get_proof_offset_and_size() else { - return shred.payload; - }; - if !shred.resigned { - return shred.payload; - } - let Some(offset) = proof_offset.checked_add(proof_size) else { - return shred.payload; - }; - shred.payload.get(..offset).unwrap_or(shred.payload) - } - get_payload(self) != get_payload(other) - } - - /// Returns true if the erasure metas of the other shred matches ours. - /// Assumes that other shred has the same fec set index as ours. - pub(crate) fn check_erasure_consistency(&self, other: &Shred) -> Result { - debug_assert!(self.fec_set_index() == other.fec_set_index()); - debug_assert!(self.shred_type == ShredType::Code); - debug_assert!(other.shred_type == ShredType::Code); - Ok(self.erasure_meta()? == other.erasure_meta()?) - } -} - -impl TryFrom for ShredVariant { - type Error = SlashingError; - fn try_from(shred_variant: u8) -> Result { - if shred_variant == u8::from(ShredType::Code) { - Ok(ShredVariant::LegacyCode) - } else if shred_variant == u8::from(ShredType::Data) { - Ok(ShredVariant::LegacyData) - } else { - let proof_size = shred_variant & 0x0F; - match shred_variant & 0xF0 { - 0x40 => Ok(ShredVariant::MerkleCode { - proof_size, - chained: false, - resigned: false, - }), - 0x60 => Ok(ShredVariant::MerkleCode { - proof_size, - chained: true, - resigned: false, - }), - 0x70 => Ok(ShredVariant::MerkleCode { - proof_size, - chained: true, - resigned: true, - }), - 0x80 => Ok(ShredVariant::MerkleData { - proof_size, - chained: false, - resigned: false, - }), - 0x90 => Ok(ShredVariant::MerkleData { - proof_size, - chained: true, - resigned: false, - }), - 0xb0 => Ok(ShredVariant::MerkleData { - proof_size, - chained: true, - resigned: true, - }), - _ => Err(SlashingError::InvalidShredVariant), - } - } - } -} - -#[cfg(test)] -pub(crate) mod tests { - use { - super::Shred, - crate::shred::ShredType, - rand::Rng, - solana_entry::entry::Entry, - solana_ledger::shred::{ - ProcessShredsStats, ReedSolomonCache, Shred as SolanaShred, Shredder, - }, - solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Keypair, system_transaction}, - std::sync::Arc, - }; - - pub(crate) fn new_rand_data_shred( - rng: &mut R, - next_shred_index: u32, - shredder: &Shredder, - keypair: &Keypair, - merkle_variant: bool, - is_last_in_slot: bool, - ) -> SolanaShred { - let (mut data_shreds, _) = new_rand_shreds( - rng, - next_shred_index, - next_shred_index, - 5, - merkle_variant, - shredder, - keypair, - is_last_in_slot, - ); - data_shreds.pop().unwrap() - } - - pub(crate) fn new_rand_coding_shreds( - rng: &mut R, - next_shred_index: u32, - num_entries: usize, - shredder: &Shredder, - keypair: &Keypair, - merkle_variant: bool, - ) -> Vec { - let (_, coding_shreds) = new_rand_shreds( - rng, - next_shred_index, - next_shred_index, - num_entries, - merkle_variant, - shredder, - keypair, - true, - ); - coding_shreds - } - - #[allow(clippy::too_many_arguments)] - pub(crate) fn new_rand_shreds( - rng: &mut R, - next_shred_index: u32, - next_code_index: u32, - num_entries: usize, - merkle_variant: bool, - shredder: &Shredder, - keypair: &Keypair, - is_last_in_slot: bool, - ) -> (Vec, Vec) { - let entries: Vec<_> = std::iter::repeat_with(|| { - let tx = system_transaction::transfer( - &Keypair::new(), // from - &Pubkey::new_unique(), // to - rng.gen(), // lamports - Hash::new_unique(), // recent blockhash - ); - Entry::new( - &Hash::new_unique(), // prev_hash - 1, // num_hashes, - vec![tx], // transactions - ) - }) - .take(num_entries) - .collect(); - shredder.entries_to_shreds( - keypair, - &entries, - is_last_in_slot, - // chained_merkle_root - Some(Hash::new_from_array(rng.gen())), - next_shred_index, - next_code_index, // next_code_index - merkle_variant, - &ReedSolomonCache::default(), - &mut ProcessShredsStats::default(), - ) - } - - #[test] - fn test_solana_shred_parity() { - // Verify that the deserialization functions match solana shred format - for _ in 0..300 { - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let slot = rng.gen_range(1..u64::MAX); - let parent_slot = slot - 1; - let reference_tick = 0; - let version = rng.gen_range(0..u16::MAX); - let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); - let next_shred_index = rng.gen_range(0..671); - let next_code_index = rng.gen_range(0..781); - let is_last_in_slot = rng.gen_bool(0.5); - let (data_solana_shreds, coding_solana_shreds) = new_rand_shreds( - &mut rng, - next_shred_index, - next_code_index, - 10, - true, - &shredder, - &leader, - is_last_in_slot, - ); - - for solana_shred in data_solana_shreds - .into_iter() - .chain(coding_solana_shreds.into_iter()) - { - let payload = solana_shred.payload().as_slice(); - let shred = Shred::new_from_payload(payload).unwrap(); - - assert_eq!(shred.slot().unwrap(), solana_shred.slot()); - assert_eq!(shred.index().unwrap(), solana_shred.index()); - assert_eq!(shred.version().unwrap(), solana_shred.version()); - assert_eq!( - u8::from(shred.shred_type()), - u8::from(solana_shred.shred_type()) - ); - if shred.shred_type() == ShredType::Data { - assert_eq!(shred.last_in_slot().unwrap(), solana_shred.last_in_slot()); - } else { - let erasure_meta = shred.erasure_meta().unwrap(); - assert_eq!( - erasure_meta.num_data_shreds, - shred.num_data_shreds().unwrap() - ); - assert_eq!( - erasure_meta.num_coding_shreds, - shred.num_coding_shreds().unwrap() - ); - // We cannot verify first_coding_index until visibility is - // changed in agave - } - assert_eq!( - shred.merkle_root().unwrap(), - solana_shred.merkle_root().unwrap() - ); - assert_eq!(&shred.payload, solana_shred.payload()); - } - } - } -} diff --git a/slashing/program/src/state.rs b/slashing/program/src/state.rs deleted file mode 100644 index addd3f50449..00000000000 --- a/slashing/program/src/state.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Program state -use { - crate::{duplicate_block_proof::DuplicateBlockProofData, error::SlashingError}, - solana_program::{clock::Slot, pubkey::Pubkey}, -}; - -const PACKET_DATA_SIZE: usize = 1232; - -/// Types of slashing proofs -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ProofType { - /// Invalid proof type - InvalidType, - /// Proof consisting of 2 shreds signed by the leader indicating the leader - /// submitted a duplicate block. - DuplicateBlockProof, -} - -impl ProofType { - /// Size of the proof account to create in order to hold the proof data - /// header and contents - pub const fn proof_account_length(&self) -> usize { - match self { - Self::InvalidType => panic!("Cannot determine size of invalid proof type"), - Self::DuplicateBlockProof => { - // Duplicate block proof consists of 2 shreds that can be `PACKET_DATA_SIZE`. - DuplicateBlockProofData::size_of(PACKET_DATA_SIZE) - } - } - } - - /// Display string for this proof type's violation - pub fn violation_str(&self) -> &str { - match self { - Self::InvalidType => "invalid", - Self::DuplicateBlockProof => "duplicate block", - } - } -} - -impl From for u8 { - fn from(value: ProofType) -> Self { - match value { - ProofType::InvalidType => 0, - ProofType::DuplicateBlockProof => 1, - } - } -} - -impl From for ProofType { - fn from(value: u8) -> Self { - match value { - 1 => Self::DuplicateBlockProof, - _ => Self::InvalidType, - } - } -} - -/// Trait that proof accounts must satisfy in order to verify via the slashing -/// program -pub trait SlashingProofData<'a> { - /// The type of proof this data represents - const PROOF_TYPE: ProofType; - - /// Zero copy from raw data buffer - fn unpack(data: &'a [u8]) -> Result - where - Self: Sized; - - /// Verification logic for this type of proof data - fn verify_proof(self, slot: Slot, pubkey: &Pubkey) -> Result<(), SlashingError>; -} - -#[cfg(test)] -mod tests { - use crate::state::PACKET_DATA_SIZE; - - #[test] - fn test_packet_size_parity() { - assert_eq!(PACKET_DATA_SIZE, solana_sdk::packet::PACKET_DATA_SIZE); - } -} diff --git a/slashing/program/tests/duplicate_block_proof.rs b/slashing/program/tests/duplicate_block_proof.rs deleted file mode 100644 index bab00c51015..00000000000 --- a/slashing/program/tests/duplicate_block_proof.rs +++ /dev/null @@ -1,390 +0,0 @@ -#![cfg(feature = "test-sbf")] - -use { - rand::Rng, - solana_entry::entry::Entry, - solana_ledger::{ - blockstore_meta::ErasureMeta, - shred::{ProcessShredsStats, ReedSolomonCache, Shred, Shredder}, - }, - solana_program::pubkey::Pubkey, - solana_program_test::*, - solana_sdk::{ - clock::{Clock, Slot}, - decode_error::DecodeError, - hash::Hash, - instruction::InstructionError, - rent::Rent, - signature::{Keypair, Signer}, - system_instruction, system_transaction, - transaction::{Transaction, TransactionError}, - }, - spl_pod::bytemuck::pod_get_packed_len, - spl_record::{instruction as record, state::RecordData}, - spl_slashing::{ - duplicate_block_proof::DuplicateBlockProofData, error::SlashingError, id, instruction, - processor::process_instruction, state::ProofType, - }, - std::sync::Arc, -}; - -const SLOT: Slot = 53084024; - -fn program_test() -> ProgramTest { - let mut program_test = ProgramTest::new("spl_slashing", id(), processor!(process_instruction)); - program_test.add_program( - "spl_record", - spl_record::id(), - processor!(spl_record::processor::process_instruction), - ); - program_test -} - -async fn setup_clock(context: &mut ProgramTestContext) { - let clock: Clock = context.banks_client.get_sysvar().await.unwrap(); - let mut new_clock = clock.clone(); - new_clock.slot = SLOT; - context.set_sysvar(&new_clock); -} - -async fn initialize_duplicate_proof_account( - context: &mut ProgramTestContext, - authority: &Keypair, - account: &Keypair, -) { - let account_length = ProofType::DuplicateBlockProof - .proof_account_length() - .saturating_add(pod_get_packed_len::()); - println!("Creating account of size {account_length}"); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &account.pubkey(), - 1.max(Rent::default().minimum_balance(account_length)), - account_length as u64, - &spl_record::id(), - ), - record::initialize(&account.pubkey(), &authority.pubkey()), - ], - Some(&context.payer.pubkey()), - &[&context.payer, account], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} - -async fn write_proof( - context: &mut ProgramTestContext, - authority: &Keypair, - account: &Keypair, - proof: &[u8], -) { - let mut offset = 0; - let proof_len = proof.len(); - let chunk_size = 800; - println!("Writing a proof of size {proof_len}"); - while offset < proof_len { - let end = std::cmp::min(offset.checked_add(chunk_size).unwrap(), proof_len); - let transaction = Transaction::new_signed_with_payer( - &[record::write( - &account.pubkey(), - &authority.pubkey(), - offset as u64, - &proof[offset..end], - )], - Some(&context.payer.pubkey()), - &[&context.payer, authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - offset = offset.checked_add(chunk_size).unwrap(); - } -} - -pub fn new_rand_data_shred( - rng: &mut R, - next_shred_index: u32, - shredder: &Shredder, - keypair: &Keypair, - is_last_in_slot: bool, -) -> Shred { - let (mut data_shreds, _) = new_rand_shreds( - rng, - next_shred_index, - next_shred_index, - 5, - shredder, - keypair, - is_last_in_slot, - ); - data_shreds.pop().unwrap() -} - -pub(crate) fn new_rand_coding_shreds( - rng: &mut R, - next_shred_index: u32, - num_entries: usize, - shredder: &Shredder, - keypair: &Keypair, -) -> Vec { - let (_, coding_shreds) = new_rand_shreds( - rng, - next_shred_index, - next_shred_index, - num_entries, - shredder, - keypair, - true, - ); - coding_shreds -} - -pub(crate) fn new_rand_shreds( - rng: &mut R, - next_shred_index: u32, - next_code_index: u32, - num_entries: usize, - shredder: &Shredder, - keypair: &Keypair, - is_last_in_slot: bool, -) -> (Vec, Vec) { - let entries: Vec<_> = std::iter::repeat_with(|| { - let tx = system_transaction::transfer( - &Keypair::new(), // from - &Pubkey::new_unique(), // to - rng.gen(), // lamports - Hash::new_unique(), // recent blockhash - ); - Entry::new( - &Hash::new_unique(), // prev_hash - 1, // num_hashes, - vec![tx], // transactions - ) - }) - .take(num_entries) - .collect(); - shredder.entries_to_shreds( - keypair, - &entries, - is_last_in_slot, - // chained_merkle_root - Some(Hash::new_from_array(rng.gen())), - next_shred_index, - next_code_index, // next_code_index - true, // merkle_variant - &ReedSolomonCache::default(), - &mut ProcessShredsStats::default(), - ) -} - -#[tokio::test] -async fn valid_proof_data() { - let mut context = program_test().start_with_context().await; - setup_clock(&mut context).await; - - let authority = Keypair::new(); - let account = Keypair::new(); - - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let (slot, parent_slot, reference_tick, version) = (SLOT, 53084023, 0, 0); - let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let shred1 = new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true); - let shred2 = new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true); - - assert_ne!( - shred1.merkle_root().unwrap(), - shred2.merkle_root().unwrap(), - "Expecting merkle root conflict", - ); - - let duplicate_proof = DuplicateBlockProofData { - shred1: shred1.payload().as_slice(), - shred2: shred2.payload().as_slice(), - }; - let data = duplicate_proof.pack(); - - initialize_duplicate_proof_account(&mut context, &authority, &account).await; - write_proof(&mut context, &authority, &account, &data).await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::duplicate_block_proof( - &account.pubkey(), - RecordData::WRITABLE_START_INDEX as u64, - slot, - leader.pubkey(), - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} - -#[tokio::test] -async fn valid_proof_coding() { - let mut context = program_test().start_with_context().await; - setup_clock(&mut context).await; - - let authority = Keypair::new(); - let account = Keypair::new(); - - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let (slot, parent_slot, reference_tick, version) = (SLOT, 53084023, 0, 0); - let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let shred1 = - new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader)[0].clone(); - let shred2 = - new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader)[1].clone(); - - assert!( - ErasureMeta::check_erasure_consistency(&shred1, &shred2), - "Expected erasure consistency failure", - ); - - let duplicate_proof = DuplicateBlockProofData { - shred1: shred1.payload().as_slice(), - shred2: shred2.payload().as_slice(), - }; - let data = duplicate_proof.pack(); - - initialize_duplicate_proof_account(&mut context, &authority, &account).await; - write_proof(&mut context, &authority, &account, &data).await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::duplicate_block_proof( - &account.pubkey(), - RecordData::WRITABLE_START_INDEX as u64, - slot, - leader.pubkey(), - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} - -#[tokio::test] -async fn invalid_proof_data() { - let mut context = program_test().start_with_context().await; - setup_clock(&mut context).await; - - let authority = Keypair::new(); - let account = Keypair::new(); - - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let (slot, parent_slot, reference_tick, version) = (SLOT, 53084023, 0, 0); - let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let shred1 = new_rand_data_shred(&mut rng, next_shred_index, &shredder, &leader, true); - let shred2 = shred1.clone(); - - let duplicate_proof = DuplicateBlockProofData { - shred1: shred1.payload().as_slice(), - shred2: shred2.payload().as_slice(), - }; - let data = duplicate_proof.pack(); - - initialize_duplicate_proof_account(&mut context, &authority, &account).await; - write_proof(&mut context, &authority, &account, &data).await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::duplicate_block_proof( - &account.pubkey(), - RecordData::WRITABLE_START_INDEX as u64, - slot, - leader.pubkey(), - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let err = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - let TransactionError::InstructionError(0, InstructionError::Custom(code)) = err else { - panic!("Invalid error {err:?}"); - }; - let err: SlashingError = SlashingError::decode_custom_error_to_enum(code).unwrap(); - assert_eq!(err, SlashingError::InvalidPayloadProof); -} - -#[tokio::test] -async fn invalid_proof_coding() { - let mut context = program_test().start_with_context().await; - setup_clock(&mut context).await; - - let authority = Keypair::new(); - let account = Keypair::new(); - - let mut rng = rand::thread_rng(); - let leader = Arc::new(Keypair::new()); - let (slot, parent_slot, reference_tick, version) = (SLOT, 53084023, 0, 0); - let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); - let next_shred_index = rng.gen_range(0..32_000); - let coding_shreds = new_rand_coding_shreds(&mut rng, next_shred_index, 10, &shredder, &leader); - let shred1 = coding_shreds[0].clone(); - let shred2 = coding_shreds[1].clone(); - - assert!( - ErasureMeta::check_erasure_consistency(&shred1, &shred2), - "Expecting no erasure conflict" - ); - let duplicate_proof = DuplicateBlockProofData { - shred1: shred1.payload().as_slice(), - shred2: shred2.payload().as_slice(), - }; - let data = duplicate_proof.pack(); - - initialize_duplicate_proof_account(&mut context, &authority, &account).await; - write_proof(&mut context, &authority, &account, &data).await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::duplicate_block_proof( - &account.pubkey(), - RecordData::WRITABLE_START_INDEX as u64, - slot, - leader.pubkey(), - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let err = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - let TransactionError::InstructionError(0, InstructionError::Custom(code)) = err else { - panic!("Invalid error {err:?}"); - }; - let err: SlashingError = SlashingError::decode_custom_error_to_enum(code).unwrap(); - assert_eq!(err, SlashingError::InvalidErasureMetaConflict); -} diff --git a/stake-pool/README.md b/stake-pool/README.md index c8df8f6b6f6..7320995c63c 100644 --- a/stake-pool/README.md +++ b/stake-pool/README.md @@ -1,14 +1,2 @@ -# stake-pool program - -Full documentation is available at https://spl.solana.com/stake-pool - -The command-line interface tool is available in the `./cli` directory. - -Javascript bindings are available in the `./js` directory. - -Python bindings are available in the `./py` directory. - -## Audit - -The repository [README](https://github.com/solana-labs/solana-program-library#audits) -contains information about program audits. +NOTE: The stake-pool program and clients are now maintained at +[solana-program/stake-pool](https://github.com/solana-program/stake-pool). diff --git a/stake-pool/cli/Cargo.toml b/stake-pool/cli/Cargo.toml deleted file mode 100644 index c902d117788..00000000000 --- a/stake-pool/cli/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -authors = ["Solana Labs Maintainers "] -description = "SPL-Stake-Pool Command-line Utility" -edition = "2021" -homepage = "https://spl.solana.com/stake-pool" -license = "Apache-2.0" -name = "spl-stake-pool-cli" -repository = "https://github.com/solana-labs/solana-program-library" -version = "2.0.0" - -[dependencies] -borsh = "1.5.3" -clap = "2.33.3" -serde = "1.0.217" -serde_derive = "1.0.130" -serde_json = "1.0.135" -solana-account-decoder = "2.1.0" -solana-clap-utils = "2.1.0" -solana-cli-config = "2.1.0" -solana-cli-output = "2.1.0" -solana-client = "2.1.0" -solana-logger = "2.1.0" -solana-program = "2.1.0" -solana-remote-wallet = "2.1.0" -solana-sdk = "2.1.0" -spl-associated-token-account = { version = "=6.0.0", path = "../../associated-token-account/program", features = [ - "no-entrypoint", -] } -spl-associated-token-account-client = { version = "=2.0.0", path = "../../associated-token-account/client" } -spl-stake-pool = { version = "=2.0.1", path = "../program", features = [ - "no-entrypoint", -] } -spl-token = { version = "=7.0", path = "../../token/program", features = [ - "no-entrypoint", -] } -bs58 = "0.5.1" -bincode = "1.3.1" - -[[bin]] -name = "spl-stake-pool" -path = "src/main.rs" diff --git a/stake-pool/cli/README.md b/stake-pool/cli/README.md deleted file mode 100644 index b3ed9f3ff84..00000000000 --- a/stake-pool/cli/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# SPL Stake Pool program command-line utility - -A basic command-line for creating and using SPL Stake Pools. See https://spl.solana.com/stake-pool for more details. - -Under `./scripts`, there are helpful Bash scripts for setting up and running a -stake pool. More information at the -[stake pool quick start guide](https://spl.solana.com/stake-pool/quickstart). diff --git a/stake-pool/cli/scripts/add-validators.sh b/stake-pool/cli/scripts/add-validators.sh deleted file mode 100755 index 4bdcbdf1e1e..00000000000 --- a/stake-pool/cli/scripts/add-validators.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -# Script to add new validators to a stake pool, given the stake pool keyfile and -# a file listing validator vote account pubkeys - -cd "$(dirname "$0")" || exit -stake_pool_keyfile=$1 -validator_list=$2 # File containing validator vote account addresses, each will be added to the stake pool after creation - -add_validator_stakes () { - stake_pool=$1 - validator_list=$2 - while read -r validator - do - $spl_stake_pool add-validator "$stake_pool" "$validator" - done < "$validator_list" -} - -spl_stake_pool=spl-stake-pool -# Uncomment to use a local build -#spl_stake_pool=../../../target/debug/spl-stake-pool - -stake_pool_pubkey=$(solana-keygen pubkey "$stake_pool_keyfile") -echo "Adding validator stake accounts to the pool" -add_validator_stakes "$stake_pool_pubkey" "$validator_list" diff --git a/stake-pool/cli/scripts/deposit.sh b/stake-pool/cli/scripts/deposit.sh deleted file mode 100755 index 56cea56148e..00000000000 --- a/stake-pool/cli/scripts/deposit.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -# Script to deposit stakes and SOL into a stake pool, given the stake pool keyfile -# and a path to a file containing a list of validator vote accounts - -cd "$(dirname "$0")" || exit -stake_pool_keyfile=$1 -validator_list=$2 -sol_amount=$3 - -create_keypair () { - if test ! -f "$1" - then - solana-keygen new --no-passphrase -s -o "$1" - fi -} - -create_user_stakes () { - validator_list=$1 - sol_amount=$2 - authority=$3 - while read -r validator - do - create_keypair "$keys_dir/stake_$validator".json - solana create-stake-account "$keys_dir/stake_$validator.json" "$sol_amount" --withdraw-authority "$authority" --stake-authority "$authority" - done < "$validator_list" -} - -delegate_user_stakes () { - validator_list=$1 - authority=$2 - while read -r validator - do - solana delegate-stake --force "$keys_dir/stake_$validator.json" "$validator" --stake-authority "$authority" - done < "$validator_list" -} - -deposit_stakes () { - stake_pool_pubkey=$1 - validator_list=$2 - authority=$3 - while read -r validator - do - stake=$(solana-keygen pubkey "$keys_dir/stake_$validator.json") - $spl_stake_pool deposit-stake "$stake_pool_pubkey" "$stake" --withdraw-authority "$authority" - done < "$validator_list" -} - -keys_dir=keys -stake_pool_pubkey=$(solana-keygen pubkey "$stake_pool_keyfile") - -spl_stake_pool=spl-stake-pool -# Uncomment to use a locally build CLI -#spl_stake_pool=../../../target/debug/spl-stake-pool - -echo "Setting up keys directory $keys_dir" -mkdir -p $keys_dir -authority=$keys_dir/authority.json -echo "Setting up authority for deposited stake accounts at $authority" -create_keypair $authority - -echo "Creating user stake accounts to deposit into the pool" -create_user_stakes "$validator_list" "$sol_amount" $authority -echo "Delegating user stakes so that deposit will work" -delegate_user_stakes "$validator_list" $authority - -echo "Waiting for stakes to activate, this may take awhile depending on the network!" -echo "If you are running on localnet with 32 slots per epoch, wait 12 seconds..." -sleep 12 -echo "Depositing stakes into stake pool" -deposit_stakes "$stake_pool_pubkey" "$validator_list" $authority diff --git a/stake-pool/cli/scripts/rebalance.sh b/stake-pool/cli/scripts/rebalance.sh deleted file mode 100755 index 3db44972227..00000000000 --- a/stake-pool/cli/scripts/rebalance.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -# Script to add a certain amount of SOL into a stake pool, given the stake pool -# keyfile and a path to a file containing a list of validator vote accounts - -cd "$(dirname "$0")" || exit -stake_pool_keyfile=$1 -validator_list=$2 -sol_amount=$3 - -spl_stake_pool=spl-stake-pool -# Uncomment to use a locally build CLI -#spl_stake_pool=../../../target/debug/spl-stake-pool - -increase_stakes () { - stake_pool_pubkey=$1 - validator_list=$2 - sol_amount=$3 - while read -r validator - do - $spl_stake_pool increase-validator-stake "$stake_pool_pubkey" "$validator" "$sol_amount" - done < "$validator_list" -} - -stake_pool_pubkey=$(solana-keygen pubkey "$stake_pool_keyfile") -echo "Increasing amount delegated to each validator in stake pool" -increase_stakes "$stake_pool_pubkey" "$validator_list" "$sol_amount" diff --git a/stake-pool/cli/scripts/setup-stake-pool.sh b/stake-pool/cli/scripts/setup-stake-pool.sh deleted file mode 100755 index 9bcb5b8b98e..00000000000 --- a/stake-pool/cli/scripts/setup-stake-pool.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash - -# Script to setup a stake pool from scratch. Please modify the parameters to -# create a stake pool to your liking! - -cd "$(dirname "$0")" || exit -command_args=() -sol_amount=$1 - -################################################### -### MODIFY PARAMETERS BELOW THIS LINE FOR YOUR POOL -################################################### - -# Epoch fee, assessed as a percentage of rewards earned by the pool every epoch, -# represented as `numerator / denominator` -command_args+=( --epoch-fee-numerator 1 ) -command_args+=( --epoch-fee-denominator 100 ) - -# Withdrawal fee for SOL and stake accounts, represented as `numerator / denominator` -command_args+=( --withdrawal-fee-numerator 2 ) -command_args+=( --withdrawal-fee-denominator 100 ) - -# Deposit fee for SOL and stake accounts, represented as `numerator / denominator` -command_args+=( --deposit-fee-numerator 3 ) -command_args+=( --deposit-fee-denominator 100 ) - -command_args+=( --referral-fee 0 ) # Percentage of deposit fee that goes towards the referrer (a number between 0 and 100, inclusive) - -command_args+=( --max-validators 2350 ) # Maximum number of validators in the stake pool, 2350 is the current maximum possible - -# (Optional) Deposit authority, required to sign all deposits into the pool. -# Setting this variable makes the pool "private" or "restricted". -# Uncomment and set to a valid keypair if you want the pool to be restricted. -#command_args+=( --deposit-authority keys/authority.json ) - -################################################### -### MODIFY PARAMETERS ABOVE THIS LINE FOR YOUR POOL -################################################### - -keys_dir=keys -spl_stake_pool=spl-stake-pool -# Uncomment to use a local build -#spl_stake_pool=../../../target/debug/spl-stake-pool - -mkdir -p $keys_dir - -create_keypair () { - if test ! -f "$1" - then - solana-keygen new --no-passphrase -s -o "$1" - fi -} - -echo "Creating pool" -stake_pool_keyfile=$keys_dir/stake-pool.json -validator_list_keyfile=$keys_dir/validator-list.json -mint_keyfile=$keys_dir/mint.json -reserve_keyfile=$keys_dir/reserve.json -create_keypair $stake_pool_keyfile -create_keypair $validator_list_keyfile -create_keypair $mint_keyfile -create_keypair $reserve_keyfile - -set -ex -$spl_stake_pool \ - create-pool \ - "${command_args[@]}" \ - --pool-keypair "$stake_pool_keyfile" \ - --validator-list-keypair "$validator_list_keyfile" \ - --mint-keypair "$mint_keyfile" \ - --reserve-keypair "$reserve_keyfile" - -set +ex -stake_pool_pubkey=$(solana-keygen pubkey "$stake_pool_keyfile") -set -ex - -echo "Creating token metadata" -$spl_stake_pool \ - create-token-metadata \ - "$stake_pool_pubkey" \ - NAME \ - SYMBOL \ - URI - -echo "Depositing SOL into stake pool" -$spl_stake_pool deposit-sol "$stake_pool_pubkey" "$sol_amount" diff --git a/stake-pool/cli/scripts/setup-test-validator.sh b/stake-pool/cli/scripts/setup-test-validator.sh deleted file mode 100755 index 073e9f01830..00000000000 --- a/stake-pool/cli/scripts/setup-test-validator.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash - -# Script to setup a local solana-test-validator with the stake pool program -# given a maximum number of validators and a file path to store the list of -# test validator vote accounts. - -cd "$(dirname "$0")" || exit -max_validators=$1 -validator_file=$2 - -create_keypair () { - if test ! -f "$1" - then - solana-keygen new --no-passphrase -s -o "$1" - fi -} - -setup_test_validator() { - solana-test-validator \ - --clone-upgradeable-program SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy \ - --clone-upgradeable-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s \ - --url mainnet-beta \ - --slots-per-epoch 32 \ - --quiet --reset & - # Uncomment to use a locally built stake program - #solana-test-validator \ - # --bpf-program SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy ../../../target/deploy/spl_stake_pool.so \ - # --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s ../../program/tests/fixtures/mpl_token_metadata.so \ - # --slots-per-epoch 32 \ - # --quiet --reset & - pid=$! - solana config set --url http://127.0.0.1:8899 - solana config set --commitment confirmed - echo "waiting for solana-test-validator, pid: $pid" - sleep 15 -} - -create_vote_accounts () { - max_validators=$1 - validator_file=$2 - for number in $(seq 1 "$max_validators") - do - create_keypair "$keys_dir/identity_$number.json" - create_keypair "$keys_dir/vote_$number.json" - create_keypair "$keys_dir/withdrawer_$number.json" - solana create-vote-account "$keys_dir/vote_$number.json" "$keys_dir/identity_$number.json" "$keys_dir/withdrawer_$number.json" --commission 1 - vote_pubkey=$(solana-keygen pubkey "$keys_dir/vote_$number.json") - echo "$vote_pubkey" >> "$validator_file" - done -} - - -echo "Setup keys directory and clear old validator list file if found" -keys_dir=keys -mkdir -p $keys_dir -if test -f "$validator_file" -then - rm "$validator_file" -fi - -echo "Setting up local test validator" -setup_test_validator - -echo "Creating vote accounts, these accounts be added to the stake pool" -create_vote_accounts "$max_validators" "$validator_file" - -echo "Done adding $max_validators validator vote accounts, their pubkeys can be found in $validator_file" diff --git a/stake-pool/cli/scripts/withdraw.sh b/stake-pool/cli/scripts/withdraw.sh deleted file mode 100755 index 6ad2e327fbc..00000000000 --- a/stake-pool/cli/scripts/withdraw.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash - -# Script to withdraw stakes and SOL from a stake pool, given the stake pool public key -# and a path to a file containing a list of validator vote accounts - -cd "$(dirname "$0")" || exit -stake_pool_keyfile=$1 -validator_list=$2 -withdraw_sol_amount=$3 - -create_keypair () { - if test ! -f "$1" - then - solana-keygen new --no-passphrase -s -o "$1" - fi -} - -create_stake_account () { - authority=$1 - while read -r validator - do - solana-keygen new --no-passphrase -o "$keys_dir/stake_account_$validator.json" - solana create-stake-account "$keys_dir/stake_account_$validator.json" 2 - solana delegate-stake --force "$keys_dir/stake_account_$validator.json" "$validator" - done < "$validator_list" -} - -withdraw_stakes () { - stake_pool_pubkey=$1 - validator_list=$2 - pool_amount=$3 - while read -r validator - do - $spl_stake_pool withdraw-stake "$stake_pool_pubkey" "$pool_amount" --vote-account "$validator" - done < "$validator_list" -} - -withdraw_stakes_to_stake_receiver () { - stake_pool_pubkey=$1 - validator_list=$2 - pool_amount=$3 - while read -r validator - do - stake_receiver=$(solana-keygen pubkey "$keys_dir/stake_account_$validator.json") - $spl_stake_pool withdraw-stake "$stake_pool_pubkey" "$pool_amount" --vote-account "$validator" --stake-receiver "$stake_receiver" - done < "$validator_list" -} - -spl_stake_pool=spl-stake-pool -# Uncomment to use a locally build CLI -# spl_stake_pool=../../../target/debug/spl-stake-pool - -stake_pool_pubkey=$(solana-keygen pubkey "$stake_pool_keyfile") -keys_dir=keys - -echo "Setting up keys directory $keys_dir" -mkdir -p $keys_dir -authority=$keys_dir/authority.json - -create_stake_account $authority -echo "Waiting for stakes to activate, this may take awhile depending on the network!" -echo "If you are running on localnet with 32 slots per epoch, wait 12 seconds..." -sleep 12 - -echo "Setting up authority for withdrawn stake accounts at $authority" -create_keypair $authority - -echo "Withdrawing stakes from stake pool" -withdraw_stakes "$stake_pool_pubkey" "$validator_list" "$withdraw_sol_amount" - -echo "Withdrawing stakes from stake pool to receive it in stake receiver account" -withdraw_stakes_to_stake_receiver "$stake_pool_pubkey" "$validator_list" "$withdraw_sol_amount" - -echo "Withdrawing SOL from stake pool to authority" -$spl_stake_pool withdraw-sol "$stake_pool_pubkey" $authority "$withdraw_sol_amount" diff --git a/stake-pool/cli/src/client.rs b/stake-pool/cli/src/client.rs deleted file mode 100644 index 5d3974a585a..00000000000 --- a/stake-pool/cli/src/client.rs +++ /dev/null @@ -1,184 +0,0 @@ -use { - bincode::deserialize, - solana_account_decoder::UiAccountEncoding, - solana_client::{ - client_error::ClientError, - rpc_client::RpcClient, - rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, - rpc_filter::{Memcmp, RpcFilterType}, - }, - solana_program::{ - borsh1::try_from_slice_unchecked, hash::Hash, instruction::Instruction, message::Message, - program_pack::Pack, pubkey::Pubkey, stake, - }, - solana_sdk::{compute_budget::ComputeBudgetInstruction, transaction::Transaction}, - spl_stake_pool::{ - find_withdraw_authority_program_address, - state::{StakePool, ValidatorList}, - }, - std::collections::HashSet, -}; - -pub(crate) type Error = Box; - -pub fn get_stake_pool( - rpc_client: &RpcClient, - stake_pool_address: &Pubkey, -) -> Result { - let account_data = rpc_client.get_account_data(stake_pool_address)?; - let stake_pool = try_from_slice_unchecked::(account_data.as_slice()) - .map_err(|err| format!("Invalid stake pool {}: {}", stake_pool_address, err))?; - Ok(stake_pool) -} - -pub fn get_validator_list( - rpc_client: &RpcClient, - validator_list_address: &Pubkey, -) -> Result { - let account_data = rpc_client.get_account_data(validator_list_address)?; - let validator_list = try_from_slice_unchecked::(account_data.as_slice()) - .map_err(|err| format!("Invalid validator list {}: {}", validator_list_address, err))?; - Ok(validator_list) -} - -pub fn get_token_account( - rpc_client: &RpcClient, - token_account_address: &Pubkey, - expected_token_mint: &Pubkey, -) -> Result { - let account_data = rpc_client.get_account_data(token_account_address)?; - let token_account = spl_token::state::Account::unpack_from_slice(account_data.as_slice()) - .map_err(|err| format!("Invalid token account {}: {}", token_account_address, err))?; - - if token_account.mint != *expected_token_mint { - Err(format!( - "Invalid token mint for {}, expected mint is {}", - token_account_address, expected_token_mint - ) - .into()) - } else { - Ok(token_account) - } -} - -pub fn get_token_mint( - rpc_client: &RpcClient, - token_mint_address: &Pubkey, -) -> Result { - let account_data = rpc_client.get_account_data(token_mint_address)?; - let token_mint = spl_token::state::Mint::unpack_from_slice(account_data.as_slice()) - .map_err(|err| format!("Invalid token mint {}: {}", token_mint_address, err))?; - - Ok(token_mint) -} - -pub(crate) fn get_stake_state( - rpc_client: &RpcClient, - stake_address: &Pubkey, -) -> Result { - let account_data = rpc_client.get_account_data(stake_address)?; - let stake_state = deserialize(account_data.as_slice()) - .map_err(|err| format!("Invalid stake account {}: {}", stake_address, err))?; - Ok(stake_state) -} - -pub(crate) fn get_stake_pools( - rpc_client: &RpcClient, -) -> Result, ClientError> { - rpc_client - .get_program_accounts_with_config( - &spl_stake_pool::id(), - RpcProgramAccountsConfig { - // 0 is the account type - filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes( - 0, - vec![1], - ))]), - account_config: RpcAccountInfoConfig { - encoding: Some(UiAccountEncoding::Base64), - ..RpcAccountInfoConfig::default() - }, - ..RpcProgramAccountsConfig::default() - }, - ) - .map(|accounts| { - accounts - .into_iter() - .filter_map(|(address, account)| { - let pool_withdraw_authority = - find_withdraw_authority_program_address(&spl_stake_pool::id(), &address).0; - match try_from_slice_unchecked::(account.data.as_slice()) { - Ok(stake_pool) => { - get_validator_list(rpc_client, &stake_pool.validator_list) - .map(|validator_list| { - (address, stake_pool, validator_list, pool_withdraw_authority) - }) - .ok() - } - Err(err) => { - eprintln!("Invalid stake pool data for {}: {}", address, err); - None - } - } - }) - .collect() - }) -} - -pub(crate) fn get_all_stake( - rpc_client: &RpcClient, - authorized_staker: &Pubkey, -) -> Result, ClientError> { - let all_stake_accounts = rpc_client.get_program_accounts_with_config( - &stake::program::id(), - RpcProgramAccountsConfig { - filters: Some(vec![ - // Filter by `Meta::authorized::staker`, which begins at byte offset 12 - RpcFilterType::Memcmp(Memcmp::new_base58_encoded(12, authorized_staker.as_ref())), - ]), - account_config: RpcAccountInfoConfig { - encoding: Some(solana_account_decoder::UiAccountEncoding::Base64), - commitment: Some(rpc_client.commitment()), - ..RpcAccountInfoConfig::default() - }, - ..RpcProgramAccountsConfig::default() - }, - )?; - - Ok(all_stake_accounts - .into_iter() - .map(|(address, _)| address) - .collect()) -} - -/// Helper function to add a compute unit limit instruction to a given set -/// of instructions -pub(crate) fn add_compute_unit_limit_from_simulation( - rpc_client: &RpcClient, - instructions: &mut Vec, - payer: &Pubkey, - blockhash: &Hash, -) -> Result<(), Error> { - // add a max compute unit limit instruction for the simulation - const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; - instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( - MAX_COMPUTE_UNIT_LIMIT, - )); - - let transaction = Transaction::new_unsigned(Message::new_with_blockhash( - instructions, - Some(payer), - blockhash, - )); - let simulation_result = rpc_client.simulate_transaction(&transaction)?.value; - let units_consumed = simulation_result - .units_consumed - .ok_or("No units consumed on simulation")?; - // Overwrite the compute unit limit instruction with the actual units consumed - let compute_unit_limit = u32::try_from(units_consumed)?; - instructions - .last_mut() - .expect("Compute budget instruction was added earlier") - .data = ComputeBudgetInstruction::set_compute_unit_limit(compute_unit_limit).data; - Ok(()) -} diff --git a/stake-pool/cli/src/main.rs b/stake-pool/cli/src/main.rs deleted file mode 100644 index e56990f1c9b..00000000000 --- a/stake-pool/cli/src/main.rs +++ /dev/null @@ -1,3479 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -mod client; -mod output; - -use { - crate::{ - client::*, - output::{CliStakePool, CliStakePoolDetails, CliStakePoolStakeAccountInfo, CliStakePools}, - }, - bincode::deserialize, - clap::{ - crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings, - Arg, ArgGroup, ArgMatches, SubCommand, - }, - solana_clap_utils::{ - compute_unit_price::{compute_unit_price_arg, COMPUTE_UNIT_PRICE_ARG}, - input_parsers::{keypair_of, pubkey_of}, - input_validators::{ - is_amount, is_keypair_or_ask_keyword, is_parsable, is_pubkey, is_url, - is_valid_percentage, is_valid_pubkey, is_valid_signer, - }, - keypair::{signer_from_path_with_config, SignerFromPathConfig}, - ArgConstant, - }, - solana_cli_output::OutputFormat, - solana_client::rpc_client::RpcClient, - solana_program::{ - borsh1::{get_instance_packed_len, get_packed_len}, - instruction::Instruction, - program_pack::Pack, - pubkey::Pubkey, - stake, - }, - solana_remote_wallet::remote_wallet::RemoteWalletManager, - solana_sdk::{ - commitment_config::CommitmentConfig, - compute_budget::ComputeBudgetInstruction, - hash::Hash, - message::Message, - native_token::{self, Sol}, - signature::{Keypair, Signer}, - signers::Signers, - system_instruction, - transaction::Transaction, - }, - spl_associated_token_account::instruction::create_associated_token_account, - spl_associated_token_account_client::address::get_associated_token_address, - spl_stake_pool::{ - self, find_stake_program_address, find_transient_stake_program_address, - find_withdraw_authority_program_address, - instruction::{FundingType, PreferredValidatorType}, - minimum_delegation, - state::{Fee, FeeType, StakePool, ValidatorList, ValidatorStakeInfo}, - MINIMUM_RESERVE_LAMPORTS, - }, - std::{cmp::Ordering, num::NonZeroU32, process::exit, rc::Rc}, -}; - -pub(crate) struct Config { - rpc_client: RpcClient, - verbose: bool, - output_format: OutputFormat, - manager: Box, - staker: Box, - funding_authority: Option>, - token_owner: Box, - fee_payer: Box, - dry_run: bool, - no_update: bool, - compute_unit_price: Option, - compute_unit_limit: ComputeUnitLimit, -} - -type CommandResult = Result<(), Error>; - -const STAKE_STATE_LEN: usize = 200; - -macro_rules! unique_signers { - ($vec:ident) => { - $vec.sort_by_key(|l| l.pubkey()); - $vec.dedup(); - }; -} - -fn check_fee_payer_balance(config: &Config, required_balance: u64) -> Result<(), Error> { - let balance = config.rpc_client.get_balance(&config.fee_payer.pubkey())?; - if balance < required_balance { - Err(format!( - "Fee payer, {}, has insufficient balance: {} required, {} available", - config.fee_payer.pubkey(), - Sol(required_balance), - Sol(balance) - ) - .into()) - } else { - Ok(()) - } -} - -const FEES_REFERENCE: &str = "Consider setting a minimal fee. \ - See https://spl.solana.com/stake-pool/fees for more \ - information about fees and best practices. If you are \ - aware of the possible risks of a stake pool with no fees, \ - you may force pool creation with the --unsafe-fees flag."; - -enum ComputeUnitLimit { - Default, - Static(u32), - Simulated, -} -const COMPUTE_UNIT_LIMIT_ARG: ArgConstant<'static> = ArgConstant { - name: "compute_unit_limit", - long: "--with-compute-unit-limit", - help: "Set compute unit limit for transaction, in compute units; also accepts \ - keyword DEFAULT to use the default compute unit limit, which is 200k per \ - top-level instruction, with a maximum of 1.4 million. \ - If nothing is set, transactions are simulated prior to sending, and the \ - compute units consumed are set as the limit. This may may fail if accounts \ - are modified by another transaction between simulation and execution.", -}; -fn is_compute_unit_limit_or_simulated(string: T) -> Result<(), String> -where - T: AsRef + std::fmt::Display, -{ - if string.as_ref().parse::().is_ok() || string.as_ref() == "DEFAULT" { - Ok(()) - } else { - Err(format!( - "Unable to parse input compute unit limit as integer or DEFAULT, provided: {string}" - )) - } -} -fn parse_compute_unit_limit(string: T) -> Result -where - T: AsRef + std::fmt::Display, -{ - match string.as_ref().parse::() { - Ok(compute_unit_limit) => Ok(ComputeUnitLimit::Static(compute_unit_limit)), - Err(_) if string.as_ref() == "DEFAULT" => Ok(ComputeUnitLimit::Default), - _ => Err(format!( - "Unable to parse compute unit limit, provided: {string}" - )), - } -} - -fn check_stake_pool_fees( - epoch_fee: &Fee, - withdrawal_fee: &Fee, - deposit_fee: &Fee, -) -> Result<(), Error> { - if epoch_fee.numerator == 0 || epoch_fee.denominator == 0 { - return Err(format!("Epoch fee should not be 0. {}", FEES_REFERENCE,).into()); - } - let is_withdrawal_fee_zero = withdrawal_fee.numerator == 0 || withdrawal_fee.denominator == 0; - let is_deposit_fee_zero = deposit_fee.numerator == 0 || deposit_fee.denominator == 0; - if is_withdrawal_fee_zero && is_deposit_fee_zero { - return Err(format!( - "Withdrawal and deposit fee should not both be 0. {}", - FEES_REFERENCE, - ) - .into()); - } - Ok(()) -} - -fn get_signer( - matches: &ArgMatches<'_>, - keypair_name: &str, - keypair_path: &str, - wallet_manager: &mut Option>, - signer_from_path_config: SignerFromPathConfig, -) -> Box { - signer_from_path_with_config( - matches, - matches.value_of(keypair_name).unwrap_or(keypair_path), - keypair_name, - wallet_manager, - &signer_from_path_config, - ) - .unwrap_or_else(|e| { - eprintln!("error: {}", e); - exit(1); - }) -} - -fn get_latest_blockhash(client: &RpcClient) -> Result { - Ok(client - .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())? - .0) -} - -fn send_transaction_no_wait( - config: &Config, - transaction: Transaction, -) -> solana_client::client_error::Result<()> { - if config.dry_run { - let result = config.rpc_client.simulate_transaction(&transaction)?; - println!("Simulate result: {:?}", result); - } else { - let signature = config.rpc_client.send_transaction(&transaction)?; - println!("Signature: {}", signature); - } - Ok(()) -} - -fn send_transaction( - config: &Config, - transaction: Transaction, -) -> solana_client::client_error::Result<()> { - if config.dry_run { - let result = config.rpc_client.simulate_transaction(&transaction)?; - println!("Simulate result: {:?}", result); - } else { - let signature = config - .rpc_client - .send_and_confirm_transaction_with_spinner(&transaction)?; - println!("Signature: {}", signature); - } - Ok(()) -} - -fn checked_transaction_with_signers_and_additional_fee( - config: &Config, - instructions: &[Instruction], - signers: &T, - additional_fee: u64, -) -> Result { - let recent_blockhash = get_latest_blockhash(&config.rpc_client)?; - let mut instructions = instructions.to_vec(); - if let Some(compute_unit_price) = config.compute_unit_price { - instructions.push(ComputeBudgetInstruction::set_compute_unit_price( - compute_unit_price, - )); - } - match config.compute_unit_limit { - ComputeUnitLimit::Default => {} - ComputeUnitLimit::Static(compute_unit_limit) => { - instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( - compute_unit_limit, - )); - } - ComputeUnitLimit::Simulated => { - add_compute_unit_limit_from_simulation( - &config.rpc_client, - &mut instructions, - &config.fee_payer.pubkey(), - &recent_blockhash, - )?; - } - } - let message = Message::new_with_blockhash( - &instructions, - Some(&config.fee_payer.pubkey()), - &recent_blockhash, - ); - check_fee_payer_balance( - config, - additional_fee.saturating_add(config.rpc_client.get_fee_for_message(&message)?), - )?; - let transaction = Transaction::new(signers, message, recent_blockhash); - Ok(transaction) -} - -fn checked_transaction_with_signers( - config: &Config, - instructions: &[Instruction], - signers: &T, -) -> Result { - checked_transaction_with_signers_and_additional_fee(config, instructions, signers, 0) -} - -fn new_stake_account( - fee_payer: &Pubkey, - instructions: &mut Vec, - lamports: u64, -) -> Keypair { - // Account for tokens not specified, creating one - let stake_receiver_keypair = Keypair::new(); - let stake_receiver_pubkey = stake_receiver_keypair.pubkey(); - println!( - "Creating account to receive stake {}", - stake_receiver_pubkey - ); - - instructions.push( - // Creating new account - system_instruction::create_account( - fee_payer, - &stake_receiver_pubkey, - lamports, - STAKE_STATE_LEN as u64, - &stake::program::id(), - ), - ); - - stake_receiver_keypair -} - -fn setup_reserve_stake_account( - config: &Config, - reserve_keypair: &Keypair, - reserve_stake_balance: u64, - withdraw_authority: &Pubkey, -) -> CommandResult { - let reserve_account_info = config.rpc_client.get_account(&reserve_keypair.pubkey()); - if let Ok(account) = reserve_account_info { - if account.owner == stake::program::id() { - if account.data.iter().any(|&x| x != 0) { - println!( - "Reserve stake account {} already exists and is initialized", - reserve_keypair.pubkey() - ); - return Ok(()); - } else { - let instructions = vec![stake::instruction::initialize( - &reserve_keypair.pubkey(), - &stake::state::Authorized { - staker: *withdraw_authority, - withdrawer: *withdraw_authority, - }, - &stake::state::Lockup::default(), - )]; - let signers = vec![config.fee_payer.as_ref()]; - let transaction = - checked_transaction_with_signers(config, &instructions, &signers)?; - println!( - "Initializing existing reserve stake account {}", - reserve_keypair.pubkey() - ); - send_transaction(config, transaction)?; - return Ok(()); - } - } - } - - let instructions = vec![ - system_instruction::create_account( - &config.fee_payer.pubkey(), - &reserve_keypair.pubkey(), - reserve_stake_balance, - STAKE_STATE_LEN as u64, - &stake::program::id(), - ), - stake::instruction::initialize( - &reserve_keypair.pubkey(), - &stake::state::Authorized { - staker: *withdraw_authority, - withdrawer: *withdraw_authority, - }, - &stake::state::Lockup::default(), - ), - ]; - - let signers = vec![config.fee_payer.as_ref(), reserve_keypair]; - let transaction = checked_transaction_with_signers(config, &instructions, &signers)?; - - println!( - "Creating and initializing reserve stake account {}", - reserve_keypair.pubkey() - ); - send_transaction(config, transaction)?; - Ok(()) -} - -fn setup_mint_account( - config: &Config, - mint_keypair: &Keypair, - mint_account_balance: u64, - withdraw_authority: &Pubkey, - default_decimals: u8, -) -> CommandResult { - let mint_account_info = config.rpc_client.get_account(&mint_keypair.pubkey()); - if let Ok(account) = mint_account_info { - if account.owner == spl_token::id() { - if account.data.iter().any(|&x| x != 0) { - println!( - "Mint account {} already exists and is initialized", - mint_keypair.pubkey() - ); - return Ok(()); - } else { - let instructions = vec![spl_token::instruction::initialize_mint( - &spl_token::id(), - &mint_keypair.pubkey(), - withdraw_authority, - None, - default_decimals, - )?]; - let signers = vec![config.fee_payer.as_ref()]; - let transaction = - checked_transaction_with_signers(config, &instructions, &signers)?; - println!( - "Initializing existing mint account {}", - mint_keypair.pubkey() - ); - send_transaction(config, transaction)?; - return Ok(()); - } - } - } - - let instructions = vec![ - system_instruction::create_account( - &config.fee_payer.pubkey(), - &mint_keypair.pubkey(), - mint_account_balance, - spl_token::state::Mint::LEN as u64, - &spl_token::id(), - ), - spl_token::instruction::initialize_mint( - &spl_token::id(), - &mint_keypair.pubkey(), - withdraw_authority, - None, - default_decimals, - )?, - ]; - - let signers = vec![config.fee_payer.as_ref(), mint_keypair]; - let transaction = checked_transaction_with_signers(config, &instructions, &signers)?; - - println!( - "Creating and initializing mint account {}", - mint_keypair.pubkey() - ); - send_transaction(config, transaction)?; - Ok(()) -} - -fn setup_pool_fee_account( - config: &Config, - mint_pubkey: &Pubkey, - total_rent_free_balances: &mut u64, -) -> CommandResult { - let pool_fee_account = get_associated_token_address(&config.manager.pubkey(), mint_pubkey); - let pool_fee_account_info = config.rpc_client.get_account(&pool_fee_account); - if let Ok(account) = pool_fee_account_info { - if account.owner == spl_token::id() { - println!("Pool fee account {} already exists", pool_fee_account); - return Ok(()); - } - } - // Create pool fee account - let mut instructions = vec![]; - add_associated_token_account( - config, - mint_pubkey, - &config.manager.pubkey(), - &mut instructions, - total_rent_free_balances, - ); - - println!("Creating pool fee collection account {}", pool_fee_account); - - let signers = vec![config.fee_payer.as_ref()]; - let transaction = checked_transaction_with_signers(config, &instructions, &signers)?; - - send_transaction(config, transaction)?; - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn setup_and_initialize_validator_list_with_stake_pool( - config: &Config, - stake_pool_keypair: &Keypair, - validator_list_keypair: &Keypair, - reserve_keypair: &Keypair, - mint_keypair: &Keypair, - pool_fee_account: &Pubkey, - deposit_authority: Option, - epoch_fee: Fee, - withdrawal_fee: Fee, - deposit_fee: Fee, - referral_fee: u8, - max_validators: u32, - withdraw_authority: &Pubkey, - validator_list_balance: u64, - validator_list_size: usize, -) -> CommandResult { - let stake_pool_account_info = config.rpc_client.get_account(&stake_pool_keypair.pubkey()); - let validator_list_account_info = config - .rpc_client - .get_account(&validator_list_keypair.pubkey()); - - let stake_pool_account_lamports = config - .rpc_client - .get_minimum_balance_for_rent_exemption(get_packed_len::())?; - - let mut instructions = vec![]; - let mut signers = vec![config.fee_payer.as_ref(), config.manager.as_ref()]; - - if let Ok(account) = validator_list_account_info { - if account.owner == spl_stake_pool::id() { - if account.data.iter().all(|&x| x == 0) { - println!( - "Validator list account {} already exists and is ready to be initialized", - validator_list_keypair.pubkey() - ); - } else { - println!( - "Validator list account {} already exists and is initialized", - validator_list_keypair.pubkey() - ); - return Ok(()); - } - } - } else { - instructions.push(system_instruction::create_account( - &config.fee_payer.pubkey(), - &validator_list_keypair.pubkey(), - validator_list_balance, - validator_list_size as u64, - &spl_stake_pool::id(), - )); - signers.push(validator_list_keypair); - } - - if let Ok(account) = stake_pool_account_info { - if account.owner == spl_stake_pool::id() { - if account.data.iter().all(|&x| x == 0) { - println!( - "Stake pool account {} already exists but is not initialized", - stake_pool_keypair.pubkey() - ); - } else { - println!( - "Stake pool account {} already exists and is initialized", - stake_pool_keypair.pubkey() - ); - return Ok(()); - } - } - } else { - instructions.push(system_instruction::create_account( - &config.fee_payer.pubkey(), - &stake_pool_keypair.pubkey(), - stake_pool_account_lamports, - get_packed_len::() as u64, - &spl_stake_pool::id(), - )); - } - instructions.push(spl_stake_pool::instruction::initialize( - &spl_stake_pool::id(), - &stake_pool_keypair.pubkey(), - &config.manager.pubkey(), - &config.staker.pubkey(), - withdraw_authority, - &validator_list_keypair.pubkey(), - &reserve_keypair.pubkey(), - &mint_keypair.pubkey(), - pool_fee_account, - &spl_token::id(), - deposit_authority.as_ref().map(|x| x.pubkey()), - epoch_fee, - withdrawal_fee, - deposit_fee, - referral_fee, - max_validators, - )); - signers.push(stake_pool_keypair); - - if let Some(ref deposit_auth) = deposit_authority { - signers.push(deposit_auth); - println!( - "Deposits will be restricted to {} only, this can be changed using the set-funding-authority command.", - deposit_auth.pubkey() - ); - } - - unique_signers!(signers); - let transaction = checked_transaction_with_signers(config, &instructions, &signers)?; - - println!( - "Setting up and initializing stake pool account {} with validator list {}", - stake_pool_keypair.pubkey(), - validator_list_keypair.pubkey() - ); - send_transaction(config, transaction)?; - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn command_create_pool( - config: &Config, - deposit_authority: Option, - epoch_fee: Fee, - withdrawal_fee: Fee, - deposit_fee: Fee, - referral_fee: u8, - max_validators: u32, - stake_pool_keypair: Option, - validator_list_keypair: Option, - mint_keypair: Option, - reserve_keypair: Option, - unsafe_fees: bool, -) -> CommandResult { - if !unsafe_fees { - check_stake_pool_fees(&epoch_fee, &withdrawal_fee, &deposit_fee)?; - } - - let reserve_keypair = reserve_keypair.unwrap_or_else(Keypair::new); - let mint_keypair = mint_keypair.unwrap_or_else(Keypair::new); - let stake_pool_keypair = stake_pool_keypair.unwrap_or_else(Keypair::new); - let validator_list_keypair = validator_list_keypair.unwrap_or_else(Keypair::new); - - let reserve_stake_balance = config - .rpc_client - .get_minimum_balance_for_rent_exemption(STAKE_STATE_LEN)? - + MINIMUM_RESERVE_LAMPORTS; - let mint_account_balance = config - .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token::state::Mint::LEN)?; - let pool_fee_account_balance = config - .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN)?; - let stake_pool_account_lamports = config - .rpc_client - .get_minimum_balance_for_rent_exemption(get_packed_len::())?; - let empty_validator_list = ValidatorList::new(max_validators); - let validator_list_size = get_instance_packed_len(&empty_validator_list)?; - let validator_list_balance = config - .rpc_client - .get_minimum_balance_for_rent_exemption(validator_list_size)?; - let mut total_rent_free_balances = reserve_stake_balance - + mint_account_balance - + pool_fee_account_balance - + stake_pool_account_lamports - + validator_list_balance; - - let default_decimals = spl_token::native_mint::DECIMALS; - - // Calculate withdraw authority used for minting pool tokens - let (withdraw_authority, _) = find_withdraw_authority_program_address( - &spl_stake_pool::id(), - &stake_pool_keypair.pubkey(), - ); - - if config.verbose { - println!("Stake pool withdraw authority {}", withdraw_authority); - } - - setup_reserve_stake_account( - config, - &reserve_keypair, - reserve_stake_balance, - &withdraw_authority, - )?; - setup_mint_account( - config, - &mint_keypair, - mint_account_balance, - &withdraw_authority, - default_decimals, - )?; - setup_pool_fee_account( - config, - &mint_keypair.pubkey(), - &mut total_rent_free_balances, - )?; - - let pool_fee_account = - get_associated_token_address(&config.manager.pubkey(), &mint_keypair.pubkey()); - - setup_and_initialize_validator_list_with_stake_pool( - config, - &stake_pool_keypair, - &validator_list_keypair, - &reserve_keypair, - &mint_keypair, - &pool_fee_account, - deposit_authority, - epoch_fee, - withdrawal_fee, - deposit_fee, - referral_fee, - max_validators, - &withdraw_authority, - validator_list_balance, - validator_list_size, - )?; - - Ok(()) -} - -fn create_token_metadata( - config: &Config, - stake_pool_address: &Pubkey, - name: String, - symbol: String, - uri: String, -) -> CommandResult { - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - - let mut signers = vec![config.fee_payer.as_ref(), config.manager.as_ref()]; - let instructions = vec![spl_stake_pool::instruction::create_token_metadata( - &spl_stake_pool::id(), - stake_pool_address, - &stake_pool.manager, - &stake_pool.pool_mint, - &config.fee_payer.pubkey(), - name, - symbol, - uri, - )]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers(config, &instructions, &signers)?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn update_token_metadata( - config: &Config, - stake_pool_address: &Pubkey, - name: String, - symbol: String, - uri: String, -) -> CommandResult { - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - - let mut signers = vec![config.fee_payer.as_ref(), config.manager.as_ref()]; - let instructions = vec![spl_stake_pool::instruction::update_token_metadata( - &spl_stake_pool::id(), - stake_pool_address, - &stake_pool.manager, - &stake_pool.pool_mint, - name, - symbol, - uri, - )]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers(config, &instructions, &signers)?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_vsa_add( - config: &Config, - stake_pool_address: &Pubkey, - vote_account: &Pubkey, -) -> CommandResult { - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - if validator_list.contains(vote_account) { - println!( - "Stake pool already contains validator {}, ignoring", - vote_account - ); - return Ok(()); - } - - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - - // iterate until a free account is found - let (stake_account_address, validator_seed) = { - let mut i = 0; - loop { - let seed = NonZeroU32::new(i); - let (address, _) = find_stake_program_address( - &spl_stake_pool::id(), - vote_account, - stake_pool_address, - seed, - ); - let maybe_account = config - .rpc_client - .get_account_with_commitment( - &stake_pool.reserve_stake, - config.rpc_client.commitment(), - )? - .value; - if maybe_account.is_some() { - break (address, seed); - } - i += 1; - } - }; - println!( - "Adding stake account {}, delegated to {}", - stake_account_address, vote_account - ); - - let mut signers = vec![config.fee_payer.as_ref(), config.staker.as_ref()]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers( - config, - &[ - spl_stake_pool::instruction::add_validator_to_pool_with_vote( - &spl_stake_pool::id(), - &stake_pool, - stake_pool_address, - vote_account, - validator_seed, - ), - ], - &signers, - )?; - - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_vsa_remove( - config: &Config, - stake_pool_address: &Pubkey, - vote_account: &Pubkey, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - let validator_stake_info = validator_list - .find(vote_account) - .ok_or("Vote account not found in validator list")?; - - let validator_seed = NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()); - let (stake_account_address, _) = find_stake_program_address( - &spl_stake_pool::id(), - vote_account, - stake_pool_address, - validator_seed, - ); - println!( - "Removing stake account {}, delegated to {}", - stake_account_address, vote_account - ); - - let mut signers = vec![config.fee_payer.as_ref(), config.staker.as_ref()]; - let instructions = vec![ - // Create new validator stake account address - spl_stake_pool::instruction::remove_validator_from_pool_with_vote( - &spl_stake_pool::id(), - &stake_pool, - stake_pool_address, - vote_account, - validator_seed, - validator_stake_info.transient_seed_suffix.into(), - ), - ]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers(config, &instructions, &signers)?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_increase_validator_stake( - config: &Config, - stake_pool_address: &Pubkey, - vote_account: &Pubkey, - amount: f64, -) -> CommandResult { - let lamports = native_token::sol_to_lamports(amount); - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - let validator_stake_info = validator_list - .find(vote_account) - .ok_or("Vote account not found in validator list")?; - let validator_seed = NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()); - - let mut signers = vec![config.fee_payer.as_ref(), config.staker.as_ref()]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers( - config, - &[ - spl_stake_pool::instruction::increase_validator_stake_with_vote( - &spl_stake_pool::id(), - &stake_pool, - stake_pool_address, - vote_account, - lamports, - validator_seed, - validator_stake_info.transient_seed_suffix.into(), - ), - ], - &signers, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_decrease_validator_stake( - config: &Config, - stake_pool_address: &Pubkey, - vote_account: &Pubkey, - amount: f64, -) -> CommandResult { - let lamports = native_token::sol_to_lamports(amount); - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - let validator_stake_info = validator_list - .find(vote_account) - .ok_or("Vote account not found in validator list")?; - let validator_seed = NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()); - - let mut signers = vec![config.fee_payer.as_ref(), config.staker.as_ref()]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers( - config, - &[ - spl_stake_pool::instruction::decrease_validator_stake_with_vote( - &spl_stake_pool::id(), - &stake_pool, - stake_pool_address, - vote_account, - lamports, - validator_seed, - validator_stake_info.transient_seed_suffix.into(), - ), - ], - &signers, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_set_preferred_validator( - config: &Config, - stake_pool_address: &Pubkey, - preferred_type: PreferredValidatorType, - vote_address: Option, -) -> CommandResult { - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let mut signers = vec![config.fee_payer.as_ref(), config.staker.as_ref()]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers( - config, - &[spl_stake_pool::instruction::set_preferred_validator( - &spl_stake_pool::id(), - stake_pool_address, - &config.staker.pubkey(), - &stake_pool.validator_list, - preferred_type, - vote_address, - )], - &signers, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn add_associated_token_account( - config: &Config, - mint: &Pubkey, - owner: &Pubkey, - instructions: &mut Vec, - rent_free_balances: &mut u64, -) -> Pubkey { - // Account for tokens not specified, creating one - let account = get_associated_token_address(owner, mint); - if get_token_account(&config.rpc_client, &account, mint).is_err() { - println!("Creating associated token account {} to receive stake pool tokens of mint {}, owned by {}", account, mint, owner); - - let min_account_balance = config - .rpc_client - .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) - .unwrap(); - - instructions.push(create_associated_token_account( - &config.fee_payer.pubkey(), - owner, - mint, - &spl_token::id(), - )); - - *rent_free_balances += min_account_balance; - } else { - println!("Using existing associated token account {} to receive stake pool tokens of mint {}, owned by {}", account, mint, owner); - } - - account -} - -fn command_deposit_stake( - config: &Config, - stake_pool_address: &Pubkey, - stake: &Pubkey, - withdraw_authority: Box, - pool_token_receiver_account: &Option, - referrer_token_account: &Option, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let stake_state = get_stake_state(&config.rpc_client, stake)?; - - if config.verbose { - println!("Depositing stake account {:?}", stake_state); - } - let vote_account = match stake_state { - stake::state::StakeStateV2::Stake(_, stake, _) => Ok(stake.delegation.voter_pubkey), - _ => Err("Wrong stake account state, must be delegated to validator"), - }?; - - // Check if this vote account has staking account in the pool - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - let validator_stake_info = validator_list - .find(&vote_account) - .ok_or("Vote account not found in the stake pool")?; - let validator_seed = NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()); - - // Calculate validator stake account address linked to the pool - let (validator_stake_account, _) = find_stake_program_address( - &spl_stake_pool::id(), - &vote_account, - stake_pool_address, - validator_seed, - ); - - let validator_stake_state = get_stake_state(&config.rpc_client, &validator_stake_account)?; - println!( - "Depositing stake {} into stake pool account {}", - stake, validator_stake_account - ); - if config.verbose { - println!("{:?}", validator_stake_state); - } - - let mut instructions: Vec = vec![]; - let mut signers = vec![config.fee_payer.as_ref(), withdraw_authority.as_ref()]; - - let mut total_rent_free_balances: u64 = 0; - - // Create token account if not specified - let pool_token_receiver_account = - pool_token_receiver_account.unwrap_or(add_associated_token_account( - config, - &stake_pool.pool_mint, - &config.token_owner.pubkey(), - &mut instructions, - &mut total_rent_free_balances, - )); - - let referrer_token_account = referrer_token_account.unwrap_or(pool_token_receiver_account); - - let pool_withdraw_authority = - find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0; - - let mut deposit_instructions = - if let Some(stake_deposit_authority) = config.funding_authority.as_ref() { - signers.push(stake_deposit_authority.as_ref()); - if stake_deposit_authority.pubkey() != stake_pool.stake_deposit_authority { - let error = format!( - "Invalid deposit authority specified, expected {}, received {}", - stake_pool.stake_deposit_authority, - stake_deposit_authority.pubkey() - ); - return Err(error.into()); - } - - spl_stake_pool::instruction::deposit_stake_with_authority( - &spl_stake_pool::id(), - stake_pool_address, - &stake_pool.validator_list, - &stake_deposit_authority.pubkey(), - &pool_withdraw_authority, - stake, - &withdraw_authority.pubkey(), - &validator_stake_account, - &stake_pool.reserve_stake, - &pool_token_receiver_account, - &stake_pool.manager_fee_account, - &referrer_token_account, - &stake_pool.pool_mint, - &spl_token::id(), - ) - } else { - spl_stake_pool::instruction::deposit_stake( - &spl_stake_pool::id(), - stake_pool_address, - &stake_pool.validator_list, - &pool_withdraw_authority, - stake, - &withdraw_authority.pubkey(), - &validator_stake_account, - &stake_pool.reserve_stake, - &pool_token_receiver_account, - &stake_pool.manager_fee_account, - &referrer_token_account, - &stake_pool.pool_mint, - &spl_token::id(), - ) - }; - - instructions.append(&mut deposit_instructions); - - unique_signers!(signers); - let transaction = checked_transaction_with_signers_and_additional_fee( - config, - &instructions, - &signers, - total_rent_free_balances, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_deposit_all_stake( - config: &Config, - stake_pool_address: &Pubkey, - stake_authority: &Pubkey, - withdraw_authority: Box, - pool_token_receiver_account: &Option, - referrer_token_account: &Option, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - - let stake_addresses = get_all_stake(&config.rpc_client, stake_authority)?; - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - - // Create token account if not specified - let mut total_rent_free_balances = 0; - let mut create_token_account_instructions = vec![]; - let pool_token_receiver_account = - pool_token_receiver_account.unwrap_or(add_associated_token_account( - config, - &stake_pool.pool_mint, - &config.token_owner.pubkey(), - &mut create_token_account_instructions, - &mut total_rent_free_balances, - )); - if !create_token_account_instructions.is_empty() { - let transaction = checked_transaction_with_signers_and_additional_fee( - config, - &create_token_account_instructions, - &[config.fee_payer.as_ref()], - total_rent_free_balances, - )?; - send_transaction(config, transaction)?; - } - - let referrer_token_account = referrer_token_account.unwrap_or(pool_token_receiver_account); - - let pool_withdraw_authority = - find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0; - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - let mut signers = if let Some(stake_deposit_authority) = config.funding_authority.as_ref() { - if stake_deposit_authority.pubkey() != stake_pool.stake_deposit_authority { - let error = format!( - "Invalid deposit authority specified, expected {}, received {}", - stake_pool.stake_deposit_authority, - stake_deposit_authority.pubkey() - ); - return Err(error.into()); - } - - vec![ - config.fee_payer.as_ref(), - withdraw_authority.as_ref(), - stake_deposit_authority.as_ref(), - ] - } else { - vec![config.fee_payer.as_ref(), withdraw_authority.as_ref()] - }; - unique_signers!(signers); - - for stake_address in stake_addresses { - let stake_state = get_stake_state(&config.rpc_client, &stake_address)?; - - let vote_account = match stake_state { - stake::state::StakeStateV2::Stake(_, stake, _) => Ok(stake.delegation.voter_pubkey), - _ => Err("Wrong stake account state, must be delegated to validator"), - }?; - - let validator_stake_info = validator_list - .find(&vote_account) - .ok_or("Vote account not found in the stake pool")?; - let validator_seed = NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()); - - // Calculate validator stake account address linked to the pool - let (validator_stake_account, _) = find_stake_program_address( - &spl_stake_pool::id(), - &vote_account, - stake_pool_address, - validator_seed, - ); - - let validator_stake_state = get_stake_state(&config.rpc_client, &validator_stake_account)?; - println!("Depositing user stake {}: {:?}", stake_address, stake_state); - println!( - "..into pool stake {}: {:?}", - validator_stake_account, validator_stake_state - ); - - let instructions = if let Some(stake_deposit_authority) = config.funding_authority.as_ref() - { - spl_stake_pool::instruction::deposit_stake_with_authority( - &spl_stake_pool::id(), - stake_pool_address, - &stake_pool.validator_list, - &stake_deposit_authority.pubkey(), - &pool_withdraw_authority, - &stake_address, - &withdraw_authority.pubkey(), - &validator_stake_account, - &stake_pool.reserve_stake, - &pool_token_receiver_account, - &stake_pool.manager_fee_account, - &referrer_token_account, - &stake_pool.pool_mint, - &spl_token::id(), - ) - } else { - spl_stake_pool::instruction::deposit_stake( - &spl_stake_pool::id(), - stake_pool_address, - &stake_pool.validator_list, - &pool_withdraw_authority, - &stake_address, - &withdraw_authority.pubkey(), - &validator_stake_account, - &stake_pool.reserve_stake, - &pool_token_receiver_account, - &stake_pool.manager_fee_account, - &referrer_token_account, - &stake_pool.pool_mint, - &spl_token::id(), - ) - }; - - let transaction = checked_transaction_with_signers(config, &instructions, &signers)?; - send_transaction(config, transaction)?; - } - Ok(()) -} - -fn command_deposit_sol( - config: &Config, - stake_pool_address: &Pubkey, - from: &Option, - pool_token_receiver_account: &Option, - referrer_token_account: &Option, - amount: f64, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - - let amount = native_token::sol_to_lamports(amount); - - // Check withdraw_from balance - let from_pubkey = from - .as_ref() - .map_or_else(|| config.fee_payer.pubkey(), |keypair| keypair.pubkey()); - let from_balance = config.rpc_client.get_balance(&from_pubkey)?; - if from_balance < amount { - return Err(format!( - "Not enough SOL to deposit into pool: {}.\nMaximum deposit amount is {} SOL.", - Sol(amount), - Sol(from_balance) - ) - .into()); - } - - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - - let mut instructions: Vec = vec![]; - - // ephemeral SOL account just to do the transfer - let user_sol_transfer = Keypair::new(); - let mut signers = vec![config.fee_payer.as_ref(), &user_sol_transfer]; - if let Some(keypair) = from.as_ref() { - signers.push(keypair) - } - - let mut total_rent_free_balances: u64 = 0; - - // Create the ephemeral SOL account - instructions.push(system_instruction::transfer( - &from_pubkey, - &user_sol_transfer.pubkey(), - amount, - )); - - // Create token account if not specified - let pool_token_receiver_account = - pool_token_receiver_account.unwrap_or(add_associated_token_account( - config, - &stake_pool.pool_mint, - &config.token_owner.pubkey(), - &mut instructions, - &mut total_rent_free_balances, - )); - - let referrer_token_account = referrer_token_account.unwrap_or(pool_token_receiver_account); - - let pool_withdraw_authority = - find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0; - - let deposit_instruction = if let Some(deposit_authority) = config.funding_authority.as_ref() { - let expected_sol_deposit_authority = stake_pool.sol_deposit_authority.ok_or_else(|| { - "SOL deposit authority specified in arguments but stake pool has none".to_string() - })?; - signers.push(deposit_authority.as_ref()); - if deposit_authority.pubkey() != expected_sol_deposit_authority { - let error = format!( - "Invalid deposit authority specified, expected {}, received {}", - expected_sol_deposit_authority, - deposit_authority.pubkey() - ); - return Err(error.into()); - } - - spl_stake_pool::instruction::deposit_sol_with_authority( - &spl_stake_pool::id(), - stake_pool_address, - &deposit_authority.pubkey(), - &pool_withdraw_authority, - &stake_pool.reserve_stake, - &user_sol_transfer.pubkey(), - &pool_token_receiver_account, - &stake_pool.manager_fee_account, - &referrer_token_account, - &stake_pool.pool_mint, - &spl_token::id(), - amount, - ) - } else { - spl_stake_pool::instruction::deposit_sol( - &spl_stake_pool::id(), - stake_pool_address, - &pool_withdraw_authority, - &stake_pool.reserve_stake, - &user_sol_transfer.pubkey(), - &pool_token_receiver_account, - &stake_pool.manager_fee_account, - &referrer_token_account, - &stake_pool.pool_mint, - &spl_token::id(), - amount, - ) - }; - - instructions.push(deposit_instruction); - - unique_signers!(signers); - let transaction = checked_transaction_with_signers_and_additional_fee( - config, - &instructions, - &signers, - total_rent_free_balances, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_list(config: &Config, stake_pool_address: &Pubkey) -> CommandResult { - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let reserve_stake_account_address = stake_pool.reserve_stake.to_string(); - let total_lamports = stake_pool.total_lamports; - let last_update_epoch = stake_pool.last_update_epoch; - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - let max_number_of_validators = validator_list.header.max_validators; - let current_number_of_validators = validator_list.validators.len(); - let pool_mint = get_token_mint(&config.rpc_client, &stake_pool.pool_mint)?; - let epoch_info = config.rpc_client.get_epoch_info()?; - let pool_withdraw_authority = - find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0; - let reserve_stake = config.rpc_client.get_account(&stake_pool.reserve_stake)?; - let minimum_reserve_stake_balance = config - .rpc_client - .get_minimum_balance_for_rent_exemption(STAKE_STATE_LEN)? - + MINIMUM_RESERVE_LAMPORTS; - let cli_stake_pool_stake_account_infos = validator_list - .validators - .iter() - .map(|validator| { - let validator_seed = NonZeroU32::new(validator.validator_seed_suffix.into()); - let (stake_account_address, _) = find_stake_program_address( - &spl_stake_pool::id(), - &validator.vote_account_address, - stake_pool_address, - validator_seed, - ); - let (transient_stake_account_address, _) = find_transient_stake_program_address( - &spl_stake_pool::id(), - &validator.vote_account_address, - stake_pool_address, - validator.transient_seed_suffix.into(), - ); - let update_required = u64::from(validator.last_update_epoch) != epoch_info.epoch; - CliStakePoolStakeAccountInfo { - vote_account_address: validator.vote_account_address.to_string(), - stake_account_address: stake_account_address.to_string(), - validator_active_stake_lamports: validator.active_stake_lamports.into(), - validator_last_update_epoch: validator.last_update_epoch.into(), - validator_lamports: validator.stake_lamports().unwrap(), - validator_transient_stake_account_address: transient_stake_account_address - .to_string(), - validator_transient_stake_lamports: validator.transient_stake_lamports.into(), - update_required, - } - }) - .collect(); - let total_pool_tokens = - spl_token::amount_to_ui_amount(stake_pool.pool_token_supply, pool_mint.decimals); - let mut cli_stake_pool = CliStakePool::from(( - *stake_pool_address, - stake_pool, - validator_list, - pool_withdraw_authority, - )); - let update_required = last_update_epoch != epoch_info.epoch; - let cli_stake_pool_details = CliStakePoolDetails { - reserve_stake_account_address, - reserve_stake_lamports: reserve_stake.lamports, - minimum_reserve_stake_balance, - stake_accounts: cli_stake_pool_stake_account_infos, - total_lamports, - total_pool_tokens, - current_number_of_validators: current_number_of_validators as u32, - max_number_of_validators, - update_required, - }; - cli_stake_pool.details = Some(cli_stake_pool_details); - println!("{}", config.output_format.formatted_string(&cli_stake_pool)); - Ok(()) -} - -fn command_update( - config: &Config, - stake_pool_address: &Pubkey, - force: bool, - no_merge: bool, - stale_only: bool, -) -> CommandResult { - if config.no_update { - println!("Update requested, but --no-update flag specified, so doing nothing"); - return Ok(()); - } - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let epoch_info = config.rpc_client.get_epoch_info()?; - - if stake_pool.last_update_epoch == epoch_info.epoch { - if force { - println!("Update not required, but --force flag specified, so doing it anyway"); - } else { - println!("Update not required"); - return Ok(()); - } - } - - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - - let (mut update_list_instructions, final_instructions) = if stale_only { - spl_stake_pool::instruction::update_stale_stake_pool( - &spl_stake_pool::id(), - &stake_pool, - &validator_list, - stake_pool_address, - no_merge, - epoch_info.epoch, - ) - } else { - spl_stake_pool::instruction::update_stake_pool( - &spl_stake_pool::id(), - &stake_pool, - &validator_list, - stake_pool_address, - no_merge, - ) - }; - - let update_list_instructions_len = update_list_instructions.len(); - if update_list_instructions_len > 0 { - let last_instruction = update_list_instructions.split_off(update_list_instructions_len - 1); - // send the first ones without waiting - for instruction in update_list_instructions { - let transaction = checked_transaction_with_signers( - config, - &[instruction], - &[config.fee_payer.as_ref()], - )?; - send_transaction_no_wait(config, transaction)?; - } - - // wait on the last one - let transaction = checked_transaction_with_signers( - config, - &last_instruction, - &[config.fee_payer.as_ref()], - )?; - send_transaction(config, transaction)?; - } - let transaction = checked_transaction_with_signers( - config, - &final_instructions, - &[config.fee_payer.as_ref()], - )?; - send_transaction(config, transaction)?; - - Ok(()) -} - -#[derive(PartialEq, Debug)] -struct WithdrawAccount { - stake_address: Pubkey, - vote_address: Option, - pool_amount: u64, -} - -fn sorted_accounts( - validator_list: &ValidatorList, - stake_pool: &StakePool, - get_info: F, -) -> Vec<(Pubkey, u64, Option)> -where - F: Fn(&ValidatorStakeInfo) -> (Pubkey, u64, Option), -{ - let mut result: Vec<(Pubkey, u64, Option)> = validator_list - .validators - .iter() - .map(get_info) - .collect::>(); - - result.sort_by(|left, right| { - if left.2 == stake_pool.preferred_withdraw_validator_vote_address { - Ordering::Less - } else if right.2 == stake_pool.preferred_withdraw_validator_vote_address { - Ordering::Greater - } else { - right.1.cmp(&left.1) - } - }); - - result -} - -fn prepare_withdraw_accounts( - rpc_client: &RpcClient, - stake_pool: &StakePool, - pool_amount: u64, - stake_pool_address: &Pubkey, - skip_fee: bool, -) -> Result, Error> { - let stake_minimum_delegation = rpc_client.get_stake_minimum_delegation()?; - let stake_pool_minimum_delegation = minimum_delegation(stake_minimum_delegation); - let min_balance = rpc_client - .get_minimum_balance_for_rent_exemption(STAKE_STATE_LEN)? - .saturating_add(stake_pool_minimum_delegation); - let pool_mint = get_token_mint(rpc_client, &stake_pool.pool_mint)?; - let validator_list: ValidatorList = get_validator_list(rpc_client, &stake_pool.validator_list)?; - - let mut accounts: Vec<(Pubkey, u64, Option)> = Vec::new(); - - accounts.append(&mut sorted_accounts( - &validator_list, - stake_pool, - |validator| { - let validator_seed = NonZeroU32::new(validator.validator_seed_suffix.into()); - let (stake_account_address, _) = find_stake_program_address( - &spl_stake_pool::id(), - &validator.vote_account_address, - stake_pool_address, - validator_seed, - ); - - ( - stake_account_address, - validator.active_stake_lamports.into(), - Some(validator.vote_account_address), - ) - }, - )); - - accounts.append(&mut sorted_accounts( - &validator_list, - stake_pool, - |validator| { - let (transient_stake_account_address, _) = find_transient_stake_program_address( - &spl_stake_pool::id(), - &validator.vote_account_address, - stake_pool_address, - validator.transient_seed_suffix.into(), - ); - - ( - transient_stake_account_address, - u64::from(validator.transient_stake_lamports).saturating_sub(min_balance), - Some(validator.vote_account_address), - ) - }, - )); - - let reserve_stake = rpc_client.get_account(&stake_pool.reserve_stake)?; - - accounts.push(( - stake_pool.reserve_stake, - reserve_stake.lamports - - rpc_client.get_minimum_balance_for_rent_exemption(STAKE_STATE_LEN)? - - MINIMUM_RESERVE_LAMPORTS, - None, - )); - - // Prepare the list of accounts to withdraw from - let mut withdraw_from: Vec = vec![]; - let mut remaining_amount = pool_amount; - - let fee = stake_pool.stake_withdrawal_fee; - let inverse_fee = Fee { - numerator: fee.denominator - fee.numerator, - denominator: fee.denominator, - }; - - // Go through available accounts and withdraw from largest to smallest - for (stake_address, lamports, vote_address_opt) in accounts { - if lamports <= min_balance { - continue; - } - - let available_for_withdrawal_wo_fee = - stake_pool.calc_pool_tokens_for_deposit(lamports).unwrap(); - - let available_for_withdrawal = if skip_fee { - available_for_withdrawal_wo_fee - } else { - available_for_withdrawal_wo_fee * inverse_fee.denominator / inverse_fee.numerator - }; - - let pool_amount = u64::min(available_for_withdrawal, remaining_amount); - - // Those accounts will be withdrawn completely with `claim` instruction - withdraw_from.push(WithdrawAccount { - stake_address, - vote_address: vote_address_opt, - pool_amount, - }); - remaining_amount -= pool_amount; - - if remaining_amount == 0 { - break; - } - } - - // Not enough stake to withdraw the specified amount - if remaining_amount > 0 { - return Err(format!( - "No stake accounts found in this pool with enough balance to withdraw {} pool tokens.", - spl_token::amount_to_ui_amount(pool_amount, pool_mint.decimals) - ) - .into()); - } - - Ok(withdraw_from) -} - -fn command_withdraw_stake( - config: &Config, - stake_pool_address: &Pubkey, - use_reserve: bool, - vote_account_address: &Option, - stake_receiver_param: &Option, - pool_token_account: &Option, - pool_amount: f64, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let pool_mint = get_token_mint(&config.rpc_client, &stake_pool.pool_mint)?; - let pool_amount = spl_token::ui_amount_to_amount(pool_amount, pool_mint.decimals); - - let pool_withdraw_authority = - find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0; - - let pool_token_account = pool_token_account.unwrap_or(get_associated_token_address( - &config.token_owner.pubkey(), - &stake_pool.pool_mint, - )); - let token_account = get_token_account( - &config.rpc_client, - &pool_token_account, - &stake_pool.pool_mint, - )?; - let stake_account_rent_exemption = config - .rpc_client - .get_minimum_balance_for_rent_exemption(STAKE_STATE_LEN)?; - - // Check withdraw_from balance - if token_account.amount < pool_amount { - return Err(format!( - "Not enough token balance to withdraw {} pool tokens.\nMaximum withdraw amount is {} pool tokens.", - spl_token::amount_to_ui_amount(pool_amount, pool_mint.decimals), - spl_token::amount_to_ui_amount(token_account.amount, pool_mint.decimals) - ) - .into()); - } - - // Check for the delegated stake receiver - let maybe_stake_receiver_state = stake_receiver_param - .map(|stake_receiver_pubkey| { - let stake_account = config.rpc_client.get_account(&stake_receiver_pubkey).ok()?; - let stake_state: stake::state::StakeStateV2 = - deserialize(stake_account.data.as_slice()) - .map_err(|err| { - format!("Invalid stake account {}: {}", stake_receiver_pubkey, err) - }) - .ok()?; - if stake_state.delegation().is_some() && stake_account.owner == stake::program::id() { - Some(stake_state) - } else { - None - } - }) - .flatten(); - - let stake_minimum_delegation = config.rpc_client.get_stake_minimum_delegation()?; - let stake_pool_minimum_delegation = minimum_delegation(stake_minimum_delegation); - - let withdraw_accounts = if use_reserve { - vec![WithdrawAccount { - stake_address: stake_pool.reserve_stake, - vote_address: None, - pool_amount, - }] - } else if maybe_stake_receiver_state.is_some() { - let vote_account = maybe_stake_receiver_state - .unwrap() - .delegation() - .unwrap() - .voter_pubkey; - if let Some(vote_account_address) = vote_account_address { - if *vote_account_address != vote_account { - return Err(format!("Provided withdrawal vote account {} does not match delegation on stake receiver account {}, - remove this flag or provide a different stake account delegated to {}", vote_account_address, vote_account, vote_account_address).into()); - } - } - // Check if the vote account exists in the stake pool - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - let validator_stake_info = validator_list - .find(&vote_account) - .ok_or(format!("Provided stake account is delegated to a vote account {} which does not exist in the stake pool", vote_account))?; - let validator_seed = NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()); - let (stake_account_address, _) = find_stake_program_address( - &spl_stake_pool::id(), - &vote_account, - stake_pool_address, - validator_seed, - ); - let stake_account = config.rpc_client.get_account(&stake_account_address)?; - - let available_for_withdrawal = stake_pool - .calc_lamports_withdraw_amount( - stake_account - .lamports - .saturating_sub(stake_pool_minimum_delegation) - .saturating_sub(stake_account_rent_exemption), - ) - .unwrap(); - - if available_for_withdrawal < pool_amount { - return Err(format!( - "Not enough lamports available for withdrawal from {}, {} asked, {} available", - stake_account_address, pool_amount, available_for_withdrawal - ) - .into()); - } - vec![WithdrawAccount { - stake_address: stake_account_address, - vote_address: Some(vote_account), - pool_amount, - }] - } else if let Some(vote_account_address) = vote_account_address { - let validator_list = get_validator_list(&config.rpc_client, &stake_pool.validator_list)?; - let validator_stake_info = validator_list.find(vote_account_address).ok_or(format!( - "Provided vote account address {} does not exist in the stake pool", - vote_account_address - ))?; - let validator_seed = NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()); - let (stake_account_address, _) = find_stake_program_address( - &spl_stake_pool::id(), - vote_account_address, - stake_pool_address, - validator_seed, - ); - let stake_account = config.rpc_client.get_account(&stake_account_address)?; - - let available_for_withdrawal = stake_pool - .calc_lamports_withdraw_amount( - stake_account - .lamports - .saturating_sub(stake_pool_minimum_delegation) - .saturating_sub(stake_account_rent_exemption), - ) - .unwrap(); - - if available_for_withdrawal < pool_amount { - return Err(format!( - "Not enough lamports available for withdrawal from {}, {} asked, {} available", - stake_account_address, pool_amount, available_for_withdrawal - ) - .into()); - } - vec![WithdrawAccount { - stake_address: stake_account_address, - vote_address: Some(*vote_account_address), - pool_amount, - }] - } else { - // Get the list of accounts to withdraw from - prepare_withdraw_accounts( - &config.rpc_client, - &stake_pool, - pool_amount, - stake_pool_address, - stake_pool.manager_fee_account == pool_token_account, - )? - }; - - // Construct transaction to withdraw from withdraw_accounts account list - let mut instructions: Vec = vec![]; - let user_transfer_authority = Keypair::new(); // ephemeral keypair just to do the transfer - let mut signers = vec![ - config.fee_payer.as_ref(), - config.token_owner.as_ref(), - &user_transfer_authority, - ]; - let mut new_stake_keypairs = vec![]; - - instructions.push( - // Approve spending token - spl_token::instruction::approve( - &spl_token::id(), - &pool_token_account, - &user_transfer_authority.pubkey(), - &config.token_owner.pubkey(), - &[], - pool_amount, - )?, - ); - - let mut total_rent_free_balances = 0; - // Go through prepared accounts and withdraw/claim them - for withdraw_account in withdraw_accounts { - // Convert pool tokens amount to lamports - let sol_withdraw_amount = stake_pool - .calc_lamports_withdraw_amount(withdraw_account.pool_amount) - .unwrap(); - - if let Some(vote_address) = withdraw_account.vote_address { - println!( - "Withdrawing {}, or {} pool tokens, from stake account {}, delegated to {}", - Sol(sol_withdraw_amount), - spl_token::amount_to_ui_amount(withdraw_account.pool_amount, pool_mint.decimals), - withdraw_account.stake_address, - vote_address, - ); - } else { - println!( - "Withdrawing {}, or {} pool tokens, from stake account {}", - Sol(sol_withdraw_amount), - spl_token::amount_to_ui_amount(withdraw_account.pool_amount, pool_mint.decimals), - withdraw_account.stake_address, - ); - } - let stake_receiver = - if (stake_receiver_param.is_none()) || (maybe_stake_receiver_state.is_some()) { - // Creating new account to split the stake into new account - let stake_keypair = new_stake_account( - &config.fee_payer.pubkey(), - &mut instructions, - stake_account_rent_exemption, - ); - let stake_pubkey = stake_keypair.pubkey(); - total_rent_free_balances += stake_account_rent_exemption; - new_stake_keypairs.push(stake_keypair); - stake_pubkey - } else { - stake_receiver_param.unwrap() - }; - - instructions.push(spl_stake_pool::instruction::withdraw_stake( - &spl_stake_pool::id(), - stake_pool_address, - &stake_pool.validator_list, - &pool_withdraw_authority, - &withdraw_account.stake_address, - &stake_receiver, - &config.staker.pubkey(), - &user_transfer_authority.pubkey(), - &pool_token_account, - &stake_pool.manager_fee_account, - &stake_pool.pool_mint, - &spl_token::id(), - withdraw_account.pool_amount, - )); - } - - // Merging the stake with account provided by user - if maybe_stake_receiver_state.is_some() { - for new_stake_keypair in &new_stake_keypairs { - instructions.extend(stake::instruction::merge( - &stake_receiver_param.unwrap(), - &new_stake_keypair.pubkey(), - &config.fee_payer.pubkey(), - )); - } - } - - for new_stake_keypair in &new_stake_keypairs { - signers.push(new_stake_keypair); - } - unique_signers!(signers); - let transaction = checked_transaction_with_signers_and_additional_fee( - config, - &instructions, - &signers, - total_rent_free_balances, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_withdraw_sol( - config: &Config, - stake_pool_address: &Pubkey, - pool_token_account: &Option, - sol_receiver: &Pubkey, - pool_amount: f64, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - let pool_mint = get_token_mint(&config.rpc_client, &stake_pool.pool_mint)?; - let pool_amount = spl_token::ui_amount_to_amount(pool_amount, pool_mint.decimals); - - let pool_token_account = pool_token_account.unwrap_or(get_associated_token_address( - &config.token_owner.pubkey(), - &stake_pool.pool_mint, - )); - let token_account = get_token_account( - &config.rpc_client, - &pool_token_account, - &stake_pool.pool_mint, - )?; - - // Check withdraw_from balance - if token_account.amount < pool_amount { - return Err(format!( - "Not enough token balance to withdraw {} pool tokens.\nMaximum withdraw amount is {} pool tokens.", - spl_token::amount_to_ui_amount(pool_amount, pool_mint.decimals), - spl_token::amount_to_ui_amount(token_account.amount, pool_mint.decimals) - ) - .into()); - } - - // Construct transaction to withdraw from withdraw_accounts account list - let user_transfer_authority = Keypair::new(); // ephemeral keypair just to do the transfer - let mut signers = vec![ - config.fee_payer.as_ref(), - config.token_owner.as_ref(), - &user_transfer_authority, - ]; - - let mut instructions = vec![ - // Approve spending token - spl_token::instruction::approve( - &spl_token::id(), - &pool_token_account, - &user_transfer_authority.pubkey(), - &config.token_owner.pubkey(), - &[], - pool_amount, - )?, - ]; - - let pool_withdraw_authority = - find_withdraw_authority_program_address(&spl_stake_pool::id(), stake_pool_address).0; - - let withdraw_instruction = if let Some(withdraw_authority) = config.funding_authority.as_ref() { - let expected_sol_withdraw_authority = - stake_pool.sol_withdraw_authority.ok_or_else(|| { - "SOL withdraw authority specified in arguments but stake pool has none".to_string() - })?; - signers.push(withdraw_authority.as_ref()); - if withdraw_authority.pubkey() != expected_sol_withdraw_authority { - let error = format!( - "Invalid deposit withdraw specified, expected {}, received {}", - expected_sol_withdraw_authority, - withdraw_authority.pubkey() - ); - return Err(error.into()); - } - - spl_stake_pool::instruction::withdraw_sol_with_authority( - &spl_stake_pool::id(), - stake_pool_address, - &withdraw_authority.pubkey(), - &pool_withdraw_authority, - &user_transfer_authority.pubkey(), - &pool_token_account, - &stake_pool.reserve_stake, - sol_receiver, - &stake_pool.manager_fee_account, - &stake_pool.pool_mint, - &spl_token::id(), - pool_amount, - ) - } else { - spl_stake_pool::instruction::withdraw_sol( - &spl_stake_pool::id(), - stake_pool_address, - &pool_withdraw_authority, - &user_transfer_authority.pubkey(), - &pool_token_account, - &stake_pool.reserve_stake, - sol_receiver, - &stake_pool.manager_fee_account, - &stake_pool.pool_mint, - &spl_token::id(), - pool_amount, - ) - }; - - instructions.push(withdraw_instruction); - - unique_signers!(signers); - let transaction = checked_transaction_with_signers(config, &instructions, &signers)?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_set_manager( - config: &Config, - stake_pool_address: &Pubkey, - new_manager: &Option>, - new_fee_receiver: &Option, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - let stake_pool = get_stake_pool(&config.rpc_client, stake_pool_address)?; - - // If new accounts are missing in the arguments use the old ones - let (new_manager_pubkey, mut signers): (Pubkey, Vec<&dyn Signer>) = match new_manager { - None => (stake_pool.manager, vec![]), - Some(value) => (value.pubkey(), vec![value.as_ref()]), - }; - - let new_fee_receiver = match new_fee_receiver { - None => stake_pool.manager_fee_account, - Some(value) => { - // Check for fee receiver being a valid token account and have to same mint as - // the stake pool - let token_account = - get_token_account(&config.rpc_client, value, &stake_pool.pool_mint)?; - if token_account.mint != stake_pool.pool_mint { - return Err("Fee receiver account belongs to a different mint" - .to_string() - .into()); - } - *value - } - }; - - signers.append(&mut vec![ - config.fee_payer.as_ref(), - config.manager.as_ref(), - ]); - unique_signers!(signers); - let transaction = checked_transaction_with_signers( - config, - &[spl_stake_pool::instruction::set_manager( - &spl_stake_pool::id(), - stake_pool_address, - &config.manager.pubkey(), - &new_manager_pubkey, - &new_fee_receiver, - )], - &signers, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_set_staker( - config: &Config, - stake_pool_address: &Pubkey, - new_staker: &Pubkey, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - let mut signers = vec![config.fee_payer.as_ref(), config.manager.as_ref()]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers( - config, - &[spl_stake_pool::instruction::set_staker( - &spl_stake_pool::id(), - stake_pool_address, - &config.manager.pubkey(), - new_staker, - )], - &signers, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_set_funding_authority( - config: &Config, - stake_pool_address: &Pubkey, - new_authority: Option, - funding_type: FundingType, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - let mut signers = vec![config.fee_payer.as_ref(), config.manager.as_ref()]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers( - config, - &[spl_stake_pool::instruction::set_funding_authority( - &spl_stake_pool::id(), - stake_pool_address, - &config.manager.pubkey(), - new_authority.as_ref(), - funding_type, - )], - &signers, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_set_fee( - config: &Config, - stake_pool_address: &Pubkey, - new_fee: FeeType, -) -> CommandResult { - if !config.no_update { - command_update(config, stake_pool_address, false, false, false)?; - } - let mut signers = vec![config.fee_payer.as_ref(), config.manager.as_ref()]; - unique_signers!(signers); - let transaction = checked_transaction_with_signers( - config, - &[spl_stake_pool::instruction::set_fee( - &spl_stake_pool::id(), - stake_pool_address, - &config.manager.pubkey(), - new_fee, - )], - &signers, - )?; - send_transaction(config, transaction)?; - Ok(()) -} - -fn command_list_all_pools(config: &Config) -> CommandResult { - let all_pools = get_stake_pools(&config.rpc_client)?; - let cli_stake_pool_vec: Vec = - all_pools.into_iter().map(CliStakePool::from).collect(); - let cli_stake_pools = CliStakePools { - pools: cli_stake_pool_vec, - }; - println!( - "{}", - config.output_format.formatted_string(&cli_stake_pools) - ); - Ok(()) -} - -fn main() { - solana_logger::setup_with_default("solana=info"); - - let matches = App::new(crate_name!()) - .about(crate_description!()) - .version(crate_version!()) - .setting(AppSettings::SubcommandRequiredElseHelp) - .arg({ - let arg = Arg::with_name("config_file") - .short("C") - .long("config") - .value_name("PATH") - .takes_value(true) - .global(true) - .help("Configuration file to use"); - if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE { - arg.default_value(config_file) - } else { - arg - } - }) - .arg( - Arg::with_name("verbose") - .long("verbose") - .short("v") - .takes_value(false) - .global(true) - .help("Show additional information"), - ) - .arg( - Arg::with_name("output_format") - .long("output") - .value_name("FORMAT") - .global(true) - .takes_value(true) - .possible_values(&["json", "json-compact"]) - .help("Return information in specified output format"), - ) - .arg( - Arg::with_name("dry_run") - .long("dry-run") - .takes_value(false) - .global(true) - .help("Simulate transaction instead of executing"), - ) - .arg( - Arg::with_name("no_update") - .long("no-update") - .takes_value(false) - .global(true) - .help("Do not automatically update the stake pool if needed"), - ) - .arg( - Arg::with_name("json_rpc_url") - .long("url") - .value_name("URL") - .takes_value(true) - .validator(is_url) - .global(true) - .help("JSON RPC URL for the cluster. Default from the configuration file."), - ) - .arg( - Arg::with_name("staker") - .long("staker") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .global(true) - .help("Stake pool staker. [default: cli config keypair]"), - ) - .arg( - Arg::with_name("manager") - .long("manager") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .global(true) - .help("Stake pool manager. [default: cli config keypair]"), - ) - .arg( - Arg::with_name("funding_authority") - .long("funding-authority") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .global(true) - .help("Stake pool funding authority for deposits or withdrawals. [default: cli config keypair]"), - ) - .arg( - Arg::with_name("token_owner") - .long("token-owner") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .global(true) - .help("Owner of pool token account [default: cli config keypair]"), - ) - .arg( - Arg::with_name("fee_payer") - .long("fee-payer") - .value_name("KEYPAIR") - .validator(is_valid_signer) - .takes_value(true) - .global(true) - .help("Transaction fee payer account [default: cli config keypair]"), - ) - .arg(compute_unit_price_arg().validator(is_parsable::).global(true)) - .arg( - Arg::with_name(COMPUTE_UNIT_LIMIT_ARG.name) - .long(COMPUTE_UNIT_LIMIT_ARG.long) - .takes_value(true) - .value_name("COMPUTE-UNIT-LIMIT") - .help(COMPUTE_UNIT_LIMIT_ARG.help) - .validator(is_compute_unit_limit_or_simulated) - .global(true) - ) - .subcommand(SubCommand::with_name("create-pool") - .about("Create a new stake pool") - .arg( - Arg::with_name("epoch_fee_numerator") - .long("epoch-fee-numerator") - .short("n") - .validator(is_parsable::) - .value_name("NUMERATOR") - .takes_value(true) - .required(true) - .help("Epoch fee numerator, fee amount is numerator divided by denominator."), - ) - .arg( - Arg::with_name("epoch_fee_denominator") - .long("epoch-fee-denominator") - .short("d") - .validator(is_parsable::) - .value_name("DENOMINATOR") - .takes_value(true) - .required(true) - .help("Epoch fee denominator, fee amount is numerator divided by denominator."), - ) - .arg( - Arg::with_name("withdrawal_fee_numerator") - .long("withdrawal-fee-numerator") - .validator(is_parsable::) - .value_name("NUMERATOR") - .takes_value(true) - .requires("withdrawal_fee_denominator") - .help("Withdrawal fee numerator, fee amount is numerator divided by denominator [default: 0]"), - ).arg( - Arg::with_name("withdrawal_fee_denominator") - .long("withdrawal-fee-denominator") - .validator(is_parsable::) - .value_name("DENOMINATOR") - .takes_value(true) - .requires("withdrawal_fee_numerator") - .help("Withdrawal fee denominator, fee amount is numerator divided by denominator [default: 0]"), - ) - .arg( - Arg::with_name("deposit_fee_numerator") - .long("deposit-fee-numerator") - .validator(is_parsable::) - .value_name("NUMERATOR") - .takes_value(true) - .requires("deposit_fee_denominator") - .help("Deposit fee numerator, fee amount is numerator divided by denominator [default: 0]"), - ).arg( - Arg::with_name("deposit_fee_denominator") - .long("deposit-fee-denominator") - .validator(is_parsable::) - .value_name("DENOMINATOR") - .takes_value(true) - .requires("deposit_fee_numerator") - .help("Deposit fee denominator, fee amount is numerator divided by denominator [default: 0]"), - ) - .arg( - Arg::with_name("referral_fee") - .long("referral-fee") - .validator(is_valid_percentage) - .value_name("FEE_PERCENTAGE") - .takes_value(true) - .help("Referral fee percentage, maximum 100"), - ) - .arg( - Arg::with_name("max_validators") - .long("max-validators") - .short("m") - .validator(is_parsable::) - .value_name("NUMBER") - .takes_value(true) - .required(true) - .help("Max number of validators included in the stake pool"), - ) - .arg( - Arg::with_name("deposit_authority") - .long("deposit-authority") - .short("a") - .validator(is_valid_signer) - .value_name("DEPOSIT_AUTHORITY_KEYPAIR") - .takes_value(true) - .help("Deposit authority required to sign all deposits into the stake pool"), - ) - .arg( - Arg::with_name("pool_keypair") - .long("pool-keypair") - .short("p") - .validator(is_keypair_or_ask_keyword) - .value_name("PATH") - .takes_value(true) - .help("Stake pool keypair [default: new keypair]"), - ) - .arg( - Arg::with_name("validator_list_keypair") - .long("validator-list-keypair") - .validator(is_keypair_or_ask_keyword) - .value_name("PATH") - .takes_value(true) - .help("Validator list keypair [default: new keypair]"), - ) - .arg( - Arg::with_name("mint_keypair") - .long("mint-keypair") - .validator(is_keypair_or_ask_keyword) - .value_name("PATH") - .takes_value(true) - .help("Stake pool mint keypair [default: new keypair]"), - ) - .arg( - Arg::with_name("reserve_keypair") - .long("reserve-keypair") - .validator(is_keypair_or_ask_keyword) - .value_name("PATH") - .takes_value(true) - .help("Stake pool reserve keypair [default: new keypair]"), - ) - .arg( - Arg::with_name("unsafe_fees") - .long("unsafe-fees") - .takes_value(false) - .help("Bypass fee checks, allowing pool to be created with unsafe fees"), - ) - ) - .subcommand(SubCommand::with_name("create-token-metadata") - .about("Creates stake pool token metadata") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ) - .arg( - Arg::with_name("name") - .index(2) - .value_name("TOKEN_NAME") - .takes_value(true) - .required(true) - .help("Name of the token"), - ) - .arg( - Arg::with_name("symbol") - .index(3) - .value_name("TOKEN_SYMBOL") - .takes_value(true) - .required(true) - .help("Symbol of the token"), - ) - .arg( - Arg::with_name("uri") - .index(4) - .value_name("TOKEN_URI") - .takes_value(true) - .required(true) - .help("URI of the token metadata json"), - ) - ) - .subcommand(SubCommand::with_name("update-token-metadata") - .about("Updates stake pool token metadata") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ) - .arg( - Arg::with_name("name") - .index(2) - .value_name("TOKEN_NAME") - .takes_value(true) - .required(true) - .help("Name of the token"), - ) - .arg( - Arg::with_name("symbol") - .index(3) - .value_name("TOKEN_SYMBOL") - .takes_value(true) - .required(true) - .help("Symbol of the token"), - ) - .arg( - Arg::with_name("uri") - .index(4) - .value_name("TOKEN_URI") - .takes_value(true) - .required(true) - .help("URI of the token metadata json"), - ) - ) - .subcommand(SubCommand::with_name("add-validator") - .about("Add validator account to the stake pool. Must be signed by the pool staker.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ) - .arg( - Arg::with_name("vote_account") - .index(2) - .validator(is_pubkey) - .value_name("VOTE_ACCOUNT_ADDRESS") - .takes_value(true) - .required(true) - .help("The validator vote account that the stake is delegated to"), - ) - ) - .subcommand(SubCommand::with_name("remove-validator") - .about("Remove validator account from the stake pool. Must be signed by the pool staker.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ) - .arg( - Arg::with_name("vote_account") - .index(2) - .validator(is_pubkey) - .value_name("VOTE_ACCOUNT_ADDRESS") - .takes_value(true) - .required(true) - .help("Vote account for the validator to remove from the pool"), - ) - ) - .subcommand(SubCommand::with_name("increase-validator-stake") - .about("Increase stake to a validator, drawing from the stake pool reserve. Must be signed by the pool staker.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ) - .arg( - Arg::with_name("vote_account") - .index(2) - .validator(is_pubkey) - .value_name("VOTE_ACCOUNT_ADDRESS") - .takes_value(true) - .required(true) - .help("Vote account for the validator to increase stake to"), - ) - .arg( - Arg::with_name("amount") - .index(3) - .validator(is_amount) - .value_name("AMOUNT") - .takes_value(true) - .help("Amount in SOL to add to the validator stake account. Must be at least the rent-exempt amount for a stake plus 1 SOL for merging."), - ) - ) - .subcommand(SubCommand::with_name("decrease-validator-stake") - .about("Decrease stake to a validator, splitting from the active stake. Must be signed by the pool staker.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ) - .arg( - Arg::with_name("vote_account") - .index(2) - .validator(is_pubkey) - .value_name("VOTE_ACCOUNT_ADDRESS") - .takes_value(true) - .required(true) - .help("Vote account for the validator to decrease stake from"), - ) - .arg( - Arg::with_name("amount") - .index(3) - .validator(is_amount) - .value_name("AMOUNT") - .takes_value(true) - .help("Amount in SOL to remove from the validator stake account. Must be at least the rent-exempt amount for a stake."), - ) - ) - .subcommand(SubCommand::with_name("set-preferred-validator") - .about("Set the preferred validator for deposits or withdrawals. Must be signed by the pool staker.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ) - .arg( - Arg::with_name("preferred_type") - .index(2) - .value_name("OPERATION") - .possible_values(&["deposit", "withdraw"]) // PreferredValidatorType enum - .takes_value(true) - .required(true) - .help("Operation for which to restrict the validator"), - ) - .arg( - Arg::with_name("vote_account") - .long("vote-account") - .validator(is_pubkey) - .value_name("VOTE_ACCOUNT_ADDRESS") - .takes_value(true) - .help("Vote account for the validator that users must deposit into."), - ) - .arg( - Arg::with_name("unset") - .long("unset") - .takes_value(false) - .help("Unset the preferred validator."), - ) - .group(ArgGroup::with_name("validator") - .arg("vote_account") - .arg("unset") - .required(true) - ) - ) - .subcommand(SubCommand::with_name("deposit-stake") - .about("Deposit active stake account into the stake pool in exchange for pool tokens") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ) - .arg( - Arg::with_name("stake_account") - .index(2) - .validator(is_pubkey) - .value_name("STAKE_ACCOUNT_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake address to join the pool"), - ) - .arg( - Arg::with_name("withdraw_authority") - .long("withdraw-authority") - .validator(is_valid_signer) - .value_name("KEYPAIR") - .takes_value(true) - .help("Withdraw authority for the stake account to be deposited. [default: cli config keypair]"), - ) - .arg( - Arg::with_name("token_receiver") - .long("token-receiver") - .validator(is_pubkey) - .value_name("ADDRESS") - .takes_value(true) - .help("Account to receive the minted pool tokens. \ - Defaults to the token-owner's associated pool token account. \ - Creates the account if it does not exist."), - ) - .arg( - Arg::with_name("referrer") - .validator(is_pubkey) - .value_name("ADDRESS") - .takes_value(true) - .help("Pool token account to receive the referral fees for deposits. \ - Defaults to the token receiver."), - ) - ) - .subcommand(SubCommand::with_name("deposit-all-stake") - .about("Deposit all active stake accounts into the stake pool in exchange for pool tokens") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ) - .arg( - Arg::with_name("stake_authority") - .index(2) - .validator(is_pubkey) - .value_name("ADDRESS") - .takes_value(true) - .required(true) - .help("Stake authority address to search for stake accounts"), - ) - .arg( - Arg::with_name("withdraw_authority") - .long("withdraw-authority") - .validator(is_valid_signer) - .value_name("KEYPAIR") - .takes_value(true) - .help("Withdraw authority for the stake account to be deposited. [default: cli config keypair]"), - ) - .arg( - Arg::with_name("token_receiver") - .long("token-receiver") - .validator(is_pubkey) - .value_name("ADDRESS") - .takes_value(true) - .help("Account to receive the minted pool tokens. \ - Defaults to the token-owner's associated pool token account. \ - Creates the account if it does not exist."), - ) - .arg( - Arg::with_name("referrer") - .validator(is_pubkey) - .value_name("ADDRESS") - .takes_value(true) - .help("Pool token account to receive the referral fees for deposits. \ - Defaults to the token receiver."), - ) - ) - .subcommand(SubCommand::with_name("deposit-sol") - .about("Deposit SOL into the stake pool in exchange for pool tokens") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address"), - ).arg( - Arg::with_name("amount") - .index(2) - .validator(is_amount) - .value_name("AMOUNT") - .takes_value(true) - .help("Amount in SOL to deposit into the stake pool reserve account."), - ) - .arg( - Arg::with_name("from") - .long("from") - .validator(is_valid_signer) - .value_name("KEYPAIR") - .takes_value(true) - .help("Source account of funds. [default: cli config keypair]"), - ) - .arg( - Arg::with_name("token_receiver") - .long("token-receiver") - .validator(is_pubkey) - .value_name("POOL_TOKEN_RECEIVER_ADDRESS") - .takes_value(true) - .help("Account to receive the minted pool tokens. \ - Defaults to the token-owner's associated pool token account. \ - Creates the account if it does not exist."), - ) - .arg( - Arg::with_name("referrer") - .long("referrer") - .validator(is_pubkey) - .value_name("REFERRER_TOKEN_ADDRESS") - .takes_value(true) - .help("Account to receive the referral fees for deposits. \ - Defaults to the token receiver."), - ) - ) - .subcommand(SubCommand::with_name("list") - .about("List stake accounts managed by this pool") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address."), - ) - ) - .subcommand(SubCommand::with_name("update") - .about("Updates all balances in the pool after validator stake accounts receive rewards.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address."), - ) - .arg( - Arg::with_name("force") - .long("force") - .takes_value(false) - .help("Update balances, even if it has already been performed this epoch."), - ) - .arg( - Arg::with_name("no_merge") - .long("no-merge") - .takes_value(false) - .help("Do not automatically merge transient stakes. Useful if the stake pool is in an expected state, but the balances still need to be updated."), - ) - .arg( - Arg::with_name("stale_only") - .long("stale-only") - .takes_value(false) - .help("If set, only updates validator list balances that have not been updated for this epoch. Otherwise, updates all validator balances on the validator list."), - ) - ) - .subcommand(SubCommand::with_name("withdraw-stake") - .about("Withdraw active stake from the stake pool in exchange for pool tokens") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address."), - ) - .arg( - Arg::with_name("amount") - .index(2) - .validator(is_amount) - .value_name("AMOUNT") - .takes_value(true) - .required(true) - .help("Amount of pool tokens to withdraw for activated stake."), - ) - .arg( - Arg::with_name("pool_account") - .long("pool-account") - .validator(is_pubkey) - .value_name("ADDRESS") - .takes_value(true) - .help("Pool token account to withdraw tokens from. Defaults to the token-owner's associated token account."), - ) - .arg( - Arg::with_name("stake_receiver") - .long("stake-receiver") - .validator(is_pubkey) - .value_name("STAKE_ACCOUNT_ADDRESS") - .takes_value(true) - .requires("withdraw_from") - .help("Stake account from which to receive a stake from the stake pool. Defaults to a new stake account."), - ) - .arg( - Arg::with_name("vote_account") - .long("vote-account") - .validator(is_pubkey) - .value_name("VOTE_ACCOUNT_ADDRESS") - .takes_value(true) - .help("Validator to withdraw from. Defaults to the largest validator stakes in the pool."), - ) - .arg( - Arg::with_name("use_reserve") - .long("use-reserve") - .takes_value(false) - .help("Withdraw from the stake pool's reserve. Only possible if all validator stakes are at the minimum possible amount."), - ) - .group(ArgGroup::with_name("withdraw_from") - .arg("use_reserve") - .arg("vote_account") - ) - ) - .subcommand(SubCommand::with_name("withdraw-sol") - .about("Withdraw SOL from the stake pool's reserve in exchange for pool tokens") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address."), - ) - .arg( - Arg::with_name("sol_receiver") - .index(2) - .validator(is_valid_pubkey) - .value_name("SYSTEM_ACCOUNT_ADDRESS_OR_KEYPAIR") - .takes_value(true) - .required(true) - .help("System account to receive SOL from the stake pool. Defaults to the payer."), - ) - .arg( - Arg::with_name("amount") - .index(3) - .validator(is_amount) - .value_name("AMOUNT") - .takes_value(true) - .required(true) - .help("Amount of pool tokens to withdraw for SOL."), - ) - .arg( - Arg::with_name("pool_account") - .long("pool-account") - .validator(is_pubkey) - .value_name("ADDRESS") - .takes_value(true) - .help("Pool token account to withdraw tokens from. Defaults to the token-owner's associated token account."), - ) - ) - .subcommand(SubCommand::with_name("set-manager") - .about("Change manager or fee receiver account for the stake pool. Must be signed by the current manager.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address."), - ) - .arg( - Arg::with_name("new_manager") - .long("new-manager") - .validator(is_valid_signer) - .value_name("KEYPAIR") - .takes_value(true) - .help("Keypair for the new stake pool manager."), - ) - .arg( - Arg::with_name("new_fee_receiver") - .long("new-fee-receiver") - .validator(is_pubkey) - .value_name("ADDRESS") - .takes_value(true) - .help("Public key for the new account to set as the stake pool fee receiver."), - ) - .group(ArgGroup::with_name("new_accounts") - .arg("new_manager") - .arg("new_fee_receiver") - .required(true) - .multiple(true) - ) - ) - .subcommand(SubCommand::with_name("set-staker") - .about("Change staker account for the stake pool. Must be signed by the manager or current staker.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address."), - ) - .arg( - Arg::with_name("new_staker") - .index(2) - .validator(is_pubkey) - .value_name("ADDRESS") - .takes_value(true) - .help("Public key for the new stake pool staker."), - ) - ) - .subcommand(SubCommand::with_name("set-funding-authority") - .about("Change one of the funding authorities for the stake pool. Must be signed by the manager.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address."), - ) - .arg( - Arg::with_name("funding_type") - .index(2) - .value_name("FUNDING_TYPE") - .possible_values(&["stake-deposit", "sol-deposit", "sol-withdraw"]) // FundingType enum - .takes_value(true) - .required(true) - .help("Funding type to be updated."), - ) - .arg( - Arg::with_name("new_authority") - .index(3) - .validator(is_pubkey) - .value_name("AUTHORITY_ADDRESS") - .takes_value(true) - .help("Public key for the new stake pool funding authority."), - ) - .arg( - Arg::with_name("unset") - .long("unset") - .takes_value(false) - .help("Unset the stake deposit authority. The program will use a program derived address.") - ) - .group(ArgGroup::with_name("validator") - .arg("new_authority") - .arg("unset") - .required(true) - ) - ) - .subcommand(SubCommand::with_name("set-fee") - .about("Change the [epoch/withdraw/stake deposit/sol deposit] fee assessed by the stake pool. Must be signed by the manager.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address."), - ) - .arg(Arg::with_name("fee_type") - .index(2) - .value_name("FEE_TYPE") - .possible_values(&["epoch", "stake-deposit", "sol-deposit", "stake-withdrawal", "sol-withdrawal"]) // FeeType enum - .takes_value(true) - .required(true) - .help("Fee type to be updated."), - ) - .arg( - Arg::with_name("fee_numerator") - .index(3) - .validator(is_parsable::) - .value_name("NUMERATOR") - .takes_value(true) - .required(true) - .help("Fee numerator, fee amount is numerator divided by denominator."), - ) - .arg( - Arg::with_name("fee_denominator") - .index(4) - .validator(is_parsable::) - .value_name("DENOMINATOR") - .takes_value(true) - .required(true) - .help("Fee denominator, fee amount is numerator divided by denominator."), - ) - ) - .subcommand(SubCommand::with_name("set-referral-fee") - .about("Change the referral fee assessed by the stake pool for stake deposits. Must be signed by the manager.") - .arg( - Arg::with_name("pool") - .index(1) - .validator(is_pubkey) - .value_name("POOL_ADDRESS") - .takes_value(true) - .required(true) - .help("Stake pool address."), - ) - .arg(Arg::with_name("fee_type") - .index(2) - .value_name("FEE_TYPE") - .possible_values(&["stake", "sol"]) // FeeType enum, kind of - .takes_value(true) - .required(true) - .help("Fee type to be updated."), - ) - .arg( - Arg::with_name("fee") - .index(3) - .validator(is_valid_percentage) - .value_name("FEE_PERCENTAGE") - .takes_value(true) - .required(true) - .help("Fee percentage, maximum 100"), - ) - ) - .subcommand(SubCommand::with_name("list-all") - .about("List information about all stake pools") - ) - .get_matches(); - - let mut wallet_manager = None; - let cli_config = if let Some(config_file) = matches.value_of("config_file") { - solana_cli_config::Config::load(config_file).unwrap_or_default() - } else { - solana_cli_config::Config::default() - }; - let config = { - let json_rpc_url = value_t!(matches, "json_rpc_url", String) - .unwrap_or_else(|_| cli_config.json_rpc_url.clone()); - - let staker = get_signer( - &matches, - "staker", - &cli_config.keypair_path, - &mut wallet_manager, - SignerFromPathConfig { - allow_null_signer: false, - }, - ); - - let funding_authority = if matches.is_present("funding_authority") { - Some(get_signer( - &matches, - "funding_authority", - &cli_config.keypair_path, - &mut wallet_manager, - SignerFromPathConfig { - allow_null_signer: false, - }, - )) - } else { - None - }; - let manager = get_signer( - &matches, - "manager", - &cli_config.keypair_path, - &mut wallet_manager, - SignerFromPathConfig { - allow_null_signer: false, - }, - ); - let token_owner = get_signer( - &matches, - "token_owner", - &cli_config.keypair_path, - &mut wallet_manager, - SignerFromPathConfig { - allow_null_signer: false, - }, - ); - let fee_payer = get_signer( - &matches, - "fee_payer", - &cli_config.keypair_path, - &mut wallet_manager, - SignerFromPathConfig { - allow_null_signer: false, - }, - ); - let verbose = matches.is_present("verbose"); - let output_format = matches - .value_of("output_format") - .map(|value| match value { - "json" => OutputFormat::Json, - "json-compact" => OutputFormat::JsonCompact, - _ => unreachable!(), - }) - .unwrap_or(if verbose { - OutputFormat::DisplayVerbose - } else { - OutputFormat::Display - }); - let dry_run = matches.is_present("dry_run"); - let no_update = matches.is_present("no_update"); - let compute_unit_price = value_t!(matches, COMPUTE_UNIT_PRICE_ARG.name, u64).ok(); - let compute_unit_limit = matches - .value_of(COMPUTE_UNIT_LIMIT_ARG.name) - .map(|x| parse_compute_unit_limit(x).unwrap()) - .unwrap_or_else(|| { - if compute_unit_price.is_some() { - ComputeUnitLimit::Simulated - } else { - ComputeUnitLimit::Default - } - }); - - Config { - rpc_client: RpcClient::new_with_commitment(json_rpc_url, CommitmentConfig::confirmed()), - verbose, - output_format, - manager, - staker, - funding_authority, - token_owner, - fee_payer, - dry_run, - no_update, - compute_unit_price, - compute_unit_limit, - } - }; - - let _ = match matches.subcommand() { - ("create-pool", Some(arg_matches)) => { - let deposit_authority = keypair_of(arg_matches, "deposit_authority"); - let e_numerator = value_t_or_exit!(arg_matches, "epoch_fee_numerator", u64); - let e_denominator = value_t_or_exit!(arg_matches, "epoch_fee_denominator", u64); - let w_numerator = value_t!(arg_matches, "withdrawal_fee_numerator", u64); - let w_denominator = value_t!(arg_matches, "withdrawal_fee_denominator", u64); - let d_numerator = value_t!(arg_matches, "deposit_fee_numerator", u64); - let d_denominator = value_t!(arg_matches, "deposit_fee_denominator", u64); - let referral_fee = value_t!(arg_matches, "referral_fee", u8); - let max_validators = value_t_or_exit!(arg_matches, "max_validators", u32); - let pool_keypair = keypair_of(arg_matches, "pool_keypair"); - let validator_list_keypair = keypair_of(arg_matches, "validator_list_keypair"); - let mint_keypair = keypair_of(arg_matches, "mint_keypair"); - let reserve_keypair = keypair_of(arg_matches, "reserve_keypair"); - let unsafe_fees = arg_matches.is_present("unsafe_fees"); - command_create_pool( - &config, - deposit_authority, - Fee { - numerator: e_numerator, - denominator: e_denominator, - }, - Fee { - numerator: w_numerator.unwrap_or(0), - denominator: w_denominator.unwrap_or(0), - }, - Fee { - numerator: d_numerator.unwrap_or(0), - denominator: d_denominator.unwrap_or(0), - }, - referral_fee.unwrap_or(0), - max_validators, - pool_keypair, - validator_list_keypair, - mint_keypair, - reserve_keypair, - unsafe_fees, - ) - } - ("create-token-metadata", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let name = value_t_or_exit!(arg_matches, "name", String); - let symbol = value_t_or_exit!(arg_matches, "symbol", String); - let uri = value_t_or_exit!(arg_matches, "uri", String); - create_token_metadata(&config, &stake_pool_address, name, symbol, uri) - } - ("update-token-metadata", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let name = value_t_or_exit!(arg_matches, "name", String); - let symbol = value_t_or_exit!(arg_matches, "symbol", String); - let uri = value_t_or_exit!(arg_matches, "uri", String); - update_token_metadata(&config, &stake_pool_address, name, symbol, uri) - } - ("add-validator", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let vote_account_address = pubkey_of(arg_matches, "vote_account").unwrap(); - command_vsa_add(&config, &stake_pool_address, &vote_account_address) - } - ("remove-validator", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let vote_account = pubkey_of(arg_matches, "vote_account").unwrap(); - command_vsa_remove(&config, &stake_pool_address, &vote_account) - } - ("increase-validator-stake", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let vote_account = pubkey_of(arg_matches, "vote_account").unwrap(); - let amount = value_t_or_exit!(arg_matches, "amount", f64); - command_increase_validator_stake(&config, &stake_pool_address, &vote_account, amount) - } - ("decrease-validator-stake", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let vote_account = pubkey_of(arg_matches, "vote_account").unwrap(); - let amount = value_t_or_exit!(arg_matches, "amount", f64); - command_decrease_validator_stake(&config, &stake_pool_address, &vote_account, amount) - } - ("set-preferred-validator", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let preferred_type = match arg_matches.value_of("preferred_type").unwrap() { - "deposit" => PreferredValidatorType::Deposit, - "withdraw" => PreferredValidatorType::Withdraw, - _ => unreachable!(), - }; - let vote_account = pubkey_of(arg_matches, "vote_account"); - let _unset = arg_matches.is_present("unset"); - // since unset and vote_account can't both be set, if unset is set - // then vote_account will be None, which is valid for the program - command_set_preferred_validator( - &config, - &stake_pool_address, - preferred_type, - vote_account, - ) - } - ("deposit-stake", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let stake_account = pubkey_of(arg_matches, "stake_account").unwrap(); - let token_receiver: Option = pubkey_of(arg_matches, "token_receiver"); - let referrer: Option = pubkey_of(arg_matches, "referrer"); - let withdraw_authority = get_signer( - arg_matches, - "withdraw_authority", - &cli_config.keypair_path, - &mut wallet_manager, - SignerFromPathConfig { - allow_null_signer: false, - }, - ); - command_deposit_stake( - &config, - &stake_pool_address, - &stake_account, - withdraw_authority, - &token_receiver, - &referrer, - ) - } - ("deposit-sol", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let token_receiver: Option = pubkey_of(arg_matches, "token_receiver"); - let referrer: Option = pubkey_of(arg_matches, "referrer"); - let from = keypair_of(arg_matches, "from"); - let amount = value_t_or_exit!(arg_matches, "amount", f64); - command_deposit_sol( - &config, - &stake_pool_address, - &from, - &token_receiver, - &referrer, - amount, - ) - } - ("list", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - command_list(&config, &stake_pool_address) - } - ("update", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let no_merge = arg_matches.is_present("no_merge"); - let force = arg_matches.is_present("force"); - let stale_only = arg_matches.is_present("stale_only"); - command_update(&config, &stake_pool_address, force, no_merge, stale_only) - } - ("withdraw-stake", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let vote_account = pubkey_of(arg_matches, "vote_account"); - let pool_account = pubkey_of(arg_matches, "pool_account"); - let pool_amount = value_t_or_exit!(arg_matches, "amount", f64); - let stake_receiver = pubkey_of(arg_matches, "stake_receiver"); - let use_reserve = arg_matches.is_present("use_reserve"); - command_withdraw_stake( - &config, - &stake_pool_address, - use_reserve, - &vote_account, - &stake_receiver, - &pool_account, - pool_amount, - ) - } - ("withdraw-sol", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let pool_account = pubkey_of(arg_matches, "pool_account"); - let pool_amount = value_t_or_exit!(arg_matches, "amount", f64); - let sol_receiver = get_signer( - arg_matches, - "sol_receiver", - &cli_config.keypair_path, - &mut wallet_manager, - SignerFromPathConfig { - allow_null_signer: true, - }, - ) - .pubkey(); - command_withdraw_sol( - &config, - &stake_pool_address, - &pool_account, - &sol_receiver, - pool_amount, - ) - } - ("set-manager", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - - let new_manager = if arg_matches.value_of("new_manager").is_some() { - let signer = get_signer( - arg_matches, - "new-manager", - arg_matches - .value_of("new_manager") - .expect("new manager argument not found!"), - &mut wallet_manager, - SignerFromPathConfig { - allow_null_signer: true, - }, - ); - Some(signer) - } else { - None - }; - - let new_fee_receiver: Option = pubkey_of(arg_matches, "new_fee_receiver"); - command_set_manager( - &config, - &stake_pool_address, - &new_manager, - &new_fee_receiver, - ) - } - ("set-staker", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let new_staker = pubkey_of(arg_matches, "new_staker").unwrap(); - command_set_staker(&config, &stake_pool_address, &new_staker) - } - ("set-funding-authority", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let new_authority = pubkey_of(arg_matches, "new_authority"); - let funding_type = match arg_matches.value_of("funding_type").unwrap() { - "sol-deposit" => FundingType::SolDeposit, - "stake-deposit" => FundingType::StakeDeposit, - "sol-withdraw" => FundingType::SolWithdraw, - _ => unreachable!(), - }; - let _unset = arg_matches.is_present("unset"); - command_set_funding_authority(&config, &stake_pool_address, new_authority, funding_type) - } - ("set-fee", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let numerator = value_t_or_exit!(arg_matches, "fee_numerator", u64); - let denominator = value_t_or_exit!(arg_matches, "fee_denominator", u64); - let new_fee = Fee { - denominator, - numerator, - }; - match arg_matches.value_of("fee_type").unwrap() { - "epoch" => command_set_fee(&config, &stake_pool_address, FeeType::Epoch(new_fee)), - "stake-deposit" => { - command_set_fee(&config, &stake_pool_address, FeeType::StakeDeposit(new_fee)) - } - "sol-deposit" => { - command_set_fee(&config, &stake_pool_address, FeeType::SolDeposit(new_fee)) - } - "stake-withdrawal" => command_set_fee( - &config, - &stake_pool_address, - FeeType::StakeWithdrawal(new_fee), - ), - "sol-withdrawal" => command_set_fee( - &config, - &stake_pool_address, - FeeType::SolWithdrawal(new_fee), - ), - _ => unreachable!(), - } - } - ("set-referral-fee", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let fee = value_t_or_exit!(arg_matches, "fee", u8); - assert!( - fee <= 100u8, - "Invalid fee {}%. Fee needs to be in range [0-100]", - fee - ); - let fee_type = match arg_matches.value_of("fee_type").unwrap() { - "sol" => FeeType::SolReferral(fee), - "stake" => FeeType::StakeReferral(fee), - _ => unreachable!(), - }; - command_set_fee(&config, &stake_pool_address, fee_type) - } - ("list-all", _) => command_list_all_pools(&config), - ("deposit-all-stake", Some(arg_matches)) => { - let stake_pool_address = pubkey_of(arg_matches, "pool").unwrap(); - let stake_authority = pubkey_of(arg_matches, "stake_authority").unwrap(); - let token_receiver: Option = pubkey_of(arg_matches, "token_receiver"); - let referrer: Option = pubkey_of(arg_matches, "referrer"); - let withdraw_authority = get_signer( - arg_matches, - "withdraw_authority", - &cli_config.keypair_path, - &mut wallet_manager, - SignerFromPathConfig { - allow_null_signer: false, - }, - ); - command_deposit_all_stake( - &config, - &stake_pool_address, - &stake_authority, - withdraw_authority, - &token_receiver, - &referrer, - ) - } - _ => unreachable!(), - } - .map_err(|err| { - eprintln!("{}", err); - exit(1); - }); -} diff --git a/stake-pool/cli/src/output.rs b/stake-pool/cli/src/output.rs deleted file mode 100644 index 39d85a0bcac..00000000000 --- a/stake-pool/cli/src/output.rs +++ /dev/null @@ -1,505 +0,0 @@ -use { - serde::{Deserialize, Serialize}, - solana_cli_output::{QuietDisplay, VerboseDisplay}, - solana_sdk::{native_token::Sol, pubkey::Pubkey, stake::state::Lockup}, - spl_stake_pool::state::{ - Fee, PodStakeStatus, StakePool, StakeStatus, ValidatorList, ValidatorStakeInfo, - }, - std::fmt::{Display, Formatter, Result, Write}, -}; - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliStakePools { - pub pools: Vec, -} - -impl Display for CliStakePools { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - for pool in &self.pools { - writeln!( - f, - "Address: {}\tManager: {}\tLamports: {}\tPool tokens: {}\tValidators: {}", - pool.address, - pool.manager, - pool.total_lamports, - pool.pool_token_supply, - pool.validator_list.len() - )?; - } - writeln!(f, "Total number of pools: {}", &self.pools.len())?; - Ok(()) - } -} - -impl QuietDisplay for CliStakePools {} -impl VerboseDisplay for CliStakePools {} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliStakePool { - pub address: String, - pub pool_withdraw_authority: String, - pub manager: String, - pub staker: String, - pub stake_deposit_authority: String, - pub stake_withdraw_bump_seed: u8, - pub max_validators: u32, - pub validator_list: Vec, - pub validator_list_storage_account: String, - pub reserve_stake: String, - pub pool_mint: String, - pub manager_fee_account: String, - pub token_program_id: String, - pub total_lamports: u64, - pub pool_token_supply: u64, - pub last_update_epoch: u64, - pub lockup: CliStakePoolLockup, - pub epoch_fee: CliStakePoolFee, - pub next_epoch_fee: Option, - pub preferred_deposit_validator_vote_address: Option, - pub preferred_withdraw_validator_vote_address: Option, - pub stake_deposit_fee: CliStakePoolFee, - pub stake_withdrawal_fee: CliStakePoolFee, - pub next_stake_withdrawal_fee: Option, - pub stake_referral_fee: u8, - pub sol_deposit_authority: Option, - pub sol_deposit_fee: CliStakePoolFee, - pub sol_referral_fee: u8, - pub sol_withdraw_authority: Option, - pub sol_withdrawal_fee: CliStakePoolFee, - pub next_sol_withdrawal_fee: Option, - pub last_epoch_pool_token_supply: u64, - pub last_epoch_total_lamports: u64, - pub details: Option, -} - -impl QuietDisplay for CliStakePool {} -impl VerboseDisplay for CliStakePool { - fn write_str(&self, w: &mut dyn Write) -> Result { - writeln!(w, "Stake Pool Info")?; - writeln!(w, "===============")?; - writeln!(w, "Stake Pool: {}", &self.address)?; - writeln!( - w, - "Validator List: {}", - &self.validator_list_storage_account - )?; - writeln!(w, "Manager: {}", &self.manager)?; - writeln!(w, "Staker: {}", &self.staker)?; - writeln!(w, "Depositor: {}", &self.stake_deposit_authority)?; - writeln!( - w, - "SOL Deposit Authority: {}", - &self - .sol_deposit_authority - .as_ref() - .unwrap_or(&"None".to_string()) - )?; - writeln!( - w, - "SOL Withdraw Authority: {}", - &self - .sol_withdraw_authority - .as_ref() - .unwrap_or(&"None".to_string()) - )?; - writeln!(w, "Withdraw Authority: {}", &self.pool_withdraw_authority)?; - writeln!(w, "Pool Token Mint: {}", &self.pool_mint)?; - writeln!(w, "Fee Account: {}", &self.manager_fee_account)?; - match &self.preferred_deposit_validator_vote_address { - None => {} - Some(s) => { - writeln!(w, "Preferred Deposit Validator: {}", s)?; - } - } - match &self.preferred_withdraw_validator_vote_address { - None => {} - Some(s) => { - writeln!(w, "Preferred Withdraw Validator: {}", s)?; - } - } - writeln!(w, "Epoch Fee: {} of epoch rewards", &self.epoch_fee)?; - if let Some(next_epoch_fee) = &self.next_epoch_fee { - writeln!(w, "Next Epoch Fee: {} of epoch rewards", next_epoch_fee)?; - } - writeln!( - w, - "Stake Withdrawal Fee: {} of withdrawal amount", - &self.stake_withdrawal_fee - )?; - if let Some(next_stake_withdrawal_fee) = &self.next_stake_withdrawal_fee { - writeln!( - w, - "Next Stake Withdrawal Fee: {} of withdrawal amount", - next_stake_withdrawal_fee - )?; - } - writeln!( - w, - "SOL Withdrawal Fee: {} of withdrawal amount", - &self.sol_withdrawal_fee - )?; - if let Some(next_sol_withdrawal_fee) = &self.next_sol_withdrawal_fee { - writeln!( - w, - "Next SOL Withdrawal Fee: {} of withdrawal amount", - next_sol_withdrawal_fee - )?; - } - writeln!( - w, - "Stake Deposit Fee: {} of deposit amount", - &self.stake_deposit_fee - )?; - writeln!( - w, - "SOL Deposit Fee: {} of deposit amount", - &self.sol_deposit_fee - )?; - writeln!( - w, - "Stake Deposit Referral Fee: {}% of Stake Deposit Fee", - &self.stake_referral_fee - )?; - writeln!( - w, - "SOL Deposit Referral Fee: {}% of SOL Deposit Fee", - &self.sol_referral_fee - )?; - writeln!(w)?; - - match &self.details { - None => {} - Some(details) => { - VerboseDisplay::write_str(details, w)?; - } - } - Ok(()) - } -} - -impl Display for CliStakePool { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - writeln!(f, "Stake Pool: {}", &self.address)?; - writeln!( - f, - "Validator List: {}", - &self.validator_list_storage_account - )?; - writeln!(f, "Pool Token Mint: {}", &self.pool_mint)?; - match &self.preferred_deposit_validator_vote_address { - None => {} - Some(s) => { - writeln!(f, "Preferred Deposit Validator: {}", s)?; - } - } - match &self.preferred_withdraw_validator_vote_address { - None => {} - Some(s) => { - writeln!(f, "Preferred Withdraw Validator: {}", s)?; - } - } - writeln!(f, "Epoch Fee: {} of epoch rewards", &self.epoch_fee)?; - writeln!( - f, - "Stake Withdrawal Fee: {} of withdrawal amount", - &self.stake_withdrawal_fee - )?; - writeln!( - f, - "SOL Withdrawal Fee: {} of withdrawal amount", - &self.sol_withdrawal_fee - )?; - writeln!( - f, - "Stake Deposit Fee: {} of deposit amount", - &self.stake_deposit_fee - )?; - writeln!( - f, - "SOL Deposit Fee: {} of deposit amount", - &self.sol_deposit_fee - )?; - writeln!( - f, - "Stake Deposit Referral Fee: {}% of Stake Deposit Fee", - &self.stake_referral_fee - )?; - writeln!( - f, - "SOL Deposit Referral Fee: {}% of SOL Deposit Fee", - &self.sol_referral_fee - )?; - Ok(()) - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliStakePoolDetails { - pub reserve_stake_account_address: String, - pub reserve_stake_lamports: u64, - pub minimum_reserve_stake_balance: u64, - pub stake_accounts: Vec, - pub total_lamports: u64, - pub total_pool_tokens: f64, - pub current_number_of_validators: u32, - pub max_number_of_validators: u32, - pub update_required: bool, -} - -impl Display for CliStakePoolDetails { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - writeln!( - f, - "Reserve Account: {}\tAvailable Balance: {}", - &self.reserve_stake_account_address, - Sol(self.reserve_stake_lamports - self.minimum_reserve_stake_balance), - )?; - for stake_account in &self.stake_accounts { - writeln!( - f, - "Vote Account: {}\tBalance: {}\tLast Update Epoch: {}", - stake_account.vote_account_address, - Sol(stake_account.validator_lamports), - stake_account.validator_last_update_epoch, - )?; - } - writeln!( - f, - "Total Pool Stake: {} {}", - Sol(self.total_lamports), - if self.update_required { - " [UPDATE REQUIRED]" - } else { - "" - }, - )?; - writeln!(f, "Total Pool Tokens: {}", &self.total_pool_tokens,)?; - writeln!( - f, - "Current Number of Validators: {}", - &self.current_number_of_validators, - )?; - writeln!( - f, - "Max Number of Validators: {}", - &self.max_number_of_validators, - )?; - Ok(()) - } -} - -impl QuietDisplay for CliStakePoolDetails {} -impl VerboseDisplay for CliStakePoolDetails { - fn write_str(&self, w: &mut dyn Write) -> Result { - writeln!(w, "Stake Accounts")?; - writeln!(w, "--------------")?; - writeln!( - w, - "Reserve Account: {}\tAvailable Balance: {}", - &self.reserve_stake_account_address, - Sol(self.reserve_stake_lamports - self.minimum_reserve_stake_balance), - )?; - for stake_account in &self.stake_accounts { - writeln!( - w, - "Vote Account: {}\tStake Account: {}\tActive Balance: {}\tTransient Stake Account: {}\tTransient Balance: {}\tLast Update Epoch: {}{}", - stake_account.vote_account_address, - stake_account.stake_account_address, - Sol(stake_account.validator_active_stake_lamports), - stake_account.validator_transient_stake_account_address, - Sol(stake_account.validator_transient_stake_lamports), - stake_account.validator_last_update_epoch, - if stake_account.update_required { - " [UPDATE REQUIRED]" - } else { - "" - }, - )?; - } - writeln!( - w, - "Total Pool Stake: {} {}", - Sol(self.total_lamports), - if self.update_required { - " [UPDATE REQUIRED]" - } else { - "" - }, - )?; - writeln!(w, "Total Pool Tokens: {}", &self.total_pool_tokens,)?; - writeln!( - w, - "Current Number of Validators: {}", - &self.current_number_of_validators, - )?; - writeln!( - w, - "Max Number of Validators: {}", - &self.max_number_of_validators, - )?; - Ok(()) - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliStakePoolStakeAccountInfo { - pub vote_account_address: String, - pub stake_account_address: String, - pub validator_active_stake_lamports: u64, - pub validator_last_update_epoch: u64, - pub validator_lamports: u64, - pub validator_transient_stake_account_address: String, - pub validator_transient_stake_lamports: u64, - pub update_required: bool, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliStakePoolValidator { - pub active_stake_lamports: u64, - pub transient_stake_lamports: u64, - pub last_update_epoch: u64, - pub transient_seed_suffix: u64, - pub unused: u32, - pub validator_seed_suffix: u32, - pub status: CliStakePoolValidatorStakeStatus, - pub vote_account_address: String, -} - -impl From for CliStakePoolValidator { - fn from(v: ValidatorStakeInfo) -> Self { - Self { - active_stake_lamports: v.active_stake_lamports.into(), - transient_stake_lamports: v.transient_stake_lamports.into(), - last_update_epoch: v.last_update_epoch.into(), - transient_seed_suffix: v.transient_seed_suffix.into(), - unused: v.unused.into(), - validator_seed_suffix: v.validator_seed_suffix.into(), - status: CliStakePoolValidatorStakeStatus::from(v.status), - vote_account_address: v.vote_account_address.to_string(), - } - } -} - -impl From for CliStakePoolValidatorStakeStatus { - fn from(s: PodStakeStatus) -> CliStakePoolValidatorStakeStatus { - let s = StakeStatus::try_from(s).unwrap(); - match s { - StakeStatus::Active => CliStakePoolValidatorStakeStatus::Active, - StakeStatus::DeactivatingTransient => { - CliStakePoolValidatorStakeStatus::DeactivatingTransient - } - StakeStatus::ReadyForRemoval => CliStakePoolValidatorStakeStatus::ReadyForRemoval, - StakeStatus::DeactivatingValidator => { - CliStakePoolValidatorStakeStatus::DeactivatingValidator - } - StakeStatus::DeactivatingAll => CliStakePoolValidatorStakeStatus::DeactivatingAll, - } - } -} - -#[derive(Serialize, Deserialize)] -pub(crate) enum CliStakePoolValidatorStakeStatus { - Active, - DeactivatingTransient, - ReadyForRemoval, - DeactivatingValidator, - DeactivatingAll, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CliStakePoolLockup { - pub unix_timestamp: i64, - pub epoch: u64, - pub custodian: String, -} - -impl From for CliStakePoolLockup { - fn from(l: Lockup) -> Self { - Self { - unix_timestamp: l.unix_timestamp, - epoch: l.epoch, - custodian: l.custodian.to_string(), - } - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliStakePoolFee { - pub denominator: u64, - pub numerator: u64, -} - -impl Display for CliStakePoolFee { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!(f, "{}/{}", &self.numerator, &self.denominator) - } -} - -impl From for CliStakePoolFee { - fn from(f: Fee) -> Self { - Self { - denominator: f.denominator, - numerator: f.numerator, - } - } -} - -impl From<(Pubkey, StakePool, ValidatorList, Pubkey)> for CliStakePool { - fn from(s: (Pubkey, StakePool, ValidatorList, Pubkey)) -> Self { - let (address, stake_pool, validator_list, pool_withdraw_authority) = s; - Self { - address: address.to_string(), - pool_withdraw_authority: pool_withdraw_authority.to_string(), - manager: stake_pool.manager.to_string(), - staker: stake_pool.staker.to_string(), - stake_deposit_authority: stake_pool.stake_deposit_authority.to_string(), - stake_withdraw_bump_seed: stake_pool.stake_withdraw_bump_seed, - max_validators: validator_list.header.max_validators, - validator_list: validator_list - .validators - .into_iter() - .map(CliStakePoolValidator::from) - .collect(), - validator_list_storage_account: stake_pool.validator_list.to_string(), - reserve_stake: stake_pool.reserve_stake.to_string(), - pool_mint: stake_pool.pool_mint.to_string(), - manager_fee_account: stake_pool.manager_fee_account.to_string(), - token_program_id: stake_pool.token_program_id.to_string(), - total_lamports: stake_pool.total_lamports, - pool_token_supply: stake_pool.pool_token_supply, - last_update_epoch: stake_pool.last_update_epoch, - lockup: CliStakePoolLockup::from(stake_pool.lockup), - epoch_fee: CliStakePoolFee::from(stake_pool.epoch_fee), - next_epoch_fee: Option::::from(stake_pool.next_epoch_fee) - .map(CliStakePoolFee::from), - preferred_deposit_validator_vote_address: stake_pool - .preferred_deposit_validator_vote_address - .map(|x| x.to_string()), - preferred_withdraw_validator_vote_address: stake_pool - .preferred_withdraw_validator_vote_address - .map(|x| x.to_string()), - stake_deposit_fee: CliStakePoolFee::from(stake_pool.stake_deposit_fee), - stake_withdrawal_fee: CliStakePoolFee::from(stake_pool.stake_withdrawal_fee), - next_stake_withdrawal_fee: Option::::from(stake_pool.next_stake_withdrawal_fee) - .map(CliStakePoolFee::from), - stake_referral_fee: stake_pool.stake_referral_fee, - sol_deposit_authority: stake_pool.sol_deposit_authority.map(|x| x.to_string()), - sol_deposit_fee: CliStakePoolFee::from(stake_pool.sol_deposit_fee), - sol_referral_fee: stake_pool.sol_referral_fee, - sol_withdraw_authority: stake_pool.sol_withdraw_authority.map(|x| x.to_string()), - sol_withdrawal_fee: CliStakePoolFee::from(stake_pool.sol_withdrawal_fee), - next_sol_withdrawal_fee: Option::::from(stake_pool.next_sol_withdrawal_fee) - .map(CliStakePoolFee::from), - last_epoch_pool_token_supply: stake_pool.last_epoch_pool_token_supply, - last_epoch_total_lamports: stake_pool.last_epoch_total_lamports, - details: None, - } - } -} diff --git a/stake-pool/js/.eslintignore b/stake-pool/js/.eslintignore deleted file mode 100644 index 58542507948..00000000000 --- a/stake-pool/js/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -dist -node_modules -.vscode -.idea diff --git a/stake-pool/js/.eslintrc.js b/stake-pool/js/.eslintrc.js deleted file mode 100644 index 63db7b8405f..00000000000 --- a/stake-pool/js/.eslintrc.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = { - root: true, - env: { - es6: true, - node: true, - jest: true, - }, - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], - plugins: ['@typescript-eslint/eslint-plugin'], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - }, - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - }, -}; diff --git a/stake-pool/js/.gitignore b/stake-pool/js/.gitignore deleted file mode 100644 index 3764afb8006..00000000000 --- a/stake-pool/js/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -# IDE & OS specific -.DS_Store -.idea -.vscode - -# Logs -logs -*.log - -# Dependencies -node_modules - -# Coverage -coverage -.nyc_output - -# Release -dist -dist.browser - -# TypeScript -declarations diff --git a/stake-pool/js/.prettierrc.js b/stake-pool/js/.prettierrc.js deleted file mode 100644 index 8446d684477..00000000000 --- a/stake-pool/js/.prettierrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - singleQuote: true, - trailingComma: 'all', - printWidth: 100, - endOfLine: 'lf', - semi: true, -}; diff --git a/stake-pool/js/README.md b/stake-pool/js/README.md deleted file mode 100644 index 947ec739973..00000000000 --- a/stake-pool/js/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# TypeScript bindings for stake-pool program - -For use with both node.js and in-browser. - -## Installation - -``` -npm install -``` - -## Build and run - -In the `js` folder: - -``` -npm run build -``` - -The build is available at `dist/index.js` (or `dist.browser/index.iife.js` in the browser). - -## Browser bundle -```html - - - - - -``` - -## Test - -``` -npm test -``` - -## Usage - -### JavaScript -```javascript -const solanaStakePool = require('@solana/spl-stake-pool'); -console.log(solanaStakePool); -``` - -### ES6 -```javascript -import * as solanaStakePool from '@solana/spl-stake-pool'; -console.log(solanaStakePool); -``` - -### Browser bundle -```javascript -// `solanaStakePool` is provided in the global namespace by the script bundle. -console.log(solanaStakePool); -``` diff --git a/stake-pool/js/package.json b/stake-pool/js/package.json deleted file mode 100644 index f970941aef3..00000000000 --- a/stake-pool/js/package.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "@solana/spl-stake-pool", - "version": "1.1.8", - "description": "SPL Stake Pool Program JS API", - "scripts": { - "build": "tsc && cross-env NODE_ENV=production rollup -c", - "build:program": "cargo build-sbf --manifest-path=../program/Cargo.toml", - "lint": "eslint --max-warnings 0 .", - "lint:fix": "eslint . --fix", - "test": "jest", - "clean": "rimraf ./dist" - }, - "keywords": [], - "contributors": [ - "Solana Labs Maintainers ", - "Lieu Zheng Hong", - "mFactory Team (https://mfactory.ch/)", - "SolBlaze (https://solblaze.org/)" - ], - "homepage": "https://solana.com", - "repository": { - "type": "git", - "url": "https://github.com/solana-labs/solana-program-library" - }, - "publishConfig": { - "access": "public" - }, - "browser": { - "./dist/index.cjs.js": "./dist/index.browser.cjs.js", - "./dist/index.esm.js": "./dist/index.browser.esm.js" - }, - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts", - "browserslist": [ - "defaults", - "not IE 11", - "maintained node versions" - ], - "files": [ - "/dist", - "/src" - ], - "license": "ISC", - "dependencies": { - "@solana/buffer-layout": "^4.0.1", - "@solana/spl-token": "0.4.9", - "@solana/web3.js": "^1.95.5", - "bn.js": "^5.2.0", - "buffer": "^6.0.3", - "buffer-layout": "^1.2.2", - "superstruct": "^2.0.2" - }, - "devDependencies": { - "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-multi-entry": "^6.0.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "@rollup/plugin-terser": "^0.4.4", - "@rollup/plugin-typescript": "^12.1.2", - "@types/bn.js": "^5.1.6", - "@types/jest": "^29.5.14", - "@types/node": "^22.10.5", - "@types/node-fetch": "^2.6.12", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "@typescript-eslint/parser": "^8.4.0", - "cross-env": "^7.0.3", - "eslint": "^8.57.0", - "jest": "^29.0.0", - "rimraf": "^6.0.1", - "rollup": "^4.30.1", - "rollup-plugin-dts": "^6.1.1", - "ts-jest": "^29.2.5", - "typescript": "^5.7.2" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": ".", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "testRegex": ".*\\.test\\.ts$", - "testEnvironment": "node" - } -} diff --git a/stake-pool/js/rollup.config.mjs b/stake-pool/js/rollup.config.mjs deleted file mode 100644 index f1ceb7e4904..00000000000 --- a/stake-pool/js/rollup.config.mjs +++ /dev/null @@ -1,113 +0,0 @@ -import typescript from '@rollup/plugin-typescript'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import nodeResolve from '@rollup/plugin-node-resolve'; -import terser from '@rollup/plugin-terser'; - -const extensions = ['.js', '.ts']; - -function generateConfig(configType, format) { - const browser = configType === 'browser'; - - const config = { - input: 'src/index.ts', - plugins: [ - commonjs(), - nodeResolve({ - browser, - dedupe: ['bn.js', 'buffer'], - extensions, - preferBuiltins: !browser, - }), - typescript(), - ], - onwarn: function (warning, rollupWarn) { - if (warning.code !== 'CIRCULAR_DEPENDENCY') { - rollupWarn(warning); - } - }, - treeshake: { - moduleSideEffects: false, - }, - }; - - if (browser) { - if (format === 'iife') { - config.external = ['http', 'https']; - - config.output = [ - { - file: 'dist/index.iife.js', - format: 'iife', - name: 'solanaStakePool', - sourcemap: true, - }, - { - file: 'dist/index.iife.min.js', - format: 'iife', - name: 'solanaStakePool', - sourcemap: true, - plugins: [terser({ mangle: false, compress: false })], - }, - ]; - } else { - config.output = [ - { - file: 'dist/index.browser.cjs.js', - format: 'cjs', - sourcemap: true, - }, - { - file: 'dist/index.browser.esm.js', - format: 'es', - sourcemap: true, - }, - ]; - - // Prevent dependencies from being bundled - config.external = [ - '@solana/buffer-layout', - '@solana/spl-token', - '@solana/web3.js', - 'bn.js', - 'buffer', - 'buffer-layout', - ]; - } - - // TODO: Find a workaround to avoid resolving the following JSON file: - // `node_modules/secp256k1/node_modules/elliptic/package.json` - config.plugins.push(json()); - } else { - config.output = [ - { - file: 'dist/index.cjs.js', - format: 'cjs', - sourcemap: true, - }, - { - file: 'dist/index.esm.js', - format: 'es', - sourcemap: true, - }, - ]; - - // Prevent dependencies from being bundled - config.external = [ - '@solana/buffer-layout', - '@solana/spl-token', - '@solana/web3.js', - 'bn.js', - 'buffer', - 'buffer-layout', - ]; - } - - return config; -} - -export default [ - generateConfig('node'), - generateConfig('browser'), - generateConfig('browser', 'iife'), -]; diff --git a/stake-pool/js/src/codecs.ts b/stake-pool/js/src/codecs.ts deleted file mode 100644 index 0dbbde55ac8..00000000000 --- a/stake-pool/js/src/codecs.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { blob, Layout as LayoutCls, offset, seq, struct, u32, u8 } from 'buffer-layout'; -import { PublicKey } from '@solana/web3.js'; -import BN from 'bn.js'; - -export interface Layout { - span: number; - property?: string; - - decode(b: Buffer, offset?: number): T; - - encode(src: T, b: Buffer, offset?: number): number; - - getSpan(b: Buffer, offset?: number): number; - - replicate(name: string): this; -} - -class BNLayout extends LayoutCls { - blob: Layout; - signed: boolean; - - constructor(span: number, signed: boolean, property?: string) { - super(span, property); - this.blob = blob(span); - this.signed = signed; - } - - decode(b: Buffer, offset = 0) { - const num = new BN(this.blob.decode(b, offset), 10, 'le'); - if (this.signed) { - return num.fromTwos(this.span * 8).clone(); - } - return num; - } - - encode(src: BN, b: Buffer, offset = 0) { - if (this.signed) { - src = src.toTwos(this.span * 8); - } - return this.blob.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset); - } -} - -export function u64(property?: string): Layout { - return new BNLayout(8, false, property); -} - -class WrappedLayout extends LayoutCls { - layout: Layout; - decoder: (data: T) => U; - encoder: (src: U) => T; - - constructor( - layout: Layout, - decoder: (data: T) => U, - encoder: (src: U) => T, - property?: string, - ) { - super(layout.span, property); - this.layout = layout; - this.decoder = decoder; - this.encoder = encoder; - } - - decode(b: Buffer, offset?: number): U { - return this.decoder(this.layout.decode(b, offset)); - } - - encode(src: U, b: Buffer, offset?: number): number { - return this.layout.encode(this.encoder(src), b, offset); - } - - getSpan(b: Buffer, offset?: number): number { - return this.layout.getSpan(b, offset); - } -} - -export function publicKey(property?: string): Layout { - return new WrappedLayout( - blob(32), - (b: Buffer) => new PublicKey(b), - (key: PublicKey) => key.toBuffer(), - property, - ); -} - -class OptionLayout extends LayoutCls { - layout: Layout; - discriminator: Layout; - - constructor(layout: Layout, property?: string) { - super(-1, property); - this.layout = layout; - this.discriminator = u8(); - } - - encode(src: T | null, b: Buffer, offset = 0): number { - if (src === null || src === undefined) { - return this.discriminator.encode(0, b, offset); - } - this.discriminator.encode(1, b, offset); - return this.layout.encode(src, b, offset + 1) + 1; - } - - decode(b: Buffer, offset = 0): T | null { - const discriminator = this.discriminator.decode(b, offset); - if (discriminator === 0) { - return null; - } else if (discriminator === 1) { - return this.layout.decode(b, offset + 1); - } - throw new Error('Invalid option ' + this.property); - } - - getSpan(b: Buffer, offset = 0): number { - const discriminator = this.discriminator.decode(b, offset); - if (discriminator === 0) { - return 1; - } else if (discriminator === 1) { - return this.layout.getSpan(b, offset + 1) + 1; - } - throw new Error('Invalid option ' + this.property); - } -} - -export function option(layout: Layout, property?: string): Layout { - return new OptionLayout(layout, property); -} - -export function bool(property?: string): Layout { - return new WrappedLayout(u8(), decodeBool, encodeBool, property); -} - -function decodeBool(value: number): boolean { - if (value === 0) { - return false; - } else if (value === 1) { - return true; - } - throw new Error('Invalid bool: ' + value); -} - -function encodeBool(value: boolean): number { - return value ? 1 : 0; -} - -export function vec(elementLayout: Layout, property?: string): Layout { - const length = u32('length'); - const layout: Layout<{ values: T[] }> = struct([ - length, - seq(elementLayout, offset(length, -length.span), 'values'), - ]); - return new WrappedLayout( - layout, - ({ values }) => values, - (values) => ({ values }), - property, - ); -} diff --git a/stake-pool/js/src/constants.ts b/stake-pool/js/src/constants.ts deleted file mode 100644 index d0f24ffa078..00000000000 --- a/stake-pool/js/src/constants.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Buffer } from 'buffer'; -import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; - -// Public key that identifies the metadata program. -export const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'); -export const METADATA_MAX_NAME_LENGTH = 32; -export const METADATA_MAX_SYMBOL_LENGTH = 10; -export const METADATA_MAX_URI_LENGTH = 200; - -// Public key that identifies the SPL Stake Pool program. -export const STAKE_POOL_PROGRAM_ID = new PublicKey('SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy'); - -// Maximum number of validators to update during UpdateValidatorListBalance. -export const MAX_VALIDATORS_TO_UPDATE = 5; - -// Seed for ephemeral stake account -export const EPHEMERAL_STAKE_SEED_PREFIX = Buffer.from('ephemeral'); - -// Seed used to derive transient stake accounts. -export const TRANSIENT_STAKE_SEED_PREFIX = Buffer.from('transient'); - -// Minimum amount of staked SOL required in a validator stake account to allow -// for merges without a mismatch on credits observed -export const MINIMUM_ACTIVE_STAKE = LAMPORTS_PER_SOL; diff --git a/stake-pool/js/src/index.ts b/stake-pool/js/src/index.ts deleted file mode 100644 index 41414eac194..00000000000 --- a/stake-pool/js/src/index.ts +++ /dev/null @@ -1,1228 +0,0 @@ -import { - AccountInfo, - Connection, - Keypair, - PublicKey, - Signer, - StakeAuthorizationLayout, - StakeProgram, - SystemProgram, - TransactionInstruction, -} from '@solana/web3.js'; -import { - createApproveInstruction, - createAssociatedTokenAccountIdempotentInstruction, - getAccount, - getAssociatedTokenAddressSync, -} from '@solana/spl-token'; -import { - ValidatorAccount, - arrayChunk, - calcLamportsWithdrawAmount, - findStakeProgramAddress, - findTransientStakeProgramAddress, - findWithdrawAuthorityProgramAddress, - getValidatorListAccount, - newStakeAccount, - prepareWithdrawAccounts, - lamportsToSol, - solToLamports, - findEphemeralStakeProgramAddress, - findMetadataAddress, -} from './utils'; -import { StakePoolInstruction } from './instructions'; -import { - StakeAccount, - StakePool, - StakePoolLayout, - ValidatorList, - ValidatorListLayout, - ValidatorStakeInfo, -} from './layouts'; -import { MAX_VALIDATORS_TO_UPDATE, MINIMUM_ACTIVE_STAKE, STAKE_POOL_PROGRAM_ID } from './constants'; -import { create } from 'superstruct'; -import BN from 'bn.js'; - -export type { StakePool, AccountType, ValidatorList, ValidatorStakeInfo } from './layouts'; -export { STAKE_POOL_PROGRAM_ID } from './constants'; -export * from './instructions'; -export { StakePoolLayout, ValidatorListLayout, ValidatorStakeInfoLayout } from './layouts'; - -export interface ValidatorListAccount { - pubkey: PublicKey; - account: AccountInfo; -} - -export interface StakePoolAccount { - pubkey: PublicKey; - account: AccountInfo; -} - -export interface WithdrawAccount { - stakeAddress: PublicKey; - voteAddress?: PublicKey; - poolAmount: BN; -} - -/** - * Wrapper class for a stake pool. - * Each stake pool has a stake pool account and a validator list account. - */ -export interface StakePoolAccounts { - stakePool: StakePoolAccount | undefined; - validatorList: ValidatorListAccount | undefined; -} - -/** - * Retrieves and deserializes a StakePool account using a web3js connection and the stake pool address. - * @param connection: An active web3js connection. - * @param stakePoolAddress: The public key (address) of the stake pool account. - */ -export async function getStakePoolAccount( - connection: Connection, - stakePoolAddress: PublicKey, -): Promise { - const account = await connection.getAccountInfo(stakePoolAddress); - - if (!account) { - throw new Error('Invalid stake pool account'); - } - - return { - pubkey: stakePoolAddress, - account: { - data: StakePoolLayout.decode(account.data), - executable: account.executable, - lamports: account.lamports, - owner: account.owner, - }, - }; -} - -/** - * Retrieves and deserializes a Stake account using a web3js connection and the stake address. - * @param connection: An active web3js connection. - * @param stakeAccount: The public key (address) of the stake account. - */ -export async function getStakeAccount( - connection: Connection, - stakeAccount: PublicKey, -): Promise { - const result = (await connection.getParsedAccountInfo(stakeAccount)).value; - if (!result || !('parsed' in result.data)) { - throw new Error('Invalid stake account'); - } - const program = result.data.program; - if (program != 'stake') { - throw new Error('Not a stake account'); - } - const parsed = create(result.data.parsed, StakeAccount); - - return parsed; -} - -/** - * Retrieves all StakePool and ValidatorList accounts that are running a particular StakePool program. - * @param connection: An active web3js connection. - * @param stakePoolProgramAddress: The public key (address) of the StakePool program. - */ -export async function getStakePoolAccounts( - connection: Connection, - stakePoolProgramAddress: PublicKey, -): Promise<(StakePoolAccount | ValidatorListAccount | undefined)[] | undefined> { - const response = await connection.getProgramAccounts(stakePoolProgramAddress); - - return response - .map((a) => { - try { - if (a.account.data.readUInt8() === 1) { - const data = StakePoolLayout.decode(a.account.data); - return { - pubkey: a.pubkey, - account: { - data, - executable: a.account.executable, - lamports: a.account.lamports, - owner: a.account.owner, - }, - }; - } else if (a.account.data.readUInt8() === 2) { - const data = ValidatorListLayout.decode(a.account.data); - return { - pubkey: a.pubkey, - account: { - data, - executable: a.account.executable, - lamports: a.account.lamports, - owner: a.account.owner, - }, - }; - } else { - console.error( - `Could not decode. StakePoolAccount Enum is ${a.account.data.readUInt8()}, expected 1 or 2!`, - ); - return undefined; - } - } catch (error) { - console.error('Could not decode account. Error:', error); - return undefined; - } - }) - .filter((a) => a !== undefined); -} - -/** - * Creates instructions required to deposit stake to stake pool. - */ -export async function depositStake( - connection: Connection, - stakePoolAddress: PublicKey, - authorizedPubkey: PublicKey, - validatorVote: PublicKey, - depositStake: PublicKey, - poolTokenReceiverAccount?: PublicKey, -) { - const stakePool = await getStakePoolAccount(connection, stakePoolAddress); - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - const validatorStake = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validatorVote, - stakePoolAddress, - ); - - const instructions: TransactionInstruction[] = []; - const signers: Signer[] = []; - - const poolMint = stakePool.account.data.poolMint; - - // Create token account if not specified - if (!poolTokenReceiverAccount) { - const associatedAddress = getAssociatedTokenAddressSync(poolMint, authorizedPubkey); - instructions.push( - createAssociatedTokenAccountIdempotentInstruction( - authorizedPubkey, - associatedAddress, - authorizedPubkey, - poolMint, - ), - ); - poolTokenReceiverAccount = associatedAddress; - } - - instructions.push( - ...StakeProgram.authorize({ - stakePubkey: depositStake, - authorizedPubkey, - newAuthorizedPubkey: stakePool.account.data.stakeDepositAuthority, - stakeAuthorizationType: StakeAuthorizationLayout.Staker, - }).instructions, - ); - - instructions.push( - ...StakeProgram.authorize({ - stakePubkey: depositStake, - authorizedPubkey, - newAuthorizedPubkey: stakePool.account.data.stakeDepositAuthority, - stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer, - }).instructions, - ); - - instructions.push( - StakePoolInstruction.depositStake({ - stakePool: stakePoolAddress, - validatorList: stakePool.account.data.validatorList, - depositAuthority: stakePool.account.data.stakeDepositAuthority, - reserveStake: stakePool.account.data.reserveStake, - managerFeeAccount: stakePool.account.data.managerFeeAccount, - referralPoolAccount: poolTokenReceiverAccount, - destinationPoolAccount: poolTokenReceiverAccount, - withdrawAuthority, - depositStake, - validatorStake, - poolMint, - }), - ); - - return { - instructions, - signers, - }; -} - -/** - * Creates instructions required to deposit sol to stake pool. - */ -export async function depositSol( - connection: Connection, - stakePoolAddress: PublicKey, - from: PublicKey, - lamports: number, - destinationTokenAccount?: PublicKey, - referrerTokenAccount?: PublicKey, - depositAuthority?: PublicKey, -) { - const fromBalance = await connection.getBalance(from, 'confirmed'); - if (fromBalance < lamports) { - throw new Error( - `Not enough SOL to deposit into pool. Maximum deposit amount is ${lamportsToSol( - fromBalance, - )} SOL.`, - ); - } - - const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress); - const stakePool = stakePoolAccount.account.data; - - // Ephemeral SOL account just to do the transfer - const userSolTransfer = new Keypair(); - const signers: Signer[] = [userSolTransfer]; - const instructions: TransactionInstruction[] = []; - - // Create the ephemeral SOL account - instructions.push( - SystemProgram.transfer({ - fromPubkey: from, - toPubkey: userSolTransfer.publicKey, - lamports, - }), - ); - - // Create token account if not specified - if (!destinationTokenAccount) { - const associatedAddress = getAssociatedTokenAddressSync(stakePool.poolMint, from); - instructions.push( - createAssociatedTokenAccountIdempotentInstruction( - from, - associatedAddress, - from, - stakePool.poolMint, - ), - ); - destinationTokenAccount = associatedAddress; - } - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - instructions.push( - StakePoolInstruction.depositSol({ - stakePool: stakePoolAddress, - reserveStake: stakePool.reserveStake, - fundingAccount: userSolTransfer.publicKey, - destinationPoolAccount: destinationTokenAccount, - managerFeeAccount: stakePool.managerFeeAccount, - referralPoolAccount: referrerTokenAccount ?? destinationTokenAccount, - poolMint: stakePool.poolMint, - lamports, - withdrawAuthority, - depositAuthority, - }), - ); - - return { - instructions, - signers, - }; -} - -/** - * Creates instructions required to withdraw stake from a stake pool. - */ -export async function withdrawStake( - connection: Connection, - stakePoolAddress: PublicKey, - tokenOwner: PublicKey, - amount: number, - useReserve = false, - voteAccountAddress?: PublicKey, - stakeReceiver?: PublicKey, - poolTokenAccount?: PublicKey, - validatorComparator?: (_a: ValidatorAccount, _b: ValidatorAccount) => number, -) { - const stakePool = await getStakePoolAccount(connection, stakePoolAddress); - const poolAmount = new BN(solToLamports(amount)); - - if (!poolTokenAccount) { - poolTokenAccount = getAssociatedTokenAddressSync(stakePool.account.data.poolMint, tokenOwner); - } - - const tokenAccount = await getAccount(connection, poolTokenAccount); - - // Check withdrawFrom balance - if (tokenAccount.amount < poolAmount.toNumber()) { - throw new Error( - `Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens. - Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`, - ); - } - - const stakeAccountRentExemption = await connection.getMinimumBalanceForRentExemption( - StakeProgram.space, - ); - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - let stakeReceiverAccount = null; - if (stakeReceiver) { - stakeReceiverAccount = await getStakeAccount(connection, stakeReceiver); - } - - const withdrawAccounts: WithdrawAccount[] = []; - - if (useReserve) { - withdrawAccounts.push({ - stakeAddress: stakePool.account.data.reserveStake, - voteAddress: undefined, - poolAmount, - }); - } else if (stakeReceiverAccount && stakeReceiverAccount?.type == 'delegated') { - const voteAccount = stakeReceiverAccount.info?.stake?.delegation.voter; - if (!voteAccount) throw new Error(`Invalid stake receiver ${stakeReceiver} delegation`); - const validatorListAccount = await connection.getAccountInfo( - stakePool.account.data.validatorList, - ); - const validatorList = ValidatorListLayout.decode(validatorListAccount?.data) as ValidatorList; - const isValidVoter = validatorList.validators.find((val) => - val.voteAccountAddress.equals(voteAccount), - ); - if (voteAccountAddress && voteAccountAddress !== voteAccount) { - throw new Error(`Provided withdrawal vote account ${voteAccountAddress} does not match delegation on stake receiver account ${voteAccount}, - remove this flag or provide a different stake account delegated to ${voteAccountAddress}`); - } - if (isValidVoter) { - const stakeAccountAddress = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - voteAccount, - stakePoolAddress, - ); - - const stakeAccount = await connection.getAccountInfo(stakeAccountAddress); - if (!stakeAccount) { - throw new Error(`Preferred withdraw valdator's stake account is invalid`); - } - - const availableForWithdrawal = calcLamportsWithdrawAmount( - stakePool.account.data, - new BN(stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption), - ); - - if (availableForWithdrawal.lt(poolAmount)) { - throw new Error( - `Not enough lamports available for withdrawal from ${stakeAccountAddress}, - ${poolAmount} asked, ${availableForWithdrawal} available.`, - ); - } - withdrawAccounts.push({ - stakeAddress: stakeAccountAddress, - voteAddress: voteAccount, - poolAmount, - }); - } else { - throw new Error( - `Provided stake account is delegated to a vote account ${voteAccount} which does not exist in the stake pool`, - ); - } - } else if (voteAccountAddress) { - const stakeAccountAddress = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - voteAccountAddress, - stakePoolAddress, - ); - const stakeAccount = await connection.getAccountInfo(stakeAccountAddress); - if (!stakeAccount) { - throw new Error('Invalid Stake Account'); - } - - const availableLamports = new BN( - stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption, - ); - if (availableLamports.lt(new BN(0))) { - throw new Error('Invalid Stake Account'); - } - const availableForWithdrawal = calcLamportsWithdrawAmount( - stakePool.account.data, - availableLamports, - ); - - if (availableForWithdrawal.lt(poolAmount)) { - // noinspection ExceptionCaughtLocallyJS - throw new Error( - `Not enough lamports available for withdrawal from ${stakeAccountAddress}, - ${poolAmount} asked, ${availableForWithdrawal} available.`, - ); - } - withdrawAccounts.push({ - stakeAddress: stakeAccountAddress, - voteAddress: voteAccountAddress, - poolAmount, - }); - } else { - // Get the list of accounts to withdraw from - withdrawAccounts.push( - ...(await prepareWithdrawAccounts( - connection, - stakePool.account.data, - stakePoolAddress, - poolAmount, - validatorComparator, - poolTokenAccount.equals(stakePool.account.data.managerFeeAccount), - )), - ); - } - - // Construct transaction to withdraw from withdrawAccounts account list - const instructions: TransactionInstruction[] = []; - const userTransferAuthority = Keypair.generate(); - - const signers: Signer[] = [userTransferAuthority]; - - instructions.push( - createApproveInstruction( - poolTokenAccount, - userTransferAuthority.publicKey, - tokenOwner, - poolAmount.toNumber(), - ), - ); - - let totalRentFreeBalances = 0; - - // Max 5 accounts to prevent an error: "Transaction too large" - const maxWithdrawAccounts = 5; - let i = 0; - - // Go through prepared accounts and withdraw/claim them - for (const withdrawAccount of withdrawAccounts) { - if (i > maxWithdrawAccounts) { - break; - } - // Convert pool tokens amount to lamports - const solWithdrawAmount = calcLamportsWithdrawAmount( - stakePool.account.data, - withdrawAccount.poolAmount, - ); - - let infoMsg = `Withdrawing ◎${solWithdrawAmount}, - from stake account ${withdrawAccount.stakeAddress?.toBase58()}`; - - if (withdrawAccount.voteAddress) { - infoMsg = `${infoMsg}, delegated to ${withdrawAccount.voteAddress?.toBase58()}`; - } - - console.info(infoMsg); - let stakeToReceive; - - if (!stakeReceiver || (stakeReceiverAccount && stakeReceiverAccount.type === 'delegated')) { - const stakeKeypair = newStakeAccount(tokenOwner, instructions, stakeAccountRentExemption); - signers.push(stakeKeypair); - totalRentFreeBalances += stakeAccountRentExemption; - stakeToReceive = stakeKeypair.publicKey; - } else { - stakeToReceive = stakeReceiver; - } - - instructions.push( - StakePoolInstruction.withdrawStake({ - stakePool: stakePoolAddress, - validatorList: stakePool.account.data.validatorList, - validatorStake: withdrawAccount.stakeAddress, - destinationStake: stakeToReceive, - destinationStakeAuthority: tokenOwner, - sourceTransferAuthority: userTransferAuthority.publicKey, - sourcePoolAccount: poolTokenAccount, - managerFeeAccount: stakePool.account.data.managerFeeAccount, - poolMint: stakePool.account.data.poolMint, - poolTokens: withdrawAccount.poolAmount.toNumber(), - withdrawAuthority, - }), - ); - i++; - } - if (stakeReceiver && stakeReceiverAccount && stakeReceiverAccount.type === 'delegated') { - signers.forEach((newStakeKeypair) => { - instructions.concat( - StakeProgram.merge({ - stakePubkey: stakeReceiver, - sourceStakePubKey: newStakeKeypair.publicKey, - authorizedPubkey: tokenOwner, - }).instructions, - ); - }); - } - - return { - instructions, - signers, - stakeReceiver, - totalRentFreeBalances, - }; -} - -/** - * Creates instructions required to withdraw SOL directly from a stake pool. - */ -export async function withdrawSol( - connection: Connection, - stakePoolAddress: PublicKey, - tokenOwner: PublicKey, - solReceiver: PublicKey, - amount: number, - solWithdrawAuthority?: PublicKey, -) { - const stakePool = await getStakePoolAccount(connection, stakePoolAddress); - const poolAmount = solToLamports(amount); - - const poolTokenAccount = getAssociatedTokenAddressSync( - stakePool.account.data.poolMint, - tokenOwner, - ); - - const tokenAccount = await getAccount(connection, poolTokenAccount); - - // Check withdrawFrom balance - if (tokenAccount.amount < poolAmount) { - throw new Error( - `Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens. - Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`, - ); - } - - // Construct transaction to withdraw from withdrawAccounts account list - const instructions: TransactionInstruction[] = []; - const userTransferAuthority = Keypair.generate(); - const signers: Signer[] = [userTransferAuthority]; - - instructions.push( - createApproveInstruction( - poolTokenAccount, - userTransferAuthority.publicKey, - tokenOwner, - poolAmount, - ), - ); - - const poolWithdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - if (solWithdrawAuthority) { - const expectedSolWithdrawAuthority = stakePool.account.data.solWithdrawAuthority; - if (!expectedSolWithdrawAuthority) { - throw new Error('SOL withdraw authority specified in arguments but stake pool has none'); - } - if (solWithdrawAuthority.toBase58() != expectedSolWithdrawAuthority.toBase58()) { - throw new Error( - `Invalid deposit withdraw specified, expected ${expectedSolWithdrawAuthority.toBase58()}, received ${solWithdrawAuthority.toBase58()}`, - ); - } - } - - const withdrawTransaction = StakePoolInstruction.withdrawSol({ - stakePool: stakePoolAddress, - withdrawAuthority: poolWithdrawAuthority, - reserveStake: stakePool.account.data.reserveStake, - sourcePoolAccount: poolTokenAccount, - sourceTransferAuthority: userTransferAuthority.publicKey, - destinationSystemAccount: solReceiver, - managerFeeAccount: stakePool.account.data.managerFeeAccount, - poolMint: stakePool.account.data.poolMint, - poolTokens: poolAmount, - solWithdrawAuthority, - }); - - instructions.push(withdrawTransaction); - - return { - instructions, - signers, - }; -} - -export async function addValidatorToPool( - connection: Connection, - stakePoolAddress: PublicKey, - validatorVote: PublicKey, - seed?: number, -) { - const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress); - const stakePool = stakePoolAccount.account.data; - const { reserveStake, staker, validatorList } = stakePool; - - const validatorListAccount = await getValidatorListAccount(connection, validatorList); - - const validatorInfo = validatorListAccount.account.data.validators.find( - (v) => v.voteAccountAddress.toBase58() == validatorVote.toBase58(), - ); - - if (validatorInfo) { - throw new Error('Vote account is already in validator list'); - } - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - const validatorStake = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validatorVote, - stakePoolAddress, - seed, - ); - - const instructions: TransactionInstruction[] = [ - StakePoolInstruction.addValidatorToPool({ - stakePool: stakePoolAddress, - staker: staker, - reserveStake: reserveStake, - withdrawAuthority: withdrawAuthority, - validatorList: validatorList, - validatorStake: validatorStake, - validatorVote: validatorVote, - }), - ]; - - return { - instructions, - }; -} - -export async function removeValidatorFromPool( - connection: Connection, - stakePoolAddress: PublicKey, - validatorVote: PublicKey, - seed?: number, -) { - const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress); - const stakePool = stakePoolAccount.account.data; - const { staker, validatorList } = stakePool; - - const validatorListAccount = await getValidatorListAccount(connection, validatorList); - - const validatorInfo = validatorListAccount.account.data.validators.find( - (v) => v.voteAccountAddress.toBase58() == validatorVote.toBase58(), - ); - - if (!validatorInfo) { - throw new Error('Vote account is not already in validator list'); - } - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - const validatorStake = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validatorVote, - stakePoolAddress, - seed, - ); - - const transientStakeSeed = validatorInfo.transientSeedSuffixStart; - - const transientStake = await findTransientStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validatorInfo.voteAccountAddress, - stakePoolAddress, - transientStakeSeed, - ); - - const instructions: TransactionInstruction[] = [ - StakePoolInstruction.removeValidatorFromPool({ - stakePool: stakePoolAddress, - staker: staker, - withdrawAuthority, - validatorList, - validatorStake, - transientStake, - }), - ]; - - return { - instructions, - }; -} - -/** - * Creates instructions required to increase validator stake. - */ -export async function increaseValidatorStake( - connection: Connection, - stakePoolAddress: PublicKey, - validatorVote: PublicKey, - lamports: number, - ephemeralStakeSeed?: number, -) { - const stakePool = await getStakePoolAccount(connection, stakePoolAddress); - - const validatorList = await getValidatorListAccount( - connection, - stakePool.account.data.validatorList, - ); - - const validatorInfo = validatorList.account.data.validators.find( - (v) => v.voteAccountAddress.toBase58() == validatorVote.toBase58(), - ); - - if (!validatorInfo) { - throw new Error('Vote account not found in validator list'); - } - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - // Bump transient seed suffix by one to avoid reuse when not using the increaseAdditionalStake instruction - const transientStakeSeed = - ephemeralStakeSeed == undefined - ? validatorInfo.transientSeedSuffixStart.addn(1) - : validatorInfo.transientSeedSuffixStart; - - const transientStake = await findTransientStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validatorInfo.voteAccountAddress, - stakePoolAddress, - transientStakeSeed, - ); - - const validatorStake = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validatorInfo.voteAccountAddress, - stakePoolAddress, - ); - - const instructions: TransactionInstruction[] = []; - - if (ephemeralStakeSeed != undefined) { - const ephemeralStake = await findEphemeralStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - new BN(ephemeralStakeSeed), - ); - instructions.push( - StakePoolInstruction.increaseAdditionalValidatorStake({ - stakePool: stakePoolAddress, - staker: stakePool.account.data.staker, - validatorList: stakePool.account.data.validatorList, - reserveStake: stakePool.account.data.reserveStake, - transientStakeSeed: transientStakeSeed.toNumber(), - withdrawAuthority, - transientStake, - validatorStake, - validatorVote, - lamports, - ephemeralStake, - ephemeralStakeSeed, - }), - ); - } else { - instructions.push( - StakePoolInstruction.increaseValidatorStake({ - stakePool: stakePoolAddress, - staker: stakePool.account.data.staker, - validatorList: stakePool.account.data.validatorList, - reserveStake: stakePool.account.data.reserveStake, - transientStakeSeed: transientStakeSeed.toNumber(), - withdrawAuthority, - transientStake, - validatorStake, - validatorVote, - lamports, - }), - ); - } - - return { - instructions, - }; -} - -/** - * Creates instructions required to decrease validator stake. - */ -export async function decreaseValidatorStake( - connection: Connection, - stakePoolAddress: PublicKey, - validatorVote: PublicKey, - lamports: number, - ephemeralStakeSeed?: number, -) { - const stakePool = await getStakePoolAccount(connection, stakePoolAddress); - const validatorList = await getValidatorListAccount( - connection, - stakePool.account.data.validatorList, - ); - - const validatorInfo = validatorList.account.data.validators.find( - (v) => v.voteAccountAddress.toBase58() == validatorVote.toBase58(), - ); - - if (!validatorInfo) { - throw new Error('Vote account not found in validator list'); - } - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - const validatorStake = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validatorInfo.voteAccountAddress, - stakePoolAddress, - ); - - // Bump transient seed suffix by one to avoid reuse when not using the decreaseAdditionalStake instruction - const transientStakeSeed = - ephemeralStakeSeed == undefined - ? validatorInfo.transientSeedSuffixStart.addn(1) - : validatorInfo.transientSeedSuffixStart; - - const transientStake = await findTransientStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validatorInfo.voteAccountAddress, - stakePoolAddress, - transientStakeSeed, - ); - - const instructions: TransactionInstruction[] = []; - - if (ephemeralStakeSeed != undefined) { - const ephemeralStake = await findEphemeralStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - new BN(ephemeralStakeSeed), - ); - instructions.push( - StakePoolInstruction.decreaseAdditionalValidatorStake({ - stakePool: stakePoolAddress, - staker: stakePool.account.data.staker, - validatorList: stakePool.account.data.validatorList, - reserveStake: stakePool.account.data.reserveStake, - transientStakeSeed: transientStakeSeed.toNumber(), - withdrawAuthority, - validatorStake, - transientStake, - lamports, - ephemeralStake, - ephemeralStakeSeed, - }), - ); - } else { - instructions.push( - StakePoolInstruction.decreaseValidatorStakeWithReserve({ - stakePool: stakePoolAddress, - staker: stakePool.account.data.staker, - validatorList: stakePool.account.data.validatorList, - reserveStake: stakePool.account.data.reserveStake, - transientStakeSeed: transientStakeSeed.toNumber(), - withdrawAuthority, - validatorStake, - transientStake, - lamports, - }), - ); - } - - return { - instructions, - }; -} - -/** - * Creates instructions required to completely update a stake pool after epoch change. - */ -export async function updateStakePool( - connection: Connection, - stakePool: StakePoolAccount, - noMerge = false, -) { - const stakePoolAddress = stakePool.pubkey; - - const validatorList = await getValidatorListAccount( - connection, - stakePool.account.data.validatorList, - ); - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - const updateListInstructions: TransactionInstruction[] = []; - const instructions: TransactionInstruction[] = []; - - let startIndex = 0; - const validatorChunks: Array = arrayChunk( - validatorList.account.data.validators, - MAX_VALIDATORS_TO_UPDATE, - ); - - for (const validatorChunk of validatorChunks) { - const validatorAndTransientStakePairs: PublicKey[] = []; - - for (const validator of validatorChunk) { - const validatorStake = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validator.voteAccountAddress, - stakePoolAddress, - ); - validatorAndTransientStakePairs.push(validatorStake); - - const transientStake = await findTransientStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validator.voteAccountAddress, - stakePoolAddress, - validator.transientSeedSuffixStart, - ); - validatorAndTransientStakePairs.push(transientStake); - } - - updateListInstructions.push( - StakePoolInstruction.updateValidatorListBalance({ - stakePool: stakePoolAddress, - validatorList: stakePool.account.data.validatorList, - reserveStake: stakePool.account.data.reserveStake, - validatorAndTransientStakePairs, - withdrawAuthority, - startIndex, - noMerge, - }), - ); - startIndex += MAX_VALIDATORS_TO_UPDATE; - } - - instructions.push( - StakePoolInstruction.updateStakePoolBalance({ - stakePool: stakePoolAddress, - validatorList: stakePool.account.data.validatorList, - reserveStake: stakePool.account.data.reserveStake, - managerFeeAccount: stakePool.account.data.managerFeeAccount, - poolMint: stakePool.account.data.poolMint, - withdrawAuthority, - }), - ); - - instructions.push( - StakePoolInstruction.cleanupRemovedValidatorEntries({ - stakePool: stakePoolAddress, - validatorList: stakePool.account.data.validatorList, - }), - ); - - return { - updateListInstructions, - finalInstructions: instructions, - }; -} - -/** - * Retrieves detailed information about the StakePool. - */ -export async function stakePoolInfo(connection: Connection, stakePoolAddress: PublicKey) { - const stakePool = await getStakePoolAccount(connection, stakePoolAddress); - const reserveAccountStakeAddress = stakePool.account.data.reserveStake; - const totalLamports = stakePool.account.data.totalLamports; - const lastUpdateEpoch = stakePool.account.data.lastUpdateEpoch; - - const validatorList = await getValidatorListAccount( - connection, - stakePool.account.data.validatorList, - ); - - const maxNumberOfValidators = validatorList.account.data.maxValidators; - const currentNumberOfValidators = validatorList.account.data.validators.length; - - const epochInfo = await connection.getEpochInfo(); - const reserveStake = await connection.getAccountInfo(reserveAccountStakeAddress); - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - const minimumReserveStakeBalance = await connection.getMinimumBalanceForRentExemption( - StakeProgram.space, - ); - - const stakeAccounts = await Promise.all( - validatorList.account.data.validators.map(async (validator) => { - const stakeAccountAddress = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validator.voteAccountAddress, - stakePoolAddress, - ); - const transientStakeAccountAddress = await findTransientStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validator.voteAccountAddress, - stakePoolAddress, - validator.transientSeedSuffixStart, - ); - const updateRequired = !validator.lastUpdateEpoch.eqn(epochInfo.epoch); - return { - voteAccountAddress: validator.voteAccountAddress.toBase58(), - stakeAccountAddress: stakeAccountAddress.toBase58(), - validatorActiveStakeLamports: validator.activeStakeLamports.toString(), - validatorLastUpdateEpoch: validator.lastUpdateEpoch.toString(), - validatorLamports: validator.activeStakeLamports - .add(validator.transientStakeLamports) - .toString(), - validatorTransientStakeAccountAddress: transientStakeAccountAddress.toBase58(), - validatorTransientStakeLamports: validator.transientStakeLamports.toString(), - updateRequired, - }; - }), - ); - - const totalPoolTokens = lamportsToSol(stakePool.account.data.poolTokenSupply); - const updateRequired = !lastUpdateEpoch.eqn(epochInfo.epoch); - - return { - address: stakePoolAddress.toBase58(), - poolWithdrawAuthority: withdrawAuthority.toBase58(), - manager: stakePool.account.data.manager.toBase58(), - staker: stakePool.account.data.staker.toBase58(), - stakeDepositAuthority: stakePool.account.data.stakeDepositAuthority.toBase58(), - stakeWithdrawBumpSeed: stakePool.account.data.stakeWithdrawBumpSeed, - maxValidators: maxNumberOfValidators, - validatorList: validatorList.account.data.validators.map((validator) => { - return { - activeStakeLamports: validator.activeStakeLamports.toString(), - transientStakeLamports: validator.transientStakeLamports.toString(), - lastUpdateEpoch: validator.lastUpdateEpoch.toString(), - transientSeedSuffixStart: validator.transientSeedSuffixStart.toString(), - transientSeedSuffixEnd: validator.transientSeedSuffixEnd.toString(), - status: validator.status.toString(), - voteAccountAddress: validator.voteAccountAddress.toString(), - }; - }), // CliStakePoolValidator - validatorListStorageAccount: stakePool.account.data.validatorList.toBase58(), - reserveStake: stakePool.account.data.reserveStake.toBase58(), - poolMint: stakePool.account.data.poolMint.toBase58(), - managerFeeAccount: stakePool.account.data.managerFeeAccount.toBase58(), - tokenProgramId: stakePool.account.data.tokenProgramId.toBase58(), - totalLamports: stakePool.account.data.totalLamports.toString(), - poolTokenSupply: stakePool.account.data.poolTokenSupply.toString(), - lastUpdateEpoch: stakePool.account.data.lastUpdateEpoch.toString(), - lockup: stakePool.account.data.lockup, // pub lockup: CliStakePoolLockup - epochFee: stakePool.account.data.epochFee, - nextEpochFee: stakePool.account.data.nextEpochFee, - preferredDepositValidatorVoteAddress: - stakePool.account.data.preferredDepositValidatorVoteAddress, - preferredWithdrawValidatorVoteAddress: - stakePool.account.data.preferredWithdrawValidatorVoteAddress, - stakeDepositFee: stakePool.account.data.stakeDepositFee, - stakeWithdrawalFee: stakePool.account.data.stakeWithdrawalFee, - // CliStakePool the same - nextStakeWithdrawalFee: stakePool.account.data.nextStakeWithdrawalFee, - stakeReferralFee: stakePool.account.data.stakeReferralFee, - solDepositAuthority: stakePool.account.data.solDepositAuthority?.toBase58(), - solDepositFee: stakePool.account.data.solDepositFee, - solReferralFee: stakePool.account.data.solReferralFee, - solWithdrawAuthority: stakePool.account.data.solWithdrawAuthority?.toBase58(), - solWithdrawalFee: stakePool.account.data.solWithdrawalFee, - nextSolWithdrawalFee: stakePool.account.data.nextSolWithdrawalFee, - lastEpochPoolTokenSupply: stakePool.account.data.lastEpochPoolTokenSupply.toString(), - lastEpochTotalLamports: stakePool.account.data.lastEpochTotalLamports.toString(), - details: { - reserveStakeLamports: reserveStake?.lamports, - reserveAccountStakeAddress: reserveAccountStakeAddress.toBase58(), - minimumReserveStakeBalance, - stakeAccounts, - totalLamports, - totalPoolTokens, - currentNumberOfValidators, - maxNumberOfValidators, - updateRequired, - }, // CliStakePoolDetails - }; -} - -/** - * Creates instructions required to create pool token metadata. - */ -export async function createPoolTokenMetadata( - connection: Connection, - stakePoolAddress: PublicKey, - payer: PublicKey, - name: string, - symbol: string, - uri: string, -) { - const stakePool = await getStakePoolAccount(connection, stakePoolAddress); - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint); - const manager = stakePool.account.data.manager; - - const instructions: TransactionInstruction[] = []; - instructions.push( - StakePoolInstruction.createTokenMetadata({ - stakePool: stakePoolAddress, - poolMint: stakePool.account.data.poolMint, - payer, - manager, - tokenMetadata, - withdrawAuthority, - name, - symbol, - uri, - }), - ); - - return { - instructions, - }; -} - -/** - * Creates instructions required to update pool token metadata. - */ -export async function updatePoolTokenMetadata( - connection: Connection, - stakePoolAddress: PublicKey, - name: string, - symbol: string, - uri: string, -) { - const stakePool = await getStakePoolAccount(connection, stakePoolAddress); - - const withdrawAuthority = await findWithdrawAuthorityProgramAddress( - STAKE_POOL_PROGRAM_ID, - stakePoolAddress, - ); - - const tokenMetadata = findMetadataAddress(stakePool.account.data.poolMint); - - const instructions: TransactionInstruction[] = []; - instructions.push( - StakePoolInstruction.updateTokenMetadata({ - stakePool: stakePoolAddress, - manager: stakePool.account.data.manager, - tokenMetadata, - withdrawAuthority, - name, - symbol, - uri, - }), - ); - - return { - instructions, - }; -} diff --git a/stake-pool/js/src/instructions.ts b/stake-pool/js/src/instructions.ts deleted file mode 100644 index eba2f149538..00000000000 --- a/stake-pool/js/src/instructions.ts +++ /dev/null @@ -1,1095 +0,0 @@ -import { - PublicKey, - STAKE_CONFIG_ID, - SYSVAR_CLOCK_PUBKEY, - SYSVAR_RENT_PUBKEY, - SYSVAR_STAKE_HISTORY_PUBKEY, - StakeProgram, - SystemProgram, - TransactionInstruction, -} from '@solana/web3.js'; -import * as BufferLayout from '@solana/buffer-layout'; -import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; -import { InstructionType, encodeData, decodeData } from './utils'; -import { - METADATA_MAX_NAME_LENGTH, - METADATA_MAX_SYMBOL_LENGTH, - METADATA_MAX_URI_LENGTH, - METADATA_PROGRAM_ID, - STAKE_POOL_PROGRAM_ID, -} from './constants'; - -/** - * An enumeration of valid StakePoolInstructionType's - */ -export type StakePoolInstructionType = - | 'IncreaseValidatorStake' - | 'DecreaseValidatorStake' - | 'UpdateValidatorListBalance' - | 'UpdateStakePoolBalance' - | 'CleanupRemovedValidatorEntries' - | 'DepositStake' - | 'DepositSol' - | 'WithdrawStake' - | 'WithdrawSol' - | 'IncreaseAdditionalValidatorStake' - | 'DecreaseAdditionalValidatorStake' - | 'DecreaseValidatorStakeWithReserve' - | 'Redelegate' - | 'AddValidatorToPool' - | 'RemoveValidatorFromPool'; - -// 'UpdateTokenMetadata' and 'CreateTokenMetadata' have dynamic layouts - -const MOVE_STAKE_LAYOUT = BufferLayout.struct([ - BufferLayout.u8('instruction'), - BufferLayout.ns64('lamports'), - BufferLayout.ns64('transientStakeSeed'), -]); - -const UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT = BufferLayout.struct([ - BufferLayout.u8('instruction'), - BufferLayout.u32('startIndex'), - BufferLayout.u8('noMerge'), -]); - -export function tokenMetadataLayout( - instruction: number, - nameLength: number, - symbolLength: number, - uriLength: number, -) { - if (nameLength > METADATA_MAX_NAME_LENGTH) { - throw 'maximum token name length is 32 characters'; - } - - if (symbolLength > METADATA_MAX_SYMBOL_LENGTH) { - throw 'maximum token symbol length is 10 characters'; - } - - if (uriLength > METADATA_MAX_URI_LENGTH) { - throw 'maximum token uri length is 200 characters'; - } - - return { - index: instruction, - layout: BufferLayout.struct([ - BufferLayout.u8('instruction'), - BufferLayout.u32('nameLen'), - BufferLayout.blob(nameLength, 'name'), - BufferLayout.u32('symbolLen'), - BufferLayout.blob(symbolLength, 'symbol'), - BufferLayout.u32('uriLen'), - BufferLayout.blob(uriLength, 'uri'), - ]), - }; -} - -/** - * An enumeration of valid stake InstructionType's - * @internal - */ -export const STAKE_POOL_INSTRUCTION_LAYOUTS: { - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - [type in StakePoolInstructionType]: InstructionType; -} = Object.freeze({ - AddValidatorToPool: { - index: 1, - layout: BufferLayout.struct([BufferLayout.u8('instruction'), BufferLayout.u32('seed')]), - }, - RemoveValidatorFromPool: { - index: 2, - layout: BufferLayout.struct([BufferLayout.u8('instruction')]), - }, - DecreaseValidatorStake: { - index: 3, - layout: MOVE_STAKE_LAYOUT, - }, - IncreaseValidatorStake: { - index: 4, - layout: MOVE_STAKE_LAYOUT, - }, - UpdateValidatorListBalance: { - index: 6, - layout: UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT, - }, - UpdateStakePoolBalance: { - index: 7, - layout: BufferLayout.struct([BufferLayout.u8('instruction')]), - }, - CleanupRemovedValidatorEntries: { - index: 8, - layout: BufferLayout.struct([BufferLayout.u8('instruction')]), - }, - DepositStake: { - index: 9, - layout: BufferLayout.struct([BufferLayout.u8('instruction')]), - }, - /// Withdraw the token from the pool at the current ratio. - WithdrawStake: { - index: 10, - layout: BufferLayout.struct([ - BufferLayout.u8('instruction'), - BufferLayout.ns64('poolTokens'), - ]), - }, - /// Deposit SOL directly into the pool's reserve account. The output is a "pool" token - /// representing ownership into the pool. Inputs are converted to the current ratio. - DepositSol: { - index: 14, - layout: BufferLayout.struct([ - BufferLayout.u8('instruction'), - BufferLayout.ns64('lamports'), - ]), - }, - /// Withdraw SOL directly from the pool's reserve account. Fails if the - /// reserve does not have enough SOL. - WithdrawSol: { - index: 16, - layout: BufferLayout.struct([ - BufferLayout.u8('instruction'), - BufferLayout.ns64('poolTokens'), - ]), - }, - IncreaseAdditionalValidatorStake: { - index: 19, - layout: BufferLayout.struct([ - BufferLayout.u8('instruction'), - BufferLayout.ns64('lamports'), - BufferLayout.ns64('transientStakeSeed'), - BufferLayout.ns64('ephemeralStakeSeed'), - ]), - }, - DecreaseAdditionalValidatorStake: { - index: 20, - layout: BufferLayout.struct([ - BufferLayout.u8('instruction'), - BufferLayout.ns64('lamports'), - BufferLayout.ns64('transientStakeSeed'), - BufferLayout.ns64('ephemeralStakeSeed'), - ]), - }, - DecreaseValidatorStakeWithReserve: { - index: 21, - layout: MOVE_STAKE_LAYOUT, - }, - Redelegate: { - index: 22, - layout: BufferLayout.struct([BufferLayout.u8('instruction')]), - }, -}); - -/** - * Cleans up validator stake account entries marked as `ReadyForRemoval` - */ -export type CleanupRemovedValidatorEntriesParams = { - stakePool: PublicKey; - validatorList: PublicKey; -}; - -/** - * Updates balances of validator and transient stake accounts in the pool. - */ -export type UpdateValidatorListBalanceParams = { - stakePool: PublicKey; - withdrawAuthority: PublicKey; - validatorList: PublicKey; - reserveStake: PublicKey; - validatorAndTransientStakePairs: PublicKey[]; - startIndex: number; - noMerge: boolean; -}; - -/** - * Updates total pool balance based on balances in the reserve and validator list. - */ -export type UpdateStakePoolBalanceParams = { - stakePool: PublicKey; - withdrawAuthority: PublicKey; - validatorList: PublicKey; - reserveStake: PublicKey; - managerFeeAccount: PublicKey; - poolMint: PublicKey; -}; - -/** - * (Staker only) Decrease active stake on a validator, eventually moving it to the reserve - */ -export type DecreaseValidatorStakeParams = { - stakePool: PublicKey; - staker: PublicKey; - withdrawAuthority: PublicKey; - validatorList: PublicKey; - validatorStake: PublicKey; - transientStake: PublicKey; - // Amount of lamports to split into the transient stake account - lamports: number; - // Seed to used to create the transient stake account - transientStakeSeed: number; -}; - -export interface DecreaseValidatorStakeWithReserveParams extends DecreaseValidatorStakeParams { - reserveStake: PublicKey; -} - -export interface DecreaseAdditionalValidatorStakeParams extends DecreaseValidatorStakeParams { - reserveStake: PublicKey; - ephemeralStake: PublicKey; - ephemeralStakeSeed: number; -} - -/** - * (Staker only) Increase stake on a validator from the reserve account. - */ -export type IncreaseValidatorStakeParams = { - stakePool: PublicKey; - staker: PublicKey; - withdrawAuthority: PublicKey; - validatorList: PublicKey; - reserveStake: PublicKey; - transientStake: PublicKey; - validatorStake: PublicKey; - validatorVote: PublicKey; - // Amount of lamports to split into the transient stake account - lamports: number; - // Seed to used to create the transient stake account - transientStakeSeed: number; -}; - -export interface IncreaseAdditionalValidatorStakeParams extends IncreaseValidatorStakeParams { - ephemeralStake: PublicKey; - ephemeralStakeSeed: number; -} - -/** - * Deposits a stake account into the pool in exchange for pool tokens - */ -export type DepositStakeParams = { - stakePool: PublicKey; - validatorList: PublicKey; - depositAuthority: PublicKey; - withdrawAuthority: PublicKey; - depositStake: PublicKey; - validatorStake: PublicKey; - reserveStake: PublicKey; - destinationPoolAccount: PublicKey; - managerFeeAccount: PublicKey; - referralPoolAccount: PublicKey; - poolMint: PublicKey; -}; - -/** - * Withdraws a stake account from the pool in exchange for pool tokens - */ -export type WithdrawStakeParams = { - stakePool: PublicKey; - validatorList: PublicKey; - withdrawAuthority: PublicKey; - validatorStake: PublicKey; - destinationStake: PublicKey; - destinationStakeAuthority: PublicKey; - sourceTransferAuthority: PublicKey; - sourcePoolAccount: PublicKey; - managerFeeAccount: PublicKey; - poolMint: PublicKey; - poolTokens: number; -}; - -/** - * Withdraw sol instruction params - */ -export type WithdrawSolParams = { - stakePool: PublicKey; - sourcePoolAccount: PublicKey; - withdrawAuthority: PublicKey; - reserveStake: PublicKey; - destinationSystemAccount: PublicKey; - sourceTransferAuthority: PublicKey; - solWithdrawAuthority?: PublicKey | undefined; - managerFeeAccount: PublicKey; - poolMint: PublicKey; - poolTokens: number; -}; - -/** - * Deposit SOL directly into the pool's reserve account. The output is a "pool" token - * representing ownership into the pool. Inputs are converted to the current ratio. - */ -export type DepositSolParams = { - stakePool: PublicKey; - depositAuthority?: PublicKey | undefined; - withdrawAuthority: PublicKey; - reserveStake: PublicKey; - fundingAccount: PublicKey; - destinationPoolAccount: PublicKey; - managerFeeAccount: PublicKey; - referralPoolAccount: PublicKey; - poolMint: PublicKey; - lamports: number; -}; - -export type CreateTokenMetadataParams = { - stakePool: PublicKey; - manager: PublicKey; - tokenMetadata: PublicKey; - withdrawAuthority: PublicKey; - poolMint: PublicKey; - payer: PublicKey; - name: string; - symbol: string; - uri: string; -}; - -export type UpdateTokenMetadataParams = { - stakePool: PublicKey; - manager: PublicKey; - tokenMetadata: PublicKey; - withdrawAuthority: PublicKey; - name: string; - symbol: string; - uri: string; -}; - -export type AddValidatorToPoolParams = { - stakePool: PublicKey; - staker: PublicKey; - reserveStake: PublicKey; - withdrawAuthority: PublicKey; - validatorList: PublicKey; - validatorStake: PublicKey; - validatorVote: PublicKey; - seed?: number; -}; - -export type RemoveValidatorFromPoolParams = { - stakePool: PublicKey; - staker: PublicKey; - withdrawAuthority: PublicKey; - validatorList: PublicKey; - validatorStake: PublicKey; - transientStake: PublicKey; -}; - -/** - * Stake Pool Instruction class - */ -export class StakePoolInstruction { - /** - * Creates instruction to add a validator into the stake pool. - */ - static addValidatorToPool(params: AddValidatorToPoolParams): TransactionInstruction { - const { - stakePool, - staker, - reserveStake, - withdrawAuthority, - validatorList, - validatorStake, - validatorVote, - seed, - } = params; - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.AddValidatorToPool; - const data = encodeData(type, { seed: seed == undefined ? 0 : seed }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: true }, - { pubkey: staker, isSigner: true, isWritable: false }, - { pubkey: reserveStake, isSigner: false, isWritable: true }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: validatorStake, isSigner: false, isWritable: true }, - { pubkey: validatorVote, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: STAKE_CONFIG_ID, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates instruction to remove a validator from the stake pool. - */ - static removeValidatorFromPool(params: RemoveValidatorFromPoolParams): TransactionInstruction { - const { stakePool, staker, withdrawAuthority, validatorList, validatorStake, transientStake } = - params; - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.RemoveValidatorFromPool; - const data = encodeData(type); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: true }, - { pubkey: staker, isSigner: true, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: validatorStake, isSigner: false, isWritable: true }, - { pubkey: transientStake, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates instruction to update a set of validators in the stake pool. - */ - static updateValidatorListBalance( - params: UpdateValidatorListBalanceParams, - ): TransactionInstruction { - const { - stakePool, - withdrawAuthority, - validatorList, - reserveStake, - startIndex, - noMerge, - validatorAndTransientStakePairs, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.UpdateValidatorListBalance; - const data = encodeData(type, { startIndex, noMerge: noMerge ? 1 : 0 }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: reserveStake, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ...validatorAndTransientStakePairs.map((pubkey) => ({ - pubkey, - isSigner: false, - isWritable: true, - })), - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates instruction to update the overall stake pool balance. - */ - static updateStakePoolBalance(params: UpdateStakePoolBalanceParams): TransactionInstruction { - const { - stakePool, - withdrawAuthority, - validatorList, - reserveStake, - managerFeeAccount, - poolMint, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.UpdateStakePoolBalance; - const data = encodeData(type); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: true }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: reserveStake, isSigner: false, isWritable: false }, - { pubkey: managerFeeAccount, isSigner: false, isWritable: true }, - { pubkey: poolMint, isSigner: false, isWritable: true }, - { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates instruction to cleanup removed validator entries. - */ - static cleanupRemovedValidatorEntries( - params: CleanupRemovedValidatorEntriesParams, - ): TransactionInstruction { - const { stakePool, validatorList } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.CleanupRemovedValidatorEntries; - const data = encodeData(type); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates `IncreaseValidatorStake` instruction (rebalance from reserve account to - * transient account) - */ - static increaseValidatorStake(params: IncreaseValidatorStakeParams): TransactionInstruction { - const { - stakePool, - staker, - withdrawAuthority, - validatorList, - reserveStake, - transientStake, - validatorStake, - validatorVote, - lamports, - transientStakeSeed, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.IncreaseValidatorStake; - const data = encodeData(type, { lamports, transientStakeSeed }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: false }, - { pubkey: staker, isSigner: true, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: reserveStake, isSigner: false, isWritable: true }, - { pubkey: transientStake, isSigner: false, isWritable: true }, - { pubkey: validatorStake, isSigner: false, isWritable: false }, - { pubkey: validatorVote, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: STAKE_CONFIG_ID, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates `IncreaseAdditionalValidatorStake` instruction (rebalance from reserve account to - * transient account) - */ - static increaseAdditionalValidatorStake( - params: IncreaseAdditionalValidatorStakeParams, - ): TransactionInstruction { - const { - stakePool, - staker, - withdrawAuthority, - validatorList, - reserveStake, - transientStake, - validatorStake, - validatorVote, - lamports, - transientStakeSeed, - ephemeralStake, - ephemeralStakeSeed, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.IncreaseAdditionalValidatorStake; - const data = encodeData(type, { lamports, transientStakeSeed, ephemeralStakeSeed }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: false }, - { pubkey: staker, isSigner: true, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: reserveStake, isSigner: false, isWritable: true }, - { pubkey: ephemeralStake, isSigner: false, isWritable: true }, - { pubkey: transientStake, isSigner: false, isWritable: true }, - { pubkey: validatorStake, isSigner: false, isWritable: false }, - { pubkey: validatorVote, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: STAKE_CONFIG_ID, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates `DecreaseValidatorStake` instruction (rebalance from validator account to - * transient account) - */ - static decreaseValidatorStake(params: DecreaseValidatorStakeParams): TransactionInstruction { - const { - stakePool, - staker, - withdrawAuthority, - validatorList, - validatorStake, - transientStake, - lamports, - transientStakeSeed, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseValidatorStake; - const data = encodeData(type, { lamports, transientStakeSeed }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: false }, - { pubkey: staker, isSigner: true, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: validatorStake, isSigner: false, isWritable: true }, - { pubkey: transientStake, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates `DecreaseValidatorStakeWithReserve` instruction (rebalance from - * validator account to transient account) - */ - static decreaseValidatorStakeWithReserve( - params: DecreaseValidatorStakeWithReserveParams, - ): TransactionInstruction { - const { - stakePool, - staker, - withdrawAuthority, - validatorList, - reserveStake, - validatorStake, - transientStake, - lamports, - transientStakeSeed, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseValidatorStakeWithReserve; - const data = encodeData(type, { lamports, transientStakeSeed }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: false }, - { pubkey: staker, isSigner: true, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: reserveStake, isSigner: false, isWritable: true }, - { pubkey: validatorStake, isSigner: false, isWritable: true }, - { pubkey: transientStake, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates `DecreaseAdditionalValidatorStake` instruction (rebalance from - * validator account to transient account) - */ - static decreaseAdditionalValidatorStake( - params: DecreaseAdditionalValidatorStakeParams, - ): TransactionInstruction { - const { - stakePool, - staker, - withdrawAuthority, - validatorList, - reserveStake, - validatorStake, - transientStake, - lamports, - transientStakeSeed, - ephemeralStakeSeed, - ephemeralStake, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseAdditionalValidatorStake; - const data = encodeData(type, { lamports, transientStakeSeed, ephemeralStakeSeed }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: false }, - { pubkey: staker, isSigner: true, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: reserveStake, isSigner: false, isWritable: true }, - { pubkey: validatorStake, isSigner: false, isWritable: true }, - { pubkey: ephemeralStake, isSigner: false, isWritable: true }, - { pubkey: transientStake, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates a transaction instruction to deposit a stake account into a stake pool. - */ - static depositStake(params: DepositStakeParams): TransactionInstruction { - const { - stakePool, - validatorList, - depositAuthority, - withdrawAuthority, - depositStake, - validatorStake, - reserveStake, - destinationPoolAccount, - managerFeeAccount, - referralPoolAccount, - poolMint, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DepositStake; - const data = encodeData(type); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: true }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: depositAuthority, isSigner: false, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: depositStake, isSigner: false, isWritable: true }, - { pubkey: validatorStake, isSigner: false, isWritable: true }, - { pubkey: reserveStake, isSigner: false, isWritable: true }, - { pubkey: destinationPoolAccount, isSigner: false, isWritable: true }, - { pubkey: managerFeeAccount, isSigner: false, isWritable: true }, - { pubkey: referralPoolAccount, isSigner: false, isWritable: true }, - { pubkey: poolMint, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates a transaction instruction to deposit SOL into a stake pool. - */ - static depositSol(params: DepositSolParams): TransactionInstruction { - const { - stakePool, - withdrawAuthority, - depositAuthority, - reserveStake, - fundingAccount, - destinationPoolAccount, - managerFeeAccount, - referralPoolAccount, - poolMint, - lamports, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol; - const data = encodeData(type, { lamports }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: true }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: reserveStake, isSigner: false, isWritable: true }, - { pubkey: fundingAccount, isSigner: true, isWritable: true }, - { pubkey: destinationPoolAccount, isSigner: false, isWritable: true }, - { pubkey: managerFeeAccount, isSigner: false, isWritable: true }, - { pubkey: referralPoolAccount, isSigner: false, isWritable: true }, - { pubkey: poolMint, isSigner: false, isWritable: true }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, - ]; - - if (depositAuthority) { - keys.push({ - pubkey: depositAuthority, - isSigner: true, - isWritable: false, - }); - } - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates a transaction instruction to withdraw active stake from a stake pool. - */ - static withdrawStake(params: WithdrawStakeParams): TransactionInstruction { - const { - stakePool, - validatorList, - withdrawAuthority, - validatorStake, - destinationStake, - destinationStakeAuthority, - sourceTransferAuthority, - sourcePoolAccount, - managerFeeAccount, - poolMint, - poolTokens, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawStake; - const data = encodeData(type, { poolTokens }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: true }, - { pubkey: validatorList, isSigner: false, isWritable: true }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: validatorStake, isSigner: false, isWritable: true }, - { pubkey: destinationStake, isSigner: false, isWritable: true }, - { pubkey: destinationStakeAuthority, isSigner: false, isWritable: false }, - { pubkey: sourceTransferAuthority, isSigner: true, isWritable: false }, - { pubkey: sourcePoolAccount, isSigner: false, isWritable: true }, - { pubkey: managerFeeAccount, isSigner: false, isWritable: true }, - { pubkey: poolMint, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates a transaction instruction to withdraw SOL from a stake pool. - */ - static withdrawSol(params: WithdrawSolParams): TransactionInstruction { - const { - stakePool, - withdrawAuthority, - sourceTransferAuthority, - sourcePoolAccount, - reserveStake, - destinationSystemAccount, - managerFeeAccount, - solWithdrawAuthority, - poolMint, - poolTokens, - } = params; - - const type = STAKE_POOL_INSTRUCTION_LAYOUTS.WithdrawSol; - const data = encodeData(type, { poolTokens }); - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: true }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: sourceTransferAuthority, isSigner: true, isWritable: false }, - { pubkey: sourcePoolAccount, isSigner: false, isWritable: true }, - { pubkey: reserveStake, isSigner: false, isWritable: true }, - { pubkey: destinationSystemAccount, isSigner: false, isWritable: true }, - { pubkey: managerFeeAccount, isSigner: false, isWritable: true }, - { pubkey: poolMint, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false }, - { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, - { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, - ]; - - if (solWithdrawAuthority) { - keys.push({ - pubkey: solWithdrawAuthority, - isSigner: true, - isWritable: false, - }); - } - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates an instruction to create metadata - * using the mpl token metadata program for the pool token - */ - static createTokenMetadata(params: CreateTokenMetadataParams): TransactionInstruction { - const { - stakePool, - withdrawAuthority, - tokenMetadata, - manager, - payer, - poolMint, - name, - symbol, - uri, - } = params; - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: false }, - { pubkey: manager, isSigner: true, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: poolMint, isSigner: false, isWritable: false }, - { pubkey: payer, isSigner: true, isWritable: true }, - { pubkey: tokenMetadata, isSigner: false, isWritable: true }, - { pubkey: METADATA_PROGRAM_ID, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - ]; - - const type = tokenMetadataLayout(17, name.length, symbol.length, uri.length); - const data = encodeData(type, { - nameLen: name.length, - name: Buffer.from(name), - symbolLen: symbol.length, - symbol: Buffer.from(symbol), - uriLen: uri.length, - uri: Buffer.from(uri), - }); - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Creates an instruction to update metadata - * in the mpl token metadata program account for the pool token - */ - static updateTokenMetadata(params: UpdateTokenMetadataParams): TransactionInstruction { - const { stakePool, withdrawAuthority, tokenMetadata, manager, name, symbol, uri } = params; - - const keys = [ - { pubkey: stakePool, isSigner: false, isWritable: false }, - { pubkey: manager, isSigner: true, isWritable: false }, - { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, - { pubkey: tokenMetadata, isSigner: false, isWritable: true }, - { pubkey: METADATA_PROGRAM_ID, isSigner: false, isWritable: false }, - ]; - - const type = tokenMetadataLayout(18, name.length, symbol.length, uri.length); - const data = encodeData(type, { - nameLen: name.length, - name: Buffer.from(name), - symbolLen: symbol.length, - symbol: Buffer.from(symbol), - uriLen: uri.length, - uri: Buffer.from(uri), - }); - - return new TransactionInstruction({ - programId: STAKE_POOL_PROGRAM_ID, - keys, - data, - }); - } - - /** - * Decode a deposit stake pool instruction and retrieve the instruction params. - */ - static decodeDepositStake(instruction: TransactionInstruction): DepositStakeParams { - this.checkProgramId(instruction.programId); - this.checkKeyLength(instruction.keys, 11); - - decodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositStake, instruction.data); - - return { - stakePool: instruction.keys[0].pubkey, - validatorList: instruction.keys[1].pubkey, - depositAuthority: instruction.keys[2].pubkey, - withdrawAuthority: instruction.keys[3].pubkey, - depositStake: instruction.keys[4].pubkey, - validatorStake: instruction.keys[5].pubkey, - reserveStake: instruction.keys[6].pubkey, - destinationPoolAccount: instruction.keys[7].pubkey, - managerFeeAccount: instruction.keys[8].pubkey, - referralPoolAccount: instruction.keys[9].pubkey, - poolMint: instruction.keys[10].pubkey, - }; - } - - /** - * Decode a deposit sol instruction and retrieve the instruction params. - */ - static decodeDepositSol(instruction: TransactionInstruction): DepositSolParams { - this.checkProgramId(instruction.programId); - this.checkKeyLength(instruction.keys, 9); - - const { amount } = decodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol, instruction.data); - - return { - stakePool: instruction.keys[0].pubkey, - depositAuthority: instruction.keys[1].pubkey, - withdrawAuthority: instruction.keys[2].pubkey, - reserveStake: instruction.keys[3].pubkey, - fundingAccount: instruction.keys[4].pubkey, - destinationPoolAccount: instruction.keys[5].pubkey, - managerFeeAccount: instruction.keys[6].pubkey, - referralPoolAccount: instruction.keys[7].pubkey, - poolMint: instruction.keys[8].pubkey, - lamports: amount, - }; - } - - /** - * @internal - */ - private static checkProgramId(programId: PublicKey) { - if (!programId.equals(StakeProgram.programId)) { - throw new Error('Invalid instruction; programId is not StakeProgram'); - } - } - - /** - * @internal - */ - private static checkKeyLength(keys: Array, expectedLength: number) { - if (keys.length < expectedLength) { - throw new Error( - `Invalid instruction; found ${keys.length} keys, expected at least ${expectedLength}`, - ); - } - } -} diff --git a/stake-pool/js/src/layouts.ts b/stake-pool/js/src/layouts.ts deleted file mode 100644 index bc6c3cd3138..00000000000 --- a/stake-pool/js/src/layouts.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { Layout, publicKey, u64, option, vec } from './codecs'; -import { struct, Layout as LayoutCls, u8, u32 } from 'buffer-layout'; -import { PublicKey } from '@solana/web3.js'; -import BN from 'bn.js'; -import { - Infer, - number, - nullable, - enums, - type, - coerce, - instance, - string, - optional, -} from 'superstruct'; - -export interface Fee { - denominator: BN; - numerator: BN; -} - -const feeFields = [u64('denominator'), u64('numerator')]; - -export enum AccountType { - Uninitialized, - StakePool, - ValidatorList, -} - -export const BigNumFromString = coerce(instance(BN), string(), (value) => { - if (typeof value === 'string') return new BN(value, 10); - throw new Error('invalid big num'); -}); - -export const PublicKeyFromString = coerce( - instance(PublicKey), - string(), - (value) => new PublicKey(value), -); - -export class FutureEpochLayout extends LayoutCls { - layout: Layout; - discriminator: Layout; - - constructor(layout: Layout, property?: string) { - super(-1, property); - this.layout = layout; - this.discriminator = u8(); - } - - encode(src: T | null, b: Buffer, offset = 0): number { - if (src === null || src === undefined) { - return this.discriminator.encode(0, b, offset); - } - // This isn't right, but we don't typically encode outside of tests - this.discriminator.encode(2, b, offset); - return this.layout.encode(src, b, offset + 1) + 1; - } - - decode(b: Buffer, offset = 0): T | null { - const discriminator = this.discriminator.decode(b, offset); - if (discriminator === 0) { - return null; - } else if (discriminator === 1 || discriminator === 2) { - return this.layout.decode(b, offset + 1); - } - throw new Error('Invalid future epoch ' + this.property); - } - - getSpan(b: Buffer, offset = 0): number { - const discriminator = this.discriminator.decode(b, offset); - if (discriminator === 0) { - return 1; - } else if (discriminator === 1 || discriminator === 2) { - return this.layout.getSpan(b, offset + 1) + 1; - } - throw new Error('Invalid future epoch ' + this.property); - } -} - -export function futureEpoch(layout: Layout, property?: string): LayoutCls { - return new FutureEpochLayout(layout, property); -} - -export type StakeAccountType = Infer; -export const StakeAccountType = enums(['uninitialized', 'initialized', 'delegated', 'rewardsPool']); - -export type StakeMeta = Infer; -export const StakeMeta = type({ - rentExemptReserve: BigNumFromString, - authorized: type({ - staker: PublicKeyFromString, - withdrawer: PublicKeyFromString, - }), - lockup: type({ - unixTimestamp: number(), - epoch: number(), - custodian: PublicKeyFromString, - }), -}); - -export type StakeAccountInfo = Infer; -export const StakeAccountInfo = type({ - meta: StakeMeta, - stake: nullable( - type({ - delegation: type({ - voter: PublicKeyFromString, - stake: BigNumFromString, - activationEpoch: BigNumFromString, - deactivationEpoch: BigNumFromString, - warmupCooldownRate: number(), - }), - creditsObserved: number(), - }), - ), -}); - -export type StakeAccount = Infer; -export const StakeAccount = type({ - type: StakeAccountType, - info: optional(StakeAccountInfo), -}); -export interface Lockup { - unixTimestamp: BN; - epoch: BN; - custodian: PublicKey; -} - -export interface StakePool { - accountType: AccountType; - manager: PublicKey; - staker: PublicKey; - stakeDepositAuthority: PublicKey; - stakeWithdrawBumpSeed: number; - validatorList: PublicKey; - reserveStake: PublicKey; - poolMint: PublicKey; - managerFeeAccount: PublicKey; - tokenProgramId: PublicKey; - totalLamports: BN; - poolTokenSupply: BN; - lastUpdateEpoch: BN; - lockup: Lockup; - epochFee: Fee; - nextEpochFee?: Fee | undefined; - preferredDepositValidatorVoteAddress?: PublicKey | undefined; - preferredWithdrawValidatorVoteAddress?: PublicKey | undefined; - stakeDepositFee: Fee; - stakeWithdrawalFee: Fee; - nextStakeWithdrawalFee?: Fee | undefined; - stakeReferralFee: number; - solDepositAuthority?: PublicKey | undefined; - solDepositFee: Fee; - solReferralFee: number; - solWithdrawAuthority?: PublicKey | undefined; - solWithdrawalFee: Fee; - nextSolWithdrawalFee?: Fee | undefined; - lastEpochPoolTokenSupply: BN; - lastEpochTotalLamports: BN; -} - -export const StakePoolLayout = struct([ - u8('accountType'), - publicKey('manager'), - publicKey('staker'), - publicKey('stakeDepositAuthority'), - u8('stakeWithdrawBumpSeed'), - publicKey('validatorList'), - publicKey('reserveStake'), - publicKey('poolMint'), - publicKey('managerFeeAccount'), - publicKey('tokenProgramId'), - u64('totalLamports'), - u64('poolTokenSupply'), - u64('lastUpdateEpoch'), - struct([u64('unixTimestamp'), u64('epoch'), publicKey('custodian')], 'lockup'), - struct(feeFields, 'epochFee'), - futureEpoch(struct(feeFields), 'nextEpochFee'), - option(publicKey(), 'preferredDepositValidatorVoteAddress'), - option(publicKey(), 'preferredWithdrawValidatorVoteAddress'), - struct(feeFields, 'stakeDepositFee'), - struct(feeFields, 'stakeWithdrawalFee'), - futureEpoch(struct(feeFields), 'nextStakeWithdrawalFee'), - u8('stakeReferralFee'), - option(publicKey(), 'solDepositAuthority'), - struct(feeFields, 'solDepositFee'), - u8('solReferralFee'), - option(publicKey(), 'solWithdrawAuthority'), - struct(feeFields, 'solWithdrawalFee'), - futureEpoch(struct(feeFields), 'nextSolWithdrawalFee'), - u64('lastEpochPoolTokenSupply'), - u64('lastEpochTotalLamports'), -]); - -export enum ValidatorStakeInfoStatus { - Active, - DeactivatingTransient, - ReadyForRemoval, -} - -export interface ValidatorStakeInfo { - status: ValidatorStakeInfoStatus; - voteAccountAddress: PublicKey; - activeStakeLamports: BN; - transientStakeLamports: BN; - transientSeedSuffixStart: BN; - transientSeedSuffixEnd: BN; - lastUpdateEpoch: BN; -} - -export const ValidatorStakeInfoLayout = struct([ - /// Amount of active stake delegated to this validator - /// Note that if `last_update_epoch` does not match the current epoch then - /// this field may not be accurate - u64('activeStakeLamports'), - /// Amount of transient stake delegated to this validator - /// Note that if `last_update_epoch` does not match the current epoch then - /// this field may not be accurate - u64('transientStakeLamports'), - /// Last epoch the active and transient stake lamports fields were updated - u64('lastUpdateEpoch'), - /// Start of the validator transient account seed suffixes - u64('transientSeedSuffixStart'), - /// End of the validator transient account seed suffixes - u64('transientSeedSuffixEnd'), - /// Status of the validator stake account - u8('status'), - /// Validator vote account address - publicKey('voteAccountAddress'), -]); - -export interface ValidatorList { - /// Account type, must be ValidatorList currently - accountType: number; - /// Maximum allowable number of validators - maxValidators: number; - /// List of stake info for each validator in the pool - validators: ValidatorStakeInfo[]; -} - -export const ValidatorListLayout = struct([ - u8('accountType'), - u32('maxValidators'), - vec(ValidatorStakeInfoLayout, 'validators'), -]); diff --git a/stake-pool/js/src/types/buffer-layout.d.ts b/stake-pool/js/src/types/buffer-layout.d.ts deleted file mode 100644 index 5ef8ba4c87e..00000000000 --- a/stake-pool/js/src/types/buffer-layout.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -declare module 'buffer-layout' { - export class Layout { - span: number; - property?: string; - constructor(span: number, property?: string); - decode(b: Buffer | undefined, offset?: number): T; - encode(src: T, b: Buffer, offset?: number): number; - getSpan(b: Buffer, offset?: number): number; - replicate(name: string): this; - } - export function struct( - fields: Layout[], - property?: string, - decodePrefixes?: boolean, - ): Layout; - export function seq( - elementLayout: Layout, - count: number | Layout, - property?: string, - ): Layout; - export function offset(layout: Layout, offset?: number, property?: string): Layout; - export function blob(length: number | Layout, property?: string): Layout; - export function s32(property?: string): Layout; - export function u32(property?: string): Layout; - export function s16(property?: string): Layout; - export function u16(property?: string): Layout; - export function s8(property?: string): Layout; - export function u8(property?: string): Layout; -} diff --git a/stake-pool/js/src/utils/index.ts b/stake-pool/js/src/utils/index.ts deleted file mode 100644 index e93c2cded11..00000000000 --- a/stake-pool/js/src/utils/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export * from './math'; -export * from './program-address'; -export * from './stake'; -export * from './instruction'; - -export function arrayChunk(array: any[], size: number): any[] { - const result = []; - for (let i = 0; i < array.length; i += size) { - result.push(array.slice(i, i + size)); - } - return result; -} diff --git a/stake-pool/js/src/utils/instruction.ts b/stake-pool/js/src/utils/instruction.ts deleted file mode 100644 index 24ccad8ab00..00000000000 --- a/stake-pool/js/src/utils/instruction.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as BufferLayout from '@solana/buffer-layout'; -import { Buffer } from 'buffer'; - -/** - * @internal - */ -export type InstructionType = { - /** The Instruction index (from solana upstream program) */ - index: number; - /** The BufferLayout to use to build data */ - layout: BufferLayout.Layout; -}; - -/** - * Populate a buffer of instruction data using an InstructionType - * @internal - */ -export function encodeData(type: InstructionType, fields?: any): Buffer { - const allocLength = type.layout.span; - const data = Buffer.alloc(allocLength); - const layoutFields = Object.assign({ instruction: type.index }, fields); - type.layout.encode(layoutFields, data); - - return data; -} - -/** - * Decode instruction data buffer using an InstructionType - * @internal - */ -export function decodeData(type: InstructionType, buffer: Buffer): any { - let data; - try { - data = type.layout.decode(buffer); - } catch (err) { - throw new Error('invalid instruction; ' + err); - } - - if (data.instruction !== type.index) { - throw new Error( - `invalid instruction; instruction index mismatch ${data.instruction} != ${type.index}`, - ); - } - - return data; -} diff --git a/stake-pool/js/src/utils/math.ts b/stake-pool/js/src/utils/math.ts deleted file mode 100644 index 5f939bc8611..00000000000 --- a/stake-pool/js/src/utils/math.ts +++ /dev/null @@ -1,27 +0,0 @@ -import BN from 'bn.js'; -import { LAMPORTS_PER_SOL } from '@solana/web3.js'; - -export function solToLamports(amount: number): number { - if (isNaN(amount)) return Number(0); - return Number(amount * LAMPORTS_PER_SOL); -} - -export function lamportsToSol(lamports: number | BN | bigint): number { - if (typeof lamports === 'number') { - return Math.abs(lamports) / LAMPORTS_PER_SOL; - } - if (typeof lamports === 'bigint') { - return Math.abs(Number(lamports)) / LAMPORTS_PER_SOL; - } - - let signMultiplier = 1; - if (lamports.isNeg()) { - signMultiplier = -1; - } - - const absLamports = lamports.abs(); - const lamportsString = absLamports.toString(10).padStart(10, '0'); - const splitIndex = lamportsString.length - 9; - const solString = lamportsString.slice(0, splitIndex) + '.' + lamportsString.slice(splitIndex); - return signMultiplier * parseFloat(solString); -} diff --git a/stake-pool/js/src/utils/program-address.ts b/stake-pool/js/src/utils/program-address.ts deleted file mode 100644 index 827f402113d..00000000000 --- a/stake-pool/js/src/utils/program-address.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import BN from 'bn.js'; -import { Buffer } from 'buffer'; -import { - METADATA_PROGRAM_ID, - EPHEMERAL_STAKE_SEED_PREFIX, - TRANSIENT_STAKE_SEED_PREFIX, -} from '../constants'; - -/** - * Generates the withdraw authority program address for the stake pool - */ -export async function findWithdrawAuthorityProgramAddress( - programId: PublicKey, - stakePoolAddress: PublicKey, -) { - const [publicKey] = await PublicKey.findProgramAddress( - [stakePoolAddress.toBuffer(), Buffer.from('withdraw')], - programId, - ); - return publicKey; -} - -/** - * Generates the stake program address for a validator's vote account - */ -export async function findStakeProgramAddress( - programId: PublicKey, - voteAccountAddress: PublicKey, - stakePoolAddress: PublicKey, - seed?: number, -) { - const [publicKey] = await PublicKey.findProgramAddress( - [ - voteAccountAddress.toBuffer(), - stakePoolAddress.toBuffer(), - seed ? new BN(seed).toArrayLike(Buffer, 'le', 4) : Buffer.alloc(0), - ], - programId, - ); - return publicKey; -} - -/** - * Generates the stake program address for a validator's vote account - */ -export async function findTransientStakeProgramAddress( - programId: PublicKey, - voteAccountAddress: PublicKey, - stakePoolAddress: PublicKey, - seed: BN, -) { - const [publicKey] = await PublicKey.findProgramAddress( - [ - TRANSIENT_STAKE_SEED_PREFIX, - voteAccountAddress.toBuffer(), - stakePoolAddress.toBuffer(), - seed.toArrayLike(Buffer, 'le', 8), - ], - programId, - ); - return publicKey; -} - -/** - * Generates the ephemeral program address for stake pool redelegation - */ -export async function findEphemeralStakeProgramAddress( - programId: PublicKey, - stakePoolAddress: PublicKey, - seed: BN, -) { - const [publicKey] = await PublicKey.findProgramAddress( - [EPHEMERAL_STAKE_SEED_PREFIX, stakePoolAddress.toBuffer(), seed.toArrayLike(Buffer, 'le', 8)], - programId, - ); - return publicKey; -} - -/** - * Generates the metadata program address for the stake pool - */ -export function findMetadataAddress(stakePoolMintAddress: PublicKey) { - const [publicKey] = PublicKey.findProgramAddressSync( - [Buffer.from('metadata'), METADATA_PROGRAM_ID.toBuffer(), stakePoolMintAddress.toBuffer()], - METADATA_PROGRAM_ID, - ); - return publicKey; -} diff --git a/stake-pool/js/src/utils/stake.ts b/stake-pool/js/src/utils/stake.ts deleted file mode 100644 index ed4fc1e8b0c..00000000000 --- a/stake-pool/js/src/utils/stake.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { - Connection, - Keypair, - PublicKey, - StakeProgram, - SystemProgram, - TransactionInstruction, -} from '@solana/web3.js'; -import { findStakeProgramAddress, findTransientStakeProgramAddress } from './program-address'; -import BN from 'bn.js'; - -import { lamportsToSol } from './math'; -import { WithdrawAccount } from '../index'; -import { - Fee, - StakePool, - ValidatorList, - ValidatorListLayout, - ValidatorStakeInfoStatus, -} from '../layouts'; -import { MINIMUM_ACTIVE_STAKE, STAKE_POOL_PROGRAM_ID } from '../constants'; - -export async function getValidatorListAccount(connection: Connection, pubkey: PublicKey) { - const account = await connection.getAccountInfo(pubkey); - if (!account) { - throw new Error('Invalid validator list account'); - } - - return { - pubkey, - account: { - data: ValidatorListLayout.decode(account?.data) as ValidatorList, - executable: account.executable, - lamports: account.lamports, - owner: account.owner, - }, - }; -} - -export interface ValidatorAccount { - type: 'preferred' | 'active' | 'transient' | 'reserve'; - voteAddress?: PublicKey | undefined; - stakeAddress: PublicKey; - lamports: BN; -} - -export async function prepareWithdrawAccounts( - connection: Connection, - stakePool: StakePool, - stakePoolAddress: PublicKey, - amount: BN, - compareFn?: (a: ValidatorAccount, b: ValidatorAccount) => number, - skipFee?: boolean, -): Promise { - const validatorListAcc = await connection.getAccountInfo(stakePool.validatorList); - const validatorList = ValidatorListLayout.decode(validatorListAcc?.data) as ValidatorList; - - if (!validatorList?.validators || validatorList?.validators.length == 0) { - throw new Error('No accounts found'); - } - - const minBalanceForRentExemption = await connection.getMinimumBalanceForRentExemption( - StakeProgram.space, - ); - const minBalance = new BN(minBalanceForRentExemption + MINIMUM_ACTIVE_STAKE); - - let accounts = [] as Array<{ - type: 'preferred' | 'active' | 'transient' | 'reserve'; - voteAddress?: PublicKey | undefined; - stakeAddress: PublicKey; - lamports: BN; - }>; - - // Prepare accounts - for (const validator of validatorList.validators) { - if (validator.status !== ValidatorStakeInfoStatus.Active) { - continue; - } - - const stakeAccountAddress = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validator.voteAccountAddress, - stakePoolAddress, - ); - - if (!validator.activeStakeLamports.isZero()) { - const isPreferred = stakePool?.preferredWithdrawValidatorVoteAddress?.equals( - validator.voteAccountAddress, - ); - accounts.push({ - type: isPreferred ? 'preferred' : 'active', - voteAddress: validator.voteAccountAddress, - stakeAddress: stakeAccountAddress, - lamports: validator.activeStakeLamports, - }); - } - - const transientStakeLamports = validator.transientStakeLamports.sub(minBalance); - if (transientStakeLamports.gt(new BN(0))) { - const transientStakeAccountAddress = await findTransientStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validator.voteAccountAddress, - stakePoolAddress, - validator.transientSeedSuffixStart, - ); - accounts.push({ - type: 'transient', - voteAddress: validator.voteAccountAddress, - stakeAddress: transientStakeAccountAddress, - lamports: transientStakeLamports, - }); - } - } - - // Sort from highest to lowest balance - accounts = accounts.sort(compareFn ? compareFn : (a, b) => b.lamports.sub(a.lamports).toNumber()); - - const reserveStake = await connection.getAccountInfo(stakePool.reserveStake); - const reserveStakeBalance = new BN((reserveStake?.lamports ?? 0) - minBalanceForRentExemption); - if (reserveStakeBalance.gt(new BN(0))) { - accounts.push({ - type: 'reserve', - stakeAddress: stakePool.reserveStake, - lamports: reserveStakeBalance, - }); - } - - // Prepare the list of accounts to withdraw from - const withdrawFrom: WithdrawAccount[] = []; - let remainingAmount = new BN(amount); - - const fee = stakePool.stakeWithdrawalFee; - const inverseFee: Fee = { - numerator: fee.denominator.sub(fee.numerator), - denominator: fee.denominator, - }; - - for (const type of ['preferred', 'active', 'transient', 'reserve']) { - const filteredAccounts = accounts.filter((a) => a.type == type); - - for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) { - if (lamports.lte(minBalance) && type == 'transient') { - continue; - } - - let availableForWithdrawal = calcPoolTokensForDeposit(stakePool, lamports); - - if (!skipFee && !inverseFee.numerator.isZero()) { - availableForWithdrawal = availableForWithdrawal - .mul(inverseFee.denominator) - .div(inverseFee.numerator); - } - - const poolAmount = BN.min(availableForWithdrawal, remainingAmount); - if (poolAmount.lte(new BN(0))) { - continue; - } - - // Those accounts will be withdrawn completely with `claim` instruction - withdrawFrom.push({ stakeAddress, voteAddress, poolAmount }); - remainingAmount = remainingAmount.sub(poolAmount); - - if (remainingAmount.isZero()) { - break; - } - } - - if (remainingAmount.isZero()) { - break; - } - } - - // Not enough stake to withdraw the specified amount - if (remainingAmount.gt(new BN(0))) { - throw new Error( - `No stake accounts found in this pool with enough balance to withdraw ${lamportsToSol( - amount, - )} pool tokens.`, - ); - } - - return withdrawFrom; -} - -/** - * Calculate the pool tokens that should be minted for a deposit of `stakeLamports` - */ -export function calcPoolTokensForDeposit(stakePool: StakePool, stakeLamports: BN): BN { - if (stakePool.poolTokenSupply.isZero() || stakePool.totalLamports.isZero()) { - return stakeLamports; - } - const numerator = stakeLamports.mul(stakePool.poolTokenSupply); - return numerator.div(stakePool.totalLamports); -} - -/** - * Calculate lamports amount on withdrawal - */ -export function calcLamportsWithdrawAmount(stakePool: StakePool, poolTokens: BN): BN { - const numerator = poolTokens.mul(stakePool.totalLamports); - const denominator = stakePool.poolTokenSupply; - if (numerator.lt(denominator)) { - return new BN(0); - } - return numerator.div(denominator); -} - -export function newStakeAccount( - feePayer: PublicKey, - instructions: TransactionInstruction[], - lamports: number, -): Keypair { - // Account for tokens not specified, creating one - const stakeReceiverKeypair = Keypair.generate(); - console.log(`Creating account to receive stake ${stakeReceiverKeypair.publicKey}`); - - instructions.push( - // Creating new account - SystemProgram.createAccount({ - fromPubkey: feePayer, - newAccountPubkey: stakeReceiverKeypair.publicKey, - lamports, - space: StakeProgram.space, - programId: StakeProgram.programId, - }), - ); - - return stakeReceiverKeypair; -} diff --git a/stake-pool/js/test/calculation.test.ts b/stake-pool/js/test/calculation.test.ts deleted file mode 100644 index dd6083ece24..00000000000 --- a/stake-pool/js/test/calculation.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { LAMPORTS_PER_SOL } from '@solana/web3.js'; -import { stakePoolMock } from './mocks'; -import { calcPoolTokensForDeposit } from '../src/utils/stake'; -import BN from 'bn.js'; - -describe('calculations', () => { - it('should successfully calculate pool tokens for a pool with a lot of stake', () => { - const lamports = new BN(LAMPORTS_PER_SOL * 100); - const bigStakePoolMock = stakePoolMock; - bigStakePoolMock.totalLamports = new BN('11000000000000000'); // 11 million SOL - bigStakePoolMock.poolTokenSupply = new BN('10000000000000000'); // 10 million tokens - const availableForWithdrawal = calcPoolTokensForDeposit(bigStakePoolMock, lamports); - expect(availableForWithdrawal.toNumber()).toEqual(90909090909); - }); -}); diff --git a/stake-pool/js/test/equal.ts b/stake-pool/js/test/equal.ts deleted file mode 100644 index b86e417bc9b..00000000000 --- a/stake-pool/js/test/equal.ts +++ /dev/null @@ -1,37 +0,0 @@ -import BN from 'bn.js'; - -/** - * Helper function to do deep equality check because BNs are not equal. - * TODO: write this function recursively. For now, sufficient. - */ -export function deepStrictEqualBN(a: any, b: any) { - for (const key in a) { - if (b[key] instanceof BN) { - expect(b[key].toString()).toEqual(a[key].toString()); - } else { - if (a[key] instanceof Object) { - for (const subkey in a[key]) { - if (a[key][subkey] instanceof Object) { - if (a[key][subkey] instanceof BN) { - expect(b[key][subkey].toString()).toEqual(a[key][subkey].toString()); - } else { - for (const subsubkey in a[key][subkey]) { - if (a[key][subkey][subsubkey] instanceof BN) { - expect(b[key][subkey][subsubkey].toString()).toEqual( - a[key][subkey][subsubkey].toString(), - ); - } else { - expect(b[key][subkey][subsubkey]).toStrictEqual(a[key][subkey][subsubkey]); - } - } - } - } else { - expect(b[key][subkey]).toStrictEqual(a[key][subkey]); - } - } - } else { - expect(b[key]).toStrictEqual(a[key]); - } - } - } -} diff --git a/stake-pool/js/test/instructions.test.ts b/stake-pool/js/test/instructions.test.ts deleted file mode 100644 index c3e9b7c9969..00000000000 --- a/stake-pool/js/test/instructions.test.ts +++ /dev/null @@ -1,541 +0,0 @@ -// Very important! We need to do this polyfill before any of the imports because -// some web3.js dependencies store `crypto` elsewhere. -import { randomBytes } from 'crypto'; -Object.defineProperty(globalThis, 'crypto', { - value: { - getRandomValues: (arr: any) => randomBytes(arr.length), - }, -}); - -import { - PublicKey, - Connection, - Keypair, - SystemProgram, - StakeProgram, - AccountInfo, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID, TokenAccountNotFoundError } from '@solana/spl-token'; -import { StakePoolLayout, ValidatorListLayout } from '../src/layouts'; -import { - STAKE_POOL_INSTRUCTION_LAYOUTS, - DepositSolParams, - AddValidatorToPoolParams, - RemoveValidatorFromPoolParams, - StakePoolInstruction, - depositSol, - withdrawSol, - withdrawStake, - getStakeAccount, - createPoolTokenMetadata, - updatePoolTokenMetadata, - tokenMetadataLayout, - addValidatorToPool, - removeValidatorFromPool, -} from '../src'; -import { STAKE_POOL_PROGRAM_ID } from '../src/constants'; - -import { decodeData, findStakeProgramAddress } from '../src/utils'; - -import { - mockRpc, - mockTokenAccount, - mockValidatorList, - mockValidatorsStakeAccount, - stakePoolMock, - CONSTANTS, - stakeAccountData, - uninitializedStakeAccount, - validatorListMock, -} from './mocks'; - -describe('StakePoolProgram', () => { - const connection = new Connection('http://127.0.0.1:8899'); - - connection.getMinimumBalanceForRentExemption = jest.fn(async () => 10000); - - const stakePoolAddress = new PublicKey('SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy'); - - const data = Buffer.alloc(1024); - StakePoolLayout.encode(stakePoolMock, data); - - const stakePoolAccount = >{ - executable: true, - owner: stakePoolAddress, - lamports: 99999, - data, - }; - - it('StakePoolInstruction.addValidatorToPool', () => { - const payload: AddValidatorToPoolParams = { - stakePool: stakePoolAddress, - staker: Keypair.generate().publicKey, - reserveStake: Keypair.generate().publicKey, - withdrawAuthority: Keypair.generate().publicKey, - validatorList: Keypair.generate().publicKey, - validatorStake: Keypair.generate().publicKey, - validatorVote: PublicKey.default, - seed: 0, - }; - - const instruction = StakePoolInstruction.addValidatorToPool(payload); - expect(instruction.keys).toHaveLength(13); - expect(instruction.keys[0].pubkey).toEqual(payload.stakePool); - expect(instruction.keys[1].pubkey).toEqual(payload.staker); - expect(instruction.keys[2].pubkey).toEqual(payload.reserveStake); - expect(instruction.keys[3].pubkey).toEqual(payload.withdrawAuthority); - expect(instruction.keys[4].pubkey).toEqual(payload.validatorList); - expect(instruction.keys[5].pubkey).toEqual(payload.validatorStake); - expect(instruction.keys[6].pubkey).toEqual(payload.validatorVote); - expect(instruction.keys[11].pubkey).toEqual(SystemProgram.programId); - expect(instruction.keys[12].pubkey).toEqual(StakeProgram.programId); - - const decodedData = decodeData( - STAKE_POOL_INSTRUCTION_LAYOUTS.AddValidatorToPool, - instruction.data, - ); - expect(decodedData.instruction).toEqual( - STAKE_POOL_INSTRUCTION_LAYOUTS.AddValidatorToPool.index, - ); - expect(decodedData.seed).toEqual(payload.seed); - }); - - it('StakePoolInstruction.removeValidatorFromPool', () => { - const payload: RemoveValidatorFromPoolParams = { - stakePool: stakePoolAddress, - staker: Keypair.generate().publicKey, - withdrawAuthority: Keypair.generate().publicKey, - validatorList: Keypair.generate().publicKey, - validatorStake: Keypair.generate().publicKey, - transientStake: Keypair.generate().publicKey, - }; - - const instruction = StakePoolInstruction.removeValidatorFromPool(payload); - expect(instruction.keys).toHaveLength(8); - expect(instruction.keys[0].pubkey).toEqual(payload.stakePool); - expect(instruction.keys[1].pubkey).toEqual(payload.staker); - expect(instruction.keys[2].pubkey).toEqual(payload.withdrawAuthority); - expect(instruction.keys[3].pubkey).toEqual(payload.validatorList); - expect(instruction.keys[4].pubkey).toEqual(payload.validatorStake); - expect(instruction.keys[5].pubkey).toEqual(payload.transientStake); - expect(instruction.keys[7].pubkey).toEqual(StakeProgram.programId); - - const decodedData = decodeData( - STAKE_POOL_INSTRUCTION_LAYOUTS.RemoveValidatorFromPool, - instruction.data, - ); - expect(decodedData.instruction).toEqual( - STAKE_POOL_INSTRUCTION_LAYOUTS.RemoveValidatorFromPool.index, - ); - }); - - it('StakePoolInstruction.depositSol', () => { - const payload: DepositSolParams = { - stakePool: stakePoolAddress, - withdrawAuthority: Keypair.generate().publicKey, - reserveStake: Keypair.generate().publicKey, - fundingAccount: Keypair.generate().publicKey, - destinationPoolAccount: Keypair.generate().publicKey, - managerFeeAccount: Keypair.generate().publicKey, - referralPoolAccount: Keypair.generate().publicKey, - poolMint: Keypair.generate().publicKey, - lamports: 99999, - }; - - const instruction = StakePoolInstruction.depositSol(payload); - - expect(instruction.keys).toHaveLength(10); - expect(instruction.keys[0].pubkey).toEqual(payload.stakePool); - expect(instruction.keys[1].pubkey).toEqual(payload.withdrawAuthority); - expect(instruction.keys[3].pubkey).toEqual(payload.fundingAccount); - expect(instruction.keys[4].pubkey).toEqual(payload.destinationPoolAccount); - expect(instruction.keys[5].pubkey).toEqual(payload.managerFeeAccount); - expect(instruction.keys[6].pubkey).toEqual(payload.referralPoolAccount); - expect(instruction.keys[8].pubkey).toEqual(SystemProgram.programId); - expect(instruction.keys[9].pubkey).toEqual(TOKEN_PROGRAM_ID); - - const decodedData = decodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol, instruction.data); - - expect(decodedData.instruction).toEqual(STAKE_POOL_INSTRUCTION_LAYOUTS.DepositSol.index); - expect(decodedData.lamports).toEqual(payload.lamports); - - payload.depositAuthority = Keypair.generate().publicKey; - - const instruction2 = StakePoolInstruction.depositSol(payload); - - expect(instruction2.keys).toHaveLength(11); - expect(instruction2.keys[10].pubkey).toEqual(payload.depositAuthority); - }); - - describe('addValidatorToPool', () => { - const validatorList = mockValidatorList(); - const decodedValidatorList = ValidatorListLayout.decode(validatorList.data); - const voteAccount = decodedValidatorList.validators[0].voteAccountAddress; - - it('should throw an error when trying to add an existing validator', async () => { - connection.getAccountInfo = jest.fn(async (pubKey) => { - if (pubKey === stakePoolAddress) { - return stakePoolAccount; - } - return mockValidatorList(); - }); - await expect(addValidatorToPool(connection, stakePoolAddress, voteAccount)).rejects.toThrow( - Error('Vote account is already in validator list'), - ); - }); - - it('should successfully add a validator', async () => { - connection.getAccountInfo = jest.fn(async (pubKey) => { - if (pubKey === stakePoolAddress) { - return stakePoolAccount; - } - return >{ - executable: true, - owner: new PublicKey(0), - lamports: 0, - data, - }; - }); - const res = await addValidatorToPool( - connection, - stakePoolAddress, - validatorListMock.validators[0].voteAccountAddress, - ); - expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(2); - expect(res.instructions).toHaveLength(1); - // Make sure that the validator vote account being added is the one we passed - expect(res.instructions[0].keys[6].pubkey).toEqual( - validatorListMock.validators[0].voteAccountAddress, - ); - }); - }); - - describe('removeValidatorFromPool', () => { - const voteAccount = Keypair.generate().publicKey; - - it('should throw an error when trying to remove a non-existing validator', async () => { - connection.getAccountInfo = jest.fn(async (pubKey) => { - if (pubKey === stakePoolAddress) { - return stakePoolAccount; - } - if (pubKey.equals(stakePoolMock.validatorList)) { - return mockValidatorList(); - } - return >{ - executable: true, - owner: new PublicKey(0), - lamports: 0, - data, - }; - }); - await expect( - removeValidatorFromPool(connection, stakePoolAddress, voteAccount), - ).rejects.toThrow(Error('Vote account is not already in validator list')); - }); - - it('should successfully remove a validator', async () => { - connection.getAccountInfo = jest.fn(async (pubKey) => { - if (pubKey === stakePoolAddress) { - return stakePoolAccount; - } - if (pubKey.equals(stakePoolMock.validatorList)) { - return mockValidatorList(); - } - return >{ - executable: true, - owner: new PublicKey(0), - lamports: 0, - data, - }; - }); - const res = await removeValidatorFromPool( - connection, - stakePoolAddress, - validatorListMock.validators[0].voteAccountAddress, - ); - expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(2); - expect(res.instructions).toHaveLength(1); - // Make sure that the validator stake account being removed is the one we passed - const validatorStake = await findStakeProgramAddress( - STAKE_POOL_PROGRAM_ID, - validatorListMock.validators[0].voteAccountAddress, - stakePoolAddress, - 0, - ); - expect(res.instructions[0].keys[4].pubkey).toEqual(validatorStake); - }); - }); - - describe('depositSol', () => { - const from = Keypair.generate().publicKey; - const balance = 10000; - - connection.getBalance = jest.fn(async () => balance); - - connection.getAccountInfo = jest.fn(async (pubKey) => { - if (pubKey == stakePoolAddress) { - return stakePoolAccount; - } - return >{ - executable: true, - owner: from, - lamports: balance, - data: null, - }; - }); - - it('should throw an error with invalid balance', async () => { - await expect(depositSol(connection, stakePoolAddress, from, balance + 1)).rejects.toThrow( - Error('Not enough SOL to deposit into pool. Maximum deposit amount is 0.00001 SOL.'), - ); - }); - - it('should throw an error with invalid account', async () => { - connection.getAccountInfo = jest.fn(async () => null); - await expect(depositSol(connection, stakePoolAddress, from, balance)).rejects.toThrow( - Error('Invalid stake pool account'), - ); - }); - - it('should call successfully', async () => { - connection.getAccountInfo = jest.fn(async (pubKey) => { - if (pubKey === stakePoolAddress) { - return stakePoolAccount; - } - return >{ - executable: true, - owner: from, - lamports: balance, - data: null, - }; - }); - - const res = await depositSol(connection, stakePoolAddress, from, balance); - - expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(1); - expect(res.instructions).toHaveLength(3); - expect(res.signers).toHaveLength(1); - }); - }); - - describe('withdrawSol', () => { - const tokenOwner = new PublicKey(0); - const solReceiver = new PublicKey(1); - - it('should throw an error with invalid stake pool account', async () => { - connection.getAccountInfo = jest.fn(async () => null); - await expect( - withdrawSol(connection, stakePoolAddress, tokenOwner, solReceiver, 1), - ).rejects.toThrowError('Invalid stake pool account'); - }); - - it('should throw an error with invalid token account', async () => { - connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey == stakePoolAddress) { - return stakePoolAccount; - } - if (pubKey.equals(CONSTANTS.poolTokenAccount)) { - return null; - } - return null; - }); - - await expect( - withdrawSol(connection, stakePoolAddress, tokenOwner, solReceiver, 1), - ).rejects.toThrow(TokenAccountNotFoundError); - }); - - it('should throw an error with invalid token account balance', async () => { - connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey === stakePoolAddress) { - return stakePoolAccount; - } - if (pubKey.equals(CONSTANTS.poolTokenAccount)) { - return mockTokenAccount(0); - } - return null; - }); - - await expect( - withdrawSol(connection, stakePoolAddress, tokenOwner, solReceiver, 1), - ).rejects.toThrow( - Error( - 'Not enough token balance to withdraw 1 pool tokens.\n Maximum withdraw amount is 0 pool tokens.', - ), - ); - }); - - it('should call successfully', async () => { - connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey == stakePoolAddress) { - return stakePoolAccount; - } - if (pubKey.equals(CONSTANTS.poolTokenAccount)) { - return mockTokenAccount(LAMPORTS_PER_SOL); - } - return null; - }); - const res = await withdrawSol(connection, stakePoolAddress, tokenOwner, solReceiver, 1); - - expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(2); - expect(res.instructions).toHaveLength(2); - expect(res.signers).toHaveLength(1); - }); - }); - - describe('withdrawStake', () => { - const tokenOwner = new PublicKey(0); - - it('should throw an error with invalid token account', async () => { - connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey == stakePoolAddress) { - return stakePoolAccount; - } - return null; - }); - - await expect(withdrawStake(connection, stakePoolAddress, tokenOwner, 1)).rejects.toThrow( - TokenAccountNotFoundError, - ); - }); - - it('should throw an error with invalid token account balance', async () => { - connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey == stakePoolAddress) { - return stakePoolAccount; - } - if (pubKey.equals(CONSTANTS.poolTokenAccount)) { - return mockTokenAccount(0); - } - return null; - }); - - await expect(withdrawStake(connection, stakePoolAddress, tokenOwner, 1)).rejects.toThrow( - Error( - 'Not enough token balance to withdraw 1 pool tokens.\n' + - ' Maximum withdraw amount is 0 pool tokens.', - ), - ); - }); - - it('should call successfully', async () => { - connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey == stakePoolAddress) { - return stakePoolAccount; - } - if (pubKey.equals(CONSTANTS.poolTokenAccount)) { - return mockTokenAccount(LAMPORTS_PER_SOL * 2); - } - if (pubKey.equals(stakePoolMock.validatorList)) { - return mockValidatorList(); - } - return null; - }); - const res = await withdrawStake(connection, stakePoolAddress, tokenOwner, 1); - - expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(4); - expect(res.instructions).toHaveLength(3); - expect(res.signers).toHaveLength(2); - expect(res.stakeReceiver).toEqual(undefined); - expect(res.totalRentFreeBalances).toEqual(10000); - }); - - it('withdraw to a stake account provided', async () => { - const stakeReceiver = new PublicKey(20); - connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey == stakePoolAddress) { - return stakePoolAccount; - } - if (pubKey.equals(CONSTANTS.poolTokenAccount)) { - return mockTokenAccount(LAMPORTS_PER_SOL * 2); - } - if (pubKey.equals(stakePoolMock.validatorList)) { - return mockValidatorList(); - } - if (pubKey.equals(CONSTANTS.validatorStakeAccountAddress)) - return mockValidatorsStakeAccount(); - return null; - }); - connection.getParsedAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey.equals(stakeReceiver)) { - return mockRpc(stakeAccountData); - } - return null; - }); - - const res = await withdrawStake( - connection, - stakePoolAddress, - tokenOwner, - 1, - undefined, - undefined, - stakeReceiver, - ); - - expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(4); - expect((connection.getParsedAccountInfo as jest.Mock).mock.calls.length).toBe(1); - expect(res.instructions).toHaveLength(3); - expect(res.signers).toHaveLength(2); - expect(res.stakeReceiver).toEqual(stakeReceiver); - expect(res.totalRentFreeBalances).toEqual(10000); - }); - }); - describe('getStakeAccount', () => { - it('returns an uninitialized parsed stake account', async () => { - const stakeAccount = new PublicKey(20); - connection.getParsedAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey.equals(stakeAccount)) { - return mockRpc(uninitializedStakeAccount); - } - return null; - }); - const parsedStakeAccount = await getStakeAccount(connection, stakeAccount); - expect((connection.getParsedAccountInfo as jest.Mock).mock.calls.length).toBe(1); - expect(parsedStakeAccount).toEqual(uninitializedStakeAccount.parsed); - }); - }); - - describe('createPoolTokenMetadata', () => { - it('should create pool token metadata', async () => { - connection.getAccountInfo = jest.fn(async (pubKey: PublicKey) => { - if (pubKey == stakePoolAddress) { - return stakePoolAccount; - } - return null; - }); - const name = 'test'; - const symbol = 'TEST'; - const uri = 'https://example.com'; - - const payer = new PublicKey(0); - const res = await createPoolTokenMetadata( - connection, - stakePoolAddress, - payer, - name, - symbol, - uri, - ); - - const type = tokenMetadataLayout(17, name.length, symbol.length, uri.length); - const data = decodeData(type, res.instructions[0].data); - expect(Buffer.from(data.name).toString()).toBe(name); - expect(Buffer.from(data.symbol).toString()).toBe(symbol); - expect(Buffer.from(data.uri).toString()).toBe(uri); - }); - - it('should update pool token metadata', async () => { - const name = 'test'; - const symbol = 'TEST'; - const uri = 'https://example.com'; - const res = await updatePoolTokenMetadata(connection, stakePoolAddress, name, symbol, uri); - const type = tokenMetadataLayout(18, name.length, symbol.length, uri.length); - const data = decodeData(type, res.instructions[0].data); - expect(Buffer.from(data.name).toString()).toBe(name); - expect(Buffer.from(data.symbol).toString()).toBe(symbol); - expect(Buffer.from(data.uri).toString()).toBe(uri); - }); - }); -}); diff --git a/stake-pool/js/test/layouts.test.ts b/stake-pool/js/test/layouts.test.ts deleted file mode 100644 index 053f43e1c7d..00000000000 --- a/stake-pool/js/test/layouts.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { StakePoolLayout, ValidatorListLayout, ValidatorList } from '../src/layouts'; -import { deepStrictEqualBN } from './equal'; -import { stakePoolMock, validatorListMock } from './mocks'; - -describe('layouts', () => { - describe('StakePoolAccount', () => { - it('should successfully decode StakePoolAccount data', () => { - const encodedData = Buffer.alloc(1024); - StakePoolLayout.encode(stakePoolMock, encodedData); - const decodedData = StakePoolLayout.decode(encodedData); - deepStrictEqualBN(decodedData, stakePoolMock); - }); - }); - - describe('ValidatorListAccount', () => { - it('should successfully decode ValidatorListAccount account data', () => { - const expectedData: ValidatorList = { - accountType: 0, - maxValidators: 10, - validators: [], - }; - const encodedData = Buffer.alloc(64); - ValidatorListLayout.encode(expectedData, encodedData); - const decodedData = ValidatorListLayout.decode(encodedData); - expect(decodedData).toEqual(expectedData); - }); - - it('should successfully decode ValidatorListAccount with nonempty ValidatorInfo', () => { - const encodedData = Buffer.alloc(1024); - ValidatorListLayout.encode(validatorListMock, encodedData); - const decodedData = ValidatorListLayout.decode(encodedData); - deepStrictEqualBN(decodedData, validatorListMock); - }); - }); -}); diff --git a/stake-pool/js/test/mocks.ts b/stake-pool/js/test/mocks.ts deleted file mode 100644 index c9c10839f7c..00000000000 --- a/stake-pool/js/test/mocks.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { AccountInfo, LAMPORTS_PER_SOL, PublicKey, StakeProgram } from '@solana/web3.js'; -import BN from 'bn.js'; -import { ValidatorStakeInfo } from '../src'; -import { AccountLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token'; -import { ValidatorListLayout, ValidatorStakeInfoStatus } from '../src/layouts'; - -export const CONSTANTS = { - poolTokenAccount: new PublicKey('GQkqTamwqjaNDfsbNm7r3aXPJ4oTSqKC3d5t2PF9Smqd'), - validatorStakeAccountAddress: new PublicKey( - new BN('69184b7f1bc836271c4ac0e29e53eb38a38ea0e7bcde693c45b30d1592a5a678', 'hex'), - ), -}; - -export const stakePoolMock = { - accountType: 1, - manager: new PublicKey(11), - staker: new PublicKey(12), - stakeDepositAuthority: new PublicKey(13), - stakeWithdrawBumpSeed: 255, - validatorList: new PublicKey(14), - reserveStake: new PublicKey(15), - poolMint: new PublicKey(16), - managerFeeAccount: new PublicKey(17), - tokenProgramId: new PublicKey(18), - totalLamports: new BN(LAMPORTS_PER_SOL * 999), - poolTokenSupply: new BN(LAMPORTS_PER_SOL * 100), - lastUpdateEpoch: new BN('7c', 'hex'), - lockup: { - unixTimestamp: new BN(Date.now()), - epoch: new BN(1), - custodian: new PublicKey(0), - }, - epochFee: { - denominator: new BN(0), - numerator: new BN(0), - }, - nextEpochFee: { - denominator: new BN(0), - numerator: new BN(0), - }, - preferredDepositValidatorVoteAddress: new PublicKey(1), - preferredWithdrawValidatorVoteAddress: new PublicKey(2), - stakeDepositFee: { - denominator: new BN(0), - numerator: new BN(0), - }, - stakeWithdrawalFee: { - denominator: new BN(0), - numerator: new BN(0), - }, - nextStakeWithdrawalFee: { - denominator: new BN(0), - numerator: new BN(0), - }, - stakeReferralFee: 0, - solDepositAuthority: new PublicKey(0), - solDepositFee: { - denominator: new BN(0), - numerator: new BN(0), - }, - solReferralFee: 0, - solWithdrawAuthority: new PublicKey(0), - solWithdrawalFee: { - denominator: new BN(0), - numerator: new BN(0), - }, - nextSolWithdrawalFee: { - denominator: new BN(0), - numerator: new BN(0), - }, - lastEpochPoolTokenSupply: new BN(0), - lastEpochTotalLamports: new BN(0), -}; - -export const validatorListMock = { - accountType: 0, - maxValidators: 100, - validators: [ - { - status: ValidatorStakeInfoStatus.ReadyForRemoval, - voteAccountAddress: new PublicKey( - new BN('a9946a889af14fd3c9b33d5df309489d9699271a6b09ff3190fcb41cf21a2f8c', 'hex'), - ), - lastUpdateEpoch: new BN('c3', 'hex'), - activeStakeLamports: new BN(123), - transientStakeLamports: new BN(999), - transientSeedSuffixStart: new BN(999), - transientSeedSuffixEnd: new BN(999), - }, - { - status: ValidatorStakeInfoStatus.Active, - voteAccountAddress: new PublicKey( - new BN('3796d40645ee07e3c64117e3f73430471d4c40465f696ebc9b034c1fc06a9f7d', 'hex'), - ), - lastUpdateEpoch: new BN('c3', 'hex'), - activeStakeLamports: new BN(LAMPORTS_PER_SOL * 100), - transientStakeLamports: new BN(22), - transientSeedSuffixStart: new BN(0), - transientSeedSuffixEnd: new BN(0), - }, - { - status: ValidatorStakeInfoStatus.Active, - voteAccountAddress: new PublicKey( - new BN('e4e37d6f2e80c0bb0f3da8a06304e57be5cda6efa2825b86780aa320d9784cf8', 'hex'), - ), - lastUpdateEpoch: new BN('c3', 'hex'), - activeStakeLamports: new BN(0), - transientStakeLamports: new BN(0), - transientSeedSuffixStart: new BN('a', 'hex'), - transientSeedSuffixEnd: new BN('a', 'hex'), - }, - ], -}; - -export function mockTokenAccount(amount = 0) { - const data = Buffer.alloc(165); - AccountLayout.encode( - { - mint: stakePoolMock.poolMint, - owner: new PublicKey(0), - amount: BigInt(amount), - delegateOption: 0, - delegate: new PublicKey(0), - delegatedAmount: BigInt(0), - state: 1, - isNativeOption: 0, - isNative: BigInt(0), - closeAuthorityOption: 0, - closeAuthority: new PublicKey(0), - }, - data, - ); - - return >{ - executable: true, - owner: TOKEN_PROGRAM_ID, - lamports: amount, - data, - }; -} - -export const mockRpc = (data: any): any => { - const value = { - owner: StakeProgram.programId, - lamports: LAMPORTS_PER_SOL, - data: data, - executable: false, - rentEpoch: 0, - }; - return { - context: { - slot: 11, - }, - value: value, - }; -}; - -export const stakeAccountData = { - program: 'stake', - parsed: { - type: 'delegated', - info: { - meta: { - rentExemptReserve: new BN(1), - lockup: { - epoch: 32, - unixTimestamp: 2, - custodian: new PublicKey(12), - }, - authorized: { - staker: new PublicKey(12), - withdrawer: new PublicKey(12), - }, - }, - stake: { - delegation: { - voter: new PublicKey( - new BN('e4e37d6f2e80c0bb0f3da8a06304e57be5cda6efa2825b86780aa320d9784cf8', 'hex'), - ), - stake: new BN(0), - activationEpoch: new BN(1), - deactivationEpoch: new BN(1), - warmupCooldownRate: 1.2, - }, - creditsObserved: 1, - }, - }, - }, -}; - -export const uninitializedStakeAccount = { - program: 'stake', - parsed: { - type: 'uninitialized', - }, -}; - -export function mockValidatorsStakeAccount() { - const data = Buffer.alloc(1024); - return >{ - executable: false, - owner: StakeProgram.programId, - lamports: 3000000000, - data, - }; -} - -export function mockValidatorList() { - const data = Buffer.alloc(1024); - ValidatorListLayout.encode(validatorListMock, data); - return >{ - executable: true, - owner: new PublicKey(0), - lamports: 0, - data, - }; -} diff --git a/stake-pool/js/tsconfig.json b/stake-pool/js/tsconfig.json deleted file mode 100644 index 2ae40abe0c8..00000000000 --- a/stake-pool/js/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "module": "esnext", - "target": "es2019", - "baseUrl": "./src", - "outDir": "dist", - "declaration": true, - "declarationDir": "dist", - "emitDeclarationOnly": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "skipLibCheck": true // needed to avoid re-export errors from borsh - }, - "include": ["src/**/*.ts"] -} diff --git a/stake-pool/program/Cargo.toml b/stake-pool/program/Cargo.toml deleted file mode 100644 index 59710cc253f..00000000000 --- a/stake-pool/program/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "spl-stake-pool" -version = "2.0.1" -description = "Solana Program Library Stake Pool" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -arrayref = "0.3.9" -borsh = "1.5.3" -bytemuck = "1.21" -num-derive = "0.4" -num-traits = "0.2" -num_enum = "0.7.3" -serde = "1.0.217" -serde_derive = "1.0.103" -solana-program = "2.1.0" -solana-security-txt = "1.1.1" -spl-pod = { version = "0.5.0", path = "../../libraries/pod", features = [ - "borsh", -] } -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = [ - "no-entrypoint", -] } -thiserror = "2.0" -bincode = "1.3.1" - -[dev-dependencies] -assert_matches = "1.5.0" -proptest = "1.6" -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" -solana-vote-program = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ - "no-entrypoint", -] } -test-case = "3.3" - -[lib] -crate-type = ["cdylib", "lib"] - -[lints] -workspace = true diff --git a/stake-pool/program/Xargo.toml b/stake-pool/program/Xargo.toml deleted file mode 100644 index 475fb71ed15..00000000000 --- a/stake-pool/program/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] diff --git a/stake-pool/program/program-id.md b/stake-pool/program/program-id.md deleted file mode 100644 index 7b5157e41d3..00000000000 --- a/stake-pool/program/program-id.md +++ /dev/null @@ -1 +0,0 @@ -SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy diff --git a/stake-pool/program/proptest-regressions/state.txt b/stake-pool/program/proptest-regressions/state.txt deleted file mode 100644 index 769e8a16890..00000000000 --- a/stake-pool/program/proptest-regressions/state.txt +++ /dev/null @@ -1 +0,0 @@ -cc 41ce1c46341336993e5e5d5aa94c30865e63f9e00d31d397aef88ec79fc312ca diff --git a/stake-pool/program/src/big_vec.rs b/stake-pool/program/src/big_vec.rs deleted file mode 100644 index 9ad84ff3b8c..00000000000 --- a/stake-pool/program/src/big_vec.rs +++ /dev/null @@ -1,302 +0,0 @@ -//! Big vector type, used with vectors that can't be serde'd -#![allow(clippy::arithmetic_side_effects)] // checked math involves too many compute units - -use { - arrayref::array_ref, - borsh::BorshDeserialize, - bytemuck::Pod, - solana_program::{program_error::ProgramError, program_memory::sol_memmove}, - std::mem, -}; - -/// Contains easy to use utilities for a big vector of Borsh-compatible types, -/// to avoid managing the entire struct on-chain and blow through stack limits. -pub struct BigVec<'data> { - /// Underlying data buffer, pieces of which are serialized - pub data: &'data mut [u8], -} - -const VEC_SIZE_BYTES: usize = 4; - -impl<'data> BigVec<'data> { - /// Get the length of the vector - pub fn len(&self) -> u32 { - let vec_len = array_ref![self.data, 0, VEC_SIZE_BYTES]; - u32::from_le_bytes(*vec_len) - } - - /// Find out if the vector has no contents (as demanded by clippy) - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Retain all elements that match the provided function, discard all others - pub fn retain bool>( - &mut self, - predicate: F, - ) -> Result<(), ProgramError> { - let mut vec_len = self.len(); - let mut removals_found = 0; - let mut dst_start_index = 0; - - let data_start_index = VEC_SIZE_BYTES; - let data_end_index = - data_start_index.saturating_add((vec_len as usize).saturating_mul(mem::size_of::())); - for start_index in (data_start_index..data_end_index).step_by(mem::size_of::()) { - let end_index = start_index + mem::size_of::(); - let slice = &self.data[start_index..end_index]; - if !predicate(slice) { - let gap = removals_found * mem::size_of::(); - if removals_found > 0 { - // In case the compute budget is ever bumped up, allowing us - // to use this safe code instead: - // self.data.copy_within(dst_start_index + gap..start_index, dst_start_index); - unsafe { - sol_memmove( - self.data[dst_start_index..start_index - gap].as_mut_ptr(), - self.data[dst_start_index + gap..start_index].as_mut_ptr(), - start_index - gap - dst_start_index, - ); - } - } - dst_start_index = start_index - gap; - removals_found += 1; - vec_len -= 1; - } - } - - // final memmove - if removals_found > 0 { - let gap = removals_found * mem::size_of::(); - // In case the compute budget is ever bumped up, allowing us - // to use this safe code instead: - // self.data.copy_within( - // dst_start_index + gap..data_end_index, - // dst_start_index, - // ); - unsafe { - sol_memmove( - self.data[dst_start_index..data_end_index - gap].as_mut_ptr(), - self.data[dst_start_index + gap..data_end_index].as_mut_ptr(), - data_end_index - gap - dst_start_index, - ); - } - } - - let vec_len_ref = &mut self.data[0..VEC_SIZE_BYTES]; - borsh::to_writer(vec_len_ref, &vec_len)?; - - Ok(()) - } - - /// Extracts a slice of the data types - pub fn deserialize_mut_slice( - &mut self, - skip: usize, - len: usize, - ) -> Result<&mut [T], ProgramError> { - let vec_len = self.len(); - let last_item_index = skip - .checked_add(len) - .ok_or(ProgramError::AccountDataTooSmall)?; - if last_item_index > vec_len as usize { - return Err(ProgramError::AccountDataTooSmall); - } - - let start_index = VEC_SIZE_BYTES.saturating_add(skip.saturating_mul(mem::size_of::())); - let end_index = start_index.saturating_add(len.saturating_mul(mem::size_of::())); - bytemuck::try_cast_slice_mut(&mut self.data[start_index..end_index]) - .map_err(|_| ProgramError::InvalidAccountData) - } - - /// Extracts a slice of the data types - pub fn deserialize_slice(&self, skip: usize, len: usize) -> Result<&[T], ProgramError> { - let vec_len = self.len(); - let last_item_index = skip - .checked_add(len) - .ok_or(ProgramError::AccountDataTooSmall)?; - if last_item_index > vec_len as usize { - return Err(ProgramError::AccountDataTooSmall); - } - - let start_index = VEC_SIZE_BYTES.saturating_add(skip.saturating_mul(mem::size_of::())); - let end_index = start_index.saturating_add(len.saturating_mul(mem::size_of::())); - bytemuck::try_cast_slice(&self.data[start_index..end_index]) - .map_err(|_| ProgramError::InvalidAccountData) - } - - /// Add new element to the end - pub fn push(&mut self, element: T) -> Result<(), ProgramError> { - let vec_len_ref = &mut self.data[0..VEC_SIZE_BYTES]; - let mut vec_len = u32::try_from_slice(vec_len_ref)?; - - let start_index = VEC_SIZE_BYTES + vec_len as usize * mem::size_of::(); - let end_index = start_index + mem::size_of::(); - - vec_len += 1; - borsh::to_writer(vec_len_ref, &vec_len)?; - - if self.data.len() < end_index { - return Err(ProgramError::AccountDataTooSmall); - } - let element_ref = bytemuck::try_from_bytes_mut( - &mut self.data[start_index..start_index + mem::size_of::()], - ) - .map_err(|_| ProgramError::InvalidAccountData)?; - *element_ref = element; - Ok(()) - } - - /// Find matching data in the array - pub fn find bool>(&self, predicate: F) -> Option<&T> { - let len = self.len() as usize; - let mut current = 0; - let mut current_index = VEC_SIZE_BYTES; - while current != len { - let end_index = current_index + mem::size_of::(); - let current_slice = &self.data[current_index..end_index]; - if predicate(current_slice) { - return Some(bytemuck::from_bytes(current_slice)); - } - current_index = end_index; - current += 1; - } - None - } - - /// Find matching data in the array - pub fn find_mut bool>(&mut self, predicate: F) -> Option<&mut T> { - let len = self.len() as usize; - let mut current = 0; - let mut current_index = VEC_SIZE_BYTES; - while current != len { - let end_index = current_index + mem::size_of::(); - let current_slice = &self.data[current_index..end_index]; - if predicate(current_slice) { - return Some(bytemuck::from_bytes_mut( - &mut self.data[current_index..end_index], - )); - } - current_index = end_index; - current += 1; - } - None - } -} - -#[cfg(test)] -mod tests { - use {super::*, bytemuck::Zeroable}; - - #[repr(C)] - #[derive(Debug, Copy, Clone, PartialEq, Pod, Zeroable)] - struct TestStruct { - value: [u8; 8], - } - - impl TestStruct { - fn new(value: u8) -> Self { - let value = [value, 0, 0, 0, 0, 0, 0, 0]; - Self { value } - } - } - - fn from_slice<'data>(data: &'data mut [u8], vec: &[u8]) -> BigVec<'data> { - let mut big_vec = BigVec { data }; - for element in vec { - big_vec.push(TestStruct::new(*element)).unwrap(); - } - big_vec - } - - fn check_big_vec_eq(big_vec: &BigVec, slice: &[u8]) { - assert!(big_vec - .deserialize_slice::(0, big_vec.len() as usize) - .unwrap() - .iter() - .map(|x| &x.value[0]) - .zip(slice.iter()) - .all(|(a, b)| a == b)); - } - - #[test] - fn push() { - let mut data = [0u8; 4 + 8 * 3]; - let mut v = BigVec { data: &mut data }; - v.push(TestStruct::new(1)).unwrap(); - check_big_vec_eq(&v, &[1]); - v.push(TestStruct::new(2)).unwrap(); - check_big_vec_eq(&v, &[1, 2]); - v.push(TestStruct::new(3)).unwrap(); - check_big_vec_eq(&v, &[1, 2, 3]); - assert_eq!( - v.push(TestStruct::new(4)).unwrap_err(), - ProgramError::AccountDataTooSmall - ); - } - - #[test] - fn retain() { - fn mod_2_predicate(data: &[u8]) -> bool { - u64::try_from_slice(data).unwrap() % 2 == 0 - } - - let mut data = [0u8; 4 + 8 * 4]; - let mut v = from_slice(&mut data, &[1, 2, 3, 4]); - v.retain::(mod_2_predicate).unwrap(); - check_big_vec_eq(&v, &[2, 4]); - } - - fn find_predicate(a: &[u8], b: u8) -> bool { - if a.len() != 8 { - false - } else { - a[0] == b - } - } - - #[test] - fn find() { - let mut data = [0u8; 4 + 8 * 4]; - let v = from_slice(&mut data, &[1, 2, 3, 4]); - assert_eq!( - v.find::(|x| find_predicate(x, 1)), - Some(&TestStruct::new(1)) - ); - assert_eq!( - v.find::(|x| find_predicate(x, 4)), - Some(&TestStruct::new(4)) - ); - assert_eq!(v.find::(|x| find_predicate(x, 5)), None); - } - - #[test] - fn find_mut() { - let mut data = [0u8; 4 + 8 * 4]; - let mut v = from_slice(&mut data, &[1, 2, 3, 4]); - let test_struct = v - .find_mut::(|x| find_predicate(x, 1)) - .unwrap(); - test_struct.value = [0; 8]; - check_big_vec_eq(&v, &[0, 2, 3, 4]); - assert_eq!(v.find_mut::(|x| find_predicate(x, 5)), None); - } - - #[test] - fn deserialize_mut_slice() { - let mut data = [0u8; 4 + 8 * 4]; - let mut v = from_slice(&mut data, &[1, 2, 3, 4]); - let slice = v.deserialize_mut_slice::(1, 2).unwrap(); - slice[0].value[0] = 10; - slice[1].value[0] = 11; - check_big_vec_eq(&v, &[1, 10, 11, 4]); - assert_eq!( - v.deserialize_mut_slice::(1, 4).unwrap_err(), - ProgramError::AccountDataTooSmall - ); - assert_eq!( - v.deserialize_mut_slice::(4, 1).unwrap_err(), - ProgramError::AccountDataTooSmall - ); - } -} diff --git a/stake-pool/program/src/entrypoint.rs b/stake-pool/program/src/entrypoint.rs deleted file mode 100644 index b616b219857..00000000000 --- a/stake-pool/program/src/entrypoint.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Program entrypoint - -#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] - -use { - crate::{error::StakePoolError, processor::Processor}, - solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError, - pubkey::Pubkey, - }, - solana_security_txt::security_txt, -}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - if let Err(error) = Processor::process(program_id, accounts, instruction_data) { - // catch the error so we can print it - error.print::(); - Err(error) - } else { - Ok(()) - } -} - -security_txt! { - // Required fields - name: "SPL Stake Pool", - project_url: "https://spl.solana.com/stake-pool", - contacts: "link:https://github.com/solana-labs/solana-program-library/security/advisories/new,mailto:security@solana.com,discord:https://solana.com/discord", - policy: "https://github.com/solana-labs/solana-program-library/blob/master/SECURITY.md", - - // Optional Fields - preferred_languages: "en", - source_code: "https://github.com/solana-labs/solana-program-library/tree/master/stake-pool/program", - source_revision: "b7dd8fee93815b486fce98d3d43d1d0934980226", - source_release: "stake-pool-v1.0.0", - auditors: "https://github.com/solana-labs/security-audits#stake-pool" -} diff --git a/stake-pool/program/src/error.rs b/stake-pool/program/src/error.rs deleted file mode 100644 index 74a7885fe6b..00000000000 --- a/stake-pool/program/src/error.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! Error types - -use { - num_derive::FromPrimitive, - solana_program::{decode_error::DecodeError, program_error::ProgramError}, - thiserror::Error, -}; - -/// Errors that may be returned by the StakePool program. -#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] -pub enum StakePoolError { - // 0. - /// The account cannot be initialized because it is already being used. - #[error("AlreadyInUse")] - AlreadyInUse, - /// The program address provided doesn't match the value generated by the - /// program. - #[error("InvalidProgramAddress")] - InvalidProgramAddress, - /// The stake pool state is invalid. - #[error("InvalidState")] - InvalidState, - /// The calculation failed. - #[error("CalculationFailure")] - CalculationFailure, - /// Stake pool fee > 1. - #[error("FeeTooHigh")] - FeeTooHigh, - - // 5. - /// Token account is associated with the wrong mint. - #[error("WrongAccountMint")] - WrongAccountMint, - /// Wrong pool manager account. - #[error("WrongManager")] - WrongManager, - /// Required signature is missing. - #[error("SignatureMissing")] - SignatureMissing, - /// Invalid validator stake list account. - #[error("InvalidValidatorStakeList")] - InvalidValidatorStakeList, - /// Invalid manager fee account. - #[error("InvalidFeeAccount")] - InvalidFeeAccount, - - // 10. - /// Specified pool mint account is wrong. - #[error("WrongPoolMint")] - WrongPoolMint, - /// Stake account is not in the state expected by the program. - #[error("WrongStakeStake")] - WrongStakeStake, - /// User stake is not active - #[error("UserStakeNotActive")] - UserStakeNotActive, - /// Stake account voting for this validator already exists in the pool. - #[error("ValidatorAlreadyAdded")] - ValidatorAlreadyAdded, - /// Stake account for this validator not found in the pool. - #[error("ValidatorNotFound")] - ValidatorNotFound, - - // 15. - /// Stake account address not properly derived from the validator address. - #[error("InvalidStakeAccountAddress")] - InvalidStakeAccountAddress, - /// Identify validator stake accounts with old balances and update them. - #[error("StakeListOutOfDate")] - StakeListOutOfDate, - /// First update old validator stake account balances and then pool stake - /// balance. - #[error("StakeListAndPoolOutOfDate")] - StakeListAndPoolOutOfDate, - /// Validator stake account is not found in the list storage. - #[error("UnknownValidatorStakeAccount")] - UnknownValidatorStakeAccount, - /// Wrong minting authority set for mint pool account - #[error("WrongMintingAuthority")] - WrongMintingAuthority, - - // 20. - /// The size of the given validator stake list does match the expected - /// amount - #[error("UnexpectedValidatorListAccountSize")] - UnexpectedValidatorListAccountSize, - /// Wrong pool staker account. - #[error("WrongStaker")] - WrongStaker, - /// Pool token supply is not zero on initialization - #[error("NonZeroPoolTokenSupply")] - NonZeroPoolTokenSupply, - /// The lamports in the validator stake account is not equal to the minimum - #[error("StakeLamportsNotEqualToMinimum")] - StakeLamportsNotEqualToMinimum, - /// The provided deposit stake account is not delegated to the preferred - /// deposit vote account - #[error("IncorrectDepositVoteAddress")] - IncorrectDepositVoteAddress, - - // 25. - /// The provided withdraw stake account is not the preferred deposit vote - /// account - #[error("IncorrectWithdrawVoteAddress")] - IncorrectWithdrawVoteAddress, - /// The mint has an invalid freeze authority - #[error("InvalidMintFreezeAuthority")] - InvalidMintFreezeAuthority, - /// Proposed fee increase exceeds stipulated ratio - #[error("FeeIncreaseTooHigh")] - FeeIncreaseTooHigh, - /// Not enough pool tokens provided to withdraw stake with one lamport - #[error("WithdrawalTooSmall")] - WithdrawalTooSmall, - /// Not enough lamports provided for deposit to result in one pool token - #[error("DepositTooSmall")] - DepositTooSmall, - - // 30. - /// Provided stake deposit authority does not match the program's - #[error("InvalidStakeDepositAuthority")] - InvalidStakeDepositAuthority, - /// Provided sol deposit authority does not match the program's - #[error("InvalidSolDepositAuthority")] - InvalidSolDepositAuthority, - /// Provided preferred validator is invalid - #[error("InvalidPreferredValidator")] - InvalidPreferredValidator, - /// Provided validator stake account already has a transient stake account - /// in use - #[error("TransientAccountInUse")] - TransientAccountInUse, - /// Provided sol withdraw authority does not match the program's - #[error("InvalidSolWithdrawAuthority")] - InvalidSolWithdrawAuthority, - - // 35. - /// Too much SOL withdrawn from the stake pool's reserve account - #[error("SolWithdrawalTooLarge")] - SolWithdrawalTooLarge, - /// Provided metadata account does not match metadata account derived for - /// pool mint - #[error("InvalidMetadataAccount")] - InvalidMetadataAccount, - /// The mint has an unsupported extension - #[error("UnsupportedMintExtension")] - UnsupportedMintExtension, - /// The fee account has an unsupported extension - #[error("UnsupportedFeeAccountExtension")] - UnsupportedFeeAccountExtension, - /// Instruction exceeds desired slippage limit - #[error("Instruction exceeds desired slippage limit")] - ExceededSlippage, - - // 40. - /// Provided mint does not have 9 decimals to match SOL - #[error("IncorrectMintDecimals")] - IncorrectMintDecimals, - /// Pool reserve does not have enough lamports to fund rent-exempt reserve - /// in split destination. Deposit more SOL in reserve, or pre-fund split - /// destination with the rent-exempt reserve for a stake account. - #[error("ReserveDepleted")] - ReserveDepleted, - /// Missing required sysvar account - #[error("Missing required sysvar account")] - MissingRequiredSysvar, -} -impl From for ProgramError { - fn from(e: StakePoolError) -> Self { - ProgramError::Custom(e as u32) - } -} -impl DecodeError for StakePoolError { - fn type_of() -> &'static str { - "Stake Pool Error" - } -} diff --git a/stake-pool/program/src/inline_mpl_token_metadata.rs b/stake-pool/program/src/inline_mpl_token_metadata.rs deleted file mode 100644 index a5d0836b3dd..00000000000 --- a/stake-pool/program/src/inline_mpl_token_metadata.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! Inlined MPL metadata types to avoid a direct dependency on -//! `mpl-token-metadata' NOTE: this file is sym-linked in `spl-single-pool`, so -//! be careful with changes! - -solana_program::declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); - -pub(crate) mod instruction { - use { - super::state::DataV2, - borsh::{BorshDeserialize, BorshSerialize}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - }, - }; - - #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] - struct CreateMetadataAccountArgsV3 { - /// Note that unique metadatas are disabled for now. - pub data: DataV2, - /// Whether you want your metadata to be updateable in the future. - pub is_mutable: bool, - /// UNUSED If this is a collection parent NFT. - pub collection_details: Option, - } - - #[allow(clippy::too_many_arguments)] - pub(crate) fn create_metadata_accounts_v3( - program_id: Pubkey, - metadata_account: Pubkey, - mint: Pubkey, - mint_authority: Pubkey, - payer: Pubkey, - update_authority: Pubkey, - name: String, - symbol: String, - uri: String, - ) -> Instruction { - let mut data = vec![33]; // CreateMetadataAccountV3 - data.append( - &mut borsh::to_vec(&CreateMetadataAccountArgsV3 { - data: DataV2 { - name, - symbol, - uri, - seller_fee_basis_points: 0, - creators: None, - collection: None, - uses: None, - }, - is_mutable: true, - collection_details: None, - }) - .unwrap(), - ); - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(metadata_account, false), - AccountMeta::new_readonly(mint, false), - AccountMeta::new_readonly(mint_authority, true), - AccountMeta::new(payer, true), - AccountMeta::new_readonly(update_authority, true), - AccountMeta::new_readonly(solana_program::system_program::ID, false), - ], - data, - } - } - - #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] - struct UpdateMetadataAccountArgsV2 { - pub data: Option, - pub update_authority: Option, - pub primary_sale_happened: Option, - pub is_mutable: Option, - } - pub(crate) fn update_metadata_accounts_v2( - program_id: Pubkey, - metadata_account: Pubkey, - update_authority: Pubkey, - new_update_authority: Option, - metadata: Option, - primary_sale_happened: Option, - is_mutable: Option, - ) -> Instruction { - let mut data = vec![15]; // UpdateMetadataAccountV2 - data.append( - &mut borsh::to_vec(&UpdateMetadataAccountArgsV2 { - data: metadata, - update_authority: new_update_authority, - primary_sale_happened, - is_mutable, - }) - .unwrap(), - ); - Instruction { - program_id, - accounts: vec![ - AccountMeta::new(metadata_account, false), - AccountMeta::new_readonly(update_authority, true), - ], - data, - } - } -} - -/// PDA creation helpers -pub mod pda { - use {super::ID, solana_program::pubkey::Pubkey}; - const PREFIX: &str = "metadata"; - /// Helper to find a metadata account address - pub fn find_metadata_account(mint: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address(&[PREFIX.as_bytes(), ID.as_ref(), mint.as_ref()], &ID) - } -} - -pub(crate) mod state { - use borsh::{BorshDeserialize, BorshSerialize}; - #[repr(C)] - #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] - pub(crate) struct DataV2 { - /// The name of the asset - pub name: String, - /// The symbol for the asset - pub symbol: String, - /// URI pointing to JSON representing the asset - pub uri: String, - /// Royalty basis points that goes to creators in secondary sales - /// (0-10000) - pub seller_fee_basis_points: u16, - /// UNUSED Array of creators, optional - pub creators: Option, - /// UNUSED Collection - pub collection: Option, - /// UNUSED Uses - pub uses: Option, - } -} diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs deleted file mode 100644 index b5d6dc7ae8d..00000000000 --- a/stake-pool/program/src/instruction.rs +++ /dev/null @@ -1,2648 +0,0 @@ -//! Instruction types - -// Remove the following `allow` when `Redelegate` is removed, required to avoid -// warnings from uses of deprecated types during trait derivations. -#![allow(deprecated)] -#![allow(clippy::too_many_arguments)] - -use { - crate::{ - find_deposit_authority_program_address, find_ephemeral_stake_program_address, - find_stake_program_address, find_transient_stake_program_address, - find_withdraw_authority_program_address, - inline_mpl_token_metadata::{self, pda::find_metadata_account}, - state::{Fee, FeeType, StakePool, ValidatorList, ValidatorStakeInfo}, - MAX_VALIDATORS_TO_UPDATE, - }, - borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - stake, - stake_history::Epoch, - system_program, sysvar, - }, - std::num::NonZeroU32, -}; - -/// Defines which validator vote account is set during the -/// `SetPreferredValidator` instruction -#[repr(C)] -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)] -pub enum PreferredValidatorType { - /// Set preferred validator for deposits - Deposit, - /// Set preferred validator for withdraws - Withdraw, -} - -/// Defines which authority to update in the `SetFundingAuthority` -/// instruction -#[repr(C)] -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)] -pub enum FundingType { - /// Sets the stake deposit authority - StakeDeposit, - /// Sets the SOL deposit authority - SolDeposit, - /// Sets the SOL withdraw authority - SolWithdraw, -} - -/// Instructions supported by the StakePool program. -#[repr(C)] -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)] -pub enum StakePoolInstruction { - /// Initializes a new StakePool. - /// - /// 0. `[w]` New StakePool to create. - /// 1. `[s]` Manager - /// 2. `[]` Staker - /// 3. `[]` Stake pool withdraw authority - /// 4. `[w]` Uninitialized validator stake list storage account - /// 5. `[]` Reserve stake account must be initialized, have zero balance, - /// and staker / withdrawer authority set to pool withdraw authority. - /// 6. `[]` Pool token mint. Must have zero supply, owned by withdraw - /// authority. - /// 7. `[]` Pool account to deposit the generated fee for manager. - /// 8. `[]` Token program id - /// 9. `[]` (Optional) Deposit authority that must sign all deposits. - /// Defaults to the program address generated using - /// `find_deposit_authority_program_address`, making deposits - /// permissionless. - Initialize { - /// Fee assessed as percentage of perceived rewards - fee: Fee, - /// Fee charged per withdrawal as percentage of withdrawal - withdrawal_fee: Fee, - /// Fee charged per deposit as percentage of deposit - deposit_fee: Fee, - /// Percentage [0-100] of deposit_fee that goes to referrer - referral_fee: u8, - /// Maximum expected number of validators - max_validators: u32, - }, - - /// (Staker only) Adds stake account delegated to validator to the pool's - /// list of managed validators. - /// - /// The stake account will have the rent-exempt amount plus - /// `max( - /// crate::MINIMUM_ACTIVE_STAKE, - /// solana_program::stake::tools::get_minimum_delegation() - /// )`. - /// It is funded from the stake pool reserve. - /// - /// 0. `[w]` Stake pool - /// 1. `[s]` Staker - /// 2. `[w]` Reserve stake account - /// 3. `[]` Stake pool withdraw authority - /// 4. `[w]` Validator stake list storage account - /// 5. `[w]` Stake account to add to the pool - /// 6. `[]` Validator this stake account will be delegated to - /// 7. `[]` Rent sysvar - /// 8. `[]` Clock sysvar - /// 9. '[]' Stake history sysvar - /// 10. '[]' Stake config sysvar - /// 11. `[]` System program - /// 12. `[]` Stake program - /// - /// userdata: optional non-zero u32 seed used for generating the validator - /// stake address - AddValidatorToPool(u32), - - /// (Staker only) Removes validator from the pool, deactivating its stake - /// - /// Only succeeds if the validator stake account has the minimum of - /// `max(crate::MINIMUM_ACTIVE_STAKE, - /// solana_program::stake::tools::get_minimum_delegation())`. plus the - /// rent-exempt amount. - /// - /// 0. `[w]` Stake pool - /// 1. `[s]` Staker - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Validator stake list storage account - /// 4. `[w]` Stake account to remove from the pool - /// 5. `[w]` Transient stake account, to deactivate if necessary - /// 6. `[]` Sysvar clock - /// 7. `[]` Stake program id, - RemoveValidatorFromPool, - - /// NOTE: This instruction has been deprecated since version 0.7.0. Please - /// use `DecreaseValidatorStakeWithReserve` instead. - /// - /// (Staker only) Decrease active stake on a validator, eventually moving it - /// to the reserve - /// - /// Internally, this instruction splits a validator stake account into its - /// corresponding transient stake account and deactivates it. - /// - /// In order to rebalance the pool without taking custody, the staker needs - /// a way of reducing the stake on a stake account. This instruction splits - /// some amount of stake, up to the total activated stake, from the - /// canonical validator stake account, into its "transient" stake - /// account. - /// - /// The instruction only succeeds if the transient stake account does not - /// exist. The amount of lamports to move must be at least rent-exemption - /// plus `max(crate::MINIMUM_ACTIVE_STAKE, - /// solana_program::stake::tools::get_minimum_delegation())`. - /// - /// 0. `[]` Stake pool - /// 1. `[s]` Stake pool staker - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Validator list - /// 4. `[w]` Canonical stake account to split from - /// 5. `[w]` Transient stake account to receive split - /// 6. `[]` Clock sysvar - /// 7. `[]` Rent sysvar - /// 8. `[]` System program - /// 9. `[]` Stake program - DecreaseValidatorStake { - /// amount of lamports to split into the transient stake account - lamports: u64, - /// seed used to create transient stake account - transient_stake_seed: u64, - }, - - /// (Staker only) Increase stake on a validator from the reserve account - /// - /// Internally, this instruction splits reserve stake into a transient stake - /// account and delegate to the appropriate validator. - /// `UpdateValidatorListBalance` will do the work of merging once it's - /// ready. - /// - /// This instruction only succeeds if the transient stake account does not - /// exist. The minimum amount to move is rent-exemption plus - /// `max(crate::MINIMUM_ACTIVE_STAKE, - /// solana_program::stake::tools::get_minimum_delegation())`. - /// - /// 0. `[]` Stake pool - /// 1. `[s]` Stake pool staker - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Validator list - /// 4. `[w]` Stake pool reserve stake - /// 5. `[w]` Transient stake account - /// 6. `[]` Validator stake account - /// 7. `[]` Validator vote account to delegate to - /// 8. '[]' Clock sysvar - /// 9. '[]' Rent sysvar - /// 10. `[]` Stake History sysvar - /// 11. `[]` Stake Config sysvar - /// 12. `[]` System program - /// 13. `[]` Stake program - /// - /// userdata: amount of lamports to increase on the given validator. - /// - /// The actual amount split into the transient stake account is: - /// `lamports + stake_rent_exemption`. - /// - /// The rent-exemption of the stake account is withdrawn back to the - /// reserve after it is merged. - IncreaseValidatorStake { - /// amount of lamports to increase on the given validator - lamports: u64, - /// seed used to create transient stake account - transient_stake_seed: u64, - }, - - /// (Staker only) Set the preferred deposit or withdraw stake account for - /// the stake pool - /// - /// In order to avoid users abusing the stake pool as a free conversion - /// between SOL staked on different validators, the staker can force all - /// deposits and/or withdraws to go to one chosen account, or unset that - /// account. - /// - /// 0. `[w]` Stake pool - /// 1. `[s]` Stake pool staker - /// 2. `[]` Validator list - /// - /// Fails if the validator is not part of the stake pool. - SetPreferredValidator { - /// Affected operation (deposit or withdraw) - validator_type: PreferredValidatorType, - /// Validator vote account that deposits or withdraws must go through, - /// unset with None - validator_vote_address: Option, - }, - - /// Updates balances of validator and transient stake accounts in the pool - /// - /// While going through the pairs of validator and transient stake - /// accounts, if the transient stake is inactive, it is merged into the - /// reserve stake account. If the transient stake is active and has - /// matching credits observed, it is merged into the canonical - /// validator stake account. In all other states, nothing is done, and - /// the balance is simply added to the canonical stake account balance. - /// - /// 0. `[]` Stake pool - /// 1. `[]` Stake pool withdraw authority - /// 2. `[w]` Validator stake list storage account - /// 3. `[w]` Reserve stake account - /// 4. `[]` Sysvar clock - /// 5. `[]` Sysvar stake history - /// 6. `[]` Stake program - /// 7. ..7+2N ` [] N pairs of validator and transient stake accounts - UpdateValidatorListBalance { - /// Index to start updating on the validator list - start_index: u32, - /// If true, don't try merging transient stake accounts into the reserve - /// or validator stake account. Useful for testing or if a - /// particular stake account is in a bad state, but we still - /// want to update - no_merge: bool, - }, - - /// Updates total pool balance based on balances in the reserve and - /// validator list - /// - /// 0. `[w]` Stake pool - /// 1. `[]` Stake pool withdraw authority - /// 2. `[w]` Validator stake list storage account - /// 3. `[]` Reserve stake account - /// 4. `[w]` Account to receive pool fee tokens - /// 5. `[w]` Pool mint account - /// 6. `[]` Pool token program - UpdateStakePoolBalance, - - /// Cleans up validator stake account entries marked as `ReadyForRemoval` - /// - /// 0. `[]` Stake pool - /// 1. `[w]` Validator stake list storage account - CleanupRemovedValidatorEntries, - - /// Deposit some stake into the pool. The output is a "pool" token - /// representing ownership into the pool. Inputs are converted to the - /// current ratio. - /// - /// 0. `[w]` Stake pool - /// 1. `[w]` Validator stake list storage account - /// 2. `[s]/[]` Stake pool deposit authority - /// 3. `[]` Stake pool withdraw authority - /// 4. `[w]` Stake account to join the pool (withdraw authority for the - /// stake account should be first set to the stake pool deposit - /// authority) - /// 5. `[w]` Validator stake account for the stake account to be merged - /// with - /// 6. `[w]` Reserve stake account, to withdraw rent exempt reserve - /// 7. `[w]` User account to receive pool tokens - /// 8. `[w]` Account to receive pool fee tokens - /// 9. `[w]` Account to receive a portion of pool fee tokens as referral - /// fees - /// 10. `[w]` Pool token mint account - /// 11. '[]' Sysvar clock account - /// 12. '[]' Sysvar stake history account - /// 13. `[]` Pool token program id, - /// 14. `[]` Stake program id, - DepositStake, - - /// Withdraw the token from the pool at the current ratio. - /// - /// Succeeds if the stake account has enough SOL to cover the desired - /// amount of pool tokens, and if the withdrawal keeps the total - /// staked amount above the minimum of rent-exempt amount + `max( - /// crate::MINIMUM_ACTIVE_STAKE, - /// solana_program::stake::tools::get_minimum_delegation() - /// )`. - /// - /// When allowing withdrawals, the order of priority goes: - /// - /// * preferred withdraw validator stake account (if set) - /// * validator stake accounts - /// * transient stake accounts - /// * reserve stake account OR totally remove validator stake accounts - /// - /// A user can freely withdraw from a validator stake account, and if they - /// are all at the minimum, then they can withdraw from transient stake - /// accounts, and if they are all at minimum, then they can withdraw from - /// the reserve or remove any validator from the pool. - /// - /// 0. `[w]` Stake pool - /// 1. `[w]` Validator stake list storage account - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Validator or reserve stake account to split - /// 4. `[w]` Uninitialized stake account to receive withdrawal - /// 5. `[]` User account to set as a new withdraw authority - /// 6. `[s]` User transfer authority, for pool token account - /// 7. `[w]` User account with pool tokens to burn from - /// 8. `[w]` Account to receive pool fee tokens - /// 9. `[w]` Pool token mint account - /// 10. `[]` Sysvar clock account (required) - /// 11. `[]` Pool token program id - /// 12. `[]` Stake program id, - /// - /// userdata: amount of pool tokens to withdraw - WithdrawStake(u64), - - /// (Manager only) Update manager - /// - /// 0. `[w]` StakePool - /// 1. `[s]` Manager - /// 2. `[s]` New manager - /// 3. `[]` New manager fee account - SetManager, - - /// (Manager only) Update fee - /// - /// 0. `[w]` StakePool - /// 1. `[s]` Manager - SetFee { - /// Type of fee to update and value to update it to - fee: FeeType, - }, - - /// (Manager or staker only) Update staker - /// - /// 0. `[w]` StakePool - /// 1. `[s]` Manager or current staker - /// 2. '[]` New staker pubkey - SetStaker, - - /// Deposit SOL directly into the pool's reserve account. The output is a - /// "pool" token representing ownership into the pool. Inputs are - /// converted to the current ratio. - /// - /// 0. `[w]` Stake pool - /// 1. `[]` Stake pool withdraw authority - /// 2. `[w]` Reserve stake account, to deposit SOL - /// 3. `[s]` Account providing the lamports to be deposited into the pool - /// 4. `[w]` User account to receive pool tokens - /// 5. `[w]` Account to receive fee tokens - /// 6. `[w]` Account to receive a portion of fee as referral fees - /// 7. `[w]` Pool token mint account - /// 8. `[]` System program account - /// 9. `[]` Token program id - /// 10. `[s]` (Optional) Stake pool sol deposit authority. - DepositSol(u64), - - /// (Manager only) Update SOL deposit, stake deposit, or SOL withdrawal - /// authority. - /// - /// 0. `[w]` StakePool - /// 1. `[s]` Manager - /// 2. '[]` New authority pubkey or none - SetFundingAuthority(FundingType), - - /// Withdraw SOL directly from the pool's reserve account. Fails if the - /// reserve does not have enough SOL. - /// - /// 0. `[w]` Stake pool - /// 1. `[]` Stake pool withdraw authority - /// 2. `[s]` User transfer authority, for pool token account - /// 3. `[w]` User account to burn pool tokens - /// 4. `[w]` Reserve stake account, to withdraw SOL - /// 5. `[w]` Account receiving the lamports from the reserve, must be a - /// system account - /// 6. `[w]` Account to receive pool fee tokens - /// 7. `[w]` Pool token mint account - /// 8. '[]' Clock sysvar - /// 9. '[]' Stake history sysvar - /// 10. `[]` Stake program account - /// 11. `[]` Token program id - /// 12. `[s]` (Optional) Stake pool sol withdraw authority - WithdrawSol(u64), - - /// Create token metadata for the stake-pool token in the - /// metaplex-token program - /// 0. `[]` Stake pool - /// 1. `[s]` Manager - /// 2. `[]` Stake pool withdraw authority - /// 3. `[]` Pool token mint account - /// 4. `[s, w]` Payer for creation of token metadata account - /// 5. `[w]` Token metadata account - /// 6. `[]` Metadata program id - /// 7. `[]` System program id - CreateTokenMetadata { - /// Token name - name: String, - /// Token symbol e.g. stkSOL - symbol: String, - /// URI of the uploaded metadata of the spl-token - uri: String, - }, - /// Update token metadata for the stake-pool token in the - /// metaplex-token program - /// - /// 0. `[]` Stake pool - /// 1. `[s]` Manager - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Token metadata account - /// 4. `[]` Metadata program id - UpdateTokenMetadata { - /// Token name - name: String, - /// Token symbol e.g. stkSOL - symbol: String, - /// URI of the uploaded metadata of the spl-token - uri: String, - }, - - /// (Staker only) Increase stake on a validator again in an epoch. - /// - /// Works regardless if the transient stake account exists. - /// - /// Internally, this instruction splits reserve stake into an ephemeral - /// stake account, activates it, then merges or splits it into the - /// transient stake account delegated to the appropriate validator. - /// `UpdateValidatorListBalance` will do the work of merging once it's - /// ready. - /// - /// The minimum amount to move is rent-exemption plus - /// `max(crate::MINIMUM_ACTIVE_STAKE, - /// solana_program::stake::tools::get_minimum_delegation())`. - /// - /// 0. `[]` Stake pool - /// 1. `[s]` Stake pool staker - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Validator list - /// 4. `[w]` Stake pool reserve stake - /// 5. `[w]` Uninitialized ephemeral stake account to receive stake - /// 6. `[w]` Transient stake account - /// 7. `[]` Validator stake account - /// 8. `[]` Validator vote account to delegate to - /// 9. '[]' Clock sysvar - /// 10. `[]` Stake History sysvar - /// 11. `[]` Stake Config sysvar - /// 12. `[]` System program - /// 13. `[]` Stake program - /// - /// userdata: amount of lamports to increase on the given validator. - /// - /// The actual amount split into the transient stake account is: - /// `lamports + stake_rent_exemption`. - /// - /// The rent-exemption of the stake account is withdrawn back to the - /// reserve after it is merged. - IncreaseAdditionalValidatorStake { - /// amount of lamports to increase on the given validator - lamports: u64, - /// seed used to create transient stake account - transient_stake_seed: u64, - /// seed used to create ephemeral account. - ephemeral_stake_seed: u64, - }, - - /// (Staker only) Decrease active stake again from a validator, eventually - /// moving it to the reserve - /// - /// Works regardless if the transient stake account already exists. - /// - /// Internally, this instruction: - /// * withdraws rent-exempt reserve lamports from the reserve into the - /// ephemeral stake - /// * splits a validator stake account into an ephemeral stake account - /// * deactivates the ephemeral account - /// * merges or splits the ephemeral account into the transient stake - /// account delegated to the appropriate validator - /// - /// The amount of lamports to move must be at least - /// `max(crate::MINIMUM_ACTIVE_STAKE, - /// solana_program::stake::tools::get_minimum_delegation())`. - /// - /// 0. `[]` Stake pool - /// 1. `[s]` Stake pool staker - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Validator list - /// 4. `[w]` Reserve stake account, to fund rent exempt reserve - /// 5. `[w]` Canonical stake account to split from - /// 6. `[w]` Uninitialized ephemeral stake account to receive stake - /// 7. `[w]` Transient stake account - /// 8. `[]` Clock sysvar - /// 9. '[]' Stake history sysvar - /// 10. `[]` System program - /// 11. `[]` Stake program - DecreaseAdditionalValidatorStake { - /// amount of lamports to split into the transient stake account - lamports: u64, - /// seed used to create transient stake account - transient_stake_seed: u64, - /// seed used to create ephemeral account. - ephemeral_stake_seed: u64, - }, - - /// (Staker only) Decrease active stake on a validator, eventually moving it - /// to the reserve - /// - /// Internally, this instruction: - /// * withdraws enough lamports to make the transient account rent-exempt - /// * splits from a validator stake account into a transient stake account - /// * deactivates the transient stake account - /// - /// In order to rebalance the pool without taking custody, the staker needs - /// a way of reducing the stake on a stake account. This instruction splits - /// some amount of stake, up to the total activated stake, from the - /// canonical validator stake account, into its "transient" stake - /// account. - /// - /// The instruction only succeeds if the transient stake account does not - /// exist. The amount of lamports to move must be at least rent-exemption - /// plus `max(crate::MINIMUM_ACTIVE_STAKE, - /// solana_program::stake::tools::get_minimum_delegation())`. - /// - /// 0. `[]` Stake pool - /// 1. `[s]` Stake pool staker - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Validator list - /// 4. `[w]` Reserve stake account, to fund rent exempt reserve - /// 5. `[w]` Canonical stake account to split from - /// 6. `[w]` Transient stake account to receive split - /// 7. `[]` Clock sysvar - /// 8. '[]' Stake history sysvar - /// 9. `[]` System program - /// 10. `[]` Stake program - DecreaseValidatorStakeWithReserve { - /// amount of lamports to split into the transient stake account - lamports: u64, - /// seed used to create transient stake account - transient_stake_seed: u64, - }, - - /// (Staker only) Redelegate active stake on a validator, eventually moving - /// it to another - /// - /// Internally, this instruction splits a validator stake account into its - /// corresponding transient stake account, redelegates it to an ephemeral - /// stake account, then merges that stake into the destination transient - /// stake account. - /// - /// In order to rebalance the pool without taking custody, the staker needs - /// a way of reducing the stake on a stake account. This instruction splits - /// some amount of stake, up to the total activated stake, from the - /// canonical validator stake account, into its "transient" stake - /// account. - /// - /// The instruction only succeeds if the source transient stake account and - /// ephemeral stake account do not exist. - /// - /// The amount of lamports to move must be at least rent-exemption plus the - /// minimum delegation amount. Rent-exemption plus minimum delegation - /// is required for the destination ephemeral stake account. - /// - /// The rent-exemption for the source transient account comes from the stake - /// pool reserve, if needed. - /// - /// The amount that arrives at the destination validator in the end is - /// `redelegate_lamports - rent_exemption` if the destination transient - /// account does *not* exist, and `redelegate_lamports` if the destination - /// transient account already exists. The `rent_exemption` is not activated - /// when creating the destination transient stake account, but if it already - /// exists, then the full amount is delegated. - /// - /// 0. `[]` Stake pool - /// 1. `[s]` Stake pool staker - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Validator list - /// 4. `[w]` Reserve stake account, to withdraw rent exempt reserve - /// 5. `[w]` Source canonical stake account to split from - /// 6. `[w]` Source transient stake account to receive split and be - /// redelegated - /// 7. `[w]` Uninitialized ephemeral stake account to receive redelegation - /// 8. `[w]` Destination transient stake account to receive ephemeral stake - /// by merge - /// 9. `[]` Destination stake account to receive transient stake after - /// activation - /// 10. `[]` Destination validator vote account - /// 11. `[]` Clock sysvar - /// 12. `[]` Stake History sysvar - /// 13. `[]` Stake Config sysvar - /// 14. `[]` System program - /// 15. `[]` Stake program - #[deprecated( - since = "2.0.0", - note = "The stake redelegate instruction used in this will not be enabled." - )] - Redelegate { - /// Amount of lamports to redelegate - #[allow(dead_code)] // but it's not - lamports: u64, - /// Seed used to create source transient stake account - #[allow(dead_code)] // but it's not - source_transient_stake_seed: u64, - /// Seed used to create destination ephemeral account. - #[allow(dead_code)] // but it's not - ephemeral_stake_seed: u64, - /// Seed used to create destination transient stake account. If there is - /// already transient stake, this must match the current seed, otherwise - /// it can be anything - #[allow(dead_code)] // but it's not - destination_transient_stake_seed: u64, - }, - - /// Deposit some stake into the pool, with a specified slippage - /// constraint. The output is a "pool" token representing ownership - /// into the pool. Inputs are converted at the current ratio. - /// - /// 0. `[w]` Stake pool - /// 1. `[w]` Validator stake list storage account - /// 2. `[s]/[]` Stake pool deposit authority - /// 3. `[]` Stake pool withdraw authority - /// 4. `[w]` Stake account to join the pool (withdraw authority for the - /// stake account should be first set to the stake pool deposit - /// authority) - /// 5. `[w]` Validator stake account for the stake account to be merged - /// with - /// 6. `[w]` Reserve stake account, to withdraw rent exempt reserve - /// 7. `[w]` User account to receive pool tokens - /// 8. `[w]` Account to receive pool fee tokens - /// 9. `[w]` Account to receive a portion of pool fee tokens as referral - /// fees - /// 10. `[w]` Pool token mint account - /// 11. '[]' Sysvar clock account - /// 12. '[]' Sysvar stake history account - /// 13. `[]` Pool token program id, - /// 14. `[]` Stake program id, - DepositStakeWithSlippage { - /// Minimum amount of pool tokens that must be received - minimum_pool_tokens_out: u64, - }, - - /// Withdraw the token from the pool at the current ratio, specifying a - /// minimum expected output lamport amount. - /// - /// Succeeds if the stake account has enough SOL to cover the desired - /// amount of pool tokens, and if the withdrawal keeps the total - /// staked amount above the minimum of rent-exempt amount + `max( - /// crate::MINIMUM_ACTIVE_STAKE, - /// solana_program::stake::tools::get_minimum_delegation() - /// )`. - /// - /// 0. `[w]` Stake pool - /// 1. `[w]` Validator stake list storage account - /// 2. `[]` Stake pool withdraw authority - /// 3. `[w]` Validator or reserve stake account to split - /// 4. `[w]` Uninitialized stake account to receive withdrawal - /// 5. `[]` User account to set as a new withdraw authority - /// 6. `[s]` User transfer authority, for pool token account - /// 7. `[w]` User account with pool tokens to burn from - /// 8. `[w]` Account to receive pool fee tokens - /// 9. `[w]` Pool token mint account - /// 10. `[]` Sysvar clock account (required) - /// 11. `[]` Pool token program id - /// 12. `[]` Stake program id, - /// - /// userdata: amount of pool tokens to withdraw - WithdrawStakeWithSlippage { - /// Pool tokens to burn in exchange for lamports - pool_tokens_in: u64, - /// Minimum amount of lamports that must be received - minimum_lamports_out: u64, - }, - - /// Deposit SOL directly into the pool's reserve account, with a - /// specified slippage constraint. The output is a "pool" token - /// representing ownership into the pool. Inputs are converted at the - /// current ratio. - /// - /// 0. `[w]` Stake pool - /// 1. `[]` Stake pool withdraw authority - /// 2. `[w]` Reserve stake account, to deposit SOL - /// 3. `[s]` Account providing the lamports to be deposited into the pool - /// 4. `[w]` User account to receive pool tokens - /// 5. `[w]` Account to receive fee tokens - /// 6. `[w]` Account to receive a portion of fee as referral fees - /// 7. `[w]` Pool token mint account - /// 8. `[]` System program account - /// 9. `[]` Token program id - /// 10. `[s]` (Optional) Stake pool sol deposit authority. - DepositSolWithSlippage { - /// Amount of lamports to deposit into the reserve - lamports_in: u64, - /// Minimum amount of pool tokens that must be received - minimum_pool_tokens_out: u64, - }, - - /// Withdraw SOL directly from the pool's reserve account. Fails if the - /// reserve does not have enough SOL or if the slippage constraint is not - /// met. - /// - /// 0. `[w]` Stake pool - /// 1. `[]` Stake pool withdraw authority - /// 2. `[s]` User transfer authority, for pool token account - /// 3. `[w]` User account to burn pool tokens - /// 4. `[w]` Reserve stake account, to withdraw SOL - /// 5. `[w]` Account receiving the lamports from the reserve, must be a - /// system account - /// 6. `[w]` Account to receive pool fee tokens - /// 7. `[w]` Pool token mint account - /// 8. '[]' Clock sysvar - /// 9. '[]' Stake history sysvar - /// 10. `[]` Stake program account - /// 11. `[]` Token program id - /// 12. `[s]` (Optional) Stake pool sol withdraw authority - WithdrawSolWithSlippage { - /// Pool tokens to burn in exchange for lamports - pool_tokens_in: u64, - /// Minimum amount of lamports that must be received - minimum_lamports_out: u64, - }, -} - -/// Creates an 'initialize' instruction. -pub fn initialize( - program_id: &Pubkey, - stake_pool: &Pubkey, - manager: &Pubkey, - staker: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list: &Pubkey, - reserve_stake: &Pubkey, - pool_mint: &Pubkey, - manager_pool_account: &Pubkey, - token_program_id: &Pubkey, - deposit_authority: Option, - fee: Fee, - withdrawal_fee: Fee, - deposit_fee: Fee, - referral_fee: u8, - max_validators: u32, -) -> Instruction { - let init_data = StakePoolInstruction::Initialize { - fee, - withdrawal_fee, - deposit_fee, - referral_fee, - max_validators, - }; - let data = borsh::to_vec(&init_data).unwrap(); - let mut accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*manager, true), - AccountMeta::new_readonly(*staker, false), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new_readonly(*reserve_stake, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new(*manager_pool_account, false), - AccountMeta::new_readonly(*token_program_id, false), - ]; - if let Some(deposit_authority) = deposit_authority { - accounts.push(AccountMeta::new_readonly(deposit_authority, true)); - } - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -/// Creates `AddValidatorToPool` instruction (add new validator stake account to -/// the pool) -pub fn add_validator_to_pool( - program_id: &Pubkey, - stake_pool: &Pubkey, - staker: &Pubkey, - reserve: &Pubkey, - stake_pool_withdraw: &Pubkey, - validator_list: &Pubkey, - stake: &Pubkey, - validator: &Pubkey, - seed: Option, -) -> Instruction { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*staker, true), - AccountMeta::new(*reserve, false), - AccountMeta::new_readonly(*stake_pool_withdraw, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new(*stake, false), - AccountMeta::new_readonly(*validator, false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - #[allow(deprecated)] - AccountMeta::new_readonly(stake::config::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - let data = borsh::to_vec(&StakePoolInstruction::AddValidatorToPool( - seed.map(|s| s.get()).unwrap_or(0), - )) - .unwrap(); - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -/// Creates `RemoveValidatorFromPool` instruction (remove validator stake -/// account from the pool) -pub fn remove_validator_from_pool( - program_id: &Pubkey, - stake_pool: &Pubkey, - staker: &Pubkey, - stake_pool_withdraw: &Pubkey, - validator_list: &Pubkey, - stake_account: &Pubkey, - transient_stake_account: &Pubkey, -) -> Instruction { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*staker, true), - AccountMeta::new_readonly(*stake_pool_withdraw, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new(*stake_account, false), - AccountMeta::new(*transient_stake_account, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::RemoveValidatorFromPool).unwrap(), - } -} - -/// Creates `DecreaseValidatorStake` instruction (rebalance from validator -/// account to transient account) -#[deprecated( - since = "0.7.0", - note = "please use `decrease_validator_stake_with_reserve`" -)] -pub fn decrease_validator_stake( - program_id: &Pubkey, - stake_pool: &Pubkey, - staker: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list: &Pubkey, - validator_stake: &Pubkey, - transient_stake: &Pubkey, - lamports: u64, - transient_stake_seed: u64, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*staker, true), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new(*validator_stake, false), - AccountMeta::new(*transient_stake, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::DecreaseValidatorStake { - lamports, - transient_stake_seed, - }) - .unwrap(), - } -} - -/// Creates `DecreaseAdditionalValidatorStake` instruction (rebalance from -/// validator account to transient account) -pub fn decrease_additional_validator_stake( - program_id: &Pubkey, - stake_pool: &Pubkey, - staker: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list: &Pubkey, - reserve_stake: &Pubkey, - validator_stake: &Pubkey, - ephemeral_stake: &Pubkey, - transient_stake: &Pubkey, - lamports: u64, - transient_stake_seed: u64, - ephemeral_stake_seed: u64, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*staker, true), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new(*reserve_stake, false), - AccountMeta::new(*validator_stake, false), - AccountMeta::new(*ephemeral_stake, false), - AccountMeta::new(*transient_stake, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::DecreaseAdditionalValidatorStake { - lamports, - transient_stake_seed, - ephemeral_stake_seed, - }) - .unwrap(), - } -} - -/// Creates `DecreaseValidatorStakeWithReserve` instruction (rebalance from -/// validator account to transient account) -pub fn decrease_validator_stake_with_reserve( - program_id: &Pubkey, - stake_pool: &Pubkey, - staker: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list: &Pubkey, - reserve_stake: &Pubkey, - validator_stake: &Pubkey, - transient_stake: &Pubkey, - lamports: u64, - transient_stake_seed: u64, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*staker, true), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new(*reserve_stake, false), - AccountMeta::new(*validator_stake, false), - AccountMeta::new(*transient_stake, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::DecreaseValidatorStakeWithReserve { - lamports, - transient_stake_seed, - }) - .unwrap(), - } -} - -/// Creates `IncreaseValidatorStake` instruction (rebalance from reserve account -/// to transient account) -pub fn increase_validator_stake( - program_id: &Pubkey, - stake_pool: &Pubkey, - staker: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list: &Pubkey, - reserve_stake: &Pubkey, - transient_stake: &Pubkey, - validator_stake: &Pubkey, - validator: &Pubkey, - lamports: u64, - transient_stake_seed: u64, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*staker, true), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new(*reserve_stake, false), - AccountMeta::new(*transient_stake, false), - AccountMeta::new_readonly(*validator_stake, false), - AccountMeta::new_readonly(*validator, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - #[allow(deprecated)] - AccountMeta::new_readonly(stake::config::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::IncreaseValidatorStake { - lamports, - transient_stake_seed, - }) - .unwrap(), - } -} - -/// Creates `IncreaseAdditionalValidatorStake` instruction (rebalance from -/// reserve account to transient account) -pub fn increase_additional_validator_stake( - program_id: &Pubkey, - stake_pool: &Pubkey, - staker: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list: &Pubkey, - reserve_stake: &Pubkey, - ephemeral_stake: &Pubkey, - transient_stake: &Pubkey, - validator_stake: &Pubkey, - validator: &Pubkey, - lamports: u64, - transient_stake_seed: u64, - ephemeral_stake_seed: u64, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*staker, true), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new(*reserve_stake, false), - AccountMeta::new(*ephemeral_stake, false), - AccountMeta::new(*transient_stake, false), - AccountMeta::new_readonly(*validator_stake, false), - AccountMeta::new_readonly(*validator, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - #[allow(deprecated)] - AccountMeta::new_readonly(stake::config::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::IncreaseAdditionalValidatorStake { - lamports, - transient_stake_seed, - ephemeral_stake_seed, - }) - .unwrap(), - } -} - -/// Creates `Redelegate` instruction (rebalance from one validator account to -/// another) -#[deprecated( - since = "2.0.0", - note = "The stake redelegate instruction used in this will not be enabled." -)] -pub fn redelegate( - program_id: &Pubkey, - stake_pool: &Pubkey, - staker: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list: &Pubkey, - reserve_stake: &Pubkey, - source_validator_stake: &Pubkey, - source_transient_stake: &Pubkey, - ephemeral_stake: &Pubkey, - destination_transient_stake: &Pubkey, - destination_validator_stake: &Pubkey, - validator: &Pubkey, - lamports: u64, - source_transient_stake_seed: u64, - ephemeral_stake_seed: u64, - destination_transient_stake_seed: u64, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*staker, true), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*validator_list, false), - AccountMeta::new(*reserve_stake, false), - AccountMeta::new(*source_validator_stake, false), - AccountMeta::new(*source_transient_stake, false), - AccountMeta::new(*ephemeral_stake, false), - AccountMeta::new(*destination_transient_stake, false), - AccountMeta::new_readonly(*destination_validator_stake, false), - AccountMeta::new_readonly(*validator, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - #[allow(deprecated)] - AccountMeta::new_readonly(stake::config::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::Redelegate { - lamports, - source_transient_stake_seed, - ephemeral_stake_seed, - destination_transient_stake_seed, - }) - .unwrap(), - } -} - -/// Creates `SetPreferredDepositValidator` instruction -pub fn set_preferred_validator( - program_id: &Pubkey, - stake_pool_address: &Pubkey, - staker: &Pubkey, - validator_list_address: &Pubkey, - validator_type: PreferredValidatorType, - validator_vote_address: Option, -) -> Instruction { - Instruction { - program_id: *program_id, - accounts: vec![ - AccountMeta::new(*stake_pool_address, false), - AccountMeta::new_readonly(*staker, true), - AccountMeta::new_readonly(*validator_list_address, false), - ], - data: borsh::to_vec(&StakePoolInstruction::SetPreferredValidator { - validator_type, - validator_vote_address, - }) - .unwrap(), - } -} - -/// Create an `AddValidatorToPool` instruction given an existing stake pool and -/// vote account -pub fn add_validator_to_pool_with_vote( - program_id: &Pubkey, - stake_pool: &StakePool, - stake_pool_address: &Pubkey, - vote_account_address: &Pubkey, - seed: Option, -) -> Instruction { - let pool_withdraw_authority = - find_withdraw_authority_program_address(program_id, stake_pool_address).0; - let (stake_account_address, _) = - find_stake_program_address(program_id, vote_account_address, stake_pool_address, seed); - add_validator_to_pool( - program_id, - stake_pool_address, - &stake_pool.staker, - &stake_pool.reserve_stake, - &pool_withdraw_authority, - &stake_pool.validator_list, - &stake_account_address, - vote_account_address, - seed, - ) -} - -/// Create an `RemoveValidatorFromPool` instruction given an existing stake pool -/// and vote account -pub fn remove_validator_from_pool_with_vote( - program_id: &Pubkey, - stake_pool: &StakePool, - stake_pool_address: &Pubkey, - vote_account_address: &Pubkey, - validator_stake_seed: Option, - transient_stake_seed: u64, -) -> Instruction { - let pool_withdraw_authority = - find_withdraw_authority_program_address(program_id, stake_pool_address).0; - let (stake_account_address, _) = find_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - validator_stake_seed, - ); - let (transient_stake_account, _) = find_transient_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - transient_stake_seed, - ); - remove_validator_from_pool( - program_id, - stake_pool_address, - &stake_pool.staker, - &pool_withdraw_authority, - &stake_pool.validator_list, - &stake_account_address, - &transient_stake_account, - ) -} - -/// Create an `IncreaseValidatorStake` instruction given an existing stake pool -/// and vote account -pub fn increase_validator_stake_with_vote( - program_id: &Pubkey, - stake_pool: &StakePool, - stake_pool_address: &Pubkey, - vote_account_address: &Pubkey, - lamports: u64, - validator_stake_seed: Option, - transient_stake_seed: u64, -) -> Instruction { - let pool_withdraw_authority = - find_withdraw_authority_program_address(program_id, stake_pool_address).0; - let (transient_stake_address, _) = find_transient_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - transient_stake_seed, - ); - let (validator_stake_address, _) = find_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - validator_stake_seed, - ); - - increase_validator_stake( - program_id, - stake_pool_address, - &stake_pool.staker, - &pool_withdraw_authority, - &stake_pool.validator_list, - &stake_pool.reserve_stake, - &transient_stake_address, - &validator_stake_address, - vote_account_address, - lamports, - transient_stake_seed, - ) -} - -/// Create an `IncreaseAdditionalValidatorStake` instruction given an existing -/// stake pool and vote account -pub fn increase_additional_validator_stake_with_vote( - program_id: &Pubkey, - stake_pool: &StakePool, - stake_pool_address: &Pubkey, - vote_account_address: &Pubkey, - lamports: u64, - validator_stake_seed: Option, - transient_stake_seed: u64, - ephemeral_stake_seed: u64, -) -> Instruction { - let pool_withdraw_authority = - find_withdraw_authority_program_address(program_id, stake_pool_address).0; - let (ephemeral_stake_address, _) = - find_ephemeral_stake_program_address(program_id, stake_pool_address, ephemeral_stake_seed); - let (transient_stake_address, _) = find_transient_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - transient_stake_seed, - ); - let (validator_stake_address, _) = find_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - validator_stake_seed, - ); - - increase_additional_validator_stake( - program_id, - stake_pool_address, - &stake_pool.staker, - &pool_withdraw_authority, - &stake_pool.validator_list, - &stake_pool.reserve_stake, - &ephemeral_stake_address, - &transient_stake_address, - &validator_stake_address, - vote_account_address, - lamports, - transient_stake_seed, - ephemeral_stake_seed, - ) -} - -/// Create a `DecreaseValidatorStake` instruction given an existing stake pool -/// and vote account -pub fn decrease_validator_stake_with_vote( - program_id: &Pubkey, - stake_pool: &StakePool, - stake_pool_address: &Pubkey, - vote_account_address: &Pubkey, - lamports: u64, - validator_stake_seed: Option, - transient_stake_seed: u64, -) -> Instruction { - let pool_withdraw_authority = - find_withdraw_authority_program_address(program_id, stake_pool_address).0; - let (validator_stake_address, _) = find_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - validator_stake_seed, - ); - let (transient_stake_address, _) = find_transient_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - transient_stake_seed, - ); - decrease_validator_stake_with_reserve( - program_id, - stake_pool_address, - &stake_pool.staker, - &pool_withdraw_authority, - &stake_pool.validator_list, - &stake_pool.reserve_stake, - &validator_stake_address, - &transient_stake_address, - lamports, - transient_stake_seed, - ) -} - -/// Create a `IncreaseAdditionalValidatorStake` instruction given an existing -/// stake pool, valiator list and vote account -pub fn increase_additional_validator_stake_with_list( - program_id: &Pubkey, - stake_pool: &StakePool, - validator_list: &ValidatorList, - stake_pool_address: &Pubkey, - vote_account_address: &Pubkey, - lamports: u64, - ephemeral_stake_seed: u64, -) -> Result { - let validator_info = validator_list - .find(vote_account_address) - .ok_or(ProgramError::InvalidInstructionData)?; - let transient_stake_seed = u64::from(validator_info.transient_seed_suffix); - let validator_stake_seed = NonZeroU32::new(validator_info.validator_seed_suffix.into()); - Ok(increase_additional_validator_stake_with_vote( - program_id, - stake_pool, - stake_pool_address, - vote_account_address, - lamports, - validator_stake_seed, - transient_stake_seed, - ephemeral_stake_seed, - )) -} - -/// Create a `DecreaseAdditionalValidatorStake` instruction given an existing -/// stake pool, valiator list and vote account -pub fn decrease_additional_validator_stake_with_list( - program_id: &Pubkey, - stake_pool: &StakePool, - validator_list: &ValidatorList, - stake_pool_address: &Pubkey, - vote_account_address: &Pubkey, - lamports: u64, - ephemeral_stake_seed: u64, -) -> Result { - let validator_info = validator_list - .find(vote_account_address) - .ok_or(ProgramError::InvalidInstructionData)?; - let transient_stake_seed = u64::from(validator_info.transient_seed_suffix); - let validator_stake_seed = NonZeroU32::new(validator_info.validator_seed_suffix.into()); - Ok(decrease_additional_validator_stake_with_vote( - program_id, - stake_pool, - stake_pool_address, - vote_account_address, - lamports, - validator_stake_seed, - transient_stake_seed, - ephemeral_stake_seed, - )) -} - -/// Create a `DecreaseAdditionalValidatorStake` instruction given an existing -/// stake pool and vote account -pub fn decrease_additional_validator_stake_with_vote( - program_id: &Pubkey, - stake_pool: &StakePool, - stake_pool_address: &Pubkey, - vote_account_address: &Pubkey, - lamports: u64, - validator_stake_seed: Option, - transient_stake_seed: u64, - ephemeral_stake_seed: u64, -) -> Instruction { - let pool_withdraw_authority = - find_withdraw_authority_program_address(program_id, stake_pool_address).0; - let (validator_stake_address, _) = find_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - validator_stake_seed, - ); - let (ephemeral_stake_address, _) = - find_ephemeral_stake_program_address(program_id, stake_pool_address, ephemeral_stake_seed); - let (transient_stake_address, _) = find_transient_stake_program_address( - program_id, - vote_account_address, - stake_pool_address, - transient_stake_seed, - ); - decrease_additional_validator_stake( - program_id, - stake_pool_address, - &stake_pool.staker, - &pool_withdraw_authority, - &stake_pool.validator_list, - &stake_pool.reserve_stake, - &validator_stake_address, - &ephemeral_stake_address, - &transient_stake_address, - lamports, - transient_stake_seed, - ephemeral_stake_seed, - ) -} - -/// Creates `UpdateValidatorListBalance` instruction (update validator stake -/// account balances) -#[deprecated( - since = "1.1.0", - note = "please use `update_validator_list_balance_chunk`" -)] -pub fn update_validator_list_balance( - program_id: &Pubkey, - stake_pool: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list_address: &Pubkey, - reserve_stake: &Pubkey, - validator_list: &ValidatorList, - validator_vote_accounts: &[Pubkey], - start_index: u32, - no_merge: bool, -) -> Instruction { - let mut accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*validator_list_address, false), - AccountMeta::new(*reserve_stake, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - accounts.append( - &mut validator_vote_accounts - .iter() - .flat_map(|vote_account_address| { - let validator_stake_info = validator_list.find(vote_account_address); - if let Some(validator_stake_info) = validator_stake_info { - let (validator_stake_account, _) = find_stake_program_address( - program_id, - vote_account_address, - stake_pool, - NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()), - ); - let (transient_stake_account, _) = find_transient_stake_program_address( - program_id, - vote_account_address, - stake_pool, - validator_stake_info.transient_seed_suffix.into(), - ); - vec![ - AccountMeta::new(validator_stake_account, false), - AccountMeta::new(transient_stake_account, false), - ] - } else { - vec![] - } - }) - .collect::>(), - ); - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::UpdateValidatorListBalance { - start_index, - no_merge, - }) - .unwrap(), - } -} - -/// Creates an `UpdateValidatorListBalance` instruction (update validator stake -/// account balances) to update `validator_list[start_index..start_index + -/// len]`. -/// -/// Returns `Err(ProgramError::InvalidInstructionData)` if: -/// - `start_index..start_index + len` is out of bounds for -/// `validator_list.validators` -pub fn update_validator_list_balance_chunk( - program_id: &Pubkey, - stake_pool: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list_address: &Pubkey, - reserve_stake: &Pubkey, - validator_list: &ValidatorList, - len: usize, - start_index: usize, - no_merge: bool, -) -> Result { - let mut accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*validator_list_address, false), - AccountMeta::new(*reserve_stake, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - let validator_list_subslice = validator_list - .validators - .get(start_index..start_index.saturating_add(len)) - .ok_or(ProgramError::InvalidInstructionData)?; - accounts.extend(validator_list_subslice.iter().flat_map( - |ValidatorStakeInfo { - vote_account_address, - validator_seed_suffix, - transient_seed_suffix, - .. - }| { - let (validator_stake_account, _) = find_stake_program_address( - program_id, - vote_account_address, - stake_pool, - NonZeroU32::new((*validator_seed_suffix).into()), - ); - let (transient_stake_account, _) = find_transient_stake_program_address( - program_id, - vote_account_address, - stake_pool, - (*transient_seed_suffix).into(), - ); - [ - AccountMeta::new(validator_stake_account, false), - AccountMeta::new(transient_stake_account, false), - ] - }, - )); - Ok(Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::UpdateValidatorListBalance { - start_index: start_index.try_into().unwrap(), - no_merge, - }) - .unwrap(), - }) -} - -/// Creates `UpdateValidatorListBalance` instruction (update validator stake -/// account balances) -/// -/// Returns `None` if all validators in the given chunk has already been updated -/// for this epoch, returns the required instruction otherwise. -pub fn update_stale_validator_list_balance_chunk( - program_id: &Pubkey, - stake_pool: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - validator_list_address: &Pubkey, - reserve_stake: &Pubkey, - validator_list: &ValidatorList, - len: usize, - start_index: usize, - no_merge: bool, - current_epoch: Epoch, -) -> Result, ProgramError> { - let validator_list_subslice = validator_list - .validators - .get(start_index..start_index.saturating_add(len)) - .ok_or(ProgramError::InvalidInstructionData)?; - if validator_list_subslice.iter().all(|info| { - let last_update_epoch: u64 = info.last_update_epoch.into(); - last_update_epoch >= current_epoch - }) { - return Ok(None); - } - update_validator_list_balance_chunk( - program_id, - stake_pool, - stake_pool_withdraw_authority, - validator_list_address, - reserve_stake, - validator_list, - len, - start_index, - no_merge, - ) - .map(Some) -} - -/// Creates `UpdateStakePoolBalance` instruction (pool balance from the stake -/// account list balances) -pub fn update_stake_pool_balance( - program_id: &Pubkey, - stake_pool: &Pubkey, - withdraw_authority: &Pubkey, - validator_list_storage: &Pubkey, - reserve_stake: &Pubkey, - manager_fee_account: &Pubkey, - stake_pool_mint: &Pubkey, - token_program_id: &Pubkey, -) -> Instruction { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*withdraw_authority, false), - AccountMeta::new(*validator_list_storage, false), - AccountMeta::new_readonly(*reserve_stake, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*stake_pool_mint, false), - AccountMeta::new_readonly(*token_program_id, false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::UpdateStakePoolBalance).unwrap(), - } -} - -/// Creates `CleanupRemovedValidatorEntries` instruction (removes entries from -/// the validator list) -pub fn cleanup_removed_validator_entries( - program_id: &Pubkey, - stake_pool: &Pubkey, - validator_list_storage: &Pubkey, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new(*validator_list_storage, false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::CleanupRemovedValidatorEntries).unwrap(), - } -} - -/// Creates all `UpdateValidatorListBalance` and `UpdateStakePoolBalance` -/// instructions for fully updating a stake pool each epoch -pub fn update_stake_pool( - program_id: &Pubkey, - stake_pool: &StakePool, - validator_list: &ValidatorList, - stake_pool_address: &Pubkey, - no_merge: bool, -) -> (Vec, Vec) { - let (withdraw_authority, _) = - find_withdraw_authority_program_address(program_id, stake_pool_address); - - let update_list_instructions = validator_list - .validators - .chunks(MAX_VALIDATORS_TO_UPDATE) - .enumerate() - .map(|(i, chunk)| { - // unwrap-safety: chunk len and offset are derived - update_validator_list_balance_chunk( - program_id, - stake_pool_address, - &withdraw_authority, - &stake_pool.validator_list, - &stake_pool.reserve_stake, - validator_list, - chunk.len(), - i.saturating_mul(MAX_VALIDATORS_TO_UPDATE), - no_merge, - ) - .unwrap() - }) - .collect(); - - let final_instructions = vec![ - update_stake_pool_balance( - program_id, - stake_pool_address, - &withdraw_authority, - &stake_pool.validator_list, - &stake_pool.reserve_stake, - &stake_pool.manager_fee_account, - &stake_pool.pool_mint, - &stake_pool.token_program_id, - ), - cleanup_removed_validator_entries( - program_id, - stake_pool_address, - &stake_pool.validator_list, - ), - ]; - (update_list_instructions, final_instructions) -} - -/// Creates the `UpdateValidatorListBalance` instructions only for validators on -/// `validator_list` that have not been updated for this epoch, and the -/// `UpdateStakePoolBalance` instruction for fully updating the stake pool. -/// -/// Basically same as [`update_stake_pool`], but skips validators that are -/// already updated for this epoch -pub fn update_stale_stake_pool( - program_id: &Pubkey, - stake_pool: &StakePool, - validator_list: &ValidatorList, - stake_pool_address: &Pubkey, - no_merge: bool, - current_epoch: Epoch, -) -> (Vec, Vec) { - let (withdraw_authority, _) = - find_withdraw_authority_program_address(program_id, stake_pool_address); - - let update_list_instructions = validator_list - .validators - .chunks(MAX_VALIDATORS_TO_UPDATE) - .enumerate() - .filter_map(|(i, chunk)| { - // unwrap-safety: chunk len and offset are derived - update_stale_validator_list_balance_chunk( - program_id, - stake_pool_address, - &withdraw_authority, - &stake_pool.validator_list, - &stake_pool.reserve_stake, - validator_list, - chunk.len(), - i.saturating_mul(MAX_VALIDATORS_TO_UPDATE), - no_merge, - current_epoch, - ) - .unwrap() - }) - .collect(); - - let final_instructions = vec![ - update_stake_pool_balance( - program_id, - stake_pool_address, - &withdraw_authority, - &stake_pool.validator_list, - &stake_pool.reserve_stake, - &stake_pool.manager_fee_account, - &stake_pool.pool_mint, - &stake_pool.token_program_id, - ), - cleanup_removed_validator_entries( - program_id, - stake_pool_address, - &stake_pool.validator_list, - ), - ]; - (update_list_instructions, final_instructions) -} - -fn deposit_stake_internal( - program_id: &Pubkey, - stake_pool: &Pubkey, - validator_list_storage: &Pubkey, - stake_pool_deposit_authority: Option<&Pubkey>, - stake_pool_withdraw_authority: &Pubkey, - deposit_stake_address: &Pubkey, - deposit_stake_withdraw_authority: &Pubkey, - validator_stake_account: &Pubkey, - reserve_stake_account: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - minimum_pool_tokens_out: Option, -) -> Vec { - let mut instructions = vec![]; - let mut accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new(*validator_list_storage, false), - ]; - if let Some(stake_pool_deposit_authority) = stake_pool_deposit_authority { - accounts.push(AccountMeta::new_readonly( - *stake_pool_deposit_authority, - true, - )); - instructions.extend_from_slice(&[ - stake::instruction::authorize( - deposit_stake_address, - deposit_stake_withdraw_authority, - stake_pool_deposit_authority, - stake::state::StakeAuthorize::Staker, - None, - ), - stake::instruction::authorize( - deposit_stake_address, - deposit_stake_withdraw_authority, - stake_pool_deposit_authority, - stake::state::StakeAuthorize::Withdrawer, - None, - ), - ]); - } else { - let stake_pool_deposit_authority = - find_deposit_authority_program_address(program_id, stake_pool).0; - accounts.push(AccountMeta::new_readonly( - stake_pool_deposit_authority, - false, - )); - instructions.extend_from_slice(&[ - stake::instruction::authorize( - deposit_stake_address, - deposit_stake_withdraw_authority, - &stake_pool_deposit_authority, - stake::state::StakeAuthorize::Staker, - None, - ), - stake::instruction::authorize( - deposit_stake_address, - deposit_stake_withdraw_authority, - &stake_pool_deposit_authority, - stake::state::StakeAuthorize::Withdrawer, - None, - ), - ]); - }; - - accounts.extend_from_slice(&[ - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*deposit_stake_address, false), - AccountMeta::new(*validator_stake_account, false), - AccountMeta::new(*reserve_stake_account, false), - AccountMeta::new(*pool_tokens_to, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*referrer_pool_tokens_account, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(*token_program_id, false), - AccountMeta::new_readonly(stake::program::id(), false), - ]); - instructions.push( - if let Some(minimum_pool_tokens_out) = minimum_pool_tokens_out { - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::DepositStakeWithSlippage { - minimum_pool_tokens_out, - }) - .unwrap(), - } - } else { - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::DepositStake).unwrap(), - } - }, - ); - instructions -} - -/// Creates instructions required to deposit into a stake pool, given a stake -/// account owned by the user. -pub fn deposit_stake( - program_id: &Pubkey, - stake_pool: &Pubkey, - validator_list_storage: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - deposit_stake_address: &Pubkey, - deposit_stake_withdraw_authority: &Pubkey, - validator_stake_account: &Pubkey, - reserve_stake_account: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, -) -> Vec { - deposit_stake_internal( - program_id, - stake_pool, - validator_list_storage, - None, - stake_pool_withdraw_authority, - deposit_stake_address, - deposit_stake_withdraw_authority, - validator_stake_account, - reserve_stake_account, - pool_tokens_to, - manager_fee_account, - referrer_pool_tokens_account, - pool_mint, - token_program_id, - None, - ) -} - -/// Creates instructions to deposit into a stake pool with slippage -pub fn deposit_stake_with_slippage( - program_id: &Pubkey, - stake_pool: &Pubkey, - validator_list_storage: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - deposit_stake_address: &Pubkey, - deposit_stake_withdraw_authority: &Pubkey, - validator_stake_account: &Pubkey, - reserve_stake_account: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - minimum_pool_tokens_out: u64, -) -> Vec { - deposit_stake_internal( - program_id, - stake_pool, - validator_list_storage, - None, - stake_pool_withdraw_authority, - deposit_stake_address, - deposit_stake_withdraw_authority, - validator_stake_account, - reserve_stake_account, - pool_tokens_to, - manager_fee_account, - referrer_pool_tokens_account, - pool_mint, - token_program_id, - Some(minimum_pool_tokens_out), - ) -} - -/// Creates instructions required to deposit into a stake pool, given a stake -/// account owned by the user. The difference with `deposit()` is that a deposit -/// authority must sign this instruction, which is required for private pools. -pub fn deposit_stake_with_authority( - program_id: &Pubkey, - stake_pool: &Pubkey, - validator_list_storage: &Pubkey, - stake_pool_deposit_authority: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - deposit_stake_address: &Pubkey, - deposit_stake_withdraw_authority: &Pubkey, - validator_stake_account: &Pubkey, - reserve_stake_account: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, -) -> Vec { - deposit_stake_internal( - program_id, - stake_pool, - validator_list_storage, - Some(stake_pool_deposit_authority), - stake_pool_withdraw_authority, - deposit_stake_address, - deposit_stake_withdraw_authority, - validator_stake_account, - reserve_stake_account, - pool_tokens_to, - manager_fee_account, - referrer_pool_tokens_account, - pool_mint, - token_program_id, - None, - ) -} - -/// Creates instructions required to deposit into a stake pool with slippage, -/// given a stake account owned by the user. The difference with `deposit()` is -/// that a deposit authority must sign this instruction, which is required for -/// private pools. -pub fn deposit_stake_with_authority_and_slippage( - program_id: &Pubkey, - stake_pool: &Pubkey, - validator_list_storage: &Pubkey, - stake_pool_deposit_authority: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - deposit_stake_address: &Pubkey, - deposit_stake_withdraw_authority: &Pubkey, - validator_stake_account: &Pubkey, - reserve_stake_account: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - minimum_pool_tokens_out: u64, -) -> Vec { - deposit_stake_internal( - program_id, - stake_pool, - validator_list_storage, - Some(stake_pool_deposit_authority), - stake_pool_withdraw_authority, - deposit_stake_address, - deposit_stake_withdraw_authority, - validator_stake_account, - reserve_stake_account, - pool_tokens_to, - manager_fee_account, - referrer_pool_tokens_account, - pool_mint, - token_program_id, - Some(minimum_pool_tokens_out), - ) -} - -/// Creates instructions required to deposit SOL directly into a stake pool. -fn deposit_sol_internal( - program_id: &Pubkey, - stake_pool: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_from: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - sol_deposit_authority: Option<&Pubkey>, - lamports_in: u64, - minimum_pool_tokens_out: Option, -) -> Instruction { - let mut accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new(*reserve_stake_account, false), - AccountMeta::new(*lamports_from, true), - AccountMeta::new(*pool_tokens_to, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*referrer_pool_tokens_account, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(*token_program_id, false), - ]; - if let Some(sol_deposit_authority) = sol_deposit_authority { - accounts.push(AccountMeta::new_readonly(*sol_deposit_authority, true)); - } - if let Some(minimum_pool_tokens_out) = minimum_pool_tokens_out { - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::DepositSolWithSlippage { - lamports_in, - minimum_pool_tokens_out, - }) - .unwrap(), - } - } else { - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::DepositSol(lamports_in)).unwrap(), - } - } -} - -/// Creates instruction to deposit SOL directly into a stake pool. -pub fn deposit_sol( - program_id: &Pubkey, - stake_pool: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_from: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - lamports_in: u64, -) -> Instruction { - deposit_sol_internal( - program_id, - stake_pool, - stake_pool_withdraw_authority, - reserve_stake_account, - lamports_from, - pool_tokens_to, - manager_fee_account, - referrer_pool_tokens_account, - pool_mint, - token_program_id, - None, - lamports_in, - None, - ) -} - -/// Creates instruction to deposit SOL directly into a stake pool with slippage -/// constraint. -pub fn deposit_sol_with_slippage( - program_id: &Pubkey, - stake_pool: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_from: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - lamports_in: u64, - minimum_pool_tokens_out: u64, -) -> Instruction { - deposit_sol_internal( - program_id, - stake_pool, - stake_pool_withdraw_authority, - reserve_stake_account, - lamports_from, - pool_tokens_to, - manager_fee_account, - referrer_pool_tokens_account, - pool_mint, - token_program_id, - None, - lamports_in, - Some(minimum_pool_tokens_out), - ) -} - -/// Creates instruction required to deposit SOL directly into a stake pool. -/// The difference with `deposit_sol()` is that a deposit -/// authority must sign this instruction. -pub fn deposit_sol_with_authority( - program_id: &Pubkey, - stake_pool: &Pubkey, - sol_deposit_authority: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_from: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - lamports_in: u64, -) -> Instruction { - deposit_sol_internal( - program_id, - stake_pool, - stake_pool_withdraw_authority, - reserve_stake_account, - lamports_from, - pool_tokens_to, - manager_fee_account, - referrer_pool_tokens_account, - pool_mint, - token_program_id, - Some(sol_deposit_authority), - lamports_in, - None, - ) -} - -/// Creates instruction to deposit SOL directly into a stake pool with slippage -/// constraint. -pub fn deposit_sol_with_authority_and_slippage( - program_id: &Pubkey, - stake_pool: &Pubkey, - sol_deposit_authority: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_from: &Pubkey, - pool_tokens_to: &Pubkey, - manager_fee_account: &Pubkey, - referrer_pool_tokens_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - lamports_in: u64, - minimum_pool_tokens_out: u64, -) -> Instruction { - deposit_sol_internal( - program_id, - stake_pool, - stake_pool_withdraw_authority, - reserve_stake_account, - lamports_from, - pool_tokens_to, - manager_fee_account, - referrer_pool_tokens_account, - pool_mint, - token_program_id, - Some(sol_deposit_authority), - lamports_in, - Some(minimum_pool_tokens_out), - ) -} - -fn withdraw_stake_internal( - program_id: &Pubkey, - stake_pool: &Pubkey, - validator_list_storage: &Pubkey, - stake_pool_withdraw: &Pubkey, - stake_to_split: &Pubkey, - stake_to_receive: &Pubkey, - user_stake_authority: &Pubkey, - user_transfer_authority: &Pubkey, - user_pool_token_account: &Pubkey, - manager_fee_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - pool_tokens_in: u64, - minimum_lamports_out: Option, -) -> Instruction { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new(*validator_list_storage, false), - AccountMeta::new_readonly(*stake_pool_withdraw, false), - AccountMeta::new(*stake_to_split, false), - AccountMeta::new(*stake_to_receive, false), - AccountMeta::new_readonly(*user_stake_authority, false), - AccountMeta::new_readonly(*user_transfer_authority, true), - AccountMeta::new(*user_pool_token_account, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(*token_program_id, false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - if let Some(minimum_lamports_out) = minimum_lamports_out { - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::WithdrawStakeWithSlippage { - pool_tokens_in, - minimum_lamports_out, - }) - .unwrap(), - } - } else { - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::WithdrawStake(pool_tokens_in)).unwrap(), - } - } -} - -/// Creates a 'WithdrawStake' instruction. -pub fn withdraw_stake( - program_id: &Pubkey, - stake_pool: &Pubkey, - validator_list_storage: &Pubkey, - stake_pool_withdraw: &Pubkey, - stake_to_split: &Pubkey, - stake_to_receive: &Pubkey, - user_stake_authority: &Pubkey, - user_transfer_authority: &Pubkey, - user_pool_token_account: &Pubkey, - manager_fee_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - pool_tokens_in: u64, -) -> Instruction { - withdraw_stake_internal( - program_id, - stake_pool, - validator_list_storage, - stake_pool_withdraw, - stake_to_split, - stake_to_receive, - user_stake_authority, - user_transfer_authority, - user_pool_token_account, - manager_fee_account, - pool_mint, - token_program_id, - pool_tokens_in, - None, - ) -} - -/// Creates a 'WithdrawStakeWithSlippage' instruction. -pub fn withdraw_stake_with_slippage( - program_id: &Pubkey, - stake_pool: &Pubkey, - validator_list_storage: &Pubkey, - stake_pool_withdraw: &Pubkey, - stake_to_split: &Pubkey, - stake_to_receive: &Pubkey, - user_stake_authority: &Pubkey, - user_transfer_authority: &Pubkey, - user_pool_token_account: &Pubkey, - manager_fee_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - pool_tokens_in: u64, - minimum_lamports_out: u64, -) -> Instruction { - withdraw_stake_internal( - program_id, - stake_pool, - validator_list_storage, - stake_pool_withdraw, - stake_to_split, - stake_to_receive, - user_stake_authority, - user_transfer_authority, - user_pool_token_account, - manager_fee_account, - pool_mint, - token_program_id, - pool_tokens_in, - Some(minimum_lamports_out), - ) -} - -fn withdraw_sol_internal( - program_id: &Pubkey, - stake_pool: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - user_transfer_authority: &Pubkey, - pool_tokens_from: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_to: &Pubkey, - manager_fee_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - sol_withdraw_authority: Option<&Pubkey>, - pool_tokens_in: u64, - minimum_lamports_out: Option, -) -> Instruction { - let mut accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), - AccountMeta::new_readonly(*user_transfer_authority, true), - AccountMeta::new(*pool_tokens_from, false), - AccountMeta::new(*reserve_stake_account, false), - AccountMeta::new(*lamports_to, false), - AccountMeta::new(*manager_fee_account, false), - AccountMeta::new(*pool_mint, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - AccountMeta::new_readonly(*token_program_id, false), - ]; - if let Some(sol_withdraw_authority) = sol_withdraw_authority { - accounts.push(AccountMeta::new_readonly(*sol_withdraw_authority, true)); - } - if let Some(minimum_lamports_out) = minimum_lamports_out { - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::WithdrawSolWithSlippage { - pool_tokens_in, - minimum_lamports_out, - }) - .unwrap(), - } - } else { - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::WithdrawSol(pool_tokens_in)).unwrap(), - } - } -} - -/// Creates instruction required to withdraw SOL directly from a stake pool. -pub fn withdraw_sol( - program_id: &Pubkey, - stake_pool: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - user_transfer_authority: &Pubkey, - pool_tokens_from: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_to: &Pubkey, - manager_fee_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - pool_tokens_in: u64, -) -> Instruction { - withdraw_sol_internal( - program_id, - stake_pool, - stake_pool_withdraw_authority, - user_transfer_authority, - pool_tokens_from, - reserve_stake_account, - lamports_to, - manager_fee_account, - pool_mint, - token_program_id, - None, - pool_tokens_in, - None, - ) -} - -/// Creates instruction required to withdraw SOL directly from a stake pool with -/// slippage constraints. -pub fn withdraw_sol_with_slippage( - program_id: &Pubkey, - stake_pool: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - user_transfer_authority: &Pubkey, - pool_tokens_from: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_to: &Pubkey, - manager_fee_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - pool_tokens_in: u64, - minimum_lamports_out: u64, -) -> Instruction { - withdraw_sol_internal( - program_id, - stake_pool, - stake_pool_withdraw_authority, - user_transfer_authority, - pool_tokens_from, - reserve_stake_account, - lamports_to, - manager_fee_account, - pool_mint, - token_program_id, - None, - pool_tokens_in, - Some(minimum_lamports_out), - ) -} - -/// Creates instruction required to withdraw SOL directly from a stake pool. -/// The difference with `withdraw_sol()` is that the sol withdraw authority -/// must sign this instruction. -pub fn withdraw_sol_with_authority( - program_id: &Pubkey, - stake_pool: &Pubkey, - sol_withdraw_authority: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - user_transfer_authority: &Pubkey, - pool_tokens_from: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_to: &Pubkey, - manager_fee_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - pool_tokens_in: u64, -) -> Instruction { - withdraw_sol_internal( - program_id, - stake_pool, - stake_pool_withdraw_authority, - user_transfer_authority, - pool_tokens_from, - reserve_stake_account, - lamports_to, - manager_fee_account, - pool_mint, - token_program_id, - Some(sol_withdraw_authority), - pool_tokens_in, - None, - ) -} - -/// Creates instruction required to withdraw SOL directly from a stake pool with -/// a slippage constraint. -/// The difference with `withdraw_sol()` is that the sol withdraw authority -/// must sign this instruction. -pub fn withdraw_sol_with_authority_and_slippage( - program_id: &Pubkey, - stake_pool: &Pubkey, - sol_withdraw_authority: &Pubkey, - stake_pool_withdraw_authority: &Pubkey, - user_transfer_authority: &Pubkey, - pool_tokens_from: &Pubkey, - reserve_stake_account: &Pubkey, - lamports_to: &Pubkey, - manager_fee_account: &Pubkey, - pool_mint: &Pubkey, - token_program_id: &Pubkey, - pool_tokens_in: u64, - minimum_lamports_out: u64, -) -> Instruction { - withdraw_sol_internal( - program_id, - stake_pool, - stake_pool_withdraw_authority, - user_transfer_authority, - pool_tokens_from, - reserve_stake_account, - lamports_to, - manager_fee_account, - pool_mint, - token_program_id, - Some(sol_withdraw_authority), - pool_tokens_in, - Some(minimum_lamports_out), - ) -} - -/// Creates a 'set manager' instruction. -pub fn set_manager( - program_id: &Pubkey, - stake_pool: &Pubkey, - manager: &Pubkey, - new_manager: &Pubkey, - new_fee_receiver: &Pubkey, -) -> Instruction { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*manager, true), - AccountMeta::new_readonly(*new_manager, true), - AccountMeta::new_readonly(*new_fee_receiver, false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::SetManager).unwrap(), - } -} - -/// Creates a 'set fee' instruction. -pub fn set_fee( - program_id: &Pubkey, - stake_pool: &Pubkey, - manager: &Pubkey, - fee: FeeType, -) -> Instruction { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*manager, true), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::SetFee { fee }).unwrap(), - } -} - -/// Creates a 'set staker' instruction. -pub fn set_staker( - program_id: &Pubkey, - stake_pool: &Pubkey, - set_staker_authority: &Pubkey, - new_staker: &Pubkey, -) -> Instruction { - let accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*set_staker_authority, true), - AccountMeta::new_readonly(*new_staker, false), - ]; - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::SetStaker).unwrap(), - } -} - -/// Creates a 'SetFundingAuthority' instruction. -pub fn set_funding_authority( - program_id: &Pubkey, - stake_pool: &Pubkey, - manager: &Pubkey, - new_sol_deposit_authority: Option<&Pubkey>, - funding_type: FundingType, -) -> Instruction { - let mut accounts = vec![ - AccountMeta::new(*stake_pool, false), - AccountMeta::new_readonly(*manager, true), - ]; - if let Some(auth) = new_sol_deposit_authority { - accounts.push(AccountMeta::new_readonly(*auth, false)) - } - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::SetFundingAuthority(funding_type)).unwrap(), - } -} - -/// Creates an instruction to update metadata in the mpl token metadata program -/// account for the pool token -pub fn update_token_metadata( - program_id: &Pubkey, - stake_pool: &Pubkey, - manager: &Pubkey, - pool_mint: &Pubkey, - name: String, - symbol: String, - uri: String, -) -> Instruction { - let (stake_pool_withdraw_authority, _) = - find_withdraw_authority_program_address(program_id, stake_pool); - let (token_metadata, _) = find_metadata_account(pool_mint); - - let accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*manager, true), - AccountMeta::new_readonly(stake_pool_withdraw_authority, false), - AccountMeta::new(token_metadata, false), - AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false), - ]; - - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::UpdateTokenMetadata { name, symbol, uri }) - .unwrap(), - } -} - -/// Creates an instruction to create metadata using the mpl token metadata -/// program for the pool token -pub fn create_token_metadata( - program_id: &Pubkey, - stake_pool: &Pubkey, - manager: &Pubkey, - pool_mint: &Pubkey, - payer: &Pubkey, - name: String, - symbol: String, - uri: String, -) -> Instruction { - let (stake_pool_withdraw_authority, _) = - find_withdraw_authority_program_address(program_id, stake_pool); - let (token_metadata, _) = find_metadata_account(pool_mint); - - let accounts = vec![ - AccountMeta::new_readonly(*stake_pool, false), - AccountMeta::new_readonly(*manager, true), - AccountMeta::new_readonly(stake_pool_withdraw_authority, false), - AccountMeta::new_readonly(*pool_mint, false), - AccountMeta::new(*payer, true), - AccountMeta::new(token_metadata, false), - AccountMeta::new_readonly(inline_mpl_token_metadata::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - ]; - - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&StakePoolInstruction::CreateTokenMetadata { name, symbol, uri }) - .unwrap(), - } -} diff --git a/stake-pool/program/src/lib.rs b/stake-pool/program/src/lib.rs deleted file mode 100644 index fe48880be98..00000000000 --- a/stake-pool/program/src/lib.rs +++ /dev/null @@ -1,175 +0,0 @@ -#![deny(missing_docs)] - -//! A program for creating and managing pools of stake - -pub mod big_vec; -pub mod error; -pub mod inline_mpl_token_metadata; -pub mod instruction; -pub mod processor; -pub mod state; - -#[cfg(not(feature = "no-entrypoint"))] -pub mod entrypoint; - -// Export current sdk types for downstream users building with a different sdk -// version -pub use solana_program; -use { - crate::state::Fee, - solana_program::{pubkey::Pubkey, stake::state::Meta}, - std::num::NonZeroU32, -}; - -/// Seed for deposit authority seed -const AUTHORITY_DEPOSIT: &[u8] = b"deposit"; - -/// Seed for withdraw authority seed -const AUTHORITY_WITHDRAW: &[u8] = b"withdraw"; - -/// Seed for transient stake account -const TRANSIENT_STAKE_SEED_PREFIX: &[u8] = b"transient"; - -/// Seed for ephemeral stake account -const EPHEMERAL_STAKE_SEED_PREFIX: &[u8] = b"ephemeral"; - -/// Minimum amount of staked lamports required in a validator stake account to -/// allow for merges without a mismatch on credits observed -pub const MINIMUM_ACTIVE_STAKE: u64 = 1_000_000; - -/// Minimum amount of lamports in the reserve -pub const MINIMUM_RESERVE_LAMPORTS: u64 = 0; - -/// Maximum amount of validator stake accounts to update per -/// `UpdateValidatorListBalance` instruction, based on compute limits -pub const MAX_VALIDATORS_TO_UPDATE: usize = 5; - -/// Maximum factor by which a withdrawal fee can be increased per epoch -/// protecting stakers from malicious users. -/// If current fee is 0, WITHDRAWAL_BASELINE_FEE is used as the baseline -pub const MAX_WITHDRAWAL_FEE_INCREASE: Fee = Fee { - numerator: 3, - denominator: 2, -}; -/// Drop-in baseline fee when evaluating withdrawal fee increases when fee is 0 -pub const WITHDRAWAL_BASELINE_FEE: Fee = Fee { - numerator: 1, - denominator: 1000, -}; - -/// The maximum number of transient stake accounts respecting -/// transaction account limits. -pub const MAX_TRANSIENT_STAKE_ACCOUNTS: usize = 10; - -/// Get the stake amount under consideration when calculating pool token -/// conversions -#[inline] -pub fn minimum_stake_lamports(meta: &Meta, stake_program_minimum_delegation: u64) -> u64 { - meta.rent_exempt_reserve - .saturating_add(minimum_delegation(stake_program_minimum_delegation)) -} - -/// Get the minimum delegation required by a stake account in a stake pool -#[inline] -pub fn minimum_delegation(stake_program_minimum_delegation: u64) -> u64 { - std::cmp::max(stake_program_minimum_delegation, MINIMUM_ACTIVE_STAKE) -} - -/// Get the stake amount under consideration when calculating pool token -/// conversions -#[inline] -pub fn minimum_reserve_lamports(meta: &Meta) -> u64 { - meta.rent_exempt_reserve - .saturating_add(MINIMUM_RESERVE_LAMPORTS) -} - -/// Generates the deposit authority program address for the stake pool -pub fn find_deposit_authority_program_address( - program_id: &Pubkey, - stake_pool_address: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[stake_pool_address.as_ref(), AUTHORITY_DEPOSIT], - program_id, - ) -} - -/// Generates the withdraw authority program address for the stake pool -pub fn find_withdraw_authority_program_address( - program_id: &Pubkey, - stake_pool_address: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[stake_pool_address.as_ref(), AUTHORITY_WITHDRAW], - program_id, - ) -} - -/// Generates the stake program address for a validator's vote account -pub fn find_stake_program_address( - program_id: &Pubkey, - vote_account_address: &Pubkey, - stake_pool_address: &Pubkey, - seed: Option, -) -> (Pubkey, u8) { - let seed = seed.map(|s| s.get().to_le_bytes()); - Pubkey::find_program_address( - &[ - vote_account_address.as_ref(), - stake_pool_address.as_ref(), - seed.as_ref().map(|s| s.as_slice()).unwrap_or(&[]), - ], - program_id, - ) -} - -/// Generates the stake program address for a validator's vote account -pub fn find_transient_stake_program_address( - program_id: &Pubkey, - vote_account_address: &Pubkey, - stake_pool_address: &Pubkey, - seed: u64, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[ - TRANSIENT_STAKE_SEED_PREFIX, - vote_account_address.as_ref(), - stake_pool_address.as_ref(), - &seed.to_le_bytes(), - ], - program_id, - ) -} - -/// Generates the ephemeral program address for stake pool redelegation -pub fn find_ephemeral_stake_program_address( - program_id: &Pubkey, - stake_pool_address: &Pubkey, - seed: u64, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[ - EPHEMERAL_STAKE_SEED_PREFIX, - stake_pool_address.as_ref(), - &seed.to_le_bytes(), - ], - program_id, - ) -} - -solana_program::declare_id!("SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy"); - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn validator_stake_account_derivation() { - let vote = Pubkey::new_unique(); - let stake_pool = Pubkey::new_unique(); - let function_derived = find_stake_program_address(&id(), &vote, &stake_pool, None); - let hand_derived = - Pubkey::find_program_address(&[vote.as_ref(), stake_pool.as_ref()], &id()); - assert_eq!(function_derived, hand_derived); - } -} diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs deleted file mode 100644 index 24887f26f25..00000000000 --- a/stake-pool/program/src/processor.rs +++ /dev/null @@ -1,3715 +0,0 @@ -//! Program state processor - -use { - crate::{ - error::StakePoolError, - find_deposit_authority_program_address, - inline_mpl_token_metadata::{ - self, - instruction::{create_metadata_accounts_v3, update_metadata_accounts_v2}, - pda::find_metadata_account, - state::DataV2, - }, - instruction::{FundingType, PreferredValidatorType, StakePoolInstruction}, - minimum_delegation, minimum_reserve_lamports, minimum_stake_lamports, - state::{ - is_extension_supported_for_mint, AccountType, Fee, FeeType, FutureEpoch, StakePool, - StakeStatus, StakeWithdrawSource, ValidatorList, ValidatorListHeader, - ValidatorStakeInfo, - }, - AUTHORITY_DEPOSIT, AUTHORITY_WITHDRAW, EPHEMERAL_STAKE_SEED_PREFIX, - TRANSIENT_STAKE_SEED_PREFIX, - }, - borsh::BorshDeserialize, - num_traits::FromPrimitive, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - borsh1::try_from_slice_unchecked, - clock::{Clock, Epoch}, - decode_error::DecodeError, - entrypoint::ProgramResult, - msg, - program::{invoke, invoke_signed}, - program_error::{PrintProgramError, ProgramError}, - pubkey::Pubkey, - rent::Rent, - stake, system_instruction, system_program, - sysvar::Sysvar, - }, - spl_token_2022::{ - check_spl_token_program_account, - extension::{BaseStateWithExtensions, StateWithExtensions}, - native_mint, - state::Mint, - }, - std::num::NonZeroU32, -}; - -/// Deserialize the stake state from AccountInfo -fn get_stake_state( - stake_account_info: &AccountInfo, -) -> Result<(stake::state::Meta, stake::state::Stake), ProgramError> { - let stake_state = - try_from_slice_unchecked::(&stake_account_info.data.borrow())?; - match stake_state { - stake::state::StakeStateV2::Stake(meta, stake, _) => Ok((meta, stake)), - _ => Err(StakePoolError::WrongStakeStake.into()), - } -} - -/// Check validity of vote address for a particular stake account -fn check_validator_stake_address( - program_id: &Pubkey, - stake_pool_address: &Pubkey, - stake_account_address: &Pubkey, - vote_address: &Pubkey, - seed: Option, -) -> Result<(), ProgramError> { - // Check stake account address validity - let (validator_stake_address, _) = - crate::find_stake_program_address(program_id, vote_address, stake_pool_address, seed); - if validator_stake_address != *stake_account_address { - msg!( - "Incorrect stake account address for vote {}, expected {}, received {}", - vote_address, - validator_stake_address, - stake_account_address - ); - Err(StakePoolError::InvalidStakeAccountAddress.into()) - } else { - Ok(()) - } -} - -/// Check validity of vote address for a particular stake account -fn check_transient_stake_address( - program_id: &Pubkey, - stake_pool_address: &Pubkey, - stake_account_address: &Pubkey, - vote_address: &Pubkey, - seed: u64, -) -> Result { - // Check stake account address validity - let (transient_stake_address, bump_seed) = crate::find_transient_stake_program_address( - program_id, - vote_address, - stake_pool_address, - seed, - ); - if transient_stake_address != *stake_account_address { - Err(StakePoolError::InvalidStakeAccountAddress.into()) - } else { - Ok(bump_seed) - } -} - -/// Check address validity for an ephemeral stake account -fn check_ephemeral_stake_address( - program_id: &Pubkey, - stake_pool_address: &Pubkey, - stake_account_address: &Pubkey, - seed: u64, -) -> Result { - // Check stake account address validity - let (ephemeral_stake_address, bump_seed) = - crate::find_ephemeral_stake_program_address(program_id, stake_pool_address, seed); - if ephemeral_stake_address != *stake_account_address { - Err(StakePoolError::InvalidStakeAccountAddress.into()) - } else { - Ok(bump_seed) - } -} - -/// Check mpl metadata account address for the pool mint -fn check_mpl_metadata_account_address( - metadata_address: &Pubkey, - pool_mint: &Pubkey, -) -> Result<(), ProgramError> { - let (metadata_account_pubkey, _) = find_metadata_account(pool_mint); - if metadata_account_pubkey != *metadata_address { - Err(StakePoolError::InvalidMetadataAccount.into()) - } else { - Ok(()) - } -} - -/// Check system program address -fn check_system_program(program_id: &Pubkey) -> Result<(), ProgramError> { - if *program_id != system_program::id() { - msg!( - "Expected system program {}, received {}", - system_program::id(), - program_id - ); - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } -} - -/// Check stake program address -fn check_stake_program(program_id: &Pubkey) -> Result<(), ProgramError> { - if *program_id != stake::program::id() { - msg!( - "Expected stake program {}, received {}", - stake::program::id(), - program_id - ); - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } -} - -/// Check mpl metadata program -fn check_mpl_metadata_program(program_id: &Pubkey) -> Result<(), ProgramError> { - if *program_id != inline_mpl_token_metadata::id() { - msg!( - "Expected mpl metadata program {}, received {}", - inline_mpl_token_metadata::id(), - program_id - ); - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } -} - -/// Check account owner is the given program -fn check_account_owner( - account_info: &AccountInfo, - program_id: &Pubkey, -) -> Result<(), ProgramError> { - if *program_id != *account_info.owner { - msg!( - "Expected account to be owned by program {}, received {}", - program_id, - account_info.owner - ); - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } -} - -/// Checks if a stake account can be managed by the pool -fn stake_is_usable_by_pool( - meta: &stake::state::Meta, - expected_authority: &Pubkey, - expected_lockup: &stake::state::Lockup, -) -> bool { - meta.authorized.staker == *expected_authority - && meta.authorized.withdrawer == *expected_authority - && meta.lockup == *expected_lockup -} - -/// Checks if a stake account is active, without taking into account cooldowns -fn stake_is_inactive_without_history(stake: &stake::state::Stake, epoch: Epoch) -> bool { - stake.delegation.deactivation_epoch < epoch - || (stake.delegation.activation_epoch == epoch - && stake.delegation.deactivation_epoch == epoch) -} - -/// Roughly checks if a stake account is deactivating -fn check_if_stake_deactivating( - account_info: &AccountInfo, - vote_account_address: &Pubkey, - epoch: Epoch, -) -> Result<(), ProgramError> { - let (_, stake) = get_stake_state(account_info)?; - if stake.delegation.deactivation_epoch != epoch { - msg!( - "Existing stake {} delegated to {} not deactivated in epoch {}", - account_info.key, - vote_account_address, - epoch, - ); - Err(StakePoolError::WrongStakeStake.into()) - } else { - Ok(()) - } -} - -/// Roughly checks if a stake account is activating -fn check_if_stake_activating( - account_info: &AccountInfo, - vote_account_address: &Pubkey, - epoch: Epoch, -) -> Result<(), ProgramError> { - let (_, stake) = get_stake_state(account_info)?; - if stake.delegation.deactivation_epoch != Epoch::MAX - || stake.delegation.activation_epoch != epoch - { - msg!( - "Existing stake {} delegated to {} not activated in epoch {}", - account_info.key, - vote_account_address, - epoch, - ); - Err(StakePoolError::WrongStakeStake.into()) - } else { - Ok(()) - } -} - -/// Check that the stake state is correct: usable by the pool and delegated to -/// the expected validator -fn check_stake_state( - stake_account_info: &AccountInfo, - withdraw_authority: &Pubkey, - vote_account_address: &Pubkey, - lockup: &stake::state::Lockup, -) -> Result<(), ProgramError> { - let (meta, stake) = get_stake_state(stake_account_info)?; - if !stake_is_usable_by_pool(&meta, withdraw_authority, lockup) { - msg!( - "Validator stake for {} not usable by pool, must be owned by withdraw authority", - vote_account_address - ); - return Err(StakePoolError::WrongStakeStake.into()); - } - if stake.delegation.voter_pubkey != *vote_account_address { - msg!( - "Validator stake {} not delegated to {}", - stake_account_info.key, - vote_account_address - ); - return Err(StakePoolError::WrongStakeStake.into()); - } - Ok(()) -} - -/// Checks if a validator stake account is valid, which means that it's usable -/// by the pool and delegated to the expected validator. These conditions can be -/// violated if a validator was force destaked during a cluster restart. -fn check_validator_stake_account( - stake_account_info: &AccountInfo, - program_id: &Pubkey, - stake_pool: &Pubkey, - withdraw_authority: &Pubkey, - vote_account_address: &Pubkey, - seed: u32, - lockup: &stake::state::Lockup, -) -> Result<(), ProgramError> { - check_account_owner(stake_account_info, &stake::program::id())?; - check_validator_stake_address( - program_id, - stake_pool, - stake_account_info.key, - vote_account_address, - NonZeroU32::new(seed), - )?; - check_stake_state( - stake_account_info, - withdraw_authority, - vote_account_address, - lockup, - )?; - Ok(()) -} - -/// Create a stake account on a PDA without transferring lamports -fn create_stake_account( - stake_account_info: AccountInfo<'_>, - stake_account_signer_seeds: &[&[u8]], - stake_space: usize, -) -> Result<(), ProgramError> { - invoke_signed( - &system_instruction::allocate(stake_account_info.key, stake_space as u64), - &[stake_account_info.clone()], - &[stake_account_signer_seeds], - )?; - invoke_signed( - &system_instruction::assign(stake_account_info.key, &stake::program::id()), - &[stake_account_info], - &[stake_account_signer_seeds], - ) -} - -/// Program state handler. -pub struct Processor {} -impl Processor { - /// Issue a delegate_stake instruction. - #[allow(clippy::too_many_arguments)] - fn stake_delegate<'a>( - stake_info: AccountInfo<'a>, - vote_account_info: AccountInfo<'a>, - clock_info: AccountInfo<'a>, - stake_history_info: AccountInfo<'a>, - stake_config_info: AccountInfo<'a>, - authority_info: AccountInfo<'a>, - stake_pool: &Pubkey, - authority_type: &[u8], - bump_seed: u8, - ) -> Result<(), ProgramError> { - let authority_signature_seeds = [stake_pool.as_ref(), authority_type, &[bump_seed]]; - let signers = &[&authority_signature_seeds[..]]; - - let ix = stake::instruction::delegate_stake( - stake_info.key, - authority_info.key, - vote_account_info.key, - ); - - invoke_signed( - &ix, - &[ - stake_info, - vote_account_info, - clock_info, - stake_history_info, - stake_config_info, - authority_info, - ], - signers, - ) - } - - /// Issue a stake_deactivate instruction. - fn stake_deactivate<'a>( - stake_info: AccountInfo<'a>, - clock_info: AccountInfo<'a>, - authority_info: AccountInfo<'a>, - stake_pool: &Pubkey, - authority_type: &[u8], - bump_seed: u8, - ) -> Result<(), ProgramError> { - let authority_signature_seeds = [stake_pool.as_ref(), authority_type, &[bump_seed]]; - let signers = &[&authority_signature_seeds[..]]; - - let ix = stake::instruction::deactivate_stake(stake_info.key, authority_info.key); - - invoke_signed(&ix, &[stake_info, clock_info, authority_info], signers) - } - - /// Issue a stake_split instruction. - fn stake_split<'a>( - stake_pool: &Pubkey, - stake_account: AccountInfo<'a>, - authority: AccountInfo<'a>, - authority_type: &[u8], - bump_seed: u8, - amount: u64, - split_stake: AccountInfo<'a>, - ) -> Result<(), ProgramError> { - let authority_signature_seeds = [stake_pool.as_ref(), authority_type, &[bump_seed]]; - let signers = &[&authority_signature_seeds[..]]; - - let split_instruction = - stake::instruction::split(stake_account.key, authority.key, amount, split_stake.key); - - invoke_signed( - split_instruction - .last() - .ok_or(ProgramError::InvalidInstructionData)?, - &[stake_account, split_stake, authority], - signers, - ) - } - - /// Issue a stake_merge instruction. - #[allow(clippy::too_many_arguments)] - fn stake_merge<'a>( - stake_pool: &Pubkey, - source_account: AccountInfo<'a>, - authority: AccountInfo<'a>, - authority_type: &[u8], - bump_seed: u8, - destination_account: AccountInfo<'a>, - clock: AccountInfo<'a>, - stake_history: AccountInfo<'a>, - ) -> Result<(), ProgramError> { - let authority_signature_seeds = [stake_pool.as_ref(), authority_type, &[bump_seed]]; - let signers = &[&authority_signature_seeds[..]]; - - let merge_instruction = - stake::instruction::merge(destination_account.key, source_account.key, authority.key); - - invoke_signed( - &merge_instruction[0], - &[ - destination_account, - source_account, - clock, - stake_history, - authority, - ], - signers, - ) - } - - /// Issue stake::instruction::authorize instructions to update both - /// authorities - fn stake_authorize<'a>( - stake_account: AccountInfo<'a>, - stake_authority: AccountInfo<'a>, - new_stake_authority: &Pubkey, - clock: AccountInfo<'a>, - ) -> Result<(), ProgramError> { - let authorize_instruction = stake::instruction::authorize( - stake_account.key, - stake_authority.key, - new_stake_authority, - stake::state::StakeAuthorize::Staker, - None, - ); - - invoke( - &authorize_instruction, - &[ - stake_account.clone(), - clock.clone(), - stake_authority.clone(), - ], - )?; - - let authorize_instruction = stake::instruction::authorize( - stake_account.key, - stake_authority.key, - new_stake_authority, - stake::state::StakeAuthorize::Withdrawer, - None, - ); - - invoke( - &authorize_instruction, - &[stake_account, clock, stake_authority], - ) - } - - /// Issue stake::instruction::authorize instructions to update both - /// authorities - #[allow(clippy::too_many_arguments)] - fn stake_authorize_signed<'a>( - stake_pool: &Pubkey, - stake_account: AccountInfo<'a>, - stake_authority: AccountInfo<'a>, - authority_type: &[u8], - bump_seed: u8, - new_stake_authority: &Pubkey, - clock: AccountInfo<'a>, - ) -> Result<(), ProgramError> { - let authority_signature_seeds = [stake_pool.as_ref(), authority_type, &[bump_seed]]; - let signers = &[&authority_signature_seeds[..]]; - - let authorize_instruction = stake::instruction::authorize( - stake_account.key, - stake_authority.key, - new_stake_authority, - stake::state::StakeAuthorize::Staker, - None, - ); - - invoke_signed( - &authorize_instruction, - &[ - stake_account.clone(), - clock.clone(), - stake_authority.clone(), - ], - signers, - )?; - - let authorize_instruction = stake::instruction::authorize( - stake_account.key, - stake_authority.key, - new_stake_authority, - stake::state::StakeAuthorize::Withdrawer, - None, - ); - invoke_signed( - &authorize_instruction, - &[stake_account, clock, stake_authority], - signers, - ) - } - - /// Issue stake::instruction::withdraw instruction to move additional - /// lamports - #[allow(clippy::too_many_arguments)] - fn stake_withdraw<'a>( - stake_pool: &Pubkey, - source_account: AccountInfo<'a>, - authority: AccountInfo<'a>, - authority_type: &[u8], - bump_seed: u8, - destination_account: AccountInfo<'a>, - clock: AccountInfo<'a>, - stake_history: AccountInfo<'a>, - lamports: u64, - ) -> Result<(), ProgramError> { - let authority_signature_seeds = [stake_pool.as_ref(), authority_type, &[bump_seed]]; - let signers = &[&authority_signature_seeds[..]]; - let custodian_pubkey = None; - - let withdraw_instruction = stake::instruction::withdraw( - source_account.key, - authority.key, - destination_account.key, - lamports, - custodian_pubkey, - ); - - invoke_signed( - &withdraw_instruction, - &[ - source_account, - destination_account, - clock, - stake_history, - authority, - ], - signers, - ) - } - - /// Issue a spl_token `Burn` instruction. - #[allow(clippy::too_many_arguments)] - fn token_burn<'a>( - token_program: AccountInfo<'a>, - burn_account: AccountInfo<'a>, - mint: AccountInfo<'a>, - authority: AccountInfo<'a>, - amount: u64, - ) -> Result<(), ProgramError> { - let ix = spl_token_2022::instruction::burn( - token_program.key, - burn_account.key, - mint.key, - authority.key, - &[], - amount, - )?; - - invoke(&ix, &[burn_account, mint, authority]) - } - - /// Issue a spl_token `MintTo` instruction. - #[allow(clippy::too_many_arguments)] - fn token_mint_to<'a>( - stake_pool: &Pubkey, - token_program: AccountInfo<'a>, - mint: AccountInfo<'a>, - destination: AccountInfo<'a>, - authority: AccountInfo<'a>, - authority_type: &[u8], - bump_seed: u8, - amount: u64, - ) -> Result<(), ProgramError> { - let authority_signature_seeds = [stake_pool.as_ref(), authority_type, &[bump_seed]]; - let signers = &[&authority_signature_seeds[..]]; - - let ix = spl_token_2022::instruction::mint_to( - token_program.key, - mint.key, - destination.key, - authority.key, - &[], - amount, - )?; - - invoke_signed(&ix, &[mint, destination, authority], signers) - } - - /// Issue a spl_token `Transfer` instruction. - #[allow(clippy::too_many_arguments)] - fn token_transfer<'a>( - token_program: AccountInfo<'a>, - source: AccountInfo<'a>, - mint: AccountInfo<'a>, - destination: AccountInfo<'a>, - authority: AccountInfo<'a>, - amount: u64, - decimals: u8, - ) -> Result<(), ProgramError> { - let ix = spl_token_2022::instruction::transfer_checked( - token_program.key, - source.key, - mint.key, - destination.key, - authority.key, - &[], - amount, - decimals, - )?; - invoke(&ix, &[source, mint, destination, authority]) - } - - fn sol_transfer<'a>( - source: AccountInfo<'a>, - destination: AccountInfo<'a>, - amount: u64, - ) -> Result<(), ProgramError> { - let ix = solana_program::system_instruction::transfer(source.key, destination.key, amount); - invoke(&ix, &[source, destination]) - } - - /// Processes `Initialize` instruction. - #[inline(never)] // needed due to stack size violation - fn process_initialize( - program_id: &Pubkey, - accounts: &[AccountInfo], - epoch_fee: Fee, - withdrawal_fee: Fee, - deposit_fee: Fee, - referral_fee: u8, - max_validators: u32, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let manager_info = next_account_info(account_info_iter)?; - let staker_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - let reserve_stake_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let manager_fee_info = next_account_info(account_info_iter)?; - let token_program_info = next_account_info(account_info_iter)?; - - let rent = Rent::get()?; - - if !manager_info.is_signer { - msg!("Manager did not sign initialization"); - return Err(StakePoolError::SignatureMissing.into()); - } - - if stake_pool_info.key == validator_list_info.key { - msg!("Cannot use same account for stake pool and validator list"); - return Err(StakePoolError::AlreadyInUse.into()); - } - - // This check is unnecessary since the runtime will check the ownership, - // but provides clarity that the parameter is in fact checked. - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_uninitialized() { - msg!("Provided stake pool already in use"); - return Err(StakePoolError::AlreadyInUse.into()); - } - - // This check is unnecessary since the runtime will check the ownership, - // but provides clarity that the parameter is in fact checked. - check_account_owner(validator_list_info, program_id)?; - let mut validator_list = - try_from_slice_unchecked::(&validator_list_info.data.borrow())?; - if !validator_list.header.is_uninitialized() { - msg!("Provided validator list already in use"); - return Err(StakePoolError::AlreadyInUse.into()); - } - - let data_length = validator_list_info.data_len(); - let expected_max_validators = ValidatorList::calculate_max_validators(data_length); - if expected_max_validators != max_validators as usize || max_validators == 0 { - msg!( - "Incorrect validator list size provided, expected {}, provided {}", - expected_max_validators, - max_validators - ); - return Err(StakePoolError::UnexpectedValidatorListAccountSize.into()); - } - validator_list.header.account_type = AccountType::ValidatorList; - validator_list.header.max_validators = max_validators; - validator_list.validators.clear(); - - if !rent.is_exempt(stake_pool_info.lamports(), stake_pool_info.data_len()) { - msg!("Stake pool not rent-exempt"); - return Err(ProgramError::AccountNotRentExempt); - } - - if !rent.is_exempt( - validator_list_info.lamports(), - validator_list_info.data_len(), - ) { - msg!("Validator stake list not rent-exempt"); - return Err(ProgramError::AccountNotRentExempt); - } - - // Numerator should be smaller than or equal to denominator (fee <= 1) - if epoch_fee.numerator > epoch_fee.denominator - || withdrawal_fee.numerator > withdrawal_fee.denominator - || deposit_fee.numerator > deposit_fee.denominator - || referral_fee > 100u8 - { - return Err(StakePoolError::FeeTooHigh.into()); - } - - check_spl_token_program_account(token_program_info.key)?; - - if pool_mint_info.owner != token_program_info.key { - return Err(ProgramError::IncorrectProgramId); - } - - stake_pool.token_program_id = *token_program_info.key; - stake_pool.pool_mint = *pool_mint_info.key; - - let (stake_deposit_authority, sol_deposit_authority) = - match next_account_info(account_info_iter) { - Ok(deposit_authority_info) => ( - *deposit_authority_info.key, - Some(*deposit_authority_info.key), - ), - Err(_) => ( - find_deposit_authority_program_address(program_id, stake_pool_info.key).0, - None, - ), - }; - let (withdraw_authority_key, stake_withdraw_bump_seed) = - crate::find_withdraw_authority_program_address(program_id, stake_pool_info.key); - if withdraw_authority_key != *withdraw_authority_info.key { - msg!( - "Incorrect withdraw authority provided, expected {}, received {}", - withdraw_authority_key, - withdraw_authority_info.key - ); - return Err(StakePoolError::InvalidProgramAddress.into()); - } - - { - let pool_mint_data = pool_mint_info.try_borrow_data()?; - let pool_mint = StateWithExtensions::::unpack(&pool_mint_data)?; - - if pool_mint.base.supply != 0 { - return Err(StakePoolError::NonZeroPoolTokenSupply.into()); - } - - if pool_mint.base.decimals != native_mint::DECIMALS { - return Err(StakePoolError::IncorrectMintDecimals.into()); - } - - if !pool_mint - .base - .mint_authority - .contains(&withdraw_authority_key) - { - return Err(StakePoolError::WrongMintingAuthority.into()); - } - - if pool_mint.base.freeze_authority.is_some() { - return Err(StakePoolError::InvalidMintFreezeAuthority.into()); - } - - let extensions = pool_mint.get_extension_types()?; - if extensions - .iter() - .any(|x| !is_extension_supported_for_mint(x)) - { - return Err(StakePoolError::UnsupportedMintExtension.into()); - } - } - stake_pool.check_manager_fee_info(manager_fee_info)?; - - if *reserve_stake_info.owner != stake::program::id() { - msg!("Reserve stake account not owned by stake program"); - return Err(ProgramError::IncorrectProgramId); - } - let stake_state = try_from_slice_unchecked::( - &reserve_stake_info.data.borrow(), - )?; - let total_lamports = if let stake::state::StakeStateV2::Initialized(meta) = stake_state { - if meta.lockup != stake::state::Lockup::default() { - msg!("Reserve stake account has some lockup"); - return Err(StakePoolError::WrongStakeStake.into()); - } - - if meta.authorized.staker != withdraw_authority_key { - msg!( - "Reserve stake account has incorrect staker {}, should be {}", - meta.authorized.staker, - withdraw_authority_key - ); - return Err(StakePoolError::WrongStakeStake.into()); - } - - if meta.authorized.withdrawer != withdraw_authority_key { - msg!( - "Reserve stake account has incorrect withdrawer {}, should be {}", - meta.authorized.staker, - withdraw_authority_key - ); - return Err(StakePoolError::WrongStakeStake.into()); - } - reserve_stake_info - .lamports() - .checked_sub(minimum_reserve_lamports(&meta)) - .ok_or(StakePoolError::CalculationFailure)? - } else { - msg!("Reserve stake account not in intialized state"); - return Err(StakePoolError::WrongStakeStake.into()); - }; - - if total_lamports > 0 { - Self::token_mint_to( - stake_pool_info.key, - token_program_info.clone(), - pool_mint_info.clone(), - manager_fee_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_withdraw_bump_seed, - total_lamports, - )?; - } - - borsh::to_writer( - &mut validator_list_info.data.borrow_mut()[..], - &validator_list, - )?; - - stake_pool.account_type = AccountType::StakePool; - stake_pool.manager = *manager_info.key; - stake_pool.staker = *staker_info.key; - stake_pool.stake_deposit_authority = stake_deposit_authority; - stake_pool.stake_withdraw_bump_seed = stake_withdraw_bump_seed; - stake_pool.validator_list = *validator_list_info.key; - stake_pool.reserve_stake = *reserve_stake_info.key; - stake_pool.manager_fee_account = *manager_fee_info.key; - stake_pool.total_lamports = total_lamports; - stake_pool.pool_token_supply = total_lamports; - stake_pool.last_update_epoch = Clock::get()?.epoch; - stake_pool.lockup = stake::state::Lockup::default(); - stake_pool.epoch_fee = epoch_fee; - stake_pool.next_epoch_fee = FutureEpoch::None; - stake_pool.preferred_deposit_validator_vote_address = None; - stake_pool.preferred_withdraw_validator_vote_address = None; - stake_pool.stake_deposit_fee = deposit_fee; - stake_pool.stake_withdrawal_fee = withdrawal_fee; - stake_pool.next_stake_withdrawal_fee = FutureEpoch::None; - stake_pool.stake_referral_fee = referral_fee; - stake_pool.sol_deposit_authority = sol_deposit_authority; - stake_pool.sol_deposit_fee = deposit_fee; - stake_pool.sol_referral_fee = referral_fee; - stake_pool.sol_withdraw_authority = None; - stake_pool.sol_withdrawal_fee = withdrawal_fee; - stake_pool.next_sol_withdrawal_fee = FutureEpoch::None; - stake_pool.last_epoch_pool_token_supply = 0; - stake_pool.last_epoch_total_lamports = 0; - - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool) - .map_err(|e| e.into()) - } - - /// Processes `AddValidatorToPool` instruction. - #[inline(never)] // needed due to stack size violation - fn process_add_validator_to_pool( - program_id: &Pubkey, - accounts: &[AccountInfo], - raw_validator_seed: u32, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let staker_info = next_account_info(account_info_iter)?; - let reserve_stake_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - let stake_info = next_account_info(account_info_iter)?; - let validator_vote_info = next_account_info(account_info_iter)?; - let rent_info = next_account_info(account_info_iter)?; - let rent = &Rent::from_account_info(rent_info)?; - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let stake_history_info = next_account_info(account_info_iter)?; - let stake_config_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - - check_system_program(system_program_info.key)?; - check_stake_program(stake_program_info.key)?; - - check_account_owner(stake_pool_info, program_id)?; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - - stake_pool.check_staker(staker_info)?; - stake_pool.check_reserve_stake(reserve_stake_info)?; - stake_pool.check_validator_list(validator_list_info)?; - - if stake_pool.last_update_epoch < clock.epoch { - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - - check_account_owner(validator_list_info, program_id)?; - let mut validator_list_data = validator_list_info.data.borrow_mut(); - let (header, mut validator_list) = - ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - if !header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - if header.max_validators == validator_list.len() { - return Err(ProgramError::AccountDataTooSmall); - } - let maybe_validator_stake_info = validator_list.find::(|x| { - ValidatorStakeInfo::memcmp_pubkey(x, validator_vote_info.key) - }); - if maybe_validator_stake_info.is_some() { - return Err(StakePoolError::ValidatorAlreadyAdded.into()); - } - - let validator_seed = NonZeroU32::new(raw_validator_seed); - let (stake_address, bump_seed) = crate::find_stake_program_address( - program_id, - validator_vote_info.key, - stake_pool_info.key, - validator_seed, - ); - if stake_address != *stake_info.key { - return Err(StakePoolError::InvalidStakeAccountAddress.into()); - } - - let validator_seed_bytes = validator_seed.map(|s| s.get().to_le_bytes()); - let stake_account_signer_seeds: &[&[_]] = &[ - validator_vote_info.key.as_ref(), - stake_pool_info.key.as_ref(), - validator_seed_bytes - .as_ref() - .map(|s| s.as_slice()) - .unwrap_or(&[]), - &[bump_seed], - ]; - - // Fund the stake account with the minimum + rent-exempt balance - let stake_space = std::mem::size_of::(); - let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; - let required_lamports = minimum_delegation(stake_minimum_delegation) - .saturating_add(rent.minimum_balance(stake_space)); - - // Check that we're not draining the reserve totally - let reserve_stake = try_from_slice_unchecked::( - &reserve_stake_info.data.borrow(), - )?; - let reserve_meta = reserve_stake - .meta() - .ok_or(StakePoolError::WrongStakeStake)?; - let minimum_lamports = minimum_reserve_lamports(&reserve_meta); - let reserve_lamports = reserve_stake_info.lamports(); - if reserve_lamports.saturating_sub(required_lamports) < minimum_lamports { - msg!( - "Need to add {} lamports for the reserve stake to be rent-exempt after adding a validator, reserve currently has {} lamports", - required_lamports.saturating_add(minimum_lamports).saturating_sub(reserve_lamports), - reserve_lamports - ); - return Err(ProgramError::InsufficientFunds); - } - - // Create new stake account - create_stake_account(stake_info.clone(), stake_account_signer_seeds, stake_space)?; - // split into validator stake account - Self::stake_split( - stake_pool_info.key, - reserve_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - required_lamports, - stake_info.clone(), - )?; - - Self::stake_delegate( - stake_info.clone(), - validator_vote_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - stake_config_info.clone(), - withdraw_authority_info.clone(), - stake_pool_info.key, - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - )?; - - validator_list.push(ValidatorStakeInfo { - status: StakeStatus::Active.into(), - vote_account_address: *validator_vote_info.key, - active_stake_lamports: required_lamports.into(), - transient_stake_lamports: 0.into(), - last_update_epoch: clock.epoch.into(), - transient_seed_suffix: 0.into(), - unused: 0.into(), - validator_seed_suffix: raw_validator_seed.into(), - })?; - - Ok(()) - } - - /// Processes `RemoveValidatorFromPool` instruction. - #[inline(never)] // needed due to stack size violation - fn process_remove_validator_from_pool( - program_id: &Pubkey, - accounts: &[AccountInfo], - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let staker_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - let stake_account_info = next_account_info(account_info_iter)?; - let transient_stake_account_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let stake_program_info = next_account_info(account_info_iter)?; - - check_stake_program(stake_program_info.key)?; - check_account_owner(stake_pool_info, program_id)?; - - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - stake_pool.check_staker(staker_info)?; - - if stake_pool.last_update_epoch < clock.epoch { - msg!( - "clock {} pool {}", - clock.epoch, - stake_pool.last_update_epoch - ); - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - - stake_pool.check_validator_list(validator_list_info)?; - - check_account_owner(validator_list_info, program_id)?; - let mut validator_list_data = validator_list_info.data.borrow_mut(); - let (header, mut validator_list) = - ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - if !header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - let (_, stake) = get_stake_state(stake_account_info)?; - let vote_account_address = stake.delegation.voter_pubkey; - let maybe_validator_stake_info = validator_list.find_mut::(|x| { - ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) - }); - if maybe_validator_stake_info.is_none() { - msg!( - "Vote account {} not found in stake pool", - vote_account_address - ); - return Err(StakePoolError::ValidatorNotFound.into()); - } - let validator_stake_info = maybe_validator_stake_info.unwrap(); - check_validator_stake_address( - program_id, - stake_pool_info.key, - stake_account_info.key, - &vote_account_address, - NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()), - )?; - - if validator_stake_info.status != StakeStatus::Active.into() { - msg!("Validator is already marked for removal"); - return Err(StakePoolError::ValidatorNotFound.into()); - } - - let new_status = if u64::from(validator_stake_info.transient_stake_lamports) > 0 { - check_transient_stake_address( - program_id, - stake_pool_info.key, - transient_stake_account_info.key, - &vote_account_address, - validator_stake_info.transient_seed_suffix.into(), - )?; - - match get_stake_state(transient_stake_account_info) { - Ok((meta, stake)) - if stake_is_usable_by_pool( - &meta, - withdraw_authority_info.key, - &stake_pool.lockup, - ) => - { - if stake.delegation.deactivation_epoch == Epoch::MAX { - Self::stake_deactivate( - transient_stake_account_info.clone(), - clock_info.clone(), - withdraw_authority_info.clone(), - stake_pool_info.key, - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - )?; - } - StakeStatus::DeactivatingAll - } - _ => StakeStatus::DeactivatingValidator, - } - } else { - StakeStatus::DeactivatingValidator - }; - - // If the stake was force-deactivated through deactivate-delinquent or - // some other means, we *do not* need to deactivate it again - if stake.delegation.deactivation_epoch == Epoch::MAX { - Self::stake_deactivate( - stake_account_info.clone(), - clock_info.clone(), - withdraw_authority_info.clone(), - stake_pool_info.key, - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - )?; - } - - validator_stake_info.status = new_status.into(); - - if stake_pool.preferred_deposit_validator_vote_address == Some(vote_account_address) { - stake_pool.preferred_deposit_validator_vote_address = None; - } - if stake_pool.preferred_withdraw_validator_vote_address == Some(vote_account_address) { - stake_pool.preferred_withdraw_validator_vote_address = None; - } - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - - Ok(()) - } - - /// Processes `DecreaseValidatorStake` instruction. - #[inline(never)] // needed due to stack size violation - fn process_decrease_validator_stake( - program_id: &Pubkey, - accounts: &[AccountInfo], - lamports: u64, - transient_stake_seed: u64, - maybe_ephemeral_stake_seed: Option, - fund_rent_exempt_reserve: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let staker_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - let maybe_reserve_stake_info = fund_rent_exempt_reserve - .then(|| next_account_info(account_info_iter)) - .transpose()?; - let validator_stake_account_info = next_account_info(account_info_iter)?; - let maybe_ephemeral_stake_account_info = maybe_ephemeral_stake_seed - .map(|_| next_account_info(account_info_iter)) - .transpose()?; - let transient_stake_account_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let (rent, maybe_stake_history_info) = - if maybe_ephemeral_stake_seed.is_some() || fund_rent_exempt_reserve { - (Rent::get()?, Some(next_account_info(account_info_iter)?)) - } else { - // legacy instruction takes the rent account - let rent_info = next_account_info(account_info_iter)?; - (Rent::from_account_info(rent_info)?, None) - }; - let system_program_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - - check_system_program(system_program_info.key)?; - check_stake_program(stake_program_info.key)?; - check_account_owner(stake_pool_info, program_id)?; - - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - msg!("Expected valid stake pool"); - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - stake_pool.check_staker(staker_info)?; - - if stake_pool.last_update_epoch < clock.epoch { - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - - stake_pool.check_validator_list(validator_list_info)?; - check_account_owner(validator_list_info, program_id)?; - let validator_list_data = &mut *validator_list_info.data.borrow_mut(); - let (validator_list_header, mut validator_list) = - ValidatorListHeader::deserialize_vec(validator_list_data)?; - if !validator_list_header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - if let Some(reserve_stake_info) = maybe_reserve_stake_info { - stake_pool.check_reserve_stake(reserve_stake_info)?; - } - - let (meta, stake) = get_stake_state(validator_stake_account_info)?; - let vote_account_address = stake.delegation.voter_pubkey; - - let maybe_validator_stake_info = validator_list.find_mut::(|x| { - ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) - }); - if maybe_validator_stake_info.is_none() { - msg!( - "Vote account {} not found in stake pool", - vote_account_address - ); - return Err(StakePoolError::ValidatorNotFound.into()); - } - let validator_stake_info = maybe_validator_stake_info.unwrap(); - check_validator_stake_address( - program_id, - stake_pool_info.key, - validator_stake_account_info.key, - &vote_account_address, - NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()), - )?; - if u64::from(validator_stake_info.transient_stake_lamports) > 0 { - if maybe_ephemeral_stake_seed.is_none() { - msg!("Attempting to decrease stake on a validator with pending transient stake, use DecreaseAdditionalValidatorStake with the existing seed"); - return Err(StakePoolError::TransientAccountInUse.into()); - } - if transient_stake_seed != u64::from(validator_stake_info.transient_seed_suffix) { - msg!( - "Transient stake already exists with seed {}, you must use that one", - u64::from(validator_stake_info.transient_seed_suffix) - ); - return Err(ProgramError::InvalidSeeds); - } - check_if_stake_deactivating( - transient_stake_account_info, - &vote_account_address, - clock.epoch, - )?; - } - - let stake_space = std::mem::size_of::(); - let stake_rent = rent.minimum_balance(stake_space); - - let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; - let current_minimum_lamports = minimum_delegation(stake_minimum_delegation); - if lamports < current_minimum_lamports { - msg!( - "Need at least {} lamports for transient stake to meet minimum delegation and rent-exempt requirements, {} provided", - current_minimum_lamports, - lamports - ); - return Err(ProgramError::AccountNotRentExempt); - } - - let remaining_lamports = validator_stake_account_info - .lamports() - .checked_sub(lamports) - .ok_or(ProgramError::InsufficientFunds)?; - let required_lamports = minimum_stake_lamports(&meta, stake_minimum_delegation); - if remaining_lamports < required_lamports { - msg!("Need at least {} lamports in the stake account after decrease, {} requested, {} is the current possible maximum", - required_lamports, - lamports, - validator_stake_account_info.lamports().checked_sub(required_lamports).ok_or(StakePoolError::CalculationFailure)? - ); - return Err(ProgramError::InsufficientFunds); - } - - let (source_stake_account_info, split_lamports) = - if let Some((ephemeral_stake_seed, ephemeral_stake_account_info)) = - maybe_ephemeral_stake_seed.zip(maybe_ephemeral_stake_account_info) - { - let ephemeral_stake_bump_seed = check_ephemeral_stake_address( - program_id, - stake_pool_info.key, - ephemeral_stake_account_info.key, - ephemeral_stake_seed, - )?; - let ephemeral_stake_account_signer_seeds: &[&[_]] = &[ - EPHEMERAL_STAKE_SEED_PREFIX, - stake_pool_info.key.as_ref(), - &ephemeral_stake_seed.to_le_bytes(), - &[ephemeral_stake_bump_seed], - ]; - create_stake_account( - ephemeral_stake_account_info.clone(), - ephemeral_stake_account_signer_seeds, - stake_space, - )?; - - // if needed, withdraw rent-exempt reserve for ephemeral account - if let Some(reserve_stake_info) = maybe_reserve_stake_info { - let required_lamports_for_rent_exemption = - stake_rent.saturating_sub(ephemeral_stake_account_info.lamports()); - if required_lamports_for_rent_exemption > 0 { - if required_lamports_for_rent_exemption >= reserve_stake_info.lamports() { - return Err(StakePoolError::ReserveDepleted.into()); - } - let stake_history_info = maybe_stake_history_info - .ok_or(StakePoolError::MissingRequiredSysvar)?; - Self::stake_withdraw( - stake_pool_info.key, - reserve_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - ephemeral_stake_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - required_lamports_for_rent_exemption, - )?; - } - } - - // split into ephemeral stake account - Self::stake_split( - stake_pool_info.key, - validator_stake_account_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - lamports, - ephemeral_stake_account_info.clone(), - )?; - - Self::stake_deactivate( - ephemeral_stake_account_info.clone(), - clock_info.clone(), - withdraw_authority_info.clone(), - stake_pool_info.key, - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - )?; - - ( - ephemeral_stake_account_info, - ephemeral_stake_account_info.lamports(), - ) - } else { - // if no ephemeral account is provided, split everything from the - // validator stake account, into the transient stake account - (validator_stake_account_info, lamports) - }; - - let transient_stake_bump_seed = check_transient_stake_address( - program_id, - stake_pool_info.key, - transient_stake_account_info.key, - &vote_account_address, - transient_stake_seed, - )?; - - if u64::from(validator_stake_info.transient_stake_lamports) > 0 { - let stake_history_info = maybe_stake_history_info.unwrap(); - // transient stake exists, try to merge from the source account, - // which is always an ephemeral account - Self::stake_merge( - stake_pool_info.key, - source_stake_account_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - transient_stake_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - )?; - } else { - let transient_stake_account_signer_seeds: &[&[_]] = &[ - TRANSIENT_STAKE_SEED_PREFIX, - vote_account_address.as_ref(), - stake_pool_info.key.as_ref(), - &transient_stake_seed.to_le_bytes(), - &[transient_stake_bump_seed], - ]; - - create_stake_account( - transient_stake_account_info.clone(), - transient_stake_account_signer_seeds, - stake_space, - )?; - - // if needed, withdraw rent-exempt reserve for transient account - if let Some(reserve_stake_info) = maybe_reserve_stake_info { - let required_lamports = - stake_rent.saturating_sub(transient_stake_account_info.lamports()); - // in the case of doing a full split from an ephemeral account, - // the rent-exempt reserve moves over, so no need to fund it from - // the pool reserve - if source_stake_account_info.lamports() != split_lamports { - let stake_history_info = - maybe_stake_history_info.ok_or(StakePoolError::MissingRequiredSysvar)?; - if required_lamports >= reserve_stake_info.lamports() { - return Err(StakePoolError::ReserveDepleted.into()); - } - if required_lamports > 0 { - Self::stake_withdraw( - stake_pool_info.key, - reserve_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - transient_stake_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - required_lamports, - )?; - } - } - } - - // split into transient stake account - Self::stake_split( - stake_pool_info.key, - source_stake_account_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - split_lamports, - transient_stake_account_info.clone(), - )?; - - // Deactivate transient stake if necessary - let (_, stake) = get_stake_state(transient_stake_account_info)?; - if stake.delegation.deactivation_epoch == Epoch::MAX { - Self::stake_deactivate( - transient_stake_account_info.clone(), - clock_info.clone(), - withdraw_authority_info.clone(), - stake_pool_info.key, - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - )?; - } - } - - validator_stake_info.active_stake_lamports = - u64::from(validator_stake_info.active_stake_lamports) - .checked_sub(lamports) - .ok_or(StakePoolError::CalculationFailure)? - .into(); - validator_stake_info.transient_stake_lamports = - transient_stake_account_info.lamports().into(); - validator_stake_info.transient_seed_suffix = transient_stake_seed.into(); - - Ok(()) - } - - /// Processes `IncreaseValidatorStake` instruction. - #[inline(never)] // needed due to stack size violation - fn process_increase_validator_stake( - program_id: &Pubkey, - accounts: &[AccountInfo], - lamports: u64, - transient_stake_seed: u64, - maybe_ephemeral_stake_seed: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let staker_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - let reserve_stake_account_info = next_account_info(account_info_iter)?; - let maybe_ephemeral_stake_account_info = maybe_ephemeral_stake_seed - .map(|_| next_account_info(account_info_iter)) - .transpose()?; - let transient_stake_account_info = next_account_info(account_info_iter)?; - let validator_stake_account_info = next_account_info(account_info_iter)?; - let validator_vote_account_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let rent = if maybe_ephemeral_stake_seed.is_some() { - // instruction with ephemeral account doesn't take the rent account - Rent::get()? - } else { - // legacy instruction takes the rent account - let rent_info = next_account_info(account_info_iter)?; - Rent::from_account_info(rent_info)? - }; - let stake_history_info = next_account_info(account_info_iter)?; - let stake_config_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - - check_system_program(system_program_info.key)?; - check_stake_program(stake_program_info.key)?; - check_account_owner(stake_pool_info, program_id)?; - - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - msg!("Expected valid stake pool"); - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - stake_pool.check_staker(staker_info)?; - - if stake_pool.last_update_epoch < clock.epoch { - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - - stake_pool.check_validator_list(validator_list_info)?; - stake_pool.check_reserve_stake(reserve_stake_account_info)?; - check_account_owner(validator_list_info, program_id)?; - - let mut validator_list_data = validator_list_info.data.borrow_mut(); - let (header, mut validator_list) = - ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - if !header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - let vote_account_address = validator_vote_account_info.key; - - let maybe_validator_stake_info = validator_list.find_mut::(|x| { - ValidatorStakeInfo::memcmp_pubkey(x, vote_account_address) - }); - if maybe_validator_stake_info.is_none() { - msg!( - "Vote account {} not found in stake pool", - vote_account_address - ); - return Err(StakePoolError::ValidatorNotFound.into()); - } - let validator_stake_info = maybe_validator_stake_info.unwrap(); - if u64::from(validator_stake_info.transient_stake_lamports) > 0 { - if maybe_ephemeral_stake_seed.is_none() { - msg!("Attempting to increase stake on a validator with pending transient stake, use IncreaseAdditionalValidatorStake with the existing seed"); - return Err(StakePoolError::TransientAccountInUse.into()); - } - if transient_stake_seed != u64::from(validator_stake_info.transient_seed_suffix) { - msg!( - "Transient stake already exists with seed {}, you must use that one", - u64::from(validator_stake_info.transient_seed_suffix) - ); - return Err(ProgramError::InvalidSeeds); - } - check_if_stake_activating( - transient_stake_account_info, - vote_account_address, - clock.epoch, - )?; - } - - check_validator_stake_account( - validator_stake_account_info, - program_id, - stake_pool_info.key, - withdraw_authority_info.key, - vote_account_address, - validator_stake_info.validator_seed_suffix.into(), - &stake_pool.lockup, - )?; - - if validator_stake_info.status != StakeStatus::Active.into() { - msg!("Validator is marked for removal and no longer allows increases"); - return Err(StakePoolError::ValidatorNotFound.into()); - } - - let stake_space = std::mem::size_of::(); - let stake_rent = rent.minimum_balance(stake_space); - let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; - let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); - if lamports < current_minimum_delegation { - msg!( - "Need more than {} lamports for transient stake to meet minimum delegation requirement, {} provided", - current_minimum_delegation, - lamports - ); - return Err(ProgramError::Custom( - stake::instruction::StakeError::InsufficientDelegation as u32, - )); - } - - // the stake account rent exemption is withdrawn after the merge, so - // to add `lamports` to a validator, we need to create a stake account - // with `lamports + stake_rent` - let total_lamports = lamports.saturating_add(stake_rent); - - if reserve_stake_account_info - .lamports() - .saturating_sub(total_lamports) - < stake_rent - { - let max_split_amount = reserve_stake_account_info - .lamports() - .saturating_sub(stake_rent.saturating_mul(2)); - msg!( - "Reserve stake does not have enough lamports for increase, maximum amount {}, {} requested", - max_split_amount, - lamports - ); - return Err(ProgramError::InsufficientFunds); - } - - let source_stake_account_info = - if let Some((ephemeral_stake_seed, ephemeral_stake_account_info)) = - maybe_ephemeral_stake_seed.zip(maybe_ephemeral_stake_account_info) - { - let ephemeral_stake_bump_seed = check_ephemeral_stake_address( - program_id, - stake_pool_info.key, - ephemeral_stake_account_info.key, - ephemeral_stake_seed, - )?; - let ephemeral_stake_account_signer_seeds: &[&[_]] = &[ - EPHEMERAL_STAKE_SEED_PREFIX, - stake_pool_info.key.as_ref(), - &ephemeral_stake_seed.to_le_bytes(), - &[ephemeral_stake_bump_seed], - ]; - create_stake_account( - ephemeral_stake_account_info.clone(), - ephemeral_stake_account_signer_seeds, - stake_space, - )?; - - // split into ephemeral stake account - Self::stake_split( - stake_pool_info.key, - reserve_stake_account_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - total_lamports, - ephemeral_stake_account_info.clone(), - )?; - - // activate stake to validator - Self::stake_delegate( - ephemeral_stake_account_info.clone(), - validator_vote_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - stake_config_info.clone(), - withdraw_authority_info.clone(), - stake_pool_info.key, - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - )?; - ephemeral_stake_account_info - } else { - // if no ephemeral account is provided, split everything from the - // reserve account, into the transient stake account - reserve_stake_account_info - }; - - let transient_stake_bump_seed = check_transient_stake_address( - program_id, - stake_pool_info.key, - transient_stake_account_info.key, - vote_account_address, - transient_stake_seed, - )?; - - if u64::from(validator_stake_info.transient_stake_lamports) > 0 { - // transient stake exists, try to merge from the source account, - // which is always an ephemeral account - Self::stake_merge( - stake_pool_info.key, - source_stake_account_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - transient_stake_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - )?; - } else { - // no transient stake, split - let transient_stake_account_signer_seeds: &[&[_]] = &[ - TRANSIENT_STAKE_SEED_PREFIX, - vote_account_address.as_ref(), - stake_pool_info.key.as_ref(), - &transient_stake_seed.to_le_bytes(), - &[transient_stake_bump_seed], - ]; - - create_stake_account( - transient_stake_account_info.clone(), - transient_stake_account_signer_seeds, - stake_space, - )?; - - // split into transient stake account - Self::stake_split( - stake_pool_info.key, - source_stake_account_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - total_lamports, - transient_stake_account_info.clone(), - )?; - - // Activate transient stake to validator if necessary - let stake_state = try_from_slice_unchecked::( - &transient_stake_account_info.data.borrow(), - )?; - match stake_state { - // if it was delegated on or before this epoch, we're good - stake::state::StakeStateV2::Stake(_, stake, _) - if stake.delegation.activation_epoch <= clock.epoch => {} - // all other situations, delegate! - _ => { - Self::stake_delegate( - transient_stake_account_info.clone(), - validator_vote_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - stake_config_info.clone(), - withdraw_authority_info.clone(), - stake_pool_info.key, - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - )?; - } - } - } - - validator_stake_info.transient_stake_lamports = - u64::from(validator_stake_info.transient_stake_lamports) - .checked_add(total_lamports) - .ok_or(StakePoolError::CalculationFailure)? - .into(); - validator_stake_info.transient_seed_suffix = transient_stake_seed.into(); - - Ok(()) - } - - /// Process `SetPreferredValidator` instruction - #[inline(never)] // needed due to stack size violation - fn process_set_preferred_validator( - program_id: &Pubkey, - accounts: &[AccountInfo], - validator_type: PreferredValidatorType, - vote_account_address: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let staker_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - - check_account_owner(stake_pool_info, program_id)?; - check_account_owner(validator_list_info, program_id)?; - - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - msg!("Expected valid stake pool"); - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_staker(staker_info)?; - stake_pool.check_validator_list(validator_list_info)?; - - let mut validator_list_data = validator_list_info.data.borrow_mut(); - let (header, validator_list) = - ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - if !header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - if let Some(vote_account_address) = vote_account_address { - let maybe_validator_stake_info = validator_list.find::(|x| { - ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) - }); - match maybe_validator_stake_info { - Some(vsi) => { - if vsi.status != StakeStatus::Active.into() { - msg!("Validator for {:?} about to be removed, cannot set as preferred deposit account", validator_type); - return Err(StakePoolError::InvalidPreferredValidator.into()); - } - } - None => { - msg!("Validator for {:?} not present in the stake pool, cannot set as preferred deposit account", validator_type); - return Err(StakePoolError::ValidatorNotFound.into()); - } - } - } - - match validator_type { - PreferredValidatorType::Deposit => { - stake_pool.preferred_deposit_validator_vote_address = vote_account_address - } - PreferredValidatorType::Withdraw => { - stake_pool.preferred_withdraw_validator_vote_address = vote_account_address - } - }; - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - Ok(()) - } - - /// Processes `UpdateValidatorListBalance` instruction. - #[inline(always)] // needed to maximize number of validators - fn process_update_validator_list_balance( - program_id: &Pubkey, - accounts: &[AccountInfo], - start_index: u32, - no_merge: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - let reserve_stake_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let stake_history_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - let validator_stake_accounts = account_info_iter.as_slice(); - - check_account_owner(stake_pool_info, program_id)?; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - stake_pool.check_validator_list(validator_list_info)?; - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - stake_pool.check_reserve_stake(reserve_stake_info)?; - check_stake_program(stake_program_info.key)?; - - if validator_stake_accounts - .len() - .checked_rem(2) - .ok_or(StakePoolError::CalculationFailure)? - != 0 - { - msg!("Odd number of validator stake accounts passed in, should be pairs of validator stake and transient stake accounts"); - return Err(StakePoolError::UnexpectedValidatorListAccountSize.into()); - } - - check_account_owner(validator_list_info, program_id)?; - let mut validator_list_data = validator_list_info.data.borrow_mut(); - let (validator_list_header, mut big_vec) = - ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - let validator_slice = ValidatorListHeader::deserialize_mut_slice( - &mut big_vec, - start_index as usize, - validator_stake_accounts.len() / 2, - )?; - - if !validator_list_header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - let validator_iter = &mut validator_slice - .iter_mut() - .zip(validator_stake_accounts.chunks_exact(2)); - for (validator_stake_record, validator_stakes) in validator_iter { - // chunks_exact means that we always get 2 elements, making this safe - let validator_stake_info = validator_stakes - .first() - .ok_or(ProgramError::InvalidInstructionData)?; - let transient_stake_info = validator_stakes - .last() - .ok_or(ProgramError::InvalidInstructionData)?; - if check_validator_stake_address( - program_id, - stake_pool_info.key, - validator_stake_info.key, - &validator_stake_record.vote_account_address, - NonZeroU32::new(validator_stake_record.validator_seed_suffix.into()), - ) - .is_err() - { - continue; - }; - if check_transient_stake_address( - program_id, - stake_pool_info.key, - transient_stake_info.key, - &validator_stake_record.vote_account_address, - validator_stake_record.transient_seed_suffix.into(), - ) - .is_err() - { - continue; - }; - - let mut active_stake_lamports = 0; - let mut transient_stake_lamports = 0; - let validator_stake_state = try_from_slice_unchecked::( - &validator_stake_info.data.borrow(), - ) - .ok(); - let transient_stake_state = try_from_slice_unchecked::( - &transient_stake_info.data.borrow(), - ) - .ok(); - - // Possible merge situations for transient stake - // * active -> merge into validator stake - // * activating -> nothing, just account its lamports - // * deactivating -> nothing, just account its lamports - // * inactive -> merge into reserve stake - // * not a stake -> ignore - match transient_stake_state { - Some(stake::state::StakeStateV2::Initialized(meta)) => { - if stake_is_usable_by_pool( - &meta, - withdraw_authority_info.key, - &stake_pool.lockup, - ) { - if no_merge { - transient_stake_lamports = transient_stake_info.lamports(); - } else { - // merge into reserve - Self::stake_merge( - stake_pool_info.key, - transient_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - reserve_stake_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - )?; - validator_stake_record.status.remove_transient_stake()?; - } - } - } - Some(stake::state::StakeStateV2::Stake(meta, stake, _)) => { - if stake_is_usable_by_pool( - &meta, - withdraw_authority_info.key, - &stake_pool.lockup, - ) { - if no_merge { - transient_stake_lamports = transient_stake_info.lamports(); - } else if stake_is_inactive_without_history(&stake, clock.epoch) { - // deactivated, merge into reserve - Self::stake_merge( - stake_pool_info.key, - transient_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - reserve_stake_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - )?; - validator_stake_record.status.remove_transient_stake()?; - } else if stake.delegation.activation_epoch < clock.epoch { - if let Some(stake::state::StakeStateV2::Stake(_, validator_stake, _)) = - validator_stake_state - { - if validator_stake.delegation.activation_epoch < clock.epoch { - Self::stake_merge( - stake_pool_info.key, - transient_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - validator_stake_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - )?; - } else { - msg!("Stake activating or just active, not ready to merge"); - transient_stake_lamports = transient_stake_info.lamports(); - } - } else { - msg!("Transient stake is activating or active, but validator stake is not, need to add the validator stake account on {} back into the stake pool", stake.delegation.voter_pubkey); - transient_stake_lamports = transient_stake_info.lamports(); - } - } else { - msg!("Transient stake not ready to be merged anywhere"); - transient_stake_lamports = transient_stake_info.lamports(); - } - } - } - None - | Some(stake::state::StakeStateV2::Uninitialized) - | Some(stake::state::StakeStateV2::RewardsPool) => {} // do nothing - } - - // Status for validator stake - // * active -> do everything - // * any other state / not a stake -> error state, but account for transient - // stake - let validator_stake_state = try_from_slice_unchecked::( - &validator_stake_info.data.borrow(), - ) - .ok(); - match validator_stake_state { - Some(stake::state::StakeStateV2::Stake(meta, stake, _)) => { - let additional_lamports = validator_stake_info - .lamports() - .saturating_sub(stake.delegation.stake) - .saturating_sub(meta.rent_exempt_reserve); - // withdraw any extra lamports back to the reserve - if additional_lamports > 0 - && stake_is_usable_by_pool( - &meta, - withdraw_authority_info.key, - &stake_pool.lockup, - ) - { - Self::stake_withdraw( - stake_pool_info.key, - validator_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - reserve_stake_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - additional_lamports, - )?; - } - match validator_stake_record.status.try_into()? { - StakeStatus::Active => { - active_stake_lamports = validator_stake_info.lamports(); - } - StakeStatus::DeactivatingValidator | StakeStatus::DeactivatingAll => { - if no_merge { - active_stake_lamports = validator_stake_info.lamports(); - } else if stake_is_usable_by_pool( - &meta, - withdraw_authority_info.key, - &stake_pool.lockup, - ) && stake_is_inactive_without_history(&stake, clock.epoch) - { - // Validator was removed through normal means. - // Absorb the lamports into the reserve. - Self::stake_merge( - stake_pool_info.key, - validator_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - reserve_stake_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - )?; - validator_stake_record.status.remove_validator_stake()?; - } - } - StakeStatus::DeactivatingTransient | StakeStatus::ReadyForRemoval => { - msg!("Validator stake account no longer part of the pool, ignoring"); - } - } - } - Some(stake::state::StakeStateV2::Initialized(meta)) - if stake_is_usable_by_pool( - &meta, - withdraw_authority_info.key, - &stake_pool.lockup, - ) => - { - // If a validator stake is `Initialized`, the validator could - // have been destaked during a cluster restart or removed through - // normal means. Either way, absorb those lamports into the reserve. - // The transient stake was likely absorbed into the reserve earlier. - Self::stake_merge( - stake_pool_info.key, - validator_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - reserve_stake_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - )?; - validator_stake_record.status.remove_validator_stake()?; - } - Some(stake::state::StakeStateV2::Initialized(_)) - | Some(stake::state::StakeStateV2::Uninitialized) - | Some(stake::state::StakeStateV2::RewardsPool) - | None => { - msg!("Validator stake account no longer part of the pool, ignoring"); - } - } - - validator_stake_record.last_update_epoch = clock.epoch.into(); - validator_stake_record.active_stake_lamports = active_stake_lamports.into(); - validator_stake_record.transient_stake_lamports = transient_stake_lamports.into(); - } - - Ok(()) - } - - /// Processes `UpdateStakePoolBalance` instruction. - #[inline(always)] // needed to optimize number of validators - fn process_update_stake_pool_balance( - program_id: &Pubkey, - accounts: &[AccountInfo], - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let withdraw_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - let reserve_stake_info = next_account_info(account_info_iter)?; - let manager_fee_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let token_program_info = next_account_info(account_info_iter)?; - let clock = Clock::get()?; - - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - stake_pool.check_mint(pool_mint_info)?; - stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?; - stake_pool.check_reserve_stake(reserve_stake_info)?; - if stake_pool.manager_fee_account != *manager_fee_info.key { - return Err(StakePoolError::InvalidFeeAccount.into()); - } - - if *validator_list_info.key != stake_pool.validator_list { - return Err(StakePoolError::InvalidValidatorStakeList.into()); - } - if stake_pool.token_program_id != *token_program_info.key { - return Err(ProgramError::IncorrectProgramId); - } - - check_account_owner(validator_list_info, program_id)?; - let mut validator_list_data = validator_list_info.data.borrow_mut(); - let (header, validator_list) = - ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - if !header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - let previous_lamports = stake_pool.total_lamports; - let previous_pool_token_supply = stake_pool.pool_token_supply; - let reserve_stake = try_from_slice_unchecked::( - &reserve_stake_info.data.borrow(), - )?; - let mut total_lamports = - if let stake::state::StakeStateV2::Initialized(meta) = reserve_stake { - reserve_stake_info - .lamports() - .checked_sub(minimum_reserve_lamports(&meta)) - .ok_or(StakePoolError::CalculationFailure)? - } else { - msg!("Reserve stake account in unknown state, aborting"); - return Err(StakePoolError::WrongStakeStake.into()); - }; - for validator_stake_record in validator_list - .deserialize_slice::(0, validator_list.len() as usize)? - { - if u64::from(validator_stake_record.last_update_epoch) < clock.epoch { - return Err(StakePoolError::StakeListOutOfDate.into()); - } - total_lamports = total_lamports - .checked_add(validator_stake_record.stake_lamports()?) - .ok_or(StakePoolError::CalculationFailure)?; - } - - let reward_lamports = total_lamports.saturating_sub(previous_lamports); - - // If the manager fee info is invalid, they don't deserve to receive the fee. - let fee = if stake_pool.check_manager_fee_info(manager_fee_info).is_ok() { - stake_pool - .calc_epoch_fee_amount(reward_lamports) - .ok_or(StakePoolError::CalculationFailure)? - } else { - 0 - }; - - if fee > 0 { - Self::token_mint_to( - stake_pool_info.key, - token_program_info.clone(), - pool_mint_info.clone(), - manager_fee_info.clone(), - withdraw_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - fee, - )?; - } - - if stake_pool.last_update_epoch < clock.epoch { - if let Some(fee) = stake_pool.next_epoch_fee.get() { - stake_pool.epoch_fee = *fee; - } - stake_pool.next_epoch_fee.update_epoch(); - - if let Some(fee) = stake_pool.next_stake_withdrawal_fee.get() { - stake_pool.stake_withdrawal_fee = *fee; - } - stake_pool.next_stake_withdrawal_fee.update_epoch(); - - if let Some(fee) = stake_pool.next_sol_withdrawal_fee.get() { - stake_pool.sol_withdrawal_fee = *fee; - } - stake_pool.next_sol_withdrawal_fee.update_epoch(); - - stake_pool.last_update_epoch = clock.epoch; - stake_pool.last_epoch_total_lamports = previous_lamports; - stake_pool.last_epoch_pool_token_supply = previous_pool_token_supply; - } - stake_pool.total_lamports = total_lamports; - - let pool_mint_data = pool_mint_info.try_borrow_data()?; - let pool_mint = StateWithExtensions::::unpack(&pool_mint_data)?; - stake_pool.pool_token_supply = pool_mint.base.supply; - - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - - Ok(()) - } - - /// Processes the `CleanupRemovedValidatorEntries` instruction - #[inline(never)] // needed to avoid stack size violation - fn process_cleanup_removed_validator_entries( - program_id: &Pubkey, - accounts: &[AccountInfo], - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - - check_account_owner(stake_pool_info, program_id)?; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - stake_pool.check_validator_list(validator_list_info)?; - - check_account_owner(validator_list_info, program_id)?; - let mut validator_list_data = validator_list_info.data.borrow_mut(); - let (header, mut validator_list) = - ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - if !header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - validator_list.retain::(ValidatorStakeInfo::is_not_removed)?; - - Ok(()) - } - - /// Processes [DepositStake](enum.Instruction.html). - #[inline(never)] // needed to avoid stack size violation - fn process_deposit_stake( - program_id: &Pubkey, - accounts: &[AccountInfo], - minimum_pool_tokens_out: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - let stake_deposit_authority_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let stake_info = next_account_info(account_info_iter)?; - let validator_stake_account_info = next_account_info(account_info_iter)?; - let reserve_stake_account_info = next_account_info(account_info_iter)?; - let dest_user_pool_info = next_account_info(account_info_iter)?; - let manager_fee_info = next_account_info(account_info_iter)?; - let referrer_fee_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let stake_history_info = next_account_info(account_info_iter)?; - let token_program_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - - check_stake_program(stake_program_info.key)?; - - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - stake_pool.check_stake_deposit_authority(stake_deposit_authority_info.key)?; - stake_pool.check_mint(pool_mint_info)?; - stake_pool.check_validator_list(validator_list_info)?; - stake_pool.check_reserve_stake(reserve_stake_account_info)?; - - if stake_pool.token_program_id != *token_program_info.key { - return Err(ProgramError::IncorrectProgramId); - } - - if stake_pool.manager_fee_account != *manager_fee_info.key { - return Err(StakePoolError::InvalidFeeAccount.into()); - } - // There is no bypass if the manager fee account is invalid. Deposits - // don't hold user funds hostage, so if the fee account is invalid, users - // cannot deposit in the pool. Let it fail here! - - if stake_pool.last_update_epoch < clock.epoch { - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - - check_account_owner(validator_list_info, program_id)?; - let mut validator_list_data = validator_list_info.data.borrow_mut(); - let (header, mut validator_list) = - ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - if !header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - let (_, validator_stake) = get_stake_state(validator_stake_account_info)?; - let pre_all_validator_lamports = validator_stake_account_info.lamports(); - let vote_account_address = validator_stake.delegation.voter_pubkey; - if let Some(preferred_deposit) = stake_pool.preferred_deposit_validator_vote_address { - if preferred_deposit != vote_account_address { - msg!( - "Incorrect deposit address, expected {}, received {}", - preferred_deposit, - vote_account_address - ); - return Err(StakePoolError::IncorrectDepositVoteAddress.into()); - } - } - - let validator_stake_info = validator_list - .find_mut::(|x| { - ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) - }) - .ok_or(StakePoolError::ValidatorNotFound)?; - check_validator_stake_address( - program_id, - stake_pool_info.key, - validator_stake_account_info.key, - &vote_account_address, - NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()), - )?; - - if validator_stake_info.status != StakeStatus::Active.into() { - msg!("Validator is marked for removal and no longer accepting deposits"); - return Err(StakePoolError::ValidatorNotFound.into()); - } - - msg!("Stake pre merge {}", validator_stake.delegation.stake); - - let (stake_deposit_authority_program_address, deposit_bump_seed) = - find_deposit_authority_program_address(program_id, stake_pool_info.key); - if *stake_deposit_authority_info.key == stake_deposit_authority_program_address { - Self::stake_authorize_signed( - stake_pool_info.key, - stake_info.clone(), - stake_deposit_authority_info.clone(), - AUTHORITY_DEPOSIT, - deposit_bump_seed, - withdraw_authority_info.key, - clock_info.clone(), - )?; - } else { - Self::stake_authorize( - stake_info.clone(), - stake_deposit_authority_info.clone(), - withdraw_authority_info.key, - clock_info.clone(), - )?; - } - - Self::stake_merge( - stake_pool_info.key, - stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - validator_stake_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - )?; - - let (_, post_validator_stake) = get_stake_state(validator_stake_account_info)?; - let post_all_validator_lamports = validator_stake_account_info.lamports(); - msg!("Stake post merge {}", post_validator_stake.delegation.stake); - - let total_deposit_lamports = post_all_validator_lamports - .checked_sub(pre_all_validator_lamports) - .ok_or(StakePoolError::CalculationFailure)?; - let stake_deposit_lamports = post_validator_stake - .delegation - .stake - .checked_sub(validator_stake.delegation.stake) - .ok_or(StakePoolError::CalculationFailure)?; - let sol_deposit_lamports = total_deposit_lamports - .checked_sub(stake_deposit_lamports) - .ok_or(StakePoolError::CalculationFailure)?; - - let new_pool_tokens = stake_pool - .calc_pool_tokens_for_deposit(total_deposit_lamports) - .ok_or(StakePoolError::CalculationFailure)?; - let new_pool_tokens_from_stake = stake_pool - .calc_pool_tokens_for_deposit(stake_deposit_lamports) - .ok_or(StakePoolError::CalculationFailure)?; - let new_pool_tokens_from_sol = new_pool_tokens - .checked_sub(new_pool_tokens_from_stake) - .ok_or(StakePoolError::CalculationFailure)?; - - let stake_deposit_fee = stake_pool - .calc_pool_tokens_stake_deposit_fee(new_pool_tokens_from_stake) - .ok_or(StakePoolError::CalculationFailure)?; - let sol_deposit_fee = stake_pool - .calc_pool_tokens_sol_deposit_fee(new_pool_tokens_from_sol) - .ok_or(StakePoolError::CalculationFailure)?; - - let total_fee = stake_deposit_fee - .checked_add(sol_deposit_fee) - .ok_or(StakePoolError::CalculationFailure)?; - let pool_tokens_user = new_pool_tokens - .checked_sub(total_fee) - .ok_or(StakePoolError::CalculationFailure)?; - - let pool_tokens_referral_fee = stake_pool - .calc_pool_tokens_stake_referral_fee(total_fee) - .ok_or(StakePoolError::CalculationFailure)?; - - let pool_tokens_manager_deposit_fee = total_fee - .checked_sub(pool_tokens_referral_fee) - .ok_or(StakePoolError::CalculationFailure)?; - - if pool_tokens_user - .saturating_add(pool_tokens_manager_deposit_fee) - .saturating_add(pool_tokens_referral_fee) - != new_pool_tokens - { - return Err(StakePoolError::CalculationFailure.into()); - } - - if pool_tokens_user == 0 { - return Err(StakePoolError::DepositTooSmall.into()); - } - - if let Some(minimum_pool_tokens_out) = minimum_pool_tokens_out { - if pool_tokens_user < minimum_pool_tokens_out { - return Err(StakePoolError::ExceededSlippage.into()); - } - } - - Self::token_mint_to( - stake_pool_info.key, - token_program_info.clone(), - pool_mint_info.clone(), - dest_user_pool_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - pool_tokens_user, - )?; - if pool_tokens_manager_deposit_fee > 0 { - Self::token_mint_to( - stake_pool_info.key, - token_program_info.clone(), - pool_mint_info.clone(), - manager_fee_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - pool_tokens_manager_deposit_fee, - )?; - } - if pool_tokens_referral_fee > 0 { - Self::token_mint_to( - stake_pool_info.key, - token_program_info.clone(), - pool_mint_info.clone(), - referrer_fee_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - pool_tokens_referral_fee, - )?; - } - - // withdraw additional lamports to the reserve - if sol_deposit_lamports > 0 { - Self::stake_withdraw( - stake_pool_info.key, - validator_stake_account_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - reserve_stake_account_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - sol_deposit_lamports, - )?; - } - - stake_pool.pool_token_supply = stake_pool - .pool_token_supply - .checked_add(new_pool_tokens) - .ok_or(StakePoolError::CalculationFailure)?; - // We treat the extra lamports as though they were - // transferred directly to the reserve stake account. - stake_pool.total_lamports = stake_pool - .total_lamports - .checked_add(total_deposit_lamports) - .ok_or(StakePoolError::CalculationFailure)?; - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - - validator_stake_info.active_stake_lamports = validator_stake_account_info.lamports().into(); - - Ok(()) - } - - /// Processes [DepositSol](enum.Instruction.html). - #[inline(never)] // needed to avoid stack size violation - fn process_deposit_sol( - program_id: &Pubkey, - accounts: &[AccountInfo], - deposit_lamports: u64, - minimum_pool_tokens_out: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let reserve_stake_account_info = next_account_info(account_info_iter)?; - let from_user_lamports_info = next_account_info(account_info_iter)?; - let dest_user_pool_info = next_account_info(account_info_iter)?; - let manager_fee_info = next_account_info(account_info_iter)?; - let referrer_fee_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - let token_program_info = next_account_info(account_info_iter)?; - let sol_deposit_authority_info = next_account_info(account_info_iter); - - let clock = Clock::get()?; - - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - stake_pool.check_sol_deposit_authority(sol_deposit_authority_info)?; - stake_pool.check_mint(pool_mint_info)?; - stake_pool.check_reserve_stake(reserve_stake_account_info)?; - - if stake_pool.token_program_id != *token_program_info.key { - return Err(ProgramError::IncorrectProgramId); - } - check_system_program(system_program_info.key)?; - - if stake_pool.manager_fee_account != *manager_fee_info.key { - return Err(StakePoolError::InvalidFeeAccount.into()); - } - // There is no bypass if the manager fee account is invalid. Deposits - // don't hold user funds hostage, so if the fee account is invalid, users - // cannot deposit in the pool. Let it fail here! - - // We want this to hold to ensure that deposit_sol mints pool tokens - // at the right price - if stake_pool.last_update_epoch < clock.epoch { - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - - let new_pool_tokens = stake_pool - .calc_pool_tokens_for_deposit(deposit_lamports) - .ok_or(StakePoolError::CalculationFailure)?; - - let pool_tokens_sol_deposit_fee = stake_pool - .calc_pool_tokens_sol_deposit_fee(new_pool_tokens) - .ok_or(StakePoolError::CalculationFailure)?; - let pool_tokens_user = new_pool_tokens - .checked_sub(pool_tokens_sol_deposit_fee) - .ok_or(StakePoolError::CalculationFailure)?; - - let pool_tokens_referral_fee = stake_pool - .calc_pool_tokens_sol_referral_fee(pool_tokens_sol_deposit_fee) - .ok_or(StakePoolError::CalculationFailure)?; - let pool_tokens_manager_deposit_fee = pool_tokens_sol_deposit_fee - .checked_sub(pool_tokens_referral_fee) - .ok_or(StakePoolError::CalculationFailure)?; - - if pool_tokens_user - .saturating_add(pool_tokens_manager_deposit_fee) - .saturating_add(pool_tokens_referral_fee) - != new_pool_tokens - { - return Err(StakePoolError::CalculationFailure.into()); - } - - if pool_tokens_user == 0 { - return Err(StakePoolError::DepositTooSmall.into()); - } - - if let Some(minimum_pool_tokens_out) = minimum_pool_tokens_out { - if pool_tokens_user < minimum_pool_tokens_out { - return Err(StakePoolError::ExceededSlippage.into()); - } - } - - Self::sol_transfer( - from_user_lamports_info.clone(), - reserve_stake_account_info.clone(), - deposit_lamports, - )?; - - Self::token_mint_to( - stake_pool_info.key, - token_program_info.clone(), - pool_mint_info.clone(), - dest_user_pool_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - pool_tokens_user, - )?; - - if pool_tokens_manager_deposit_fee > 0 { - Self::token_mint_to( - stake_pool_info.key, - token_program_info.clone(), - pool_mint_info.clone(), - manager_fee_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - pool_tokens_manager_deposit_fee, - )?; - } - - if pool_tokens_referral_fee > 0 { - Self::token_mint_to( - stake_pool_info.key, - token_program_info.clone(), - pool_mint_info.clone(), - referrer_fee_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - pool_tokens_referral_fee, - )?; - } - - stake_pool.pool_token_supply = stake_pool - .pool_token_supply - .checked_add(new_pool_tokens) - .ok_or(StakePoolError::CalculationFailure)?; - stake_pool.total_lamports = stake_pool - .total_lamports - .checked_add(deposit_lamports) - .ok_or(StakePoolError::CalculationFailure)?; - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - - Ok(()) - } - - /// Processes [WithdrawStake](enum.Instruction.html). - #[inline(never)] // needed to avoid stack size violation - fn process_withdraw_stake( - program_id: &Pubkey, - accounts: &[AccountInfo], - pool_tokens: u64, - minimum_lamports_out: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let validator_list_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let stake_split_from = next_account_info(account_info_iter)?; - let stake_split_to = next_account_info(account_info_iter)?; - let user_stake_authority_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; - let burn_from_pool_info = next_account_info(account_info_iter)?; - let manager_fee_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let clock = &Clock::from_account_info(clock_info)?; - let token_program_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - - check_stake_program(stake_program_info.key)?; - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - let decimals = stake_pool.check_mint(pool_mint_info)?; - stake_pool.check_validator_list(validator_list_info)?; - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - - if stake_pool.manager_fee_account != *manager_fee_info.key { - return Err(StakePoolError::InvalidFeeAccount.into()); - } - if stake_pool.token_program_id != *token_program_info.key { - return Err(ProgramError::IncorrectProgramId); - } - - if stake_pool.last_update_epoch < clock.epoch { - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - - check_account_owner(validator_list_info, program_id)?; - let mut validator_list_data = validator_list_info.data.borrow_mut(); - let (header, mut validator_list) = - ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; - if !header.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - // To prevent a faulty manager fee account from preventing withdrawals - // if the token program does not own the account, or if the account is not - // initialized - let pool_tokens_fee = if stake_pool.manager_fee_account == *burn_from_pool_info.key - || stake_pool.check_manager_fee_info(manager_fee_info).is_err() - { - 0 - } else { - stake_pool - .calc_pool_tokens_stake_withdrawal_fee(pool_tokens) - .ok_or(StakePoolError::CalculationFailure)? - }; - let pool_tokens_burnt = pool_tokens - .checked_sub(pool_tokens_fee) - .ok_or(StakePoolError::CalculationFailure)?; - - let mut withdraw_lamports = stake_pool - .calc_lamports_withdraw_amount(pool_tokens_burnt) - .ok_or(StakePoolError::CalculationFailure)?; - - if withdraw_lamports == 0 { - return Err(StakePoolError::WithdrawalTooSmall.into()); - } - - if let Some(minimum_lamports_out) = minimum_lamports_out { - if withdraw_lamports < minimum_lamports_out { - return Err(StakePoolError::ExceededSlippage.into()); - } - } - - let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; - let stake_state = try_from_slice_unchecked::( - &stake_split_from.data.borrow(), - )?; - let meta = stake_state.meta().ok_or(StakePoolError::WrongStakeStake)?; - let required_lamports = minimum_stake_lamports(&meta, stake_minimum_delegation); - - let lamports_per_pool_token = stake_pool - .get_lamports_per_pool_token() - .ok_or(StakePoolError::CalculationFailure)?; - let minimum_lamports_with_tolerance = - required_lamports.saturating_add(lamports_per_pool_token); - - let has_active_stake = validator_list - .find::(|x| { - ValidatorStakeInfo::active_lamports_greater_than( - x, - &minimum_lamports_with_tolerance, - ) - }) - .is_some(); - let has_transient_stake = validator_list - .find::(|x| { - ValidatorStakeInfo::transient_lamports_greater_than( - x, - &minimum_lamports_with_tolerance, - ) - }) - .is_some(); - - let validator_list_item_info = if *stake_split_from.key == stake_pool.reserve_stake { - // check that the validator stake accounts have no withdrawable stake - if has_transient_stake || has_active_stake { - msg!("Error withdrawing from reserve: validator stake accounts have lamports available, please use those first."); - return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into()); - } - - // check that reserve has enough (should never fail, but who knows?) - stake_split_from - .lamports() - .checked_sub(minimum_reserve_lamports(&meta)) - .ok_or(StakePoolError::StakeLamportsNotEqualToMinimum)?; - None - } else { - let delegation = stake_state - .delegation() - .ok_or(StakePoolError::WrongStakeStake)?; - let vote_account_address = delegation.voter_pubkey; - - if let Some(preferred_withdraw_validator) = - stake_pool.preferred_withdraw_validator_vote_address - { - let preferred_validator_info = validator_list - .find::(|x| { - ValidatorStakeInfo::memcmp_pubkey(x, &preferred_withdraw_validator) - }) - .ok_or(StakePoolError::ValidatorNotFound)?; - let available_lamports = u64::from(preferred_validator_info.active_stake_lamports) - .saturating_sub(minimum_lamports_with_tolerance); - if preferred_withdraw_validator != vote_account_address && available_lamports > 0 { - msg!("Validator vote address {} is preferred for withdrawals, it currently has {} lamports available. Please withdraw those before using other validator stake accounts.", preferred_withdraw_validator, u64::from(preferred_validator_info.active_stake_lamports)); - return Err(StakePoolError::IncorrectWithdrawVoteAddress.into()); - } - } - - let validator_stake_info = validator_list - .find_mut::(|x| { - ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) - }) - .ok_or(StakePoolError::ValidatorNotFound)?; - - let withdraw_source = if has_active_stake { - // if there's any active stake, we must withdraw from an active - // stake account - check_validator_stake_address( - program_id, - stake_pool_info.key, - stake_split_from.key, - &vote_account_address, - NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()), - )?; - StakeWithdrawSource::Active - } else if has_transient_stake { - // if there's any transient stake, we must withdraw from there - check_transient_stake_address( - program_id, - stake_pool_info.key, - stake_split_from.key, - &vote_account_address, - validator_stake_info.transient_seed_suffix.into(), - )?; - StakeWithdrawSource::Transient - } else { - // if there's no active or transient stake, we can take the whole account - check_validator_stake_address( - program_id, - stake_pool_info.key, - stake_split_from.key, - &vote_account_address, - NonZeroU32::new(validator_stake_info.validator_seed_suffix.into()), - )?; - StakeWithdrawSource::ValidatorRemoval - }; - - if validator_stake_info.status != StakeStatus::Active.into() { - msg!("Validator is marked for removal and no longer allowing withdrawals"); - return Err(StakePoolError::ValidatorNotFound.into()); - } - - match withdraw_source { - StakeWithdrawSource::Active | StakeWithdrawSource::Transient => { - let remaining_lamports = stake_split_from - .lamports() - .saturating_sub(withdraw_lamports); - if remaining_lamports < required_lamports { - msg!("Attempting to withdraw {} lamports from validator account with {} stake lamports, {} must remain", withdraw_lamports, stake_split_from.lamports(), required_lamports); - return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into()); - } - } - StakeWithdrawSource::ValidatorRemoval => { - let split_from_lamports = stake_split_from.lamports(); - let upper_bound = split_from_lamports.saturating_add(lamports_per_pool_token); - if withdraw_lamports < split_from_lamports || withdraw_lamports > upper_bound { - msg!( - "Cannot withdraw a whole account worth {} lamports, \ - must withdraw at least {} lamports worth of pool tokens \ - with a margin of {} lamports", - withdraw_lamports, - split_from_lamports, - lamports_per_pool_token - ); - return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into()); - } - // truncate the lamports down to the amount in the account - withdraw_lamports = split_from_lamports; - } - } - Some((validator_stake_info, withdraw_source)) - }; - - Self::token_burn( - token_program_info.clone(), - burn_from_pool_info.clone(), - pool_mint_info.clone(), - user_transfer_authority_info.clone(), - pool_tokens_burnt, - )?; - - Self::stake_split( - stake_pool_info.key, - stake_split_from.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - withdraw_lamports, - stake_split_to.clone(), - )?; - - Self::stake_authorize_signed( - stake_pool_info.key, - stake_split_to.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - user_stake_authority_info.key, - clock_info.clone(), - )?; - - if pool_tokens_fee > 0 { - Self::token_transfer( - token_program_info.clone(), - burn_from_pool_info.clone(), - pool_mint_info.clone(), - manager_fee_info.clone(), - user_transfer_authority_info.clone(), - pool_tokens_fee, - decimals, - )?; - } - - stake_pool.pool_token_supply = stake_pool - .pool_token_supply - .checked_sub(pool_tokens_burnt) - .ok_or(StakePoolError::CalculationFailure)?; - stake_pool.total_lamports = stake_pool - .total_lamports - .checked_sub(withdraw_lamports) - .ok_or(StakePoolError::CalculationFailure)?; - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - - if let Some((validator_list_item, withdraw_source)) = validator_list_item_info { - match withdraw_source { - StakeWithdrawSource::Active => { - validator_list_item.active_stake_lamports = - u64::from(validator_list_item.active_stake_lamports) - .checked_sub(withdraw_lamports) - .ok_or(StakePoolError::CalculationFailure)? - .into() - } - StakeWithdrawSource::Transient => { - validator_list_item.transient_stake_lamports = - u64::from(validator_list_item.transient_stake_lamports) - .checked_sub(withdraw_lamports) - .ok_or(StakePoolError::CalculationFailure)? - .into() - } - StakeWithdrawSource::ValidatorRemoval => { - validator_list_item.active_stake_lamports = - u64::from(validator_list_item.active_stake_lamports) - .checked_sub(withdraw_lamports) - .ok_or(StakePoolError::CalculationFailure)? - .into(); - if u64::from(validator_list_item.active_stake_lamports) != 0 { - msg!("Attempting to remove a validator from the pool, but withdrawal leaves {} lamports, update the pool to merge any unaccounted lamports", - u64::from(validator_list_item.active_stake_lamports)); - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - // since we already checked that there's no transient stake, - // we can immediately set this as ready for removal - validator_list_item.status = StakeStatus::ReadyForRemoval.into(); - } - } - } - - Ok(()) - } - - /// Processes [WithdrawSol](enum.Instruction.html). - #[inline(never)] // needed to avoid stack size violation - fn process_withdraw_sol( - program_id: &Pubkey, - accounts: &[AccountInfo], - pool_tokens: u64, - minimum_lamports_out: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let user_transfer_authority_info = next_account_info(account_info_iter)?; - let burn_from_pool_info = next_account_info(account_info_iter)?; - let reserve_stake_info = next_account_info(account_info_iter)?; - let destination_lamports_info = next_account_info(account_info_iter)?; - let manager_fee_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let clock_info = next_account_info(account_info_iter)?; - let stake_history_info = next_account_info(account_info_iter)?; - let stake_program_info = next_account_info(account_info_iter)?; - let token_program_info = next_account_info(account_info_iter)?; - let sol_withdraw_authority_info = next_account_info(account_info_iter); - - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - stake_pool.check_sol_withdraw_authority(sol_withdraw_authority_info)?; - let decimals = stake_pool.check_mint(pool_mint_info)?; - stake_pool.check_reserve_stake(reserve_stake_info)?; - - if stake_pool.token_program_id != *token_program_info.key { - return Err(ProgramError::IncorrectProgramId); - } - check_stake_program(stake_program_info.key)?; - - if stake_pool.manager_fee_account != *manager_fee_info.key { - return Err(StakePoolError::InvalidFeeAccount.into()); - } - - // We want this to hold to ensure that withdraw_sol burns pool tokens - // at the right price - if stake_pool.last_update_epoch < Clock::get()?.epoch { - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - - // To prevent a faulty manager fee account from preventing withdrawals - // if the token program does not own the account, or if the account is not - // initialized - let pool_tokens_fee = if stake_pool.manager_fee_account == *burn_from_pool_info.key - || stake_pool.check_manager_fee_info(manager_fee_info).is_err() - { - 0 - } else { - stake_pool - .calc_pool_tokens_sol_withdrawal_fee(pool_tokens) - .ok_or(StakePoolError::CalculationFailure)? - }; - let pool_tokens_burnt = pool_tokens - .checked_sub(pool_tokens_fee) - .ok_or(StakePoolError::CalculationFailure)?; - - let withdraw_lamports = stake_pool - .calc_lamports_withdraw_amount(pool_tokens_burnt) - .ok_or(StakePoolError::CalculationFailure)?; - - if withdraw_lamports == 0 { - return Err(StakePoolError::WithdrawalTooSmall.into()); - } - - if let Some(minimum_lamports_out) = minimum_lamports_out { - if withdraw_lamports < minimum_lamports_out { - return Err(StakePoolError::ExceededSlippage.into()); - } - } - - let new_reserve_lamports = reserve_stake_info - .lamports() - .saturating_sub(withdraw_lamports); - let stake_state = try_from_slice_unchecked::( - &reserve_stake_info.data.borrow(), - )?; - if let stake::state::StakeStateV2::Initialized(meta) = stake_state { - let minimum_reserve_lamports = minimum_reserve_lamports(&meta); - if new_reserve_lamports < minimum_reserve_lamports { - msg!("Attempting to withdraw {} lamports, maximum possible SOL withdrawal is {} lamports", - withdraw_lamports, - reserve_stake_info.lamports().saturating_sub(minimum_reserve_lamports) - ); - return Err(StakePoolError::SolWithdrawalTooLarge.into()); - } - } else { - msg!("Reserve stake account not in intialized state"); - return Err(StakePoolError::WrongStakeStake.into()); - }; - - Self::token_burn( - token_program_info.clone(), - burn_from_pool_info.clone(), - pool_mint_info.clone(), - user_transfer_authority_info.clone(), - pool_tokens_burnt, - )?; - - if pool_tokens_fee > 0 { - Self::token_transfer( - token_program_info.clone(), - burn_from_pool_info.clone(), - pool_mint_info.clone(), - manager_fee_info.clone(), - user_transfer_authority_info.clone(), - pool_tokens_fee, - decimals, - )?; - } - - Self::stake_withdraw( - stake_pool_info.key, - reserve_stake_info.clone(), - withdraw_authority_info.clone(), - AUTHORITY_WITHDRAW, - stake_pool.stake_withdraw_bump_seed, - destination_lamports_info.clone(), - clock_info.clone(), - stake_history_info.clone(), - withdraw_lamports, - )?; - - stake_pool.pool_token_supply = stake_pool - .pool_token_supply - .checked_sub(pool_tokens_burnt) - .ok_or(StakePoolError::CalculationFailure)?; - stake_pool.total_lamports = stake_pool - .total_lamports - .checked_sub(withdraw_lamports) - .ok_or(StakePoolError::CalculationFailure)?; - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - - Ok(()) - } - - #[inline(never)] - fn process_create_pool_token_metadata( - program_id: &Pubkey, - accounts: &[AccountInfo], - name: String, - symbol: String, - uri: String, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let manager_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let pool_mint_info = next_account_info(account_info_iter)?; - let payer_info = next_account_info(account_info_iter)?; - let metadata_info = next_account_info(account_info_iter)?; - let mpl_token_metadata_program_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - - if !payer_info.is_signer { - msg!("Payer did not sign metadata creation"); - return Err(StakePoolError::SignatureMissing.into()); - } - - check_system_program(system_program_info.key)?; - check_account_owner(payer_info, &system_program::id())?; - check_account_owner(stake_pool_info, program_id)?; - check_mpl_metadata_program(mpl_token_metadata_program_info.key)?; - - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_manager(manager_info)?; - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - stake_pool.check_mint(pool_mint_info)?; - check_mpl_metadata_account_address(metadata_info.key, &stake_pool.pool_mint)?; - - // Token mint authority for stake-pool token is stake-pool withdraw authority - let token_mint_authority = withdraw_authority_info; - - let new_metadata_instruction = create_metadata_accounts_v3( - *mpl_token_metadata_program_info.key, - *metadata_info.key, - *pool_mint_info.key, - *token_mint_authority.key, - *payer_info.key, - *token_mint_authority.key, - name, - symbol, - uri, - ); - - let (_, stake_withdraw_bump_seed) = - crate::find_withdraw_authority_program_address(program_id, stake_pool_info.key); - - let token_mint_authority_signer_seeds: &[&[_]] = &[ - stake_pool_info.key.as_ref(), - AUTHORITY_WITHDRAW, - &[stake_withdraw_bump_seed], - ]; - - invoke_signed( - &new_metadata_instruction, - &[ - metadata_info.clone(), - pool_mint_info.clone(), - withdraw_authority_info.clone(), - payer_info.clone(), - withdraw_authority_info.clone(), - system_program_info.clone(), - ], - &[token_mint_authority_signer_seeds], - )?; - - Ok(()) - } - - #[inline(never)] - fn process_update_pool_token_metadata( - program_id: &Pubkey, - accounts: &[AccountInfo], - name: String, - symbol: String, - uri: String, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let stake_pool_info = next_account_info(account_info_iter)?; - let manager_info = next_account_info(account_info_iter)?; - let withdraw_authority_info = next_account_info(account_info_iter)?; - let metadata_info = next_account_info(account_info_iter)?; - let mpl_token_metadata_program_info = next_account_info(account_info_iter)?; - - check_account_owner(stake_pool_info, program_id)?; - - check_mpl_metadata_program(mpl_token_metadata_program_info.key)?; - - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_manager(manager_info)?; - stake_pool.check_authority_withdraw( - withdraw_authority_info.key, - program_id, - stake_pool_info.key, - )?; - check_mpl_metadata_account_address(metadata_info.key, &stake_pool.pool_mint)?; - - // Token mint authority for stake-pool token is withdraw authority only - let token_mint_authority = withdraw_authority_info; - - let update_metadata_accounts_instruction = update_metadata_accounts_v2( - *mpl_token_metadata_program_info.key, - *metadata_info.key, - *token_mint_authority.key, - None, - Some(DataV2 { - name, - symbol, - uri, - seller_fee_basis_points: 0, - creators: None, - collection: None, - uses: None, - }), - None, - Some(true), - ); - - let (_, stake_withdraw_bump_seed) = - crate::find_withdraw_authority_program_address(program_id, stake_pool_info.key); - - let token_mint_authority_signer_seeds: &[&[_]] = &[ - stake_pool_info.key.as_ref(), - AUTHORITY_WITHDRAW, - &[stake_withdraw_bump_seed], - ]; - - invoke_signed( - &update_metadata_accounts_instruction, - &[metadata_info.clone(), withdraw_authority_info.clone()], - &[token_mint_authority_signer_seeds], - )?; - - Ok(()) - } - - /// Processes [SetManager](enum.Instruction.html). - #[inline(never)] // needed to avoid stack size violation - fn process_set_manager(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let manager_info = next_account_info(account_info_iter)?; - let new_manager_info = next_account_info(account_info_iter)?; - let new_manager_fee_info = next_account_info(account_info_iter)?; - - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - check_account_owner(new_manager_fee_info, &stake_pool.token_program_id)?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - stake_pool.check_manager(manager_info)?; - if !new_manager_info.is_signer { - msg!("New manager signature missing"); - return Err(StakePoolError::SignatureMissing.into()); - } - - stake_pool.check_manager_fee_info(new_manager_fee_info)?; - - stake_pool.manager = *new_manager_info.key; - stake_pool.manager_fee_account = *new_manager_fee_info.key; - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - Ok(()) - } - - /// Processes [SetFee](enum.Instruction.html). - #[inline(never)] // needed to avoid stack size violation - fn process_set_fee( - program_id: &Pubkey, - accounts: &[AccountInfo], - fee: FeeType, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let manager_info = next_account_info(account_info_iter)?; - let clock = Clock::get()?; - - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - stake_pool.check_manager(manager_info)?; - - if fee.can_only_change_next_epoch() && stake_pool.last_update_epoch < clock.epoch { - return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); - } - - fee.check_too_high()?; - stake_pool.update_fee(&fee)?; - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - Ok(()) - } - - /// Processes [SetStaker](enum.Instruction.html). - #[inline(never)] // needed to avoid stack size violation - fn process_set_staker(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let set_staker_authority_info = next_account_info(account_info_iter)?; - let new_staker_info = next_account_info(account_info_iter)?; - - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - - let staker_signed = stake_pool.check_staker(set_staker_authority_info); - let manager_signed = stake_pool.check_manager(set_staker_authority_info); - if staker_signed.is_err() && manager_signed.is_err() { - return Err(StakePoolError::SignatureMissing.into()); - } - stake_pool.staker = *new_staker_info.key; - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - Ok(()) - } - - /// Processes [SetFundingAuthority](enum.Instruction.html). - #[inline(never)] // needed to avoid stack size violation - fn process_set_funding_authority( - program_id: &Pubkey, - accounts: &[AccountInfo], - funding_type: FundingType, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let stake_pool_info = next_account_info(account_info_iter)?; - let manager_info = next_account_info(account_info_iter)?; - - let new_authority = next_account_info(account_info_iter) - .ok() - .map(|new_authority_account_info| *new_authority_account_info.key); - - check_account_owner(stake_pool_info, program_id)?; - let mut stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; - if !stake_pool.is_valid() { - return Err(StakePoolError::InvalidState.into()); - } - stake_pool.check_manager(manager_info)?; - match funding_type { - FundingType::StakeDeposit => { - stake_pool.stake_deposit_authority = new_authority.unwrap_or( - find_deposit_authority_program_address(program_id, stake_pool_info.key).0, - ); - } - FundingType::SolDeposit => stake_pool.sol_deposit_authority = new_authority, - FundingType::SolWithdraw => stake_pool.sol_withdraw_authority = new_authority, - } - borsh::to_writer(&mut stake_pool_info.data.borrow_mut()[..], &stake_pool)?; - Ok(()) - } - - /// Processes [Instruction](enum.Instruction.html). - pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { - let instruction = StakePoolInstruction::try_from_slice(input)?; - match instruction { - StakePoolInstruction::Initialize { - fee, - withdrawal_fee, - deposit_fee, - referral_fee, - max_validators, - } => { - msg!("Instruction: Initialize stake pool"); - Self::process_initialize( - program_id, - accounts, - fee, - withdrawal_fee, - deposit_fee, - referral_fee, - max_validators, - ) - } - StakePoolInstruction::AddValidatorToPool(seed) => { - msg!("Instruction: AddValidatorToPool"); - Self::process_add_validator_to_pool(program_id, accounts, seed) - } - StakePoolInstruction::RemoveValidatorFromPool => { - msg!("Instruction: RemoveValidatorFromPool"); - Self::process_remove_validator_from_pool(program_id, accounts) - } - StakePoolInstruction::DecreaseValidatorStake { - lamports, - transient_stake_seed, - } => { - msg!("Instruction: DecreaseValidatorStake"); - msg!("NOTE: This instruction is deprecated, please use `DecreaseValidatorStakeWithReserve`"); - Self::process_decrease_validator_stake( - program_id, - accounts, - lamports, - transient_stake_seed, - None, - false, - ) - } - StakePoolInstruction::DecreaseValidatorStakeWithReserve { - lamports, - transient_stake_seed, - } => { - msg!("Instruction: DecreaseValidatorStakeWithReserve"); - Self::process_decrease_validator_stake( - program_id, - accounts, - lamports, - transient_stake_seed, - None, - true, - ) - } - StakePoolInstruction::DecreaseAdditionalValidatorStake { - lamports, - transient_stake_seed, - ephemeral_stake_seed, - } => { - msg!("Instruction: DecreaseAdditionalValidatorStake"); - Self::process_decrease_validator_stake( - program_id, - accounts, - lamports, - transient_stake_seed, - Some(ephemeral_stake_seed), - true, - ) - } - StakePoolInstruction::IncreaseValidatorStake { - lamports, - transient_stake_seed, - } => { - msg!("Instruction: IncreaseValidatorStake"); - Self::process_increase_validator_stake( - program_id, - accounts, - lamports, - transient_stake_seed, - None, - ) - } - StakePoolInstruction::IncreaseAdditionalValidatorStake { - lamports, - transient_stake_seed, - ephemeral_stake_seed, - } => { - msg!("Instruction: IncreaseAdditionalValidatorStake"); - Self::process_increase_validator_stake( - program_id, - accounts, - lamports, - transient_stake_seed, - Some(ephemeral_stake_seed), - ) - } - StakePoolInstruction::SetPreferredValidator { - validator_type, - validator_vote_address, - } => { - msg!("Instruction: SetPreferredValidator"); - Self::process_set_preferred_validator( - program_id, - accounts, - validator_type, - validator_vote_address, - ) - } - StakePoolInstruction::UpdateValidatorListBalance { - start_index, - no_merge, - } => { - msg!("Instruction: UpdateValidatorListBalance"); - Self::process_update_validator_list_balance( - program_id, - accounts, - start_index, - no_merge, - ) - } - StakePoolInstruction::UpdateStakePoolBalance => { - msg!("Instruction: UpdateStakePoolBalance"); - Self::process_update_stake_pool_balance(program_id, accounts) - } - StakePoolInstruction::CleanupRemovedValidatorEntries => { - msg!("Instruction: CleanupRemovedValidatorEntries"); - Self::process_cleanup_removed_validator_entries(program_id, accounts) - } - StakePoolInstruction::DepositStake => { - msg!("Instruction: DepositStake"); - Self::process_deposit_stake(program_id, accounts, None) - } - StakePoolInstruction::WithdrawStake(amount) => { - msg!("Instruction: WithdrawStake"); - Self::process_withdraw_stake(program_id, accounts, amount, None) - } - StakePoolInstruction::SetFee { fee } => { - msg!("Instruction: SetFee"); - Self::process_set_fee(program_id, accounts, fee) - } - StakePoolInstruction::SetManager => { - msg!("Instruction: SetManager"); - Self::process_set_manager(program_id, accounts) - } - StakePoolInstruction::SetStaker => { - msg!("Instruction: SetStaker"); - Self::process_set_staker(program_id, accounts) - } - StakePoolInstruction::SetFundingAuthority(funding_type) => { - msg!("Instruction: SetFundingAuthority"); - Self::process_set_funding_authority(program_id, accounts, funding_type) - } - StakePoolInstruction::DepositSol(lamports) => { - msg!("Instruction: DepositSol"); - Self::process_deposit_sol(program_id, accounts, lamports, None) - } - StakePoolInstruction::WithdrawSol(pool_tokens) => { - msg!("Instruction: WithdrawSol"); - Self::process_withdraw_sol(program_id, accounts, pool_tokens, None) - } - StakePoolInstruction::CreateTokenMetadata { name, symbol, uri } => { - msg!("Instruction: CreateTokenMetadata"); - Self::process_create_pool_token_metadata(program_id, accounts, name, symbol, uri) - } - StakePoolInstruction::UpdateTokenMetadata { name, symbol, uri } => { - msg!("Instruction: UpdateTokenMetadata"); - Self::process_update_pool_token_metadata(program_id, accounts, name, symbol, uri) - } - #[allow(deprecated)] - StakePoolInstruction::Redelegate { .. } => { - msg!("Instruction: Redelegate will not be enabled"); - Err(ProgramError::InvalidInstructionData) - } - StakePoolInstruction::DepositStakeWithSlippage { - minimum_pool_tokens_out, - } => { - msg!("Instruction: DepositStakeWithSlippage"); - Self::process_deposit_stake(program_id, accounts, Some(minimum_pool_tokens_out)) - } - StakePoolInstruction::WithdrawStakeWithSlippage { - pool_tokens_in, - minimum_lamports_out, - } => { - msg!("Instruction: WithdrawStakeWithSlippage"); - Self::process_withdraw_stake( - program_id, - accounts, - pool_tokens_in, - Some(minimum_lamports_out), - ) - } - StakePoolInstruction::DepositSolWithSlippage { - lamports_in, - minimum_pool_tokens_out, - } => { - msg!("Instruction: DepositSolWithSlippage"); - Self::process_deposit_sol( - program_id, - accounts, - lamports_in, - Some(minimum_pool_tokens_out), - ) - } - StakePoolInstruction::WithdrawSolWithSlippage { - pool_tokens_in, - minimum_lamports_out, - } => { - msg!("Instruction: WithdrawSolWithSlippage"); - Self::process_withdraw_sol( - program_id, - accounts, - pool_tokens_in, - Some(minimum_lamports_out), - ) - } - } - } -} - -impl PrintProgramError for StakePoolError { - fn print(&self) - where - E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, - { - match self { - StakePoolError::AlreadyInUse => msg!("Error: The account cannot be initialized because it is already being used"), - StakePoolError::InvalidProgramAddress => msg!("Error: The program address provided doesn't match the value generated by the program"), - StakePoolError::InvalidState => msg!("Error: The stake pool state is invalid"), - StakePoolError::CalculationFailure => msg!("Error: The calculation failed"), - StakePoolError::FeeTooHigh => msg!("Error: Stake pool fee > 1"), - StakePoolError::WrongAccountMint => msg!("Error: Token account is associated with the wrong mint"), - StakePoolError::WrongManager => msg!("Error: Wrong pool manager account"), - StakePoolError::SignatureMissing => msg!("Error: Required signature is missing"), - StakePoolError::InvalidValidatorStakeList => msg!("Error: Invalid validator stake list account"), - StakePoolError::InvalidFeeAccount => msg!("Error: Invalid manager fee account"), - StakePoolError::WrongPoolMint => msg!("Error: Specified pool mint account is wrong"), - StakePoolError::WrongStakeStake => msg!("Error: Stake account is not in the state expected by the program"), - StakePoolError::UserStakeNotActive => msg!("Error: User stake is not active"), - StakePoolError::ValidatorAlreadyAdded => msg!("Error: Stake account voting for this validator already exists in the pool"), - StakePoolError::ValidatorNotFound => msg!("Error: Stake account for this validator not found in the pool"), - StakePoolError::InvalidStakeAccountAddress => msg!("Error: Stake account address not properly derived from the validator address"), - StakePoolError::StakeListOutOfDate => msg!("Error: Identify validator stake accounts with old balances and update them"), - StakePoolError::StakeListAndPoolOutOfDate => msg!("Error: First update old validator stake account balances and then pool stake balance"), - StakePoolError::UnknownValidatorStakeAccount => { - msg!("Error: Validator stake account is not found in the list storage") - } - StakePoolError::WrongMintingAuthority => msg!("Error: Wrong minting authority set for mint pool account"), - StakePoolError::UnexpectedValidatorListAccountSize=> msg!("Error: The size of the given validator stake list does match the expected amount"), - StakePoolError::WrongStaker=> msg!("Error: Wrong pool staker account"), - StakePoolError::NonZeroPoolTokenSupply => msg!("Error: Pool token supply is not zero on initialization"), - StakePoolError::StakeLamportsNotEqualToMinimum => msg!("Error: The lamports in the validator stake account is not equal to the minimum"), - StakePoolError::IncorrectDepositVoteAddress => msg!("Error: The provided deposit stake account is not delegated to the preferred deposit vote account"), - StakePoolError::IncorrectWithdrawVoteAddress => msg!("Error: The provided withdraw stake account is not the preferred deposit vote account"), - StakePoolError::InvalidMintFreezeAuthority => msg!("Error: The mint has an invalid freeze authority"), - StakePoolError::FeeIncreaseTooHigh => msg!("Error: The fee cannot increase by a factor exceeding the stipulated ratio"), - StakePoolError::WithdrawalTooSmall => msg!("Error: Not enough pool tokens provided to withdraw 1-lamport stake"), - StakePoolError::DepositTooSmall => msg!("Error: Not enough lamports provided for deposit to result in one pool token"), - StakePoolError::InvalidStakeDepositAuthority => msg!("Error: Provided stake deposit authority does not match the program's"), - StakePoolError::InvalidSolDepositAuthority => msg!("Error: Provided sol deposit authority does not match the program's"), - StakePoolError::InvalidPreferredValidator => msg!("Error: Provided preferred validator is invalid"), - StakePoolError::TransientAccountInUse => msg!("Error: Provided validator stake account already has a transient stake account in use"), - StakePoolError::InvalidSolWithdrawAuthority => msg!("Error: Provided sol withdraw authority does not match the program's"), - StakePoolError::SolWithdrawalTooLarge => msg!("Error: Too much SOL withdrawn from the stake pool's reserve account"), - StakePoolError::InvalidMetadataAccount => msg!("Error: Metadata account derived from pool mint account does not match the one passed to program"), - StakePoolError::UnsupportedMintExtension => msg!("Error: mint has an unsupported extension"), - StakePoolError::UnsupportedFeeAccountExtension => msg!("Error: fee account has an unsupported extension"), - StakePoolError::ExceededSlippage => msg!("Error: instruction exceeds desired slippage limit"), - StakePoolError::IncorrectMintDecimals => msg!("Error: Provided mint does not have 9 decimals to match SOL"), - StakePoolError::ReserveDepleted => msg!("Error: Pool reserve does not have enough lamports to fund rent-exempt reserve in split destination. Deposit more SOL in reserve, or pre-fund split destination with the rent-exempt reserve for a stake account."), - StakePoolError::MissingRequiredSysvar => msg!("Missing required sysvar account"), - } - } -} diff --git a/stake-pool/program/src/state.rs b/stake-pool/program/src/state.rs deleted file mode 100644 index 831c18779ef..00000000000 --- a/stake-pool/program/src/state.rs +++ /dev/null @@ -1,1465 +0,0 @@ -//! State transition types - -use { - crate::{ - big_vec::BigVec, error::StakePoolError, MAX_WITHDRAWAL_FEE_INCREASE, - WITHDRAWAL_BASELINE_FEE, - }, - borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, - bytemuck::{Pod, Zeroable}, - num_derive::{FromPrimitive, ToPrimitive}, - num_traits::{FromPrimitive, ToPrimitive}, - solana_program::{ - account_info::AccountInfo, - borsh1::get_instance_packed_len, - msg, - program_error::ProgramError, - program_memory::sol_memcmp, - program_pack::{Pack, Sealed}, - pubkey::{Pubkey, PUBKEY_BYTES}, - stake::state::Lockup, - }, - spl_pod::primitives::{PodU32, PodU64}, - spl_token_2022::{ - extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions}, - state::{Account, AccountState, Mint}, - }, - std::{borrow::Borrow, convert::TryFrom, fmt, matches}, -}; - -/// Enum representing the account type managed by the program -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub enum AccountType { - /// If the account has not been initialized, the enum will be 0 - #[default] - Uninitialized, - /// Stake pool - StakePool, - /// Validator stake list - ValidatorList, -} - -/// Initialized program details. -#[repr(C)] -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub struct StakePool { - /// Account type, must be StakePool currently - pub account_type: AccountType, - - /// Manager authority, allows for updating the staker, manager, and fee - /// account - pub manager: Pubkey, - - /// Staker authority, allows for adding and removing validators, and - /// managing stake distribution - pub staker: Pubkey, - - /// Stake deposit authority - /// - /// If a depositor pubkey is specified on initialization, then deposits must - /// be signed by this authority. If no deposit authority is specified, - /// then the stake pool will default to the result of: - /// `Pubkey::find_program_address( - /// &[&stake_pool_address.as_ref(), b"deposit"], - /// program_id, - /// )` - pub stake_deposit_authority: Pubkey, - - /// Stake withdrawal authority bump seed - /// for `create_program_address(&[state::StakePool account, "withdrawal"])` - pub stake_withdraw_bump_seed: u8, - - /// Validator stake list storage account - pub validator_list: Pubkey, - - /// Reserve stake account, holds deactivated stake - pub reserve_stake: Pubkey, - - /// Pool Mint - pub pool_mint: Pubkey, - - /// Manager fee account - pub manager_fee_account: Pubkey, - - /// Pool token program id - pub token_program_id: Pubkey, - - /// Total stake under management. - /// Note that if `last_update_epoch` does not match the current epoch then - /// this field may not be accurate - pub total_lamports: u64, - - /// Total supply of pool tokens (should always match the supply in the Pool - /// Mint) - pub pool_token_supply: u64, - - /// Last epoch the `total_lamports` field was updated - pub last_update_epoch: u64, - - /// Lockup that all stakes in the pool must have - pub lockup: Lockup, - - /// Fee taken as a proportion of rewards each epoch - pub epoch_fee: Fee, - - /// Fee for next epoch - pub next_epoch_fee: FutureEpoch, - - /// Preferred deposit validator vote account pubkey - pub preferred_deposit_validator_vote_address: Option, - - /// Preferred withdraw validator vote account pubkey - pub preferred_withdraw_validator_vote_address: Option, - - /// Fee assessed on stake deposits - pub stake_deposit_fee: Fee, - - /// Fee assessed on withdrawals - pub stake_withdrawal_fee: Fee, - - /// Future stake withdrawal fee, to be set for the following epoch - pub next_stake_withdrawal_fee: FutureEpoch, - - /// Fees paid out to referrers on referred stake deposits. - /// Expressed as a percentage (0 - 100) of deposit fees. - /// i.e. `stake_deposit_fee`% of stake deposited is collected as deposit - /// fees for every deposit and `stake_referral_fee`% of the collected - /// stake deposit fees is paid out to the referrer - pub stake_referral_fee: u8, - - /// Toggles whether the `DepositSol` instruction requires a signature from - /// this `sol_deposit_authority` - pub sol_deposit_authority: Option, - - /// Fee assessed on SOL deposits - pub sol_deposit_fee: Fee, - - /// Fees paid out to referrers on referred SOL deposits. - /// Expressed as a percentage (0 - 100) of SOL deposit fees. - /// i.e. `sol_deposit_fee`% of SOL deposited is collected as deposit fees - /// for every deposit and `sol_referral_fee`% of the collected SOL - /// deposit fees is paid out to the referrer - pub sol_referral_fee: u8, - - /// Toggles whether the `WithdrawSol` instruction requires a signature from - /// the `deposit_authority` - pub sol_withdraw_authority: Option, - - /// Fee assessed on SOL withdrawals - pub sol_withdrawal_fee: Fee, - - /// Future SOL withdrawal fee, to be set for the following epoch - pub next_sol_withdrawal_fee: FutureEpoch, - - /// Last epoch's total pool tokens, used only for APR estimation - pub last_epoch_pool_token_supply: u64, - - /// Last epoch's total lamports, used only for APR estimation - pub last_epoch_total_lamports: u64, -} -impl StakePool { - /// calculate the pool tokens that should be minted for a deposit of - /// `stake_lamports` - #[inline] - pub fn calc_pool_tokens_for_deposit(&self, stake_lamports: u64) -> Option { - if self.total_lamports == 0 || self.pool_token_supply == 0 { - return Some(stake_lamports); - } - u64::try_from( - (stake_lamports as u128) - .checked_mul(self.pool_token_supply as u128)? - .checked_div(self.total_lamports as u128)?, - ) - .ok() - } - - /// calculate lamports amount on withdrawal - #[inline] - pub fn calc_lamports_withdraw_amount(&self, pool_tokens: u64) -> Option { - // `checked_div` returns `None` for a 0 quotient result, but in this - // case, a return of 0 is valid for small amounts of pool tokens. So - // we check for that separately - let numerator = (pool_tokens as u128).checked_mul(self.total_lamports as u128)?; - let denominator = self.pool_token_supply as u128; - if numerator < denominator || denominator == 0 { - Some(0) - } else { - u64::try_from(numerator.checked_div(denominator)?).ok() - } - } - - /// calculate pool tokens to be deducted as withdrawal fees - #[inline] - pub fn calc_pool_tokens_stake_withdrawal_fee(&self, pool_tokens: u64) -> Option { - u64::try_from(self.stake_withdrawal_fee.apply(pool_tokens)?).ok() - } - - /// calculate pool tokens to be deducted as withdrawal fees - #[inline] - pub fn calc_pool_tokens_sol_withdrawal_fee(&self, pool_tokens: u64) -> Option { - u64::try_from(self.sol_withdrawal_fee.apply(pool_tokens)?).ok() - } - - /// calculate pool tokens to be deducted as stake deposit fees - #[inline] - pub fn calc_pool_tokens_stake_deposit_fee(&self, pool_tokens_minted: u64) -> Option { - u64::try_from(self.stake_deposit_fee.apply(pool_tokens_minted)?).ok() - } - - /// calculate pool tokens to be deducted from deposit fees as referral fees - #[inline] - pub fn calc_pool_tokens_stake_referral_fee(&self, stake_deposit_fee: u64) -> Option { - u64::try_from( - (stake_deposit_fee as u128) - .checked_mul(self.stake_referral_fee as u128)? - .checked_div(100u128)?, - ) - .ok() - } - - /// calculate pool tokens to be deducted as SOL deposit fees - #[inline] - pub fn calc_pool_tokens_sol_deposit_fee(&self, pool_tokens_minted: u64) -> Option { - u64::try_from(self.sol_deposit_fee.apply(pool_tokens_minted)?).ok() - } - - /// calculate pool tokens to be deducted from SOL deposit fees as referral - /// fees - #[inline] - pub fn calc_pool_tokens_sol_referral_fee(&self, sol_deposit_fee: u64) -> Option { - u64::try_from( - (sol_deposit_fee as u128) - .checked_mul(self.sol_referral_fee as u128)? - .checked_div(100u128)?, - ) - .ok() - } - - /// Calculate the fee in pool tokens that goes to the manager - /// - /// This function assumes that `reward_lamports` has not already been added - /// to the stake pool's `total_lamports` - #[inline] - pub fn calc_epoch_fee_amount(&self, reward_lamports: u64) -> Option { - if reward_lamports == 0 { - return Some(0); - } - let total_lamports = (self.total_lamports as u128).checked_add(reward_lamports as u128)?; - let fee_lamports = self.epoch_fee.apply(reward_lamports)?; - if total_lamports == fee_lamports || self.pool_token_supply == 0 { - Some(reward_lamports) - } else { - u64::try_from( - (self.pool_token_supply as u128) - .checked_mul(fee_lamports)? - .checked_div(total_lamports.checked_sub(fee_lamports)?)?, - ) - .ok() - } - } - - /// Get the current value of pool tokens, rounded up - #[inline] - pub fn get_lamports_per_pool_token(&self) -> Option { - self.total_lamports - .checked_add(self.pool_token_supply)? - .checked_sub(1)? - .checked_div(self.pool_token_supply) - } - - /// Checks that the withdraw or deposit authority is valid - fn check_program_derived_authority( - authority_address: &Pubkey, - program_id: &Pubkey, - stake_pool_address: &Pubkey, - authority_seed: &[u8], - bump_seed: u8, - ) -> Result<(), ProgramError> { - let expected_address = Pubkey::create_program_address( - &[stake_pool_address.as_ref(), authority_seed, &[bump_seed]], - program_id, - )?; - - if *authority_address == expected_address { - Ok(()) - } else { - msg!( - "Incorrect authority provided, expected {}, received {}", - expected_address, - authority_address - ); - Err(StakePoolError::InvalidProgramAddress.into()) - } - } - - /// Check if the manager fee info is a valid token program account - /// capable of receiving tokens from the mint. - pub(crate) fn check_manager_fee_info( - &self, - manager_fee_info: &AccountInfo, - ) -> Result<(), ProgramError> { - let account_data = manager_fee_info.try_borrow_data()?; - let token_account = StateWithExtensions::::unpack(&account_data)?; - if manager_fee_info.owner != &self.token_program_id - || token_account.base.state != AccountState::Initialized - || token_account.base.mint != self.pool_mint - { - msg!("Manager fee account is not owned by token program, is not initialized, or does not match stake pool's mint"); - return Err(StakePoolError::InvalidFeeAccount.into()); - } - let extensions = token_account.get_extension_types()?; - if extensions - .iter() - .any(|x| !is_extension_supported_for_fee_account(x)) - { - return Err(StakePoolError::UnsupportedFeeAccountExtension.into()); - } - Ok(()) - } - - /// Checks that the withdraw authority is valid - #[inline] - pub(crate) fn check_authority_withdraw( - &self, - withdraw_authority: &Pubkey, - program_id: &Pubkey, - stake_pool_address: &Pubkey, - ) -> Result<(), ProgramError> { - Self::check_program_derived_authority( - withdraw_authority, - program_id, - stake_pool_address, - crate::AUTHORITY_WITHDRAW, - self.stake_withdraw_bump_seed, - ) - } - /// Checks that the deposit authority is valid - #[inline] - pub(crate) fn check_stake_deposit_authority( - &self, - stake_deposit_authority: &Pubkey, - ) -> Result<(), ProgramError> { - if self.stake_deposit_authority == *stake_deposit_authority { - Ok(()) - } else { - Err(StakePoolError::InvalidStakeDepositAuthority.into()) - } - } - - /// Checks that the deposit authority is valid - /// Does nothing if `sol_deposit_authority` is currently not set - #[inline] - pub(crate) fn check_sol_deposit_authority( - &self, - maybe_sol_deposit_authority: Result<&AccountInfo, ProgramError>, - ) -> Result<(), ProgramError> { - if let Some(auth) = self.sol_deposit_authority { - let sol_deposit_authority = maybe_sol_deposit_authority?; - if auth != *sol_deposit_authority.key { - msg!("Expected {}, received {}", auth, sol_deposit_authority.key); - return Err(StakePoolError::InvalidSolDepositAuthority.into()); - } - if !sol_deposit_authority.is_signer { - msg!("SOL Deposit authority signature missing"); - return Err(StakePoolError::SignatureMissing.into()); - } - } - Ok(()) - } - - /// Checks that the sol withdraw authority is valid - /// Does nothing if `sol_withdraw_authority` is currently not set - #[inline] - pub(crate) fn check_sol_withdraw_authority( - &self, - maybe_sol_withdraw_authority: Result<&AccountInfo, ProgramError>, - ) -> Result<(), ProgramError> { - if let Some(auth) = self.sol_withdraw_authority { - let sol_withdraw_authority = maybe_sol_withdraw_authority?; - if auth != *sol_withdraw_authority.key { - return Err(StakePoolError::InvalidSolWithdrawAuthority.into()); - } - if !sol_withdraw_authority.is_signer { - msg!("SOL withdraw authority signature missing"); - return Err(StakePoolError::SignatureMissing.into()); - } - } - Ok(()) - } - - /// Check mint is correct - #[inline] - pub(crate) fn check_mint(&self, mint_info: &AccountInfo) -> Result { - if *mint_info.key != self.pool_mint { - Err(StakePoolError::WrongPoolMint.into()) - } else { - let mint_data = mint_info.try_borrow_data()?; - let mint = StateWithExtensions::::unpack(&mint_data)?; - Ok(mint.base.decimals) - } - } - - /// Check manager validity and signature - pub(crate) fn check_manager(&self, manager_info: &AccountInfo) -> Result<(), ProgramError> { - if *manager_info.key != self.manager { - msg!( - "Incorrect manager provided, expected {}, received {}", - self.manager, - manager_info.key - ); - return Err(StakePoolError::WrongManager.into()); - } - if !manager_info.is_signer { - msg!("Manager signature missing"); - return Err(StakePoolError::SignatureMissing.into()); - } - Ok(()) - } - - /// Check staker validity and signature - pub(crate) fn check_staker(&self, staker_info: &AccountInfo) -> Result<(), ProgramError> { - if *staker_info.key != self.staker { - msg!( - "Incorrect staker provided, expected {}, received {}", - self.staker, - staker_info.key - ); - return Err(StakePoolError::WrongStaker.into()); - } - if !staker_info.is_signer { - msg!("Staker signature missing"); - return Err(StakePoolError::SignatureMissing.into()); - } - Ok(()) - } - - /// Check the validator list is valid - pub fn check_validator_list( - &self, - validator_list_info: &AccountInfo, - ) -> Result<(), ProgramError> { - if *validator_list_info.key != self.validator_list { - msg!( - "Invalid validator list provided, expected {}, received {}", - self.validator_list, - validator_list_info.key - ); - Err(StakePoolError::InvalidValidatorStakeList.into()) - } else { - Ok(()) - } - } - - /// Check the reserve stake is valid - pub fn check_reserve_stake( - &self, - reserve_stake_info: &AccountInfo, - ) -> Result<(), ProgramError> { - if *reserve_stake_info.key != self.reserve_stake { - msg!( - "Invalid reserve stake provided, expected {}, received {}", - self.reserve_stake, - reserve_stake_info.key - ); - Err(StakePoolError::InvalidProgramAddress.into()) - } else { - Ok(()) - } - } - - /// Check if StakePool is actually initialized as a stake pool - pub fn is_valid(&self) -> bool { - self.account_type == AccountType::StakePool - } - - /// Check if StakePool is currently uninitialized - pub fn is_uninitialized(&self) -> bool { - self.account_type == AccountType::Uninitialized - } - - /// Updates one of the StakePool's fees. - pub fn update_fee(&mut self, fee: &FeeType) -> Result<(), StakePoolError> { - match fee { - FeeType::SolReferral(new_fee) => self.sol_referral_fee = *new_fee, - FeeType::StakeReferral(new_fee) => self.stake_referral_fee = *new_fee, - FeeType::Epoch(new_fee) => self.next_epoch_fee = FutureEpoch::new(*new_fee), - FeeType::StakeWithdrawal(new_fee) => { - new_fee.check_withdrawal(&self.stake_withdrawal_fee)?; - self.next_stake_withdrawal_fee = FutureEpoch::new(*new_fee) - } - FeeType::SolWithdrawal(new_fee) => { - new_fee.check_withdrawal(&self.sol_withdrawal_fee)?; - self.next_sol_withdrawal_fee = FutureEpoch::new(*new_fee) - } - FeeType::SolDeposit(new_fee) => self.sol_deposit_fee = *new_fee, - FeeType::StakeDeposit(new_fee) => self.stake_deposit_fee = *new_fee, - }; - Ok(()) - } -} - -/// Checks if the given extension is supported for the stake pool mint -pub fn is_extension_supported_for_mint(extension_type: &ExtensionType) -> bool { - const SUPPORTED_EXTENSIONS: [ExtensionType; 8] = [ - ExtensionType::Uninitialized, - ExtensionType::TransferFeeConfig, - ExtensionType::ConfidentialTransferMint, - ExtensionType::ConfidentialTransferFeeConfig, - ExtensionType::DefaultAccountState, // ok, but a freeze authority is not - ExtensionType::InterestBearingConfig, - ExtensionType::MetadataPointer, - ExtensionType::TokenMetadata, - ]; - if !SUPPORTED_EXTENSIONS.contains(extension_type) { - msg!( - "Stake pool mint account cannot have the {:?} extension", - extension_type - ); - false - } else { - true - } -} - -/// Checks if the given extension is supported for the stake pool's fee account -pub fn is_extension_supported_for_fee_account(extension_type: &ExtensionType) -> bool { - // Note: this does not include the `ConfidentialTransferAccount` extension - // because it is possible to block non-confidential transfers with the - // extension enabled. - const SUPPORTED_EXTENSIONS: [ExtensionType; 4] = [ - ExtensionType::Uninitialized, - ExtensionType::TransferFeeAmount, - ExtensionType::ImmutableOwner, - ExtensionType::CpiGuard, - ]; - if !SUPPORTED_EXTENSIONS.contains(extension_type) { - msg!("Fee account cannot have the {:?} extension", extension_type); - false - } else { - true - } -} - -/// Storage list for all validator stake accounts in the pool. -#[repr(C)] -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub struct ValidatorList { - /// Data outside of the validator list, separated out for cheaper - /// deserializations - pub header: ValidatorListHeader, - - /// List of stake info for each validator in the pool - pub validators: Vec, -} - -/// Helper type to deserialize just the start of a ValidatorList -#[repr(C)] -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub struct ValidatorListHeader { - /// Account type, must be ValidatorList currently - pub account_type: AccountType, - - /// Maximum allowable number of validators - pub max_validators: u32, -} - -/// Status of the stake account in the validator list, for accounting -#[derive( - ToPrimitive, - FromPrimitive, - Copy, - Clone, - Debug, - PartialEq, - BorshDeserialize, - BorshSerialize, - BorshSchema, -)] -pub enum StakeStatus { - /// Stake account is active, there may be a transient stake as well - Active, - /// Only transient stake account exists, when a transient stake is - /// deactivating during validator removal - DeactivatingTransient, - /// No more validator stake accounts exist, entry ready for removal during - /// `UpdateStakePoolBalance` - ReadyForRemoval, - /// Only the validator stake account is deactivating, no transient stake - /// account exists - DeactivatingValidator, - /// Both the transient and validator stake account are deactivating, when - /// a validator is removed with a transient stake active - DeactivatingAll, -} -impl Default for StakeStatus { - fn default() -> Self { - Self::Active - } -} - -/// Wrapper struct that can be `Pod`, containing a byte that *should* be a valid -/// `StakeStatus` underneath. -#[repr(transparent)] -#[derive( - Clone, - Copy, - Debug, - Default, - PartialEq, - Pod, - Zeroable, - BorshDeserialize, - BorshSerialize, - BorshSchema, -)] -pub struct PodStakeStatus(u8); -impl PodStakeStatus { - /// Downgrade the status towards ready for removal by removing the validator - /// stake - pub fn remove_validator_stake(&mut self) -> Result<(), ProgramError> { - let status = StakeStatus::try_from(*self)?; - let new_self = match status { - StakeStatus::Active - | StakeStatus::DeactivatingTransient - | StakeStatus::ReadyForRemoval => status, - StakeStatus::DeactivatingAll => StakeStatus::DeactivatingTransient, - StakeStatus::DeactivatingValidator => StakeStatus::ReadyForRemoval, - }; - *self = new_self.into(); - Ok(()) - } - /// Downgrade the status towards ready for removal by removing the transient - /// stake - pub fn remove_transient_stake(&mut self) -> Result<(), ProgramError> { - let status = StakeStatus::try_from(*self)?; - let new_self = match status { - StakeStatus::Active - | StakeStatus::DeactivatingValidator - | StakeStatus::ReadyForRemoval => status, - StakeStatus::DeactivatingAll => StakeStatus::DeactivatingValidator, - StakeStatus::DeactivatingTransient => StakeStatus::ReadyForRemoval, - }; - *self = new_self.into(); - Ok(()) - } -} -impl TryFrom for StakeStatus { - type Error = ProgramError; - fn try_from(pod: PodStakeStatus) -> Result { - FromPrimitive::from_u8(pod.0).ok_or(ProgramError::InvalidAccountData) - } -} -impl From for PodStakeStatus { - fn from(status: StakeStatus) -> Self { - // unwrap is safe here because the variants of `StakeStatus` fit very - // comfortably within a `u8` - PodStakeStatus(status.to_u8().unwrap()) - } -} - -/// Withdrawal type, figured out during process_withdraw_stake -#[derive(Debug, PartialEq)] -pub(crate) enum StakeWithdrawSource { - /// Some of an active stake account, but not all - Active, - /// Some of a transient stake account - Transient, - /// Take a whole validator stake account - ValidatorRemoval, -} - -/// Information about a validator in the pool -/// -/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS -/// THERE'S AN EXTREMELY GOOD REASON. -/// -/// To save on BPF instructions, the serialized bytes are reinterpreted with a -/// bytemuck transmute, which means that this structure cannot have any -/// undeclared alignment-padding in its representation. -#[repr(C)] -#[derive( - Clone, - Copy, - Debug, - Default, - PartialEq, - Pod, - Zeroable, - BorshDeserialize, - BorshSerialize, - BorshSchema, -)] -pub struct ValidatorStakeInfo { - /// Amount of lamports on the validator stake account, including rent - /// - /// Note that if `last_update_epoch` does not match the current epoch then - /// this field may not be accurate - pub active_stake_lamports: PodU64, - - /// Amount of transient stake delegated to this validator - /// - /// Note that if `last_update_epoch` does not match the current epoch then - /// this field may not be accurate - pub transient_stake_lamports: PodU64, - - /// Last epoch the active and transient stake lamports fields were updated - pub last_update_epoch: PodU64, - - /// Transient account seed suffix, used to derive the transient stake - /// account address - pub transient_seed_suffix: PodU64, - - /// Unused space, initially meant to specify the end of seed suffixes - pub unused: PodU32, - - /// Validator account seed suffix - pub validator_seed_suffix: PodU32, // really `Option` so 0 is `None` - - /// Status of the validator stake account - pub status: PodStakeStatus, - - /// Validator vote account address - pub vote_account_address: Pubkey, -} - -impl ValidatorStakeInfo { - /// Get the total lamports on this validator (active and transient) - pub fn stake_lamports(&self) -> Result { - u64::from(self.active_stake_lamports) - .checked_add(self.transient_stake_lamports.into()) - .ok_or(StakePoolError::CalculationFailure) - } - - /// Performs a very cheap comparison, for checking if this validator stake - /// info matches the vote account address - pub fn memcmp_pubkey(data: &[u8], vote_address: &Pubkey) -> bool { - sol_memcmp( - &data[41..41_usize.saturating_add(PUBKEY_BYTES)], - vote_address.as_ref(), - PUBKEY_BYTES, - ) == 0 - } - - /// Performs a comparison, used to check if this validator stake - /// info has more active lamports than some limit - pub fn active_lamports_greater_than(data: &[u8], lamports: &u64) -> bool { - // without this unwrap, compute usage goes up significantly - u64::try_from_slice(&data[0..8]).unwrap() > *lamports - } - - /// Performs a comparison, used to check if this validator stake - /// info has more transient lamports than some limit - pub fn transient_lamports_greater_than(data: &[u8], lamports: &u64) -> bool { - // without this unwrap, compute usage goes up significantly - u64::try_from_slice(&data[8..16]).unwrap() > *lamports - } - - /// Check that the validator stake info is valid - pub fn is_not_removed(data: &[u8]) -> bool { - FromPrimitive::from_u8(data[40]) != Some(StakeStatus::ReadyForRemoval) - } -} - -impl Sealed for ValidatorStakeInfo {} - -impl Pack for ValidatorStakeInfo { - const LEN: usize = 73; - fn pack_into_slice(&self, data: &mut [u8]) { - // Removing this unwrap would require changing from `Pack` to some other - // trait or `bytemuck`, so it stays in for now - borsh::to_writer(data, self).unwrap(); - } - fn unpack_from_slice(src: &[u8]) -> Result { - let unpacked = Self::try_from_slice(src)?; - Ok(unpacked) - } -} - -impl ValidatorList { - /// Create an empty instance containing space for `max_validators` and - /// preferred validator keys - pub fn new(max_validators: u32) -> Self { - Self { - header: ValidatorListHeader { - account_type: AccountType::ValidatorList, - max_validators, - }, - validators: vec![ValidatorStakeInfo::default(); max_validators as usize], - } - } - - /// Calculate the number of validator entries that fit in the provided - /// length - pub fn calculate_max_validators(buffer_length: usize) -> usize { - let header_size = ValidatorListHeader::LEN.saturating_add(4); - buffer_length - .saturating_sub(header_size) - .saturating_div(ValidatorStakeInfo::LEN) - } - - /// Check if contains validator with particular pubkey - pub fn contains(&self, vote_account_address: &Pubkey) -> bool { - self.validators - .iter() - .any(|x| x.vote_account_address == *vote_account_address) - } - - /// Check if contains validator with particular pubkey - pub fn find_mut(&mut self, vote_account_address: &Pubkey) -> Option<&mut ValidatorStakeInfo> { - self.validators - .iter_mut() - .find(|x| x.vote_account_address == *vote_account_address) - } - /// Check if contains validator with particular pubkey - pub fn find(&self, vote_account_address: &Pubkey) -> Option<&ValidatorStakeInfo> { - self.validators - .iter() - .find(|x| x.vote_account_address == *vote_account_address) - } - - /// Check if the list has any active stake - pub fn has_active_stake(&self) -> bool { - self.validators - .iter() - .any(|x| u64::from(x.active_stake_lamports) > 0) - } -} - -impl ValidatorListHeader { - const LEN: usize = 1 + 4; - - /// Check if validator stake list is actually initialized as a validator - /// stake list - pub fn is_valid(&self) -> bool { - self.account_type == AccountType::ValidatorList - } - - /// Check if the validator stake list is uninitialized - pub fn is_uninitialized(&self) -> bool { - self.account_type == AccountType::Uninitialized - } - - /// Extracts a slice of ValidatorStakeInfo types from the vec part - /// of the ValidatorList - pub fn deserialize_mut_slice<'a>( - big_vec: &'a mut BigVec, - skip: usize, - len: usize, - ) -> Result<&'a mut [ValidatorStakeInfo], ProgramError> { - big_vec.deserialize_mut_slice::(skip, len) - } - - /// Extracts the validator list into its header and internal BigVec - pub fn deserialize_vec(data: &mut [u8]) -> Result<(Self, BigVec), ProgramError> { - let mut data_mut = data.borrow(); - let header = ValidatorListHeader::deserialize(&mut data_mut)?; - let length = get_instance_packed_len(&header)?; - - let big_vec = BigVec { - data: &mut data[length..], - }; - Ok((header, big_vec)) - } -} - -/// Wrapper type that "counts down" epochs, which is Borsh-compatible with the -/// native `Option` -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)] -pub enum FutureEpoch { - /// Nothing is set - None, - /// Value is ready after the next epoch boundary - One(T), - /// Value is ready after two epoch boundaries - Two(T), -} -impl Default for FutureEpoch { - fn default() -> Self { - Self::None - } -} -impl FutureEpoch { - /// Create a new value to be unlocked in a two epochs - pub fn new(value: T) -> Self { - Self::Two(value) - } -} -impl FutureEpoch { - /// Update the epoch, to be done after `get`ting the underlying value - pub fn update_epoch(&mut self) { - match self { - Self::None => {} - Self::One(_) => { - // The value has waited its last epoch - *self = Self::None; - } - // The value still has to wait one more epoch after this - Self::Two(v) => { - *self = Self::One(v.clone()); - } - } - } - - /// Get the value if it's ready, which is only at `One` epoch remaining - pub fn get(&self) -> Option<&T> { - match self { - Self::None | Self::Two(_) => None, - Self::One(v) => Some(v), - } - } -} -impl From> for Option { - fn from(v: FutureEpoch) -> Option { - match v { - FutureEpoch::None => None, - FutureEpoch::One(inner) | FutureEpoch::Two(inner) => Some(inner), - } - } -} - -/// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of -/// the rewards -/// If either the numerator or the denominator is 0, the fee is considered to be -/// 0 -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)] -pub struct Fee { - /// denominator of the fee ratio - pub denominator: u64, - /// numerator of the fee ratio - pub numerator: u64, -} - -impl Fee { - /// Applies the Fee's rates to a given amount, `amt` - /// returning the amount to be subtracted from it as fees - /// (0 if denominator is 0 or amt is 0), - /// or None if overflow occurs - #[inline] - pub fn apply(&self, amt: u64) -> Option { - if self.denominator == 0 { - return Some(0); - } - let numerator = (amt as u128).checked_mul(self.numerator as u128)?; - // ceiling the calculation by adding (denominator - 1) to the numerator - let denominator = self.denominator as u128; - numerator - .checked_add(denominator)? - .checked_sub(1)? - .checked_div(denominator) - } - - /// Withdrawal fees have some additional restrictions, - /// this fn checks if those are met, returning an error if not. - /// Does nothing and returns Ok if fee type is not withdrawal - pub fn check_withdrawal(&self, old_withdrawal_fee: &Fee) -> Result<(), StakePoolError> { - // If the previous withdrawal fee was 0, we allow the fee to be set to a - // maximum of (WITHDRAWAL_BASELINE_FEE * MAX_WITHDRAWAL_FEE_INCREASE) - let (old_num, old_denom) = - if old_withdrawal_fee.denominator == 0 || old_withdrawal_fee.numerator == 0 { - ( - WITHDRAWAL_BASELINE_FEE.numerator, - WITHDRAWAL_BASELINE_FEE.denominator, - ) - } else { - (old_withdrawal_fee.numerator, old_withdrawal_fee.denominator) - }; - - // Check that new_fee / old_fee <= MAX_WITHDRAWAL_FEE_INCREASE - // Program fails if provided numerator or denominator is too large, resulting in - // overflow - if (old_num as u128) - .checked_mul(self.denominator as u128) - .map(|x| x.checked_mul(MAX_WITHDRAWAL_FEE_INCREASE.numerator as u128)) - .ok_or(StakePoolError::CalculationFailure)? - < (self.numerator as u128) - .checked_mul(old_denom as u128) - .map(|x| x.checked_mul(MAX_WITHDRAWAL_FEE_INCREASE.denominator as u128)) - .ok_or(StakePoolError::CalculationFailure)? - { - msg!( - "Fee increase exceeds maximum allowed, proposed increase factor ({} / {})", - self.numerator.saturating_mul(old_denom), - old_num.saturating_mul(self.denominator), - ); - return Err(StakePoolError::FeeIncreaseTooHigh); - } - Ok(()) - } -} - -impl fmt::Display for Fee { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.numerator > 0 && self.denominator > 0 { - write!(f, "{}/{}", self.numerator, self.denominator) - } else { - write!(f, "none") - } - } -} - -/// The type of fees that can be set on the stake pool -#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub enum FeeType { - /// Referral fees for SOL deposits - SolReferral(u8), - /// Referral fees for stake deposits - StakeReferral(u8), - /// Management fee paid per epoch - Epoch(Fee), - /// Stake withdrawal fee - StakeWithdrawal(Fee), - /// Deposit fee for SOL deposits - SolDeposit(Fee), - /// Deposit fee for stake deposits - StakeDeposit(Fee), - /// SOL withdrawal fee - SolWithdrawal(Fee), -} - -impl FeeType { - /// Checks if the provided fee is too high, returning an error if so - pub fn check_too_high(&self) -> Result<(), StakePoolError> { - let too_high = match self { - Self::SolReferral(pct) => *pct > 100u8, - Self::StakeReferral(pct) => *pct > 100u8, - Self::Epoch(fee) => fee.numerator > fee.denominator, - Self::StakeWithdrawal(fee) => fee.numerator > fee.denominator, - Self::SolWithdrawal(fee) => fee.numerator > fee.denominator, - Self::SolDeposit(fee) => fee.numerator > fee.denominator, - Self::StakeDeposit(fee) => fee.numerator > fee.denominator, - }; - if too_high { - msg!("Fee greater than 100%: {:?}", self); - return Err(StakePoolError::FeeTooHigh); - } - Ok(()) - } - - /// Returns if the contained fee can only be updated earliest on the next - /// epoch - #[inline] - pub fn can_only_change_next_epoch(&self) -> bool { - matches!( - self, - Self::StakeWithdrawal(_) | Self::SolWithdrawal(_) | Self::Epoch(_) - ) - } -} - -#[cfg(test)] -mod test { - #![allow(clippy::arithmetic_side_effects)] - use { - super::*, - proptest::prelude::*, - solana_program::{ - borsh1::{get_packed_len, try_from_slice_unchecked}, - clock::{DEFAULT_SLOTS_PER_EPOCH, DEFAULT_S_PER_SLOT, SECONDS_PER_DAY}, - native_token::LAMPORTS_PER_SOL, - }, - }; - - fn uninitialized_validator_list() -> ValidatorList { - ValidatorList { - header: ValidatorListHeader { - account_type: AccountType::Uninitialized, - max_validators: 0, - }, - validators: vec![], - } - } - - fn test_validator_list(max_validators: u32) -> ValidatorList { - ValidatorList { - header: ValidatorListHeader { - account_type: AccountType::ValidatorList, - max_validators, - }, - validators: vec![ - ValidatorStakeInfo { - status: StakeStatus::Active.into(), - vote_account_address: Pubkey::new_from_array([1; 32]), - active_stake_lamports: u64::from_le_bytes([255; 8]).into(), - transient_stake_lamports: u64::from_le_bytes([128; 8]).into(), - last_update_epoch: u64::from_le_bytes([64; 8]).into(), - transient_seed_suffix: 0.into(), - unused: 0.into(), - validator_seed_suffix: 0.into(), - }, - ValidatorStakeInfo { - status: StakeStatus::DeactivatingTransient.into(), - vote_account_address: Pubkey::new_from_array([2; 32]), - active_stake_lamports: 998877665544.into(), - transient_stake_lamports: 222222222.into(), - last_update_epoch: 11223445566.into(), - transient_seed_suffix: 0.into(), - unused: 0.into(), - validator_seed_suffix: 0.into(), - }, - ValidatorStakeInfo { - status: StakeStatus::ReadyForRemoval.into(), - vote_account_address: Pubkey::new_from_array([3; 32]), - active_stake_lamports: 0.into(), - transient_stake_lamports: 0.into(), - last_update_epoch: 999999999999999.into(), - transient_seed_suffix: 0.into(), - unused: 0.into(), - validator_seed_suffix: 0.into(), - }, - ], - } - } - - #[test] - fn state_packing() { - let max_validators = 10_000; - let size = get_instance_packed_len(&ValidatorList::new(max_validators)).unwrap(); - let stake_list = uninitialized_validator_list(); - let mut byte_vec = vec![0u8; size]; - let bytes = byte_vec.as_mut_slice(); - borsh::to_writer(bytes, &stake_list).unwrap(); - let stake_list_unpacked = try_from_slice_unchecked::(&byte_vec).unwrap(); - assert_eq!(stake_list_unpacked, stake_list); - - // Empty, one preferred key - let stake_list = ValidatorList { - header: ValidatorListHeader { - account_type: AccountType::ValidatorList, - max_validators: 0, - }, - validators: vec![], - }; - let mut byte_vec = vec![0u8; size]; - let bytes = byte_vec.as_mut_slice(); - borsh::to_writer(bytes, &stake_list).unwrap(); - let stake_list_unpacked = try_from_slice_unchecked::(&byte_vec).unwrap(); - assert_eq!(stake_list_unpacked, stake_list); - - // With several accounts - let stake_list = test_validator_list(max_validators); - let mut byte_vec = vec![0u8; size]; - let bytes = byte_vec.as_mut_slice(); - borsh::to_writer(bytes, &stake_list).unwrap(); - let stake_list_unpacked = try_from_slice_unchecked::(&byte_vec).unwrap(); - assert_eq!(stake_list_unpacked, stake_list); - } - - #[test] - fn validator_list_active_stake() { - let max_validators = 10_000; - let mut validator_list = test_validator_list(max_validators); - assert!(validator_list.has_active_stake()); - for validator in validator_list.validators.iter_mut() { - validator.active_stake_lamports = 0.into(); - } - assert!(!validator_list.has_active_stake()); - } - - #[test] - fn validator_list_deserialize_mut_slice() { - let max_validators = 10; - let stake_list = test_validator_list(max_validators); - let mut serialized = borsh::to_vec(&stake_list).unwrap(); - let (header, mut big_vec) = ValidatorListHeader::deserialize_vec(&mut serialized).unwrap(); - let list = ValidatorListHeader::deserialize_mut_slice( - &mut big_vec, - 0, - stake_list.validators.len(), - ) - .unwrap(); - assert_eq!(header.account_type, AccountType::ValidatorList); - assert_eq!(header.max_validators, max_validators); - assert!(list - .iter() - .zip(stake_list.validators.iter()) - .all(|(a, b)| a == b)); - - let list = ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 1, 2).unwrap(); - assert!(list - .iter() - .zip(stake_list.validators[1..].iter()) - .all(|(a, b)| a == b)); - let list = ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 2, 1).unwrap(); - assert!(list - .iter() - .zip(stake_list.validators[2..].iter()) - .all(|(a, b)| a == b)); - let list = ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 0, 2).unwrap(); - assert!(list - .iter() - .zip(stake_list.validators[..2].iter()) - .all(|(a, b)| a == b)); - - assert_eq!( - ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 0, 4).unwrap_err(), - ProgramError::AccountDataTooSmall - ); - assert_eq!( - ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 1, 3).unwrap_err(), - ProgramError::AccountDataTooSmall - ); - } - - #[test] - fn validator_list_iter() { - let max_validators = 10; - let stake_list = test_validator_list(max_validators); - let mut serialized = borsh::to_vec(&stake_list).unwrap(); - let (_, big_vec) = ValidatorListHeader::deserialize_vec(&mut serialized).unwrap(); - for (a, b) in big_vec - .deserialize_slice::(0, big_vec.len() as usize) - .unwrap() - .iter() - .zip(stake_list.validators.iter()) - { - assert_eq!(a, b); - } - } - - proptest! { - #[test] - fn stake_list_size_calculation(test_amount in 0..=100_000_u32) { - let validators = ValidatorList::new(test_amount); - let size = get_instance_packed_len(&validators).unwrap(); - assert_eq!(ValidatorList::calculate_max_validators(size), test_amount as usize); - assert_eq!(ValidatorList::calculate_max_validators(size.saturating_add(1)), test_amount as usize); - assert_eq!(ValidatorList::calculate_max_validators(size.saturating_add(get_packed_len::())), (test_amount + 1)as usize); - assert_eq!(ValidatorList::calculate_max_validators(size.saturating_sub(1)), (test_amount.saturating_sub(1)) as usize); - } - } - - prop_compose! { - fn fee()(denominator in 1..=u16::MAX)( - denominator in Just(denominator), - numerator in 0..=denominator, - ) -> (u64, u64) { - (numerator as u64, denominator as u64) - } - } - - prop_compose! { - fn total_stake_and_rewards()(total_lamports in 1..u64::MAX)( - total_lamports in Just(total_lamports), - rewards in 0..=total_lamports, - ) -> (u64, u64) { - (total_lamports - rewards, rewards) - } - } - - #[test] - fn specific_fee_calculation() { - // 10% of 10 SOL in rewards should be 1 SOL in fees - let epoch_fee = Fee { - numerator: 1, - denominator: 10, - }; - let mut stake_pool = StakePool { - total_lamports: 100 * LAMPORTS_PER_SOL, - pool_token_supply: 100 * LAMPORTS_PER_SOL, - epoch_fee, - ..StakePool::default() - }; - let reward_lamports = 10 * LAMPORTS_PER_SOL; - let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap(); - - stake_pool.total_lamports += reward_lamports; - stake_pool.pool_token_supply += pool_token_fee; - - let fee_lamports = stake_pool - .calc_lamports_withdraw_amount(pool_token_fee) - .unwrap(); - assert_eq!(fee_lamports, LAMPORTS_PER_SOL - 1); // off-by-one due to - // truncation - } - - #[test] - fn zero_withdraw_calculation() { - let epoch_fee = Fee { - numerator: 0, - denominator: 1, - }; - let stake_pool = StakePool { - epoch_fee, - ..StakePool::default() - }; - let fee_lamports = stake_pool.calc_lamports_withdraw_amount(0).unwrap(); - assert_eq!(fee_lamports, 0); - } - - #[test] - fn divide_by_zero_fee() { - let stake_pool = StakePool { - total_lamports: 0, - epoch_fee: Fee { - numerator: 1, - denominator: 10, - }, - ..StakePool::default() - }; - let rewards = 10; - let fee = stake_pool.calc_epoch_fee_amount(rewards).unwrap(); - assert_eq!(fee, rewards); - } - - #[test] - fn approximate_apr_calculation() { - // 8% / year means roughly .044% / epoch - let stake_pool = StakePool { - last_epoch_total_lamports: 100_000, - last_epoch_pool_token_supply: 100_000, - total_lamports: 100_044, - pool_token_supply: 100_000, - ..StakePool::default() - }; - let pool_token_value = - stake_pool.total_lamports as f64 / stake_pool.pool_token_supply as f64; - let last_epoch_pool_token_value = stake_pool.last_epoch_total_lamports as f64 - / stake_pool.last_epoch_pool_token_supply as f64; - let epoch_rate = pool_token_value / last_epoch_pool_token_value - 1.0; - const SECONDS_PER_EPOCH: f64 = DEFAULT_SLOTS_PER_EPOCH as f64 * DEFAULT_S_PER_SLOT; - const EPOCHS_PER_YEAR: f64 = SECONDS_PER_DAY as f64 * 365.25 / SECONDS_PER_EPOCH; - const EPSILON: f64 = 0.00001; - let yearly_rate = epoch_rate * EPOCHS_PER_YEAR; - assert!((yearly_rate - 0.080355).abs() < EPSILON); - } - - proptest! { - #[test] - fn fee_calculation( - (numerator, denominator) in fee(), - (total_lamports, reward_lamports) in total_stake_and_rewards(), - ) { - let epoch_fee = Fee { denominator, numerator }; - let mut stake_pool = StakePool { - total_lamports, - pool_token_supply: total_lamports, - epoch_fee, - ..StakePool::default() - }; - let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap(); - - stake_pool.total_lamports += reward_lamports; - stake_pool.pool_token_supply += pool_token_fee; - - let fee_lamports = stake_pool.calc_lamports_withdraw_amount(pool_token_fee).unwrap(); - let max_fee_lamports = u64::try_from((reward_lamports as u128) * (epoch_fee.numerator as u128) / (epoch_fee.denominator as u128)).unwrap(); - assert!(max_fee_lamports >= fee_lamports, - "Max possible fee must always be greater than or equal to what is actually withdrawn, max {} actual {}", - max_fee_lamports, - fee_lamports); - - // since we do two "flooring" conversions, the max epsilon should be - // correct up to 2 lamports (one for each floor division), plus a - // correction for huge discrepancies between rewards and total stake - let epsilon = 2 + reward_lamports / total_lamports; - assert!(max_fee_lamports - fee_lamports <= epsilon, - "Max expected fee in lamports {}, actually receive {}, epsilon {}", - max_fee_lamports, fee_lamports, epsilon); - } - } - - prop_compose! { - fn total_tokens_and_deposit()(total_lamports in 1..u64::MAX)( - total_lamports in Just(total_lamports), - pool_token_supply in 1..=total_lamports, - deposit_lamports in 1..total_lamports, - ) -> (u64, u64, u64) { - (total_lamports - deposit_lamports, pool_token_supply.saturating_sub(deposit_lamports).max(1), deposit_lamports) - } - } - - proptest! { - #[test] - fn deposit_and_withdraw( - (total_lamports, pool_token_supply, deposit_stake) in total_tokens_and_deposit() - ) { - let mut stake_pool = StakePool { - total_lamports, - pool_token_supply, - ..StakePool::default() - }; - let deposit_result = stake_pool.calc_pool_tokens_for_deposit(deposit_stake).unwrap(); - prop_assume!(deposit_result > 0); - stake_pool.total_lamports += deposit_stake; - stake_pool.pool_token_supply += deposit_result; - let withdraw_result = stake_pool.calc_lamports_withdraw_amount(deposit_result).unwrap(); - assert!(withdraw_result <= deposit_stake); - - // also test splitting the withdrawal in two operations - if deposit_result >= 2 { - let first_half_deposit = deposit_result / 2; - let first_withdraw_result = stake_pool.calc_lamports_withdraw_amount(first_half_deposit).unwrap(); - stake_pool.total_lamports -= first_withdraw_result; - stake_pool.pool_token_supply -= first_half_deposit; - let second_half_deposit = deposit_result - first_half_deposit; // do the whole thing - let second_withdraw_result = stake_pool.calc_lamports_withdraw_amount(second_half_deposit).unwrap(); - assert!(first_withdraw_result + second_withdraw_result <= deposit_stake); - } - } - } - - #[test] - fn specific_split_withdrawal() { - let total_lamports = 1_100_000_000_000; - let pool_token_supply = 1_000_000_000_000; - let deposit_stake = 3; - let mut stake_pool = StakePool { - total_lamports, - pool_token_supply, - ..StakePool::default() - }; - let deposit_result = stake_pool - .calc_pool_tokens_for_deposit(deposit_stake) - .unwrap(); - assert!(deposit_result > 0); - stake_pool.total_lamports += deposit_stake; - stake_pool.pool_token_supply += deposit_result; - let withdraw_result = stake_pool - .calc_lamports_withdraw_amount(deposit_result / 2) - .unwrap(); - assert!(withdraw_result * 2 <= deposit_stake); - } - - #[test] - fn withdraw_all() { - let total_lamports = 1_100_000_000_000; - let pool_token_supply = 1_000_000_000_000; - let mut stake_pool = StakePool { - total_lamports, - pool_token_supply, - ..StakePool::default() - }; - // take everything out at once - let withdraw_result = stake_pool - .calc_lamports_withdraw_amount(pool_token_supply) - .unwrap(); - assert_eq!(stake_pool.total_lamports, withdraw_result); - - // take out 1, then the rest - let withdraw_result = stake_pool.calc_lamports_withdraw_amount(1).unwrap(); - stake_pool.total_lamports -= withdraw_result; - stake_pool.pool_token_supply -= 1; - let withdraw_result = stake_pool - .calc_lamports_withdraw_amount(stake_pool.pool_token_supply) - .unwrap(); - assert_eq!(stake_pool.total_lamports, withdraw_result); - - // take out all except 1, then the rest - let mut stake_pool = StakePool { - total_lamports, - pool_token_supply, - ..StakePool::default() - }; - let withdraw_result = stake_pool - .calc_lamports_withdraw_amount(pool_token_supply - 1) - .unwrap(); - stake_pool.total_lamports -= withdraw_result; - stake_pool.pool_token_supply = 1; - assert_ne!(stake_pool.total_lamports, 0); - - let withdraw_result = stake_pool.calc_lamports_withdraw_amount(1).unwrap(); - assert_eq!(stake_pool.total_lamports, withdraw_result); - } -} diff --git a/stake-pool/program/tests/create_pool_token_metadata.rs b/stake-pool/program/tests/create_pool_token_metadata.rs deleted file mode 100644 index b56fd655b19..00000000000 --- a/stake-pool/program/tests/create_pool_token_metadata.rs +++ /dev/null @@ -1,273 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![allow(clippy::items_after_test_module)] -#![cfg(feature = "test-sbf")] -mod helpers; - -use { - helpers::*, - solana_program::{instruction::InstructionError, pubkey::Pubkey}, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error::StakePoolError::{AlreadyInUse, SignatureMissing, WrongManager}, - instruction, MINIMUM_RESERVE_LAMPORTS, - }, - test_case::test_case, -}; - -async fn setup(token_program_id: Pubkey) -> (ProgramTestContext, StakePoolAccounts) { - let mut context = program_test_with_metadata_program() - .start_with_context() - .await; - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - (context, stake_pool_accounts) -} - -#[test_case(spl_token::id(); "token")] -//#[test_case(spl_token_2022::id(); "token-2022")] enable once metaplex supports token-2022 -#[tokio::test] -async fn success(token_program_id: Pubkey) { - let (mut context, stake_pool_accounts) = setup(token_program_id).await; - - let name = "test_name"; - let symbol = "SYM"; - let uri = "test_uri"; - - let ix = instruction::create_token_metadata( - &spl_stake_pool::id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &context.payer.pubkey(), - name.to_string(), - symbol.to_string(), - uri.to_string(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let metadata = get_metadata_account( - &mut context.banks_client, - &stake_pool_accounts.pool_mint.pubkey(), - ) - .await; - - assert!(metadata.name.starts_with(name)); - assert!(metadata.symbol.starts_with(symbol)); - assert!(metadata.uri.starts_with(uri)); -} - -#[tokio::test] -async fn fail_manager_did_not_sign() { - let (context, stake_pool_accounts) = setup(spl_token::id()).await; - - let name = "test_name"; - let symbol = "SYM"; - let uri = "test_uri"; - - let mut ix = instruction::create_token_metadata( - &spl_stake_pool::id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &context.payer.pubkey(), - name.to_string(), - symbol.to_string(), - uri.to_string(), - ); - ix.accounts[1].is_signer = false; - - let transaction = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while manager signature missing"), - } -} - -#[tokio::test] -async fn fail_wrong_manager_signed() { - let (context, stake_pool_accounts) = setup(spl_token::id()).await; - - let name = "test_name"; - let symbol = "SYM"; - let uri = "test_uri"; - - let random_keypair = Keypair::new(); - let ix = instruction::create_token_metadata( - &spl_stake_pool::id(), - &stake_pool_accounts.stake_pool.pubkey(), - &random_keypair.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &context.payer.pubkey(), - name.to_string(), - symbol.to_string(), - uri.to_string(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer, &random_keypair], - context.last_blockhash, - ); - - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while signing with the wrong manager"), - } -} - -#[tokio::test] -async fn fail_wrong_mpl_metadata_program() { - let (context, stake_pool_accounts) = setup(spl_token::id()).await; - - let name = "test_name"; - let symbol = "SYM"; - let uri = "test_uri"; - - let random_keypair = Keypair::new(); - let mut ix = instruction::create_token_metadata( - &spl_stake_pool::id(), - &stake_pool_accounts.stake_pool.pubkey(), - &random_keypair.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &context.payer.pubkey(), - name.to_string(), - symbol.to_string(), - uri.to_string(), - ); - ix.accounts[7].pubkey = Pubkey::new_unique(); - - let transaction = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer, &random_keypair], - context.last_blockhash, - ); - - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, error) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!( - "Wrong error occurs while try to create metadata with wrong mpl token metadata program ID" - ), - } -} - -#[tokio::test] -async fn fail_create_metadata_twice() { - let (context, stake_pool_accounts) = setup(spl_token::id()).await; - - let name = "test_name"; - let symbol = "SYM"; - let uri = "test_uri"; - - let ix = instruction::create_token_metadata( - &spl_stake_pool::id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &context.payer.pubkey(), - name.to_string(), - symbol.to_string(), - uri.to_string(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[ix.clone()], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - - let latest_blockhash = context.banks_client.get_latest_blockhash().await.unwrap(); - let transaction_2 = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - latest_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let error = context - .banks_client - .process_transaction(transaction_2) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = AlreadyInUse as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while trying to create pool token metadata twice"), - } -} diff --git a/stake-pool/program/tests/decrease.rs b/stake-pool/program/tests/decrease.rs deleted file mode 100644 index 495f5d2169b..00000000000 --- a/stake-pool/program/tests/decrease.rs +++ /dev/null @@ -1,661 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![allow(clippy::items_after_test_module)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - assert_matches::assert_matches, - bincode::deserialize, - helpers::*, - solana_program::{clock::Epoch, instruction::InstructionError, pubkey::Pubkey, stake}, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error::StakePoolError, find_ephemeral_stake_program_address, - find_transient_stake_program_address, id, instruction, MINIMUM_RESERVE_LAMPORTS, - }, - test_case::test_case, -}; - -async fn setup() -> ( - ProgramTestContext, - StakePoolAccounts, - ValidatorStakeAccount, - DepositStakeAccount, - u64, - u64, -) { - let mut context = program_test().start_with_context().await; - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - let stake_pool_accounts = StakePoolAccounts::default(); - let reserve_lamports = MINIMUM_RESERVE_LAMPORTS + stake_rent + current_minimum_delegation; - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - reserve_lamports, - ) - .await - .unwrap(); - - let validator_stake_account = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - let decrease_lamports = (current_minimum_delegation + stake_rent) * 3; - let deposit_info = simple_deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - &validator_stake_account, - decrease_lamports, - ) - .await - .unwrap(); - - ( - context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - decrease_lamports, - reserve_lamports + stake_rent, - ) -} - -#[test_case(DecreaseInstruction::Additional; "additional")] -#[test_case(DecreaseInstruction::Reserve; "reserve")] -#[test_case(DecreaseInstruction::Deprecated; "deprecated")] -#[tokio::test] -async fn success(instruction_type: DecreaseInstruction) { - let ( - mut context, - stake_pool_accounts, - validator_stake, - _deposit_info, - decrease_lamports, - reserve_lamports, - ) = setup().await; - - // Save validator stake - let pre_validator_stake_account = - get_account(&mut context.banks_client, &validator_stake.stake_account).await; - - // Check no transient stake - let transient_account = context - .banks_client - .get_account(validator_stake.transient_stake_account) - .await - .unwrap(); - assert!(transient_account.is_none()); - - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - decrease_lamports, - validator_stake.transient_stake_seed, - instruction_type, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check validator stake account balance - let validator_stake_account = - get_account(&mut context.banks_client, &validator_stake.stake_account).await; - let validator_stake_state = - deserialize::(&validator_stake_account.data).unwrap(); - assert_eq!( - pre_validator_stake_account.lamports - decrease_lamports, - validator_stake_account.lamports - ); - assert_eq!( - validator_stake_state - .delegation() - .unwrap() - .deactivation_epoch, - Epoch::MAX - ); - - // Check transient stake account state and balance - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - let transient_stake_account = get_account( - &mut context.banks_client, - &validator_stake.transient_stake_account, - ) - .await; - let transient_stake_state = - deserialize::(&transient_stake_account.data).unwrap(); - let transient_lamports = decrease_lamports + stake_rent; - assert_eq!(transient_stake_account.lamports, transient_lamports); - let reserve_lamports = if instruction_type == DecreaseInstruction::Deprecated { - reserve_lamports - } else { - reserve_lamports - stake_rent - }; - let reserve_stake_account = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await; - assert_eq!(reserve_stake_account.lamports, reserve_lamports); - assert_ne!( - transient_stake_state - .delegation() - .unwrap() - .deactivation_epoch, - Epoch::MAX - ); -} - -#[tokio::test] -async fn fail_with_wrong_withdraw_authority() { - let (context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports, _) = - setup().await; - - let wrong_authority = Pubkey::new_unique(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::decrease_validator_stake_with_reserve( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &wrong_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - decrease_lamports, - validator_stake.transient_stake_seed, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.staker], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::InvalidProgramAddress as u32) - ) - ); -} - -#[tokio::test] -async fn fail_with_wrong_validator_list() { - let (context, mut stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports, _) = - setup().await; - - stake_pool_accounts.validator_list = Keypair::new(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::decrease_validator_stake_with_reserve( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - decrease_lamports, - validator_stake.transient_stake_seed, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.staker], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::InvalidValidatorStakeList as u32) - ) - ); -} - -#[tokio::test] -async fn fail_with_unknown_validator() { - let (mut context, stake_pool_accounts, _validator_stake, _deposit_info, decrease_lamports, _) = - setup().await; - - let unknown_stake = create_unknown_validator_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.stake_pool.pubkey(), - 0, - ) - .await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::decrease_validator_stake_with_reserve( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &unknown_stake.stake_account, - &unknown_stake.transient_stake_account, - decrease_lamports, - unknown_stake.transient_stake_seed, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.staker], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::ValidatorNotFound as u32) - ) - ); -} - -#[test_case(DecreaseInstruction::Additional; "additional")] -#[test_case(DecreaseInstruction::Reserve; "reserve")] -#[test_case(DecreaseInstruction::Deprecated; "deprecated")] -#[tokio::test] -async fn fail_twice_diff_seed(instruction_type: DecreaseInstruction) { - let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports, _) = - setup().await; - - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - decrease_lamports / 3, - validator_stake.transient_stake_seed, - instruction_type, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let transient_stake_seed = validator_stake.transient_stake_seed * 100; - let transient_stake_address = find_transient_stake_program_address( - &id(), - &validator_stake.vote.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - transient_stake_seed, - ) - .0; - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &transient_stake_address, - decrease_lamports / 2, - transient_stake_seed, - instruction_type, - ) - .await - .unwrap() - .unwrap(); - if instruction_type == DecreaseInstruction::Additional { - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::InvalidSeeds) - ); - } else { - assert_matches!( - error, - TransactionError::InstructionError( - _, - InstructionError::Custom(code) - ) if code == StakePoolError::TransientAccountInUse as u32 - ); - } -} - -#[test_case(true, DecreaseInstruction::Additional, DecreaseInstruction::Additional; "success-all-additional")] -#[test_case(true, DecreaseInstruction::Reserve, DecreaseInstruction::Additional; "success-with-additional")] -#[test_case(false, DecreaseInstruction::Additional, DecreaseInstruction::Reserve; "fail-without-additional")] -#[test_case(false, DecreaseInstruction::Reserve, DecreaseInstruction::Reserve; "fail-no-additional")] -#[tokio::test] -async fn twice( - success: bool, - first_instruction: DecreaseInstruction, - second_instruction: DecreaseInstruction, -) { - let ( - mut context, - stake_pool_accounts, - validator_stake, - _deposit_info, - decrease_lamports, - reserve_lamports, - ) = setup().await; - - let pre_stake_account = - get_account(&mut context.banks_client, &validator_stake.stake_account).await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - let first_decrease = decrease_lamports / 3; - let second_decrease = decrease_lamports / 2; - let total_decrease = first_decrease + second_decrease; - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - first_decrease, - validator_stake.transient_stake_seed, - first_instruction, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - second_decrease, - validator_stake.transient_stake_seed, - second_instruction, - ) - .await; - - if success { - assert!(error.is_none(), "{:?}", error); - // no ephemeral account - let ephemeral_stake = find_ephemeral_stake_program_address( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - 0, - ) - .0; - let ephemeral_account = context - .banks_client - .get_account(ephemeral_stake) - .await - .unwrap(); - assert!(ephemeral_account.is_none()); - - // Check validator stake account balance - let stake_account = - get_account(&mut context.banks_client, &validator_stake.stake_account).await; - let stake_state = deserialize::(&stake_account.data).unwrap(); - assert_eq!( - pre_stake_account.lamports - total_decrease, - stake_account.lamports - ); - assert_eq!( - stake_state.delegation().unwrap().deactivation_epoch, - Epoch::MAX - ); - - // Check transient stake account state and balance - let transient_stake_account = get_account( - &mut context.banks_client, - &validator_stake.transient_stake_account, - ) - .await; - let transient_stake_state = - deserialize::(&transient_stake_account.data).unwrap(); - let mut transient_lamports = total_decrease + stake_rent; - if second_instruction == DecreaseInstruction::Additional { - transient_lamports += stake_rent; - } - assert_eq!(transient_stake_account.lamports, transient_lamports); - assert_ne!( - transient_stake_state - .delegation() - .unwrap() - .deactivation_epoch, - Epoch::MAX - ); - - // marked correctly in the list - let validator_list = stake_pool_accounts - .get_validator_list(&mut context.banks_client) - .await; - let entry = validator_list.find(&validator_stake.vote.pubkey()).unwrap(); - assert_eq!( - u64::from(entry.transient_stake_lamports), - transient_lamports - ); - - // reserve deducted properly - let mut reserve_lamports = reserve_lamports - stake_rent; - if second_instruction == DecreaseInstruction::Additional { - reserve_lamports -= stake_rent; - } - let reserve_stake_account = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await; - assert_eq!(reserve_stake_account.lamports, reserve_lamports); - } else { - let error = error.unwrap().unwrap(); - assert_matches!( - error, - TransactionError::InstructionError( - _, - InstructionError::Custom(code) - ) if code == StakePoolError::TransientAccountInUse as u32 - ); - } -} - -#[test_case(DecreaseInstruction::Additional; "additional")] -#[test_case(DecreaseInstruction::Reserve; "reserve")] -#[test_case(DecreaseInstruction::Deprecated; "deprecated")] -#[tokio::test] -async fn fail_with_small_lamport_amount(instruction_type: DecreaseInstruction) { - let (mut context, stake_pool_accounts, validator_stake, _deposit_info, _decrease_lamports, _) = - setup().await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let lamports = rent.minimum_balance(std::mem::size_of::()); - - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - lamports, - validator_stake.transient_stake_seed, - instruction_type, - ) - .await - .unwrap() - .unwrap(); - - assert_matches!( - error, - TransactionError::InstructionError(_, InstructionError::AccountNotRentExempt) - ); -} - -#[test_case(DecreaseInstruction::Additional; "additional")] -#[test_case(DecreaseInstruction::Reserve; "reserve")] -#[test_case(DecreaseInstruction::Deprecated; "deprecated")] -#[tokio::test] -async fn fail_big_overdraw(instruction_type: DecreaseInstruction) { - let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports, _) = - setup().await; - - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - deposit_info.stake_lamports * 1_000_000, - validator_stake.transient_stake_seed, - instruction_type, - ) - .await - .unwrap() - .unwrap(); - - assert_matches!( - error, - TransactionError::InstructionError(_, InstructionError::InsufficientFunds) - ); -} - -#[test_case(DecreaseInstruction::Additional; "additional")] -#[test_case(DecreaseInstruction::Reserve; "reserve")] -#[test_case(DecreaseInstruction::Deprecated; "deprecated")] -#[tokio::test] -async fn fail_overdraw(instruction_type: DecreaseInstruction) { - let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports, _) = - setup().await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - deposit_info.stake_lamports + stake_rent + 1, - validator_stake.transient_stake_seed, - instruction_type, - ) - .await - .unwrap() - .unwrap(); - - assert_matches!( - error, - TransactionError::InstructionError(_, InstructionError::InsufficientFunds) - ); -} - -#[tokio::test] -async fn fail_additional_with_increasing() { - let (mut context, stake_pool_accounts, validator_stake, _, decrease_lamports, _) = - setup().await; - - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - // warp forward to activation - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let error = stake_pool_accounts - .increase_validator_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - current_minimum_delegation, - validator_stake.transient_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - decrease_lamports / 2, - validator_stake.transient_stake_seed, - DecreaseInstruction::Additional, - ) - .await - .unwrap() - .unwrap(); - - assert_matches!( - error, - TransactionError::InstructionError( - _, - InstructionError::Custom(code) - ) if code == StakePoolError::WrongStakeStake as u32 - ); -} diff --git a/stake-pool/program/tests/deposit.rs b/stake-pool/program/tests/deposit.rs deleted file mode 100644 index 752a6bec8a6..00000000000 --- a/stake-pool/program/tests/deposit.rs +++ /dev/null @@ -1,905 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, - instruction::{AccountMeta, Instruction, InstructionError}, - pubkey::Pubkey, - stake, sysvar, - }, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_stake_pool::{error::StakePoolError, id, instruction, state, MINIMUM_RESERVE_LAMPORTS}, - spl_token::error as token_error, - test_case::test_case, -}; - -async fn setup( - token_program_id: Pubkey, -) -> ( - ProgramTestContext, - StakePoolAccounts, - ValidatorStakeAccount, - Keypair, - Pubkey, - Pubkey, - u64, -) { - let mut context = program_test().start_with_context().await; - - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let validator_stake_account = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - let user = Keypair::new(); - // make stake account - let deposit_stake = Keypair::new(); - let lockup = stake::state::Lockup::default(); - - let authorized = stake::state::Authorized { - staker: user.pubkey(), - withdrawer: user.pubkey(), - }; - - let stake_lamports = create_independent_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &authorized, - &lockup, - TEST_STAKE_AMOUNT, - ) - .await; - - delegate_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake.pubkey(), - &user, - &validator_stake_account.vote.pubkey(), - ) - .await; - - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - // make pool token account - let pool_token_account = Keypair::new(); - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &pool_token_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - ( - context, - stake_pool_accounts, - validator_stake_account, - user, - deposit_stake.pubkey(), - pool_token_account.pubkey(), - stake_lamports, - ) -} - -#[test_case(spl_token::id(); "token")] -#[test_case(spl_token_2022::id(); "token-2022")] -#[tokio::test] -async fn success(token_program_id: Pubkey) { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - user, - deposit_stake, - pool_token_account, - stake_lamports, - ) = setup(token_program_id).await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - // Save stake pool state before depositing - let pre_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let pre_stake_pool = - try_from_slice_unchecked::(pre_stake_pool.data.as_slice()).unwrap(); - - // Save validator stake account record before depositing - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let pre_validator_stake_item = validator_list - .find(&validator_stake_account.vote.pubkey()) - .unwrap(); - - // Save reserve state before depositing - let pre_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - - let error = stake_pool_accounts - .deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &pool_token_account, - &validator_stake_account.stake_account, - &user, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Original stake account should be drained - assert!(context - .banks_client - .get_account(deposit_stake) - .await - .expect("get_account") - .is_none()); - - let tokens_issued = stake_lamports; // For now tokens are 1:1 to stake - - // Stake pool should add its balance to the pool balance - let post_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let post_stake_pool = - try_from_slice_unchecked::(post_stake_pool.data.as_slice()).unwrap(); - assert_eq!( - post_stake_pool.total_lamports, - pre_stake_pool.total_lamports + stake_lamports - ); - assert_eq!( - post_stake_pool.pool_token_supply, - pre_stake_pool.pool_token_supply + tokens_issued - ); - - // Check minted tokens - let user_token_balance = - get_token_balance(&mut context.banks_client, &pool_token_account).await; - let tokens_issued_user = tokens_issued - - post_stake_pool - .calc_pool_tokens_sol_deposit_fee(stake_rent) - .unwrap() - - post_stake_pool - .calc_pool_tokens_stake_deposit_fee(stake_lamports - stake_rent) - .unwrap(); - assert_eq!(user_token_balance, tokens_issued_user); - - // Check balances in validator stake account list storage - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let post_validator_stake_item = validator_list - .find(&validator_stake_account.vote.pubkey()) - .unwrap(); - assert_eq!( - post_validator_stake_item.stake_lamports().unwrap(), - pre_validator_stake_item.stake_lamports().unwrap() + stake_lamports - stake_rent, - ); - - // Check validator stake account actual SOL balance - let validator_stake_account = get_account( - &mut context.banks_client, - &validator_stake_account.stake_account, - ) - .await; - assert_eq!( - validator_stake_account.lamports, - post_validator_stake_item.stake_lamports().unwrap() - ); - assert_eq!( - u64::from(post_validator_stake_item.transient_stake_lamports), - 0 - ); - - // Check reserve - let post_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - assert_eq!(post_reserve_lamports, pre_reserve_lamports + stake_rent); -} - -#[tokio::test] -async fn success_with_extra_stake_lamports() { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - user, - deposit_stake, - pool_token_account, - stake_lamports, - ) = setup(spl_token::id()).await; - - let extra_lamports = TEST_STAKE_AMOUNT * 3 + 1; - - transfer( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - extra_lamports, - ) - .await; - - let referrer = Keypair::new(); - let referrer_token_account = Keypair::new(); - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &referrer_token_account, - &stake_pool_accounts.pool_mint.pubkey(), - &referrer, - &[], - ) - .await - .unwrap(); - - let referrer_balance_pre = - get_token_balance(&mut context.banks_client, &referrer_token_account.pubkey()).await; - - let manager_pool_balance_pre = get_token_balance( - &mut context.banks_client, - &stake_pool_accounts.pool_fee_account.pubkey(), - ) - .await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - // Save stake pool state before depositing - let pre_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let pre_stake_pool = - try_from_slice_unchecked::(pre_stake_pool.data.as_slice()).unwrap(); - - // Save validator stake account record before depositing - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let pre_validator_stake_item = validator_list - .find(&validator_stake_account.vote.pubkey()) - .unwrap(); - - // Save reserve state before depositing - let pre_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - - let error = stake_pool_accounts - .deposit_stake_with_referral( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &pool_token_account, - &validator_stake_account.stake_account, - &user, - &referrer_token_account.pubkey(), - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Original stake account should be drained - assert!(context - .banks_client - .get_account(deposit_stake) - .await - .expect("get_account") - .is_none()); - - let tokens_issued = stake_lamports + extra_lamports; - // For now tokens are 1:1 to stake - - // Stake pool should add its balance to the pool balance - - // The extra lamports will not get recorded in total stake lamports unless - // update_stake_pool_balance is called - let post_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - - let post_stake_pool = - try_from_slice_unchecked::(post_stake_pool.data.as_slice()).unwrap(); - assert_eq!( - post_stake_pool.total_lamports, - pre_stake_pool.total_lamports + extra_lamports + stake_lamports - ); - assert_eq!( - post_stake_pool.pool_token_supply, - pre_stake_pool.pool_token_supply + tokens_issued - ); - - // Check minted tokens - let user_token_balance = - get_token_balance(&mut context.banks_client, &pool_token_account).await; - - let fee_tokens = post_stake_pool - .calc_pool_tokens_sol_deposit_fee(extra_lamports + stake_rent) - .unwrap() - + post_stake_pool - .calc_pool_tokens_stake_deposit_fee(stake_lamports - stake_rent) - .unwrap(); - let tokens_issued_user = tokens_issued - fee_tokens; - assert_eq!(user_token_balance, tokens_issued_user); - - let referrer_balance_post = - get_token_balance(&mut context.banks_client, &referrer_token_account.pubkey()).await; - - let referral_fee = stake_pool_accounts.calculate_referral_fee(fee_tokens); - let manager_fee = fee_tokens - referral_fee; - - assert_eq!(referrer_balance_post - referrer_balance_pre, referral_fee); - - let manager_pool_balance_post = get_token_balance( - &mut context.banks_client, - &stake_pool_accounts.pool_fee_account.pubkey(), - ) - .await; - assert_eq!( - manager_pool_balance_post - manager_pool_balance_pre, - manager_fee - ); - - // Check balances in validator stake account list storage - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let post_validator_stake_item = validator_list - .find(&validator_stake_account.vote.pubkey()) - .unwrap(); - assert_eq!( - post_validator_stake_item.stake_lamports().unwrap(), - pre_validator_stake_item.stake_lamports().unwrap() + stake_lamports - stake_rent, - ); - - // Check validator stake account actual SOL balance - let validator_stake_account = get_account( - &mut context.banks_client, - &validator_stake_account.stake_account, - ) - .await; - assert_eq!( - validator_stake_account.lamports, - post_validator_stake_item.stake_lamports().unwrap() - ); - assert_eq!( - u64::from(post_validator_stake_item.transient_stake_lamports), - 0 - ); - - // Check reserve - let post_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - assert_eq!( - post_reserve_lamports, - pre_reserve_lamports + stake_rent + extra_lamports - ); -} - -#[tokio::test] -async fn fail_with_wrong_stake_program_id() { - let ( - context, - stake_pool_accounts, - validator_stake_account, - _user, - deposit_stake, - pool_token_account, - _stake_lamports, - ) = setup(spl_token::id()).await; - - let wrong_stake_program = Pubkey::new_unique(); - - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.stake_deposit_authority, false), - AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), - AccountMeta::new(deposit_stake, false), - AccountMeta::new(validator_stake_account.stake_account, false), - AccountMeta::new(stake_pool_accounts.reserve_stake.pubkey(), false), - AccountMeta::new(pool_token_account, false), - AccountMeta::new(stake_pool_accounts.pool_fee_account.pubkey(), false), - AccountMeta::new(stake_pool_accounts.pool_fee_account.pubkey(), false), - AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(wrong_stake_program, false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data: borsh::to_vec(&instruction::StakePoolInstruction::DepositStake).unwrap(), - }; - - let mut transaction = - Transaction::new_with_payer(&[instruction], Some(&context.payer.pubkey())); - transaction.sign(&[&context.payer], context.last_blockhash); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!("Wrong error occurs while try to make a deposit with wrong stake program ID"), - } -} - -#[tokio::test] -async fn fail_with_wrong_token_program_id() { - let ( - context, - stake_pool_accounts, - validator_stake_account, - user, - deposit_stake, - pool_token_account, - _stake_lamports, - ) = setup(spl_token::id()).await; - - let wrong_token_program = Keypair::new(); - - let mut transaction = Transaction::new_with_payer( - &instruction::deposit_stake( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.withdraw_authority, - &deposit_stake, - &user.pubkey(), - &validator_stake_account.stake_account, - &stake_pool_accounts.reserve_stake.pubkey(), - &pool_token_account, - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &wrong_token_program.pubkey(), - ), - Some(&context.payer.pubkey()), - ); - transaction.sign(&[&context.payer, &user], context.last_blockhash); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!("Wrong error occurs while try to make a deposit with wrong token program ID"), - } -} - -#[tokio::test] -async fn fail_with_wrong_validator_list_account() { - let ( - mut context, - mut stake_pool_accounts, - validator_stake_account, - user, - deposit_stake, - pool_token_account, - _stake_lamports, - ) = setup(spl_token::id()).await; - - let wrong_validator_list = Keypair::new(); - stake_pool_accounts.validator_list = wrong_validator_list; - - let transaction_error = stake_pool_accounts - .deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &pool_token_account, - &validator_stake_account.stake_account, - &user, - ) - .await - .unwrap() - .unwrap(); - - match transaction_error { - TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - ) => { - let program_error = StakePoolError::InvalidValidatorStakeList as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to make a deposit with wrong validator stake list account"), - } -} - -#[tokio::test] -async fn fail_with_unknown_validator() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let unknown_stake = create_unknown_validator_stake( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool.pubkey(), - 0, - ) - .await; - - let user = Keypair::new(); - let user_pool_account = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &user_pool_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - // make stake account - let user_stake = Keypair::new(); - let lockup = stake::state::Lockup::default(); - let authorized = stake::state::Authorized { - staker: user.pubkey(), - withdrawer: user.pubkey(), - }; - create_independent_stake_account( - &mut banks_client, - &payer, - &recent_blockhash, - &user_stake, - &authorized, - &lockup, - TEST_STAKE_AMOUNT, - ) - .await; - delegate_stake_account( - &mut banks_client, - &payer, - &recent_blockhash, - &user_stake.pubkey(), - &user, - &unknown_stake.vote.pubkey(), - ) - .await; - - let error = stake_pool_accounts - .deposit_stake( - &mut banks_client, - &payer, - &recent_blockhash, - &user_stake.pubkey(), - &user_pool_account.pubkey(), - &unknown_stake.stake_account, - &user, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(StakePoolError::ValidatorNotFound as u32) - ) - ); -} - -#[tokio::test] -async fn fail_with_wrong_withdraw_authority() { - let ( - mut context, - mut stake_pool_accounts, - validator_stake_account, - user, - deposit_stake, - pool_token_account, - _stake_lamports, - ) = setup(spl_token::id()).await; - - stake_pool_accounts.withdraw_authority = Pubkey::new_unique(); - - let transaction_error = stake_pool_accounts - .deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &pool_token_account, - &validator_stake_account.stake_account, - &user, - ) - .await - .unwrap() - .unwrap(); - - match transaction_error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = StakePoolError::InvalidProgramAddress as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to make a deposit with wrong withdraw authority"), - } -} - -#[tokio::test] -async fn fail_with_wrong_mint_for_receiver_acc() { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - user, - deposit_stake, - _pool_token_account, - _stake_lamports, - ) = setup(spl_token::id()).await; - - let outside_mint = Keypair::new(); - let outside_withdraw_auth = Keypair::new(); - let outside_manager = Keypair::new(); - let outside_pool_fee_acc = Keypair::new(); - - create_mint( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &outside_mint, - &outside_withdraw_auth.pubkey(), - 0, - &[], - ) - .await - .unwrap(); - - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &outside_pool_fee_acc, - &outside_mint.pubkey(), - &outside_manager, - &[], - ) - .await - .unwrap(); - - let transaction_error = stake_pool_accounts - .deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &outside_pool_fee_acc.pubkey(), - &validator_stake_account.stake_account, - &user, - ) - .await - .unwrap() - .unwrap(); - - match transaction_error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = token_error::TokenError::MintMismatch as u32; - assert_eq!(error_index, program_error); - } - _ => { - panic!("Wrong error occurs while try to deposit with wrong mint from receiver account") - } - } -} - -#[test_case(spl_token::id(); "token")] -#[test_case(spl_token_2022::id(); "token-2022")] -#[tokio::test] -async fn success_with_slippage(token_program_id: Pubkey) { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - user, - deposit_stake, - pool_token_account, - stake_lamports, - ) = setup(token_program_id).await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - // Save stake pool state before depositing - let pre_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let pre_stake_pool = - try_from_slice_unchecked::(pre_stake_pool.data.as_slice()).unwrap(); - - let tokens_issued = stake_lamports; // For now tokens are 1:1 to stake - let tokens_issued_user = tokens_issued - - pre_stake_pool - .calc_pool_tokens_sol_deposit_fee(stake_rent) - .unwrap() - - pre_stake_pool - .calc_pool_tokens_stake_deposit_fee(stake_lamports - stake_rent) - .unwrap(); - - let error = stake_pool_accounts - .deposit_stake_with_slippage( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &pool_token_account, - &validator_stake_account.stake_account, - &user, - tokens_issued_user + 1, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(StakePoolError::ExceededSlippage as u32) - ) - ); - - let error = stake_pool_accounts - .deposit_stake_with_slippage( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &pool_token_account, - &validator_stake_account.stake_account, - &user, - tokens_issued_user, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Original stake account should be drained - assert!(context - .banks_client - .get_account(deposit_stake) - .await - .expect("get_account") - .is_none()); - - // Stake pool should add its balance to the pool balance - let post_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let post_stake_pool = - try_from_slice_unchecked::(post_stake_pool.data.as_slice()).unwrap(); - assert_eq!( - post_stake_pool.total_lamports, - pre_stake_pool.total_lamports + stake_lamports - ); - assert_eq!( - post_stake_pool.pool_token_supply, - pre_stake_pool.pool_token_supply + tokens_issued - ); - - // Check minted tokens - let user_token_balance = - get_token_balance(&mut context.banks_client, &pool_token_account).await; - assert_eq!(user_token_balance, tokens_issued_user); -} diff --git a/stake-pool/program/tests/deposit_authority.rs b/stake-pool/program/tests/deposit_authority.rs deleted file mode 100644 index e5e46a7ea12..00000000000 --- a/stake-pool/program/tests/deposit_authority.rs +++ /dev/null @@ -1,222 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{instruction::InstructionError, stake}, - solana_program_test::*, - solana_sdk::{ - borsh1::try_from_slice_unchecked, - signature::{Keypair, Signer}, - transaction::TransactionError, - }, - spl_stake_pool::{error::StakePoolError, state::StakePool, MINIMUM_RESERVE_LAMPORTS}, -}; - -#[tokio::test] -async fn success_initialize() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let deposit_authority = Keypair::new(); - let stake_pool_accounts = StakePoolAccounts::new_with_deposit_authority(deposit_authority); - let deposit_authority = stake_pool_accounts.stake_deposit_authority; - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - // Stake pool now exists - let stake_pool_account = - get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool_account.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_deposit_authority, deposit_authority); - assert_eq!(stake_pool.sol_deposit_authority.unwrap(), deposit_authority); -} - -#[tokio::test] -async fn success_deposit() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_deposit_authority = Keypair::new(); - let stake_pool_accounts = - StakePoolAccounts::new_with_deposit_authority(stake_deposit_authority); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let validator_stake_account = simple_add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - stake_pool_accounts.stake_deposit_authority_keypair.as_ref(), - ) - .await; - - let user = Keypair::new(); - let user_stake = Keypair::new(); - let lockup = stake::state::Lockup::default(); - let authorized = stake::state::Authorized { - staker: user.pubkey(), - withdrawer: user.pubkey(), - }; - - let _stake_lamports = create_independent_stake_account( - &mut banks_client, - &payer, - &recent_blockhash, - &user_stake, - &authorized, - &lockup, - TEST_STAKE_AMOUNT, - ) - .await; - - delegate_stake_account( - &mut banks_client, - &payer, - &recent_blockhash, - &user_stake.pubkey(), - &user, - &validator_stake_account.vote.pubkey(), - ) - .await; - - // make pool token account - let user_pool_account = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &user_pool_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - let error = stake_pool_accounts - .deposit_stake( - &mut banks_client, - &payer, - &recent_blockhash, - &user_stake.pubkey(), - &user_pool_account.pubkey(), - &validator_stake_account.stake_account, - &user, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -#[tokio::test] -async fn fail_deposit_without_authority_signature() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_deposit_authority = Keypair::new(); - let mut stake_pool_accounts = - StakePoolAccounts::new_with_deposit_authority(stake_deposit_authority); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let validator_stake_account = simple_add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - stake_pool_accounts.stake_deposit_authority_keypair.as_ref(), - ) - .await; - - let user = Keypair::new(); - let user_stake = Keypair::new(); - let lockup = stake::state::Lockup::default(); - let authorized = stake::state::Authorized { - staker: user.pubkey(), - withdrawer: user.pubkey(), - }; - - let _stake_lamports = create_independent_stake_account( - &mut banks_client, - &payer, - &recent_blockhash, - &user_stake, - &authorized, - &lockup, - TEST_STAKE_AMOUNT, - ) - .await; - - delegate_stake_account( - &mut banks_client, - &payer, - &recent_blockhash, - &user_stake.pubkey(), - &user, - &validator_stake_account.vote.pubkey(), - ) - .await; - - // make pool token account - let user_pool_account = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &user_pool_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - let wrong_depositor = Keypair::new(); - stake_pool_accounts.stake_deposit_authority = wrong_depositor.pubkey(); - stake_pool_accounts.stake_deposit_authority_keypair = Some(wrong_depositor); - - let error = stake_pool_accounts - .deposit_stake( - &mut banks_client, - &payer, - &recent_blockhash, - &user_stake.pubkey(), - &user_pool_account.pubkey(), - &validator_stake_account.stake_account, - &user, - ) - .await - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - assert_eq!( - error_index, - StakePoolError::InvalidStakeDepositAuthority as u32 - ); - } - _ => panic!("Wrong error occurs while try to make a deposit with wrong stake program ID"), - } -} diff --git a/stake-pool/program/tests/deposit_edge_cases.rs b/stake-pool/program/tests/deposit_edge_cases.rs deleted file mode 100644 index a11ea428c54..00000000000 --- a/stake-pool/program/tests/deposit_edge_cases.rs +++ /dev/null @@ -1,335 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, instruction::InstructionError, pubkey::Pubkey, stake, - }, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{error::StakePoolError, id, instruction, state, MINIMUM_RESERVE_LAMPORTS}, -}; - -async fn setup( - token_program_id: Pubkey, -) -> ( - ProgramTestContext, - StakePoolAccounts, - ValidatorStakeAccount, - Keypair, - Pubkey, - Pubkey, - u64, -) { - let mut context = program_test().start_with_context().await; - - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let validator_stake_account = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - let user = Keypair::new(); - // make stake account - let deposit_stake = Keypair::new(); - let lockup = stake::state::Lockup::default(); - - let authorized = stake::state::Authorized { - staker: user.pubkey(), - withdrawer: user.pubkey(), - }; - - let stake_lamports = create_independent_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &authorized, - &lockup, - TEST_STAKE_AMOUNT, - ) - .await; - - delegate_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake.pubkey(), - &user, - &validator_stake_account.vote.pubkey(), - ) - .await; - - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - // make pool token account - let pool_token_account = Keypair::new(); - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &pool_token_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - ( - context, - stake_pool_accounts, - validator_stake_account, - user, - deposit_stake.pubkey(), - pool_token_account.pubkey(), - stake_lamports, - ) -} - -#[tokio::test] -async fn success_with_preferred_deposit() { - let ( - mut context, - stake_pool_accounts, - validator_stake, - user, - deposit_stake, - pool_token_account, - _stake_lamports, - ) = setup(spl_token::id()).await; - - stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - instruction::PreferredValidatorType::Deposit, - Some(validator_stake.vote.pubkey()), - ) - .await; - - let error = stake_pool_accounts - .deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &pool_token_account, - &validator_stake.stake_account, - &user, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -#[tokio::test] -async fn fail_with_wrong_preferred_deposit() { - let ( - mut context, - stake_pool_accounts, - validator_stake, - user, - deposit_stake, - pool_token_account, - _stake_lamports, - ) = setup(spl_token::id()).await; - - let preferred_validator = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - instruction::PreferredValidatorType::Deposit, - Some(preferred_validator.vote.pubkey()), - ) - .await; - - let error = stake_pool_accounts - .deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &pool_token_account, - &validator_stake.stake_account, - &user, - ) - .await - .unwrap() - .unwrap(); - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - assert_eq!( - error_index, - StakePoolError::IncorrectDepositVoteAddress as u32 - ); - } - _ => panic!("Wrong error occurs while try to make a deposit with wrong stake program ID"), - } -} - -#[tokio::test] -async fn success_with_referral_fee() { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - user, - deposit_stake, - pool_token_account, - stake_lamports, - ) = setup(spl_token::id()).await; - - let referrer = Keypair::new(); - let referrer_token_account = Keypair::new(); - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &referrer_token_account, - &stake_pool_accounts.pool_mint.pubkey(), - &referrer, - &[], - ) - .await - .unwrap(); - - let referrer_balance_pre = - get_token_balance(&mut context.banks_client, &referrer_token_account.pubkey()).await; - - let mut transaction = Transaction::new_with_payer( - &instruction::deposit_stake( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.withdraw_authority, - &deposit_stake, - &user.pubkey(), - &validator_stake_account.stake_account, - &stake_pool_accounts.reserve_stake.pubkey(), - &pool_token_account, - &stake_pool_accounts.pool_fee_account.pubkey(), - &referrer_token_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &spl_token::id(), - ), - Some(&context.payer.pubkey()), - ); - transaction.sign(&[&context.payer, &user], context.last_blockhash); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let referrer_balance_post = - get_token_balance(&mut context.banks_client, &referrer_token_account.pubkey()).await; - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let fee_tokens = stake_pool - .calc_pool_tokens_sol_deposit_fee(stake_rent) - .unwrap() - + stake_pool - .calc_pool_tokens_stake_deposit_fee(stake_lamports - stake_rent) - .unwrap(); - let referral_fee = stake_pool_accounts.calculate_referral_fee(fee_tokens); - assert!(referral_fee > 0); - assert_eq!(referrer_balance_pre + referral_fee, referrer_balance_post); -} - -#[tokio::test] -async fn fail_with_invalid_referrer() { - let ( - context, - stake_pool_accounts, - validator_stake_account, - user, - deposit_stake, - pool_token_account, - _stake_lamports, - ) = setup(spl_token::id()).await; - - let invalid_token_account = Keypair::new(); - - let mut transaction = Transaction::new_with_payer( - &instruction::deposit_stake( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.withdraw_authority, - &deposit_stake, - &user.pubkey(), - &validator_stake_account.stake_account, - &stake_pool_accounts.reserve_stake.pubkey(), - &pool_token_account, - &stake_pool_accounts.pool_fee_account.pubkey(), - &invalid_token_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &spl_token::id(), - ), - Some(&context.payer.pubkey()), - ); - transaction.sign(&[&context.payer, &user], context.last_blockhash); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match transaction_error { - TransactionError::InstructionError(_, InstructionError::InvalidAccountData) => (), - _ => panic!( - "Wrong error occurs while try to make a deposit with an invalid referrer account" - ), - } -} diff --git a/stake-pool/program/tests/deposit_sol.rs b/stake-pool/program/tests/deposit_sol.rs deleted file mode 100644 index aebeb4f1010..00000000000 --- a/stake-pool/program/tests/deposit_sol.rs +++ /dev/null @@ -1,605 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, instruction::InstructionError, pubkey::Pubkey, - }, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_stake_pool::{ - error, id, - instruction::{self, FundingType}, - state, MINIMUM_RESERVE_LAMPORTS, - }, - spl_token::error as token_error, - test_case::test_case, -}; - -async fn setup( - token_program_id: Pubkey, -) -> (ProgramTestContext, StakePoolAccounts, Keypair, Pubkey) { - let mut context = program_test().start_with_context().await; - - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let user = Keypair::new(); - - // make pool token account for user - let pool_token_account = Keypair::new(); - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &pool_token_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - ( - context, - stake_pool_accounts, - user, - pool_token_account.pubkey(), - ) -} - -#[test_case(spl_token::id(); "token")] -#[test_case(spl_token_2022::id(); "token-2022")] -#[tokio::test] -async fn success(token_program_id: Pubkey) { - let (mut context, stake_pool_accounts, _user, pool_token_account) = - setup(token_program_id).await; - - // Save stake pool state before depositing - let pre_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let pre_stake_pool = - try_from_slice_unchecked::(pre_stake_pool.data.as_slice()).unwrap(); - - // Save reserve state before depositing - let pre_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - - let error = stake_pool_accounts - .deposit_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &pool_token_account, - TEST_STAKE_AMOUNT, - None, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let tokens_issued = TEST_STAKE_AMOUNT; // For now tokens are 1:1 to stake - - // Stake pool should add its balance to the pool balance - let post_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let post_stake_pool = - try_from_slice_unchecked::(post_stake_pool.data.as_slice()).unwrap(); - assert_eq!( - post_stake_pool.total_lamports, - pre_stake_pool.total_lamports + TEST_STAKE_AMOUNT - ); - assert_eq!( - post_stake_pool.pool_token_supply, - pre_stake_pool.pool_token_supply + tokens_issued - ); - - // Check minted tokens - let user_token_balance = - get_token_balance(&mut context.banks_client, &pool_token_account).await; - let tokens_issued_user = - tokens_issued - stake_pool_accounts.calculate_sol_deposit_fee(tokens_issued); - assert_eq!(user_token_balance, tokens_issued_user); - - // Check reserve - let post_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - assert_eq!( - post_reserve_lamports, - pre_reserve_lamports + TEST_STAKE_AMOUNT - ); -} - -#[tokio::test] -async fn fail_with_wrong_token_program_id() { - let (context, stake_pool_accounts, _user, pool_token_account) = setup(spl_token::id()).await; - - let wrong_token_program = Keypair::new(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::deposit_sol( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.reserve_stake.pubkey(), - &context.payer.pubkey(), - &pool_token_account, - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &wrong_token_program.pubkey(), - TEST_STAKE_AMOUNT, - )], - Some(&context.payer.pubkey()), - ); - transaction.sign(&[&context.payer], context.last_blockhash); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!("Wrong error occurs while try to make a deposit with wrong token program ID"), - } -} - -#[tokio::test] -async fn fail_with_wrong_withdraw_authority() { - let (mut context, mut stake_pool_accounts, _user, pool_token_account) = - setup(spl_token::id()).await; - - stake_pool_accounts.withdraw_authority = Pubkey::new_unique(); - - let transaction_error = stake_pool_accounts - .deposit_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &pool_token_account, - TEST_STAKE_AMOUNT, - None, - ) - .await - .unwrap() - .unwrap(); - - match transaction_error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::InvalidProgramAddress as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to make a deposit with wrong withdraw authority"), - } -} - -#[tokio::test] -async fn fail_with_wrong_mint_for_receiver_acc() { - let (mut context, stake_pool_accounts, _user, _pool_token_account) = - setup(spl_token::id()).await; - - let outside_mint = Keypair::new(); - let outside_withdraw_auth = Keypair::new(); - let outside_manager = Keypair::new(); - let outside_pool_fee_acc = Keypair::new(); - - create_mint( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &outside_mint, - &outside_withdraw_auth.pubkey(), - 0, - &[], - ) - .await - .unwrap(); - - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &outside_pool_fee_acc, - &outside_mint.pubkey(), - &outside_manager, - &[], - ) - .await - .unwrap(); - - let transaction_error = stake_pool_accounts - .deposit_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &outside_pool_fee_acc.pubkey(), - TEST_STAKE_AMOUNT, - None, - ) - .await - .unwrap() - .unwrap(); - - match transaction_error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = token_error::TokenError::MintMismatch as u32; - assert_eq!(error_index, program_error); - } - _ => { - panic!("Wrong error occurs while try to deposit with wrong mint from receiver account") - } - } -} - -#[tokio::test] -async fn success_with_sol_deposit_authority() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let user = Keypair::new(); - - // make pool token account - let user_pool_account = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &user_pool_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - let error = stake_pool_accounts - .deposit_sol( - &mut banks_client, - &payer, - &recent_blockhash, - &user_pool_account.pubkey(), - TEST_STAKE_AMOUNT, - None, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let sol_deposit_authority = Keypair::new(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - Some(&sol_deposit_authority.pubkey()), - FundingType::SolDeposit, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let error = stake_pool_accounts - .deposit_sol( - &mut banks_client, - &payer, - &recent_blockhash, - &user_pool_account.pubkey(), - TEST_STAKE_AMOUNT, - Some(&sol_deposit_authority), - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -#[tokio::test] -async fn fail_without_sol_deposit_authority_signature() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let sol_deposit_authority = Keypair::new(); - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let user = Keypair::new(); - - // make pool token account - let user_pool_account = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &user_pool_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - Some(&sol_deposit_authority.pubkey()), - FundingType::SolDeposit, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let wrong_depositor = Keypair::new(); - - let error = stake_pool_accounts - .deposit_sol( - &mut banks_client, - &payer, - &recent_blockhash, - &user_pool_account.pubkey(), - TEST_STAKE_AMOUNT, - Some(&wrong_depositor), - ) - .await - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - assert_eq!( - error_index, - error::StakePoolError::InvalidSolDepositAuthority as u32 - ); - } - _ => panic!("Wrong error occurs while trying to make a deposit without SOL deposit authority signature"), - } -} - -#[tokio::test] -async fn success_with_referral_fee() { - let (mut context, stake_pool_accounts, _user, pool_token_account) = - setup(spl_token::id()).await; - - let referrer = Keypair::new(); - let referrer_token_account = Keypair::new(); - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &referrer_token_account, - &stake_pool_accounts.pool_mint.pubkey(), - &referrer, - &[], - ) - .await - .unwrap(); - - let referrer_balance_pre = - get_token_balance(&mut context.banks_client, &referrer_token_account.pubkey()).await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::deposit_sol( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.reserve_stake.pubkey(), - &context.payer.pubkey(), - &pool_token_account, - &stake_pool_accounts.pool_fee_account.pubkey(), - &referrer_token_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &spl_token::id(), - TEST_STAKE_AMOUNT, - )], - Some(&context.payer.pubkey()), - ); - transaction.sign(&[&context.payer], context.last_blockhash); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let referrer_balance_post = - get_token_balance(&mut context.banks_client, &referrer_token_account.pubkey()).await; - let referral_fee = stake_pool_accounts.calculate_sol_referral_fee( - stake_pool_accounts.calculate_sol_deposit_fee(TEST_STAKE_AMOUNT), - ); - assert!(referral_fee > 0); - assert_eq!(referrer_balance_pre + referral_fee, referrer_balance_post); -} - -#[tokio::test] -async fn fail_with_invalid_referrer() { - let (context, stake_pool_accounts, _user, pool_token_account) = setup(spl_token::id()).await; - - let invalid_token_account = Keypair::new(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::deposit_sol( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.reserve_stake.pubkey(), - &context.payer.pubkey(), - &pool_token_account, - &stake_pool_accounts.pool_fee_account.pubkey(), - &invalid_token_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &spl_token::id(), - TEST_STAKE_AMOUNT, - )], - Some(&context.payer.pubkey()), - ); - transaction.sign(&[&context.payer], context.last_blockhash); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match transaction_error { - TransactionError::InstructionError(_, InstructionError::InvalidAccountData) => (), - _ => panic!( - "Wrong error occurs while try to make a deposit with an invalid referrer account" - ), - } -} - -#[test_case(spl_token::id(); "token")] -#[test_case(spl_token_2022::id(); "token-2022")] -#[tokio::test] -async fn success_with_slippage(token_program_id: Pubkey) { - let (mut context, stake_pool_accounts, _user, pool_token_account) = - setup(token_program_id).await; - - // Save stake pool state before depositing - let pre_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let pre_stake_pool = - try_from_slice_unchecked::(pre_stake_pool.data.as_slice()).unwrap(); - - // Save reserve state before depositing - let pre_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - - let new_pool_tokens = pre_stake_pool - .calc_pool_tokens_for_deposit(TEST_STAKE_AMOUNT) - .unwrap(); - let pool_tokens_sol_deposit_fee = pre_stake_pool - .calc_pool_tokens_sol_deposit_fee(new_pool_tokens) - .unwrap(); - let tokens_issued = new_pool_tokens - pool_tokens_sol_deposit_fee; - - // Fail with 1 more token in slippage - let error = stake_pool_accounts - .deposit_sol_with_slippage( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &pool_token_account, - TEST_STAKE_AMOUNT, - tokens_issued + 1, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(error::StakePoolError::ExceededSlippage as u32) - ) - ); - - // Succeed with exact return amount - let error = stake_pool_accounts - .deposit_sol_with_slippage( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &pool_token_account, - TEST_STAKE_AMOUNT, - tokens_issued, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Stake pool should add its balance to the pool balance - let post_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let post_stake_pool = - try_from_slice_unchecked::(post_stake_pool.data.as_slice()).unwrap(); - assert_eq!( - post_stake_pool.total_lamports, - pre_stake_pool.total_lamports + TEST_STAKE_AMOUNT - ); - assert_eq!( - post_stake_pool.pool_token_supply, - pre_stake_pool.pool_token_supply + new_pool_tokens - ); - - // Check minted tokens - let user_token_balance = - get_token_balance(&mut context.banks_client, &pool_token_account).await; - assert_eq!(user_token_balance, tokens_issued); - - // Check reserve - let post_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - assert_eq!( - post_reserve_lamports, - pre_reserve_lamports + TEST_STAKE_AMOUNT - ); -} diff --git a/stake-pool/program/tests/fixtures/mpl_token_metadata.so b/stake-pool/program/tests/fixtures/mpl_token_metadata.so deleted file mode 100755 index 399c584c5c1..00000000000 Binary files a/stake-pool/program/tests/fixtures/mpl_token_metadata.so and /dev/null differ diff --git a/stake-pool/program/tests/force_destake.rs b/stake-pool/program/tests/force_destake.rs deleted file mode 100644 index bab4003ad44..00000000000 --- a/stake-pool/program/tests/force_destake.rs +++ /dev/null @@ -1,343 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, - instruction::InstructionError, - pubkey::Pubkey, - stake::{ - self, - stake_flags::StakeFlags, - state::{Authorized, Delegation, Lockup, Meta, Stake, StakeStateV2}, - }, - }, - solana_program_test::*, - solana_sdk::{ - account::{Account, WritableAccount}, - clock::Epoch, - signature::Signer, - transaction::TransactionError, - }, - spl_stake_pool::{ - error::StakePoolError, - find_stake_program_address, find_transient_stake_program_address, id, - state::{AccountType, StakeStatus, ValidatorList, ValidatorListHeader, ValidatorStakeInfo}, - MINIMUM_ACTIVE_STAKE, - }, - std::num::NonZeroU32, -}; - -async fn setup( - stake_pool_accounts: &StakePoolAccounts, - forced_stake: &StakeStateV2, - voter_pubkey: &Pubkey, -) -> (ProgramTestContext, Option) { - let mut program_test = program_test(); - - let stake_pool_pubkey = stake_pool_accounts.stake_pool.pubkey(); - let (mut stake_pool, mut validator_list) = stake_pool_accounts.state(); - - let _ = add_vote_account_with_pubkey(voter_pubkey, &mut program_test); - let mut data = vec![0; std::mem::size_of::()]; - bincode::serialize_into(&mut data[..], forced_stake).unwrap(); - - let stake_account = Account::create( - TEST_STAKE_AMOUNT + STAKE_ACCOUNT_RENT_EXEMPTION, - data, - stake::program::id(), - false, - Epoch::default(), - ); - - let raw_validator_seed = 42; - let validator_seed = NonZeroU32::new(raw_validator_seed); - let (stake_address, _) = - find_stake_program_address(&id(), voter_pubkey, &stake_pool_pubkey, validator_seed); - program_test.add_account(stake_address, stake_account); - let active_stake_lamports = TEST_STAKE_AMOUNT - MINIMUM_ACTIVE_STAKE; - // add to validator list - validator_list.validators.push(ValidatorStakeInfo { - status: StakeStatus::Active.into(), - vote_account_address: *voter_pubkey, - active_stake_lamports: active_stake_lamports.into(), - transient_stake_lamports: 0.into(), - last_update_epoch: 0.into(), - transient_seed_suffix: 0.into(), - unused: 0.into(), - validator_seed_suffix: raw_validator_seed.into(), - }); - - stake_pool.total_lamports += active_stake_lamports; - stake_pool.pool_token_supply += active_stake_lamports; - - add_reserve_stake_account( - &mut program_test, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.withdraw_authority, - TEST_STAKE_AMOUNT, - ); - add_stake_pool_account( - &mut program_test, - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool, - ); - add_validator_list_account( - &mut program_test, - &stake_pool_accounts.validator_list.pubkey(), - &validator_list, - stake_pool_accounts.max_validators, - ); - - add_mint_account( - &mut program_test, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.withdraw_authority, - stake_pool.pool_token_supply, - ); - add_token_account( - &mut program_test, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.manager.pubkey(), - ); - - let context = program_test.start_with_context().await; - (context, validator_seed) -} - -#[tokio::test] -async fn success_update() { - let stake_pool_accounts = StakePoolAccounts::default(); - let meta = Meta { - rent_exempt_reserve: STAKE_ACCOUNT_RENT_EXEMPTION, - authorized: Authorized { - staker: stake_pool_accounts.withdraw_authority, - withdrawer: stake_pool_accounts.withdraw_authority, - }, - lockup: Lockup::default(), - }; - let voter_pubkey = Pubkey::new_unique(); - let (mut context, validator_seed) = setup( - &stake_pool_accounts, - &StakeStateV2::Initialized(meta), - &voter_pubkey, - ) - .await; - let pre_reserve_lamports = context - .banks_client - .get_account(stake_pool_accounts.reserve_stake.pubkey()) - .await - .unwrap() - .unwrap() - .lamports; - let (stake_address, _) = find_stake_program_address( - &id(), - &voter_pubkey, - &stake_pool_accounts.stake_pool.pubkey(), - validator_seed, - ); - let validator_stake_lamports = context - .banks_client - .get_account(stake_address) - .await - .unwrap() - .unwrap() - .lamports; - // update should merge the destaked validator stake account into the reserve - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - let post_reserve_lamports = context - .banks_client - .get_account(stake_pool_accounts.reserve_stake.pubkey()) - .await - .unwrap() - .unwrap() - .lamports; - assert_eq!( - post_reserve_lamports, - pre_reserve_lamports + validator_stake_lamports - ); - // test no more validator stake account - assert!(context - .banks_client - .get_account(stake_address) - .await - .unwrap() - .is_none()); -} - -#[tokio::test] -async fn fail_increase() { - let stake_pool_accounts = StakePoolAccounts::default(); - let meta = Meta { - rent_exempt_reserve: STAKE_ACCOUNT_RENT_EXEMPTION, - authorized: Authorized { - staker: stake_pool_accounts.withdraw_authority, - withdrawer: stake_pool_accounts.withdraw_authority, - }, - lockup: Lockup::default(), - }; - let voter_pubkey = Pubkey::new_unique(); - let (mut context, validator_seed) = setup( - &stake_pool_accounts, - &StakeStateV2::Initialized(meta), - &voter_pubkey, - ) - .await; - let (stake_address, _) = find_stake_program_address( - &id(), - &voter_pubkey, - &stake_pool_accounts.stake_pool.pubkey(), - validator_seed, - ); - let transient_stake_seed = 0; - let transient_stake_address = find_transient_stake_program_address( - &id(), - &voter_pubkey, - &stake_pool_accounts.stake_pool.pubkey(), - transient_stake_seed, - ) - .0; - let error = stake_pool_accounts - .increase_validator_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &transient_stake_address, - &stake_address, - &voter_pubkey, - MINIMUM_ACTIVE_STAKE, - transient_stake_seed, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::WrongStakeStake as u32) - ) - ); -} - -#[tokio::test] -async fn success_remove_validator() { - let stake_pool_accounts = StakePoolAccounts::default(); - let meta = Meta { - rent_exempt_reserve: STAKE_ACCOUNT_RENT_EXEMPTION, - authorized: Authorized { - staker: stake_pool_accounts.withdraw_authority, - withdrawer: stake_pool_accounts.withdraw_authority, - }, - lockup: Lockup::default(), - }; - let voter_pubkey = Pubkey::new_unique(); - let stake = Stake { - delegation: Delegation { - voter_pubkey, - stake: TEST_STAKE_AMOUNT, - activation_epoch: 0, - deactivation_epoch: 0, - ..Delegation::default() - }, - credits_observed: 1, - }; - let (mut context, validator_seed) = setup( - &stake_pool_accounts, - &StakeStateV2::Stake(meta, stake, StakeFlags::empty()), - &voter_pubkey, - ) - .await; - - // move forward to after deactivation - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - let (stake_address, _) = find_stake_program_address( - &id(), - &voter_pubkey, - &stake_pool_accounts.stake_pool.pubkey(), - validator_seed, - ); - let transient_stake_seed = 0; - let transient_stake_address = find_transient_stake_program_address( - &id(), - &voter_pubkey, - &stake_pool_accounts.stake_pool.pubkey(), - transient_stake_seed, - ) - .0; - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_address, - &transient_stake_address, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Get a new blockhash for the next update to work - context.get_new_latest_blockhash().await.unwrap(); - - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check if account was removed from the list of stake accounts - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert_eq!( - validator_list, - ValidatorList { - header: ValidatorListHeader { - account_type: AccountType::ValidatorList, - max_validators: stake_pool_accounts.max_validators, - }, - validators: vec![] - } - ); - - // Check stake account no longer exists - let account = context - .banks_client - .get_account(stake_address) - .await - .unwrap(); - assert!(account.is_none()); -} diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs deleted file mode 100644 index 89baa021502..00000000000 --- a/stake-pool/program/tests/helpers/mod.rs +++ /dev/null @@ -1,2678 +0,0 @@ -#![allow(dead_code)] - -use { - borsh::BorshDeserialize, - solana_program::{ - borsh1::{get_instance_packed_len, get_packed_len, try_from_slice_unchecked}, - hash::Hash, - instruction::Instruction, - program_option::COption, - program_pack::Pack, - pubkey::Pubkey, - stake, system_instruction, system_program, - }, - solana_program_test::{processor, BanksClient, ProgramTest, ProgramTestContext}, - solana_sdk::{ - account::{Account as SolanaAccount, WritableAccount}, - clock::{Clock, Epoch}, - compute_budget::ComputeBudgetInstruction, - signature::{Keypair, Signer}, - transaction::Transaction, - transport::TransportError, - }, - solana_vote_program::{ - self, vote_instruction, - vote_state::{VoteInit, VoteState, VoteStateVersions}, - }, - spl_stake_pool::{ - find_deposit_authority_program_address, find_ephemeral_stake_program_address, - find_stake_program_address, find_transient_stake_program_address, - find_withdraw_authority_program_address, id, - inline_mpl_token_metadata::{self, pda::find_metadata_account}, - instruction, minimum_delegation, - processor::Processor, - state::{self, FeeType, FutureEpoch, StakePool, ValidatorList}, - MAX_VALIDATORS_TO_UPDATE, MINIMUM_RESERVE_LAMPORTS, - }, - spl_token_2022::{ - extension::{ExtensionType, StateWithExtensionsOwned}, - native_mint, - state::{Account, Mint}, - }, - std::{convert::TryInto, num::NonZeroU32}, -}; - -pub const FIRST_NORMAL_EPOCH: u64 = 15; -pub const TEST_STAKE_AMOUNT: u64 = 1_500_000_000; -pub const MAX_TEST_VALIDATORS: u32 = 10_000; -pub const DEFAULT_VALIDATOR_STAKE_SEED: Option = NonZeroU32::new(1_010); -pub const DEFAULT_TRANSIENT_STAKE_SEED: u64 = 42; -pub const STAKE_ACCOUNT_RENT_EXEMPTION: u64 = 2_282_880; -const ACCOUNT_RENT_EXEMPTION: u64 = 1_000_000_000; // go with something big to be safe - -pub fn program_test() -> ProgramTest { - let mut program_test = ProgramTest::new("spl_stake_pool", id(), processor!(Processor::process)); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(spl_token_2022::processor::Processor::process), - ); - program_test -} - -pub fn program_test_with_metadata_program() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program("spl_stake_pool", id(), processor!(Processor::process)); - program_test.add_program("mpl_token_metadata", inline_mpl_token_metadata::id(), None); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(spl_token_2022::processor::Processor::process), - ); - program_test -} - -pub async fn get_account(banks_client: &mut BanksClient, pubkey: &Pubkey) -> SolanaAccount { - banks_client - .get_account(*pubkey) - .await - .expect("client error") - .expect("account not found") -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_mint( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - program_id: &Pubkey, - pool_mint: &Keypair, - manager: &Pubkey, - decimals: u8, - extension_types: &[ExtensionType], -) -> Result<(), TransportError> { - assert!(extension_types.is_empty() || program_id != &spl_token::id()); - let rent = banks_client.get_rent().await.unwrap(); - let space = ExtensionType::try_calculate_account_len::(extension_types).unwrap(); - let mint_rent = rent.minimum_balance(space); - let mint_pubkey = pool_mint.pubkey(); - - let mut instructions = vec![system_instruction::create_account( - &payer.pubkey(), - &mint_pubkey, - mint_rent, - space as u64, - program_id, - )]; - for extension_type in extension_types { - let instruction = match extension_type { - ExtensionType::MintCloseAuthority => - spl_token_2022::instruction::initialize_mint_close_authority( - program_id, - &mint_pubkey, - Some(manager), - ), - ExtensionType::DefaultAccountState => - spl_token_2022::extension::default_account_state::instruction::initialize_default_account_state( - program_id, - &mint_pubkey, - &spl_token_2022::state::AccountState::Initialized, - ), - ExtensionType::TransferFeeConfig => spl_token_2022::extension::transfer_fee::instruction::initialize_transfer_fee_config( - program_id, - &mint_pubkey, - Some(manager), - Some(manager), - 100, - 1_000_000, - ), - ExtensionType::InterestBearingConfig => spl_token_2022::extension::interest_bearing_mint::instruction::initialize( - program_id, - &mint_pubkey, - Some(*manager), - 600, - ), - ExtensionType::NonTransferable => - spl_token_2022::instruction::initialize_non_transferable_mint(program_id, &mint_pubkey), - _ => unimplemented!(), - }; - instructions.push(instruction.unwrap()); - } - instructions.push( - spl_token_2022::instruction::initialize_mint( - program_id, - &pool_mint.pubkey(), - manager, - None, - decimals, - ) - .unwrap(), - ); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, pool_mint], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) -} - -pub async fn transfer( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - recipient: &Pubkey, - amount: u64, -) { - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - recipient, - amount, - )], - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -#[allow(clippy::too_many_arguments)] -pub async fn transfer_spl_tokens( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - program_id: &Pubkey, - source: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - authority: &Keypair, - amount: u64, - decimals: u8, -) { - let transaction = Transaction::new_signed_with_payer( - &[spl_token_2022::instruction::transfer_checked( - program_id, - source, - mint, - destination, - &authority.pubkey(), - &[], - amount, - decimals, - ) - .unwrap()], - Some(&payer.pubkey()), - &[payer, authority], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_token_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - program_id: &Pubkey, - account: &Keypair, - pool_mint: &Pubkey, - authority: &Keypair, - extensions: &[ExtensionType], -) -> Result<(), TransportError> { - let rent = banks_client.get_rent().await.unwrap(); - let space = ExtensionType::try_calculate_account_len::(extensions).unwrap(); - let account_rent = rent.minimum_balance(space); - - let mut instructions = vec![system_instruction::create_account( - &payer.pubkey(), - &account.pubkey(), - account_rent, - space as u64, - program_id, - )]; - - for extension in extensions { - match extension { - ExtensionType::ImmutableOwner => instructions.push( - spl_token_2022::instruction::initialize_immutable_owner( - program_id, - &account.pubkey(), - ) - .unwrap(), - ), - ExtensionType::TransferFeeAmount - | ExtensionType::MemoTransfer - | ExtensionType::CpiGuard - | ExtensionType::NonTransferableAccount => (), - _ => unimplemented!(), - }; - } - - instructions.push( - spl_token_2022::instruction::initialize_account( - program_id, - &account.pubkey(), - pool_mint, - &authority.pubkey(), - ) - .unwrap(), - ); - - let mut signers = vec![payer, account]; - for extension in extensions { - match extension { - ExtensionType::MemoTransfer => { - signers.push(authority); - instructions.push( - spl_token_2022::extension::memo_transfer::instruction::enable_required_transfer_memos( - program_id, - &account.pubkey(), - &authority.pubkey(), - &[], - ) - .unwrap() - ) - } - ExtensionType::CpiGuard => { - signers.push(authority); - instructions.push( - spl_token_2022::extension::cpi_guard::instruction::enable_cpi_guard( - program_id, - &account.pubkey(), - &authority.pubkey(), - &[], - ) - .unwrap(), - ) - } - ExtensionType::ImmutableOwner - | ExtensionType::TransferFeeAmount - | ExtensionType::NonTransferableAccount => (), - _ => unimplemented!(), - } - } - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &signers, - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) -} - -pub async fn close_token_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - program_id: &Pubkey, - account: &Pubkey, - lamports_destination: &Pubkey, - manager: &Keypair, -) -> Result<(), TransportError> { - let mut transaction = Transaction::new_with_payer( - &[spl_token_2022::instruction::close_account( - program_id, - account, - lamports_destination, - &manager.pubkey(), - &[], - ) - .unwrap()], - Some(&payer.pubkey()), - ); - transaction.sign(&[payer, manager], *recent_blockhash); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) -} - -pub async fn freeze_token_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - program_id: &Pubkey, - account: &Pubkey, - pool_mint: &Pubkey, - manager: &Keypair, -) -> Result<(), TransportError> { - let mut transaction = Transaction::new_with_payer( - &[spl_token_2022::instruction::freeze_account( - program_id, - account, - pool_mint, - &manager.pubkey(), - &[], - ) - .unwrap()], - Some(&payer.pubkey()), - ); - transaction.sign(&[payer, manager], *recent_blockhash); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) -} - -#[allow(clippy::too_many_arguments)] -pub async fn mint_tokens( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - program_id: &Pubkey, - mint: &Pubkey, - account: &Pubkey, - mint_authority: &Keypair, - amount: u64, -) -> Result<(), TransportError> { - let transaction = Transaction::new_signed_with_payer( - &[spl_token_2022::instruction::mint_to( - program_id, - mint, - account, - &mint_authority.pubkey(), - &[], - amount, - ) - .unwrap()], - Some(&payer.pubkey()), - &[payer, mint_authority], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) -} - -#[allow(clippy::too_many_arguments)] -pub async fn burn_tokens( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - program_id: &Pubkey, - mint: &Pubkey, - account: &Pubkey, - authority: &Keypair, - amount: u64, -) -> Result<(), TransportError> { - let transaction = Transaction::new_signed_with_payer( - &[spl_token_2022::instruction::burn( - program_id, - account, - mint, - &authority.pubkey(), - &[], - amount, - ) - .unwrap()], - Some(&payer.pubkey()), - &[payer, authority], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) -} - -pub async fn get_token_balance(banks_client: &mut BanksClient, token: &Pubkey) -> u64 { - let token_account = banks_client.get_account(*token).await.unwrap().unwrap(); - let account_info = StateWithExtensionsOwned::::unpack(token_account.data).unwrap(); - account_info.base.amount -} - -#[derive(Clone, BorshDeserialize, Debug, PartialEq, Eq)] -pub struct Metadata { - pub key: u8, - pub update_authority: Pubkey, - pub mint: Pubkey, - pub name: String, - pub symbol: String, - pub uri: String, - pub seller_fee_basis_points: u16, - pub creators: Option>, - pub primary_sale_happened: bool, - pub is_mutable: bool, -} - -pub async fn get_metadata_account(banks_client: &mut BanksClient, token_mint: &Pubkey) -> Metadata { - let (token_metadata, _) = find_metadata_account(token_mint); - let token_metadata_account = banks_client - .get_account(token_metadata) - .await - .unwrap() - .unwrap(); - try_from_slice_unchecked(token_metadata_account.data.as_slice()).unwrap() -} - -pub async fn get_token_supply(banks_client: &mut BanksClient, mint: &Pubkey) -> u64 { - let mint_account = banks_client.get_account(*mint).await.unwrap().unwrap(); - let account_info = StateWithExtensionsOwned::::unpack(mint_account.data).unwrap(); - account_info.base.supply -} - -#[allow(clippy::too_many_arguments)] -pub async fn delegate_tokens( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - program_id: &Pubkey, - account: &Pubkey, - manager: &Keypair, - delegate: &Pubkey, - amount: u64, -) { - let transaction = Transaction::new_signed_with_payer( - &[spl_token_2022::instruction::approve( - program_id, - account, - delegate, - &manager.pubkey(), - &[], - amount, - ) - .unwrap()], - Some(&payer.pubkey()), - &[payer, manager], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -pub async fn revoke_tokens( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - program_id: &Pubkey, - account: &Pubkey, - manager: &Keypair, -) { - let transaction = Transaction::new_signed_with_payer( - &[ - spl_token_2022::instruction::revoke(program_id, account, &manager.pubkey(), &[]) - .unwrap(), - ], - Some(&payer.pubkey()), - &[payer, manager], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_stake_pool( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake_pool: &Keypair, - validator_list: &Keypair, - reserve_stake: &Pubkey, - token_program_id: &Pubkey, - pool_mint: &Pubkey, - pool_token_account: &Pubkey, - manager: &Keypair, - staker: &Pubkey, - withdraw_authority: &Pubkey, - stake_deposit_authority: &Option, - epoch_fee: &state::Fee, - withdrawal_fee: &state::Fee, - deposit_fee: &state::Fee, - referral_fee: u8, - sol_deposit_fee: &state::Fee, - sol_referral_fee: u8, - max_validators: u32, -) -> Result<(), TransportError> { - let rent = banks_client.get_rent().await.unwrap(); - let rent_stake_pool = rent.minimum_balance(get_packed_len::()); - let validator_list_size = - get_instance_packed_len(&state::ValidatorList::new(max_validators)).unwrap(); - let rent_validator_list = rent.minimum_balance(validator_list_size); - - let mut transaction = Transaction::new_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &stake_pool.pubkey(), - rent_stake_pool, - get_packed_len::() as u64, - &id(), - ), - system_instruction::create_account( - &payer.pubkey(), - &validator_list.pubkey(), - rent_validator_list, - validator_list_size as u64, - &id(), - ), - instruction::initialize( - &id(), - &stake_pool.pubkey(), - &manager.pubkey(), - staker, - withdraw_authority, - &validator_list.pubkey(), - reserve_stake, - pool_mint, - pool_token_account, - token_program_id, - stake_deposit_authority.as_ref().map(|k| k.pubkey()), - *epoch_fee, - *withdrawal_fee, - *deposit_fee, - referral_fee, - max_validators, - ), - instruction::set_fee( - &id(), - &stake_pool.pubkey(), - &manager.pubkey(), - FeeType::SolDeposit(*sol_deposit_fee), - ), - instruction::set_fee( - &id(), - &stake_pool.pubkey(), - &manager.pubkey(), - FeeType::SolReferral(sol_referral_fee), - ), - ], - Some(&payer.pubkey()), - ); - let mut signers = vec![payer, stake_pool, validator_list, manager]; - if let Some(stake_deposit_authority) = stake_deposit_authority.as_ref() { - signers.push(stake_deposit_authority); - } - transaction.sign(&signers, *recent_blockhash); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) -} - -pub async fn create_vote( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - validator: &Keypair, - vote: &Keypair, -) { - let rent = banks_client.get_rent().await.unwrap(); - let rent_voter = rent.minimum_balance(VoteState::size_of()); - - let mut instructions = vec![system_instruction::create_account( - &payer.pubkey(), - &validator.pubkey(), - rent.minimum_balance(0), - 0, - &system_program::id(), - )]; - instructions.append(&mut vote_instruction::create_account_with_config( - &payer.pubkey(), - &vote.pubkey(), - &VoteInit { - node_pubkey: validator.pubkey(), - authorized_voter: validator.pubkey(), - ..VoteInit::default() - }, - rent_voter, - vote_instruction::CreateVoteAccountConfig { - space: VoteState::size_of() as u64, - ..Default::default() - }, - )); - - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[validator, vote, payer], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -pub async fn create_independent_stake_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake: &Keypair, - authorized: &stake::state::Authorized, - lockup: &stake::state::Lockup, - stake_amount: u64, -) -> u64 { - let rent = banks_client.get_rent().await.unwrap(); - let lamports = - rent.minimum_balance(std::mem::size_of::()) + stake_amount; - - let transaction = Transaction::new_signed_with_payer( - &stake::instruction::create_account( - &payer.pubkey(), - &stake.pubkey(), - authorized, - lockup, - lamports, - ), - Some(&payer.pubkey()), - &[payer, stake], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - lamports -} - -pub async fn create_blank_stake_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake: &Keypair, -) -> u64 { - let rent = banks_client.get_rent().await.unwrap(); - let lamports = rent.minimum_balance(std::mem::size_of::()); - - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::create_account( - &payer.pubkey(), - &stake.pubkey(), - lamports, - std::mem::size_of::() as u64, - &stake::program::id(), - )], - Some(&payer.pubkey()), - &[payer, stake], - *recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - lamports -} - -pub async fn delegate_stake_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake: &Pubkey, - authorized: &Keypair, - vote: &Pubkey, -) { - let mut transaction = Transaction::new_with_payer( - &[stake::instruction::delegate_stake( - stake, - &authorized.pubkey(), - vote, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[payer, authorized], *recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); -} - -pub async fn stake_get_minimum_delegation( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, -) -> u64 { - let transaction = Transaction::new_signed_with_payer( - &[stake::instruction::get_minimum_delegation()], - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - let mut data = banks_client - .simulate_transaction(transaction) - .await - .unwrap() - .simulation_details - .unwrap() - .return_data - .unwrap() - .data; - data.resize(8, 0); - data.try_into().map(u64::from_le_bytes).unwrap() -} - -pub async fn stake_pool_get_minimum_delegation( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, -) -> u64 { - let stake_minimum = stake_get_minimum_delegation(banks_client, payer, recent_blockhash).await; - minimum_delegation(stake_minimum) -} - -pub async fn authorize_stake_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake: &Pubkey, - authorized: &Keypair, - new_authorized: &Pubkey, - stake_authorize: stake::state::StakeAuthorize, -) { - let mut transaction = Transaction::new_with_payer( - &[stake::instruction::authorize( - stake, - &authorized.pubkey(), - new_authorized, - stake_authorize, - None, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[payer, authorized], *recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); -} - -pub async fn create_unknown_validator_stake( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake_pool: &Pubkey, - lamports: u64, -) -> ValidatorStakeAccount { - let mut unknown_stake = ValidatorStakeAccount::new(stake_pool, NonZeroU32::new(1), 222); - create_vote( - banks_client, - payer, - recent_blockhash, - &unknown_stake.validator, - &unknown_stake.vote, - ) - .await; - let user = Keypair::new(); - let fake_validator_stake = Keypair::new(); - let stake_minimum_delegation = - stake_get_minimum_delegation(banks_client, payer, recent_blockhash).await; - let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); - create_independent_stake_account( - banks_client, - payer, - recent_blockhash, - &fake_validator_stake, - &stake::state::Authorized { - staker: user.pubkey(), - withdrawer: user.pubkey(), - }, - &stake::state::Lockup::default(), - current_minimum_delegation + lamports, - ) - .await; - delegate_stake_account( - banks_client, - payer, - recent_blockhash, - &fake_validator_stake.pubkey(), - &user, - &unknown_stake.vote.pubkey(), - ) - .await; - unknown_stake.stake_account = fake_validator_stake.pubkey(); - unknown_stake -} - -pub struct ValidatorStakeAccount { - pub stake_account: Pubkey, - pub transient_stake_account: Pubkey, - pub transient_stake_seed: u64, - pub validator_stake_seed: Option, - pub vote: Keypair, - pub validator: Keypair, - pub stake_pool: Pubkey, -} - -impl ValidatorStakeAccount { - pub fn new( - stake_pool: &Pubkey, - validator_stake_seed: Option, - transient_stake_seed: u64, - ) -> Self { - let validator = Keypair::new(); - let vote = Keypair::new(); - let (stake_account, _) = - find_stake_program_address(&id(), &vote.pubkey(), stake_pool, validator_stake_seed); - let (transient_stake_account, _) = find_transient_stake_program_address( - &id(), - &vote.pubkey(), - stake_pool, - transient_stake_seed, - ); - ValidatorStakeAccount { - stake_account, - transient_stake_account, - transient_stake_seed, - validator_stake_seed, - vote, - validator, - stake_pool: *stake_pool, - } - } -} - -pub struct StakePoolAccounts { - pub stake_pool: Keypair, - pub validator_list: Keypair, - pub reserve_stake: Keypair, - pub token_program_id: Pubkey, - pub pool_mint: Keypair, - pub pool_fee_account: Keypair, - pub pool_decimals: u8, - pub manager: Keypair, - pub staker: Keypair, - pub withdraw_authority: Pubkey, - pub stake_deposit_authority: Pubkey, - pub stake_deposit_authority_keypair: Option, - pub epoch_fee: state::Fee, - pub withdrawal_fee: state::Fee, - pub deposit_fee: state::Fee, - pub referral_fee: u8, - pub sol_deposit_fee: state::Fee, - pub sol_referral_fee: u8, - pub max_validators: u32, - pub compute_unit_limit: Option, -} - -impl StakePoolAccounts { - pub fn new_with_deposit_authority(stake_deposit_authority: Keypair) -> Self { - Self { - stake_deposit_authority: stake_deposit_authority.pubkey(), - stake_deposit_authority_keypair: Some(stake_deposit_authority), - ..Default::default() - } - } - - pub fn new_with_token_program(token_program_id: Pubkey) -> Self { - Self { - token_program_id, - ..Default::default() - } - } - - pub fn calculate_fee(&self, amount: u64) -> u64 { - (amount * self.epoch_fee.numerator + self.epoch_fee.denominator - 1) - / self.epoch_fee.denominator - } - - pub fn calculate_withdrawal_fee(&self, pool_tokens: u64) -> u64 { - (pool_tokens * self.withdrawal_fee.numerator + self.withdrawal_fee.denominator - 1) - / self.withdrawal_fee.denominator - } - - pub fn calculate_inverse_withdrawal_fee(&self, pool_tokens: u64) -> u64 { - (pool_tokens * self.withdrawal_fee.denominator + self.withdrawal_fee.denominator - 1) - / (self.withdrawal_fee.denominator - self.withdrawal_fee.numerator) - } - - pub fn calculate_referral_fee(&self, deposit_fee_collected: u64) -> u64 { - deposit_fee_collected * self.referral_fee as u64 / 100 - } - - pub fn calculate_sol_deposit_fee(&self, pool_tokens: u64) -> u64 { - (pool_tokens * self.sol_deposit_fee.numerator + self.sol_deposit_fee.denominator - 1) - / self.sol_deposit_fee.denominator - } - - pub fn calculate_sol_referral_fee(&self, deposit_fee_collected: u64) -> u64 { - deposit_fee_collected * self.sol_referral_fee as u64 / 100 - } - - pub async fn initialize_stake_pool( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - reserve_lamports: u64, - ) -> Result<(), TransportError> { - create_mint( - banks_client, - payer, - recent_blockhash, - &self.token_program_id, - &self.pool_mint, - &self.withdraw_authority, - self.pool_decimals, - &[], - ) - .await?; - create_token_account( - banks_client, - payer, - recent_blockhash, - &self.token_program_id, - &self.pool_fee_account, - &self.pool_mint.pubkey(), - &self.manager, - &[], - ) - .await?; - create_independent_stake_account( - banks_client, - payer, - recent_blockhash, - &self.reserve_stake, - &stake::state::Authorized { - staker: self.withdraw_authority, - withdrawer: self.withdraw_authority, - }, - &stake::state::Lockup::default(), - reserve_lamports, - ) - .await; - create_stake_pool( - banks_client, - payer, - recent_blockhash, - &self.stake_pool, - &self.validator_list, - &self.reserve_stake.pubkey(), - &self.token_program_id, - &self.pool_mint.pubkey(), - &self.pool_fee_account.pubkey(), - &self.manager, - &self.staker.pubkey(), - &self.withdraw_authority, - &self.stake_deposit_authority_keypair, - &self.epoch_fee, - &self.withdrawal_fee, - &self.deposit_fee, - self.referral_fee, - &self.sol_deposit_fee, - self.sol_referral_fee, - self.max_validators, - ) - .await?; - - Ok(()) - } - - #[allow(clippy::too_many_arguments)] - pub async fn deposit_stake_with_slippage( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake: &Pubkey, - pool_account: &Pubkey, - validator_stake_account: &Pubkey, - current_staker: &Keypair, - minimum_pool_tokens_out: u64, - ) -> Option { - let mut instructions = instruction::deposit_stake_with_slippage( - &id(), - &self.stake_pool.pubkey(), - &self.validator_list.pubkey(), - &self.withdraw_authority, - stake, - ¤t_staker.pubkey(), - validator_stake_account, - &self.reserve_stake.pubkey(), - pool_account, - &self.pool_fee_account.pubkey(), - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - minimum_pool_tokens_out, - ); - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, current_staker], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn deposit_stake( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake: &Pubkey, - pool_account: &Pubkey, - validator_stake_account: &Pubkey, - current_staker: &Keypair, - ) -> Option { - self.deposit_stake_with_referral( - banks_client, - payer, - recent_blockhash, - stake, - pool_account, - validator_stake_account, - current_staker, - &self.pool_fee_account.pubkey(), - ) - .await - } - - #[allow(clippy::too_many_arguments)] - pub async fn deposit_stake_with_referral( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake: &Pubkey, - pool_account: &Pubkey, - validator_stake_account: &Pubkey, - current_staker: &Keypair, - referrer: &Pubkey, - ) -> Option { - let mut signers = vec![payer, current_staker]; - let mut instructions = - if let Some(stake_deposit_authority) = self.stake_deposit_authority_keypair.as_ref() { - signers.push(stake_deposit_authority); - instruction::deposit_stake_with_authority( - &id(), - &self.stake_pool.pubkey(), - &self.validator_list.pubkey(), - &self.stake_deposit_authority, - &self.withdraw_authority, - stake, - ¤t_staker.pubkey(), - validator_stake_account, - &self.reserve_stake.pubkey(), - pool_account, - &self.pool_fee_account.pubkey(), - referrer, - &self.pool_mint.pubkey(), - &self.token_program_id, - ) - } else { - instruction::deposit_stake( - &id(), - &self.stake_pool.pubkey(), - &self.validator_list.pubkey(), - &self.withdraw_authority, - stake, - ¤t_staker.pubkey(), - validator_stake_account, - &self.reserve_stake.pubkey(), - pool_account, - &self.pool_fee_account.pubkey(), - referrer, - &self.pool_mint.pubkey(), - &self.token_program_id, - ) - }; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &signers, - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn deposit_sol( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - pool_account: &Pubkey, - amount: u64, - sol_deposit_authority: Option<&Keypair>, - ) -> Option { - let mut signers = vec![payer]; - let instruction = if let Some(sol_deposit_authority) = sol_deposit_authority { - signers.push(sol_deposit_authority); - instruction::deposit_sol_with_authority( - &id(), - &self.stake_pool.pubkey(), - &sol_deposit_authority.pubkey(), - &self.withdraw_authority, - &self.reserve_stake.pubkey(), - &payer.pubkey(), - pool_account, - &self.pool_fee_account.pubkey(), - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - amount, - ) - } else { - instruction::deposit_sol( - &id(), - &self.stake_pool.pubkey(), - &self.withdraw_authority, - &self.reserve_stake.pubkey(), - &payer.pubkey(), - pool_account, - &self.pool_fee_account.pubkey(), - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - amount, - ) - }; - let mut instructions = vec![instruction]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &signers, - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn deposit_sol_with_slippage( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - pool_account: &Pubkey, - lamports_in: u64, - minimum_pool_tokens_out: u64, - ) -> Option { - let mut instructions = vec![instruction::deposit_sol_with_slippage( - &id(), - &self.stake_pool.pubkey(), - &self.withdraw_authority, - &self.reserve_stake.pubkey(), - &payer.pubkey(), - pool_account, - &self.pool_fee_account.pubkey(), - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - lamports_in, - minimum_pool_tokens_out, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn withdraw_stake_with_slippage( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake_recipient: &Pubkey, - user_transfer_authority: &Keypair, - pool_account: &Pubkey, - validator_stake_account: &Pubkey, - recipient_new_authority: &Pubkey, - pool_tokens_in: u64, - minimum_lamports_out: u64, - ) -> Option { - let mut instructions = vec![instruction::withdraw_stake_with_slippage( - &id(), - &self.stake_pool.pubkey(), - &self.validator_list.pubkey(), - &self.withdraw_authority, - validator_stake_account, - stake_recipient, - recipient_new_authority, - &user_transfer_authority.pubkey(), - pool_account, - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - pool_tokens_in, - minimum_lamports_out, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, user_transfer_authority], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn withdraw_stake( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake_recipient: &Pubkey, - user_transfer_authority: &Keypair, - pool_account: &Pubkey, - validator_stake_account: &Pubkey, - recipient_new_authority: &Pubkey, - amount: u64, - ) -> Option { - let mut instructions = vec![instruction::withdraw_stake( - &id(), - &self.stake_pool.pubkey(), - &self.validator_list.pubkey(), - &self.withdraw_authority, - validator_stake_account, - stake_recipient, - recipient_new_authority, - &user_transfer_authority.pubkey(), - pool_account, - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - amount, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, user_transfer_authority], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn withdraw_sol_with_slippage( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - user: &Keypair, - pool_account: &Pubkey, - amount_in: u64, - minimum_lamports_out: u64, - ) -> Option { - let mut instructions = vec![instruction::withdraw_sol_with_slippage( - &id(), - &self.stake_pool.pubkey(), - &self.withdraw_authority, - &user.pubkey(), - pool_account, - &self.reserve_stake.pubkey(), - &user.pubkey(), - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - amount_in, - minimum_lamports_out, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, user], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn withdraw_sol( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - user: &Keypair, - pool_account: &Pubkey, - amount: u64, - sol_withdraw_authority: Option<&Keypair>, - ) -> Option { - let mut signers = vec![payer, user]; - let instruction = if let Some(sol_withdraw_authority) = sol_withdraw_authority { - signers.push(sol_withdraw_authority); - instruction::withdraw_sol_with_authority( - &id(), - &self.stake_pool.pubkey(), - &sol_withdraw_authority.pubkey(), - &self.withdraw_authority, - &user.pubkey(), - pool_account, - &self.reserve_stake.pubkey(), - &user.pubkey(), - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - amount, - ) - } else { - instruction::withdraw_sol( - &id(), - &self.stake_pool.pubkey(), - &self.withdraw_authority, - &user.pubkey(), - pool_account, - &self.reserve_stake.pubkey(), - &user.pubkey(), - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - amount, - ) - }; - let mut instructions = vec![instruction]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &signers, - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - pub async fn get_stake_pool(&self, banks_client: &mut BanksClient) -> StakePool { - let stake_pool_account = get_account(banks_client, &self.stake_pool.pubkey()).await; - try_from_slice_unchecked::(stake_pool_account.data.as_slice()).unwrap() - } - - pub async fn get_validator_list(&self, banks_client: &mut BanksClient) -> ValidatorList { - let validator_list_account = get_account(banks_client, &self.validator_list.pubkey()).await; - try_from_slice_unchecked::(validator_list_account.data.as_slice()).unwrap() - } - - pub async fn update_validator_list_balance( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - len: usize, - no_merge: bool, - ) -> Option { - let validator_list = self.get_validator_list(banks_client).await; - let mut instructions = vec![instruction::update_validator_list_balance_chunk( - &id(), - &self.stake_pool.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - &self.reserve_stake.pubkey(), - &validator_list, - len, - 0, - no_merge, - ) - .unwrap()]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - pub async fn update_stake_pool_balance( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - ) -> Option { - let mut instructions = vec![instruction::update_stake_pool_balance( - &id(), - &self.stake_pool.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - &self.reserve_stake.pubkey(), - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - pub async fn cleanup_removed_validator_entries( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - ) -> Option { - let mut instructions = vec![instruction::cleanup_removed_validator_entries( - &id(), - &self.stake_pool.pubkey(), - &self.validator_list.pubkey(), - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - pub async fn update_all( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - no_merge: bool, - ) -> Option { - let validator_list = self.get_validator_list(banks_client).await; - let mut instructions = vec![]; - for (i, chunk) in validator_list - .validators - .chunks(MAX_VALIDATORS_TO_UPDATE) - .enumerate() - { - instructions.push( - instruction::update_validator_list_balance_chunk( - &id(), - &self.stake_pool.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - &self.reserve_stake.pubkey(), - &validator_list, - chunk.len(), - i * MAX_VALIDATORS_TO_UPDATE, - no_merge, - ) - .unwrap(), - ); - } - instructions.extend([ - instruction::update_stake_pool_balance( - &id(), - &self.stake_pool.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - &self.reserve_stake.pubkey(), - &self.pool_fee_account.pubkey(), - &self.pool_mint.pubkey(), - &self.token_program_id, - ), - instruction::cleanup_removed_validator_entries( - &id(), - &self.stake_pool.pubkey(), - &self.validator_list.pubkey(), - ), - ]); - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - pub async fn add_validator_to_pool( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake: &Pubkey, - validator: &Pubkey, - seed: Option, - ) -> Option { - let mut instructions = vec![instruction::add_validator_to_pool( - &id(), - &self.stake_pool.pubkey(), - &self.staker.pubkey(), - &self.reserve_stake.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - stake, - validator, - seed, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, &self.staker], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn remove_validator_from_pool( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - validator_stake: &Pubkey, - transient_stake: &Pubkey, - ) -> Option { - let mut instructions = vec![instruction::remove_validator_from_pool( - &id(), - &self.stake_pool.pubkey(), - &self.staker.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - validator_stake, - transient_stake, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, &self.staker], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn decrease_validator_stake_deprecated( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - validator_stake: &Pubkey, - transient_stake: &Pubkey, - lamports: u64, - transient_stake_seed: u64, - ) -> Option { - #[allow(deprecated)] - let mut instructions = vec![ - system_instruction::transfer( - &payer.pubkey(), - transient_stake, - STAKE_ACCOUNT_RENT_EXEMPTION, - ), - instruction::decrease_validator_stake( - &id(), - &self.stake_pool.pubkey(), - &self.staker.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - validator_stake, - transient_stake, - lamports, - transient_stake_seed, - ), - ]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, &self.staker], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn decrease_validator_stake_with_reserve( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - validator_stake: &Pubkey, - transient_stake: &Pubkey, - lamports: u64, - transient_stake_seed: u64, - ) -> Option { - let mut instructions = vec![instruction::decrease_validator_stake_with_reserve( - &id(), - &self.stake_pool.pubkey(), - &self.staker.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - &self.reserve_stake.pubkey(), - validator_stake, - transient_stake, - lamports, - transient_stake_seed, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, &self.staker], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn decrease_additional_validator_stake( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - validator_stake: &Pubkey, - ephemeral_stake: &Pubkey, - transient_stake: &Pubkey, - lamports: u64, - transient_stake_seed: u64, - ephemeral_stake_seed: u64, - ) -> Option { - let mut instructions = vec![instruction::decrease_additional_validator_stake( - &id(), - &self.stake_pool.pubkey(), - &self.staker.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - &self.reserve_stake.pubkey(), - validator_stake, - ephemeral_stake, - transient_stake, - lamports, - transient_stake_seed, - ephemeral_stake_seed, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, &self.staker], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn decrease_validator_stake_either( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - validator_stake: &Pubkey, - transient_stake: &Pubkey, - lamports: u64, - transient_stake_seed: u64, - instruction_type: DecreaseInstruction, - ) -> Option { - match instruction_type { - DecreaseInstruction::Additional => { - let ephemeral_stake_seed = 0; - let ephemeral_stake = find_ephemeral_stake_program_address( - &id(), - &self.stake_pool.pubkey(), - ephemeral_stake_seed, - ) - .0; - self.decrease_additional_validator_stake( - banks_client, - payer, - recent_blockhash, - validator_stake, - &ephemeral_stake, - transient_stake, - lamports, - transient_stake_seed, - ephemeral_stake_seed, - ) - .await - } - DecreaseInstruction::Reserve => { - self.decrease_validator_stake_with_reserve( - banks_client, - payer, - recent_blockhash, - validator_stake, - transient_stake, - lamports, - transient_stake_seed, - ) - .await - } - DecreaseInstruction::Deprecated => - { - #[allow(deprecated)] - self.decrease_validator_stake_deprecated( - banks_client, - payer, - recent_blockhash, - validator_stake, - transient_stake, - lamports, - transient_stake_seed, - ) - .await - } - } - } - - #[allow(clippy::too_many_arguments)] - pub async fn increase_validator_stake( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - transient_stake: &Pubkey, - validator_stake: &Pubkey, - validator: &Pubkey, - lamports: u64, - transient_stake_seed: u64, - ) -> Option { - let mut instructions = vec![instruction::increase_validator_stake( - &id(), - &self.stake_pool.pubkey(), - &self.staker.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - &self.reserve_stake.pubkey(), - transient_stake, - validator_stake, - validator, - lamports, - transient_stake_seed, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, &self.staker], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn increase_additional_validator_stake( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - ephemeral_stake: &Pubkey, - transient_stake: &Pubkey, - validator_stake: &Pubkey, - validator: &Pubkey, - lamports: u64, - transient_stake_seed: u64, - ephemeral_stake_seed: u64, - ) -> Option { - let mut instructions = vec![instruction::increase_additional_validator_stake( - &id(), - &self.stake_pool.pubkey(), - &self.staker.pubkey(), - &self.withdraw_authority, - &self.validator_list.pubkey(), - &self.reserve_stake.pubkey(), - ephemeral_stake, - transient_stake, - validator_stake, - validator, - lamports, - transient_stake_seed, - ephemeral_stake_seed, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, &self.staker], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - #[allow(clippy::too_many_arguments)] - pub async fn increase_validator_stake_either( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - transient_stake: &Pubkey, - validator_stake: &Pubkey, - validator: &Pubkey, - lamports: u64, - transient_stake_seed: u64, - use_additional_instruction: bool, - ) -> Option { - if use_additional_instruction { - let ephemeral_stake_seed = 0; - let ephemeral_stake = find_ephemeral_stake_program_address( - &id(), - &self.stake_pool.pubkey(), - ephemeral_stake_seed, - ) - .0; - self.increase_additional_validator_stake( - banks_client, - payer, - recent_blockhash, - &ephemeral_stake, - transient_stake, - validator_stake, - validator, - lamports, - transient_stake_seed, - ephemeral_stake_seed, - ) - .await - } else { - self.increase_validator_stake( - banks_client, - payer, - recent_blockhash, - transient_stake, - validator_stake, - validator, - lamports, - transient_stake_seed, - ) - .await - } - } - - pub async fn set_preferred_validator( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - validator_type: instruction::PreferredValidatorType, - validator: Option, - ) -> Option { - let mut instructions = vec![instruction::set_preferred_validator( - &id(), - &self.stake_pool.pubkey(), - &self.staker.pubkey(), - &self.validator_list.pubkey(), - validator_type, - validator, - )]; - self.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&payer.pubkey()), - &[payer, &self.staker], - *recent_blockhash, - ); - banks_client - .process_transaction(transaction) - .await - .map_err(|e| e.into()) - .err() - } - - pub fn state(&self) -> (state::StakePool, state::ValidatorList) { - let (_, stake_withdraw_bump_seed) = - find_withdraw_authority_program_address(&id(), &self.stake_pool.pubkey()); - let stake_pool = state::StakePool { - account_type: state::AccountType::StakePool, - manager: self.manager.pubkey(), - staker: self.staker.pubkey(), - stake_deposit_authority: self.stake_deposit_authority, - stake_withdraw_bump_seed, - validator_list: self.validator_list.pubkey(), - reserve_stake: self.reserve_stake.pubkey(), - pool_mint: self.pool_mint.pubkey(), - manager_fee_account: self.pool_fee_account.pubkey(), - token_program_id: self.token_program_id, - total_lamports: 0, - pool_token_supply: 0, - last_update_epoch: 0, - lockup: stake::state::Lockup::default(), - epoch_fee: self.epoch_fee, - next_epoch_fee: FutureEpoch::None, - preferred_deposit_validator_vote_address: None, - preferred_withdraw_validator_vote_address: None, - stake_deposit_fee: state::Fee::default(), - sol_deposit_fee: state::Fee::default(), - stake_withdrawal_fee: state::Fee::default(), - next_stake_withdrawal_fee: FutureEpoch::None, - stake_referral_fee: 0, - sol_referral_fee: 0, - sol_deposit_authority: None, - sol_withdraw_authority: None, - sol_withdrawal_fee: state::Fee::default(), - next_sol_withdrawal_fee: FutureEpoch::None, - last_epoch_pool_token_supply: 0, - last_epoch_total_lamports: 0, - }; - let mut validator_list = ValidatorList::new(self.max_validators); - validator_list.validators = vec![]; - (stake_pool, validator_list) - } - - pub fn maybe_add_compute_budget_instruction(&self, instructions: &mut Vec) { - if let Some(compute_unit_limit) = self.compute_unit_limit { - instructions.insert( - 0, - ComputeBudgetInstruction::set_compute_unit_limit(compute_unit_limit), - ); - } - } -} -impl Default for StakePoolAccounts { - fn default() -> Self { - let stake_pool = Keypair::new(); - let validator_list = Keypair::new(); - let stake_pool_address = &stake_pool.pubkey(); - let (stake_deposit_authority, _) = - find_deposit_authority_program_address(&id(), stake_pool_address); - let (withdraw_authority, _) = - find_withdraw_authority_program_address(&id(), stake_pool_address); - let reserve_stake = Keypair::new(); - let pool_mint = Keypair::new(); - let pool_fee_account = Keypair::new(); - let manager = Keypair::new(); - let staker = Keypair::new(); - - Self { - stake_pool, - validator_list, - reserve_stake, - token_program_id: spl_token::id(), - pool_mint, - pool_fee_account, - pool_decimals: native_mint::DECIMALS, - manager, - staker, - withdraw_authority, - stake_deposit_authority, - stake_deposit_authority_keypair: None, - epoch_fee: state::Fee { - numerator: 1, - denominator: 100, - }, - withdrawal_fee: state::Fee { - numerator: 3, - denominator: 1000, - }, - deposit_fee: state::Fee { - numerator: 1, - denominator: 1000, - }, - referral_fee: 25, - sol_deposit_fee: state::Fee { - numerator: 3, - denominator: 100, - }, - sol_referral_fee: 50, - max_validators: MAX_TEST_VALIDATORS, - compute_unit_limit: None, - } - } -} - -pub async fn simple_add_validator_to_pool( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake_pool_accounts: &StakePoolAccounts, - sol_deposit_authority: Option<&Keypair>, -) -> ValidatorStakeAccount { - let validator_stake = ValidatorStakeAccount::new( - &stake_pool_accounts.stake_pool.pubkey(), - DEFAULT_VALIDATOR_STAKE_SEED, - DEFAULT_TRANSIENT_STAKE_SEED, - ); - - let rent = banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = - stake_pool_get_minimum_delegation(banks_client, payer, recent_blockhash).await; - - let pool_token_account = Keypair::new(); - create_token_account( - banks_client, - payer, - recent_blockhash, - &stake_pool_accounts.token_program_id, - &pool_token_account, - &stake_pool_accounts.pool_mint.pubkey(), - payer, - &[], - ) - .await - .unwrap(); - let error = stake_pool_accounts - .deposit_sol( - banks_client, - payer, - recent_blockhash, - &pool_token_account.pubkey(), - stake_rent + current_minimum_delegation, - sol_deposit_authority, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - create_vote( - banks_client, - payer, - recent_blockhash, - &validator_stake.validator, - &validator_stake.vote, - ) - .await; - - let error = stake_pool_accounts - .add_validator_to_pool( - banks_client, - payer, - recent_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - validator_stake -} - -#[derive(Debug)] -pub struct DepositStakeAccount { - pub authority: Keypair, - pub stake: Keypair, - pub pool_account: Keypair, - pub stake_lamports: u64, - pub pool_tokens: u64, - pub vote_account: Pubkey, - pub validator_stake_account: Pubkey, -} - -impl DepositStakeAccount { - pub fn new_with_vote( - vote_account: Pubkey, - validator_stake_account: Pubkey, - stake_lamports: u64, - ) -> Self { - let authority = Keypair::new(); - let stake = Keypair::new(); - let pool_account = Keypair::new(); - Self { - authority, - stake, - pool_account, - vote_account, - validator_stake_account, - stake_lamports, - pool_tokens: 0, - } - } - - pub async fn create_and_delegate( - &self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - ) { - let lockup = stake::state::Lockup::default(); - let authorized = stake::state::Authorized { - staker: self.authority.pubkey(), - withdrawer: self.authority.pubkey(), - }; - create_independent_stake_account( - banks_client, - payer, - recent_blockhash, - &self.stake, - &authorized, - &lockup, - self.stake_lamports, - ) - .await; - delegate_stake_account( - banks_client, - payer, - recent_blockhash, - &self.stake.pubkey(), - &self.authority, - &self.vote_account, - ) - .await; - } - - pub async fn deposit_stake( - &mut self, - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake_pool_accounts: &StakePoolAccounts, - ) { - // make pool token account - create_token_account( - banks_client, - payer, - recent_blockhash, - &stake_pool_accounts.token_program_id, - &self.pool_account, - &stake_pool_accounts.pool_mint.pubkey(), - &self.authority, - &[], - ) - .await - .unwrap(); - - let error = stake_pool_accounts - .deposit_stake( - banks_client, - payer, - recent_blockhash, - &self.stake.pubkey(), - &self.pool_account.pubkey(), - &self.validator_stake_account, - &self.authority, - ) - .await; - self.pool_tokens = get_token_balance(banks_client, &self.pool_account.pubkey()).await; - assert!(error.is_none(), "{:?}", error); - } -} - -pub async fn simple_deposit_stake( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake_pool_accounts: &StakePoolAccounts, - validator_stake_account: &ValidatorStakeAccount, - stake_lamports: u64, -) -> Option { - let authority = Keypair::new(); - // make stake account - let stake = Keypair::new(); - let lockup = stake::state::Lockup::default(); - let authorized = stake::state::Authorized { - staker: authority.pubkey(), - withdrawer: authority.pubkey(), - }; - create_independent_stake_account( - banks_client, - payer, - recent_blockhash, - &stake, - &authorized, - &lockup, - stake_lamports, - ) - .await; - let vote_account = validator_stake_account.vote.pubkey(); - delegate_stake_account( - banks_client, - payer, - recent_blockhash, - &stake.pubkey(), - &authority, - &vote_account, - ) - .await; - // make pool token account - let pool_account = Keypair::new(); - create_token_account( - banks_client, - payer, - recent_blockhash, - &stake_pool_accounts.token_program_id, - &pool_account, - &stake_pool_accounts.pool_mint.pubkey(), - &authority, - &[], - ) - .await - .unwrap(); - - let validator_stake_account = validator_stake_account.stake_account; - let error = stake_pool_accounts - .deposit_stake( - banks_client, - payer, - recent_blockhash, - &stake.pubkey(), - &pool_account.pubkey(), - &validator_stake_account, - &authority, - ) - .await; - // backwards, but oh well! - if error.is_some() { - return None; - } - - let pool_tokens = get_token_balance(banks_client, &pool_account.pubkey()).await; - - Some(DepositStakeAccount { - authority, - stake, - pool_account, - stake_lamports, - pool_tokens, - vote_account, - validator_stake_account, - }) -} - -pub async fn get_validator_list_sum( - banks_client: &mut BanksClient, - reserve_stake: &Pubkey, - validator_list: &Pubkey, -) -> u64 { - let validator_list = banks_client - .get_account(*validator_list) - .await - .unwrap() - .unwrap(); - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let reserve_stake = banks_client - .get_account(*reserve_stake) - .await - .unwrap() - .unwrap(); - - let validator_sum: u64 = validator_list - .validators - .iter() - .map(|info| info.stake_lamports().unwrap()) - .sum(); - let rent = banks_client.get_rent().await.unwrap(); - let rent = rent.minimum_balance(std::mem::size_of::()); - validator_sum + reserve_stake.lamports - rent - MINIMUM_RESERVE_LAMPORTS -} - -pub fn add_vote_account_with_pubkey( - voter_pubkey: &Pubkey, - program_test: &mut ProgramTest, -) -> Pubkey { - let authorized_voter = Pubkey::new_unique(); - let authorized_withdrawer = Pubkey::new_unique(); - let commission = 1; - - // create vote account - let node_pubkey = Pubkey::new_unique(); - let vote_state = VoteStateVersions::new_current(VoteState::new( - &VoteInit { - node_pubkey, - authorized_voter, - authorized_withdrawer, - commission, - }, - &Clock::default(), - )); - let vote_account = SolanaAccount::create( - ACCOUNT_RENT_EXEMPTION, - bincode::serialize::(&vote_state).unwrap(), - solana_vote_program::id(), - false, - Epoch::default(), - ); - program_test.add_account(*voter_pubkey, vote_account); - *voter_pubkey -} - -pub fn add_vote_account(program_test: &mut ProgramTest) -> Pubkey { - let voter_pubkey = Pubkey::new_unique(); - add_vote_account_with_pubkey(&voter_pubkey, program_test) -} - -#[allow(clippy::too_many_arguments)] -pub fn add_validator_stake_account( - program_test: &mut ProgramTest, - stake_pool: &mut state::StakePool, - validator_list: &mut state::ValidatorList, - stake_pool_pubkey: &Pubkey, - withdraw_authority: &Pubkey, - voter_pubkey: &Pubkey, - stake_amount: u64, - status: state::StakeStatus, -) { - let meta = stake::state::Meta { - rent_exempt_reserve: STAKE_ACCOUNT_RENT_EXEMPTION, - authorized: stake::state::Authorized { - staker: *withdraw_authority, - withdrawer: *withdraw_authority, - }, - lockup: stake_pool.lockup, - }; - - // create validator stake account - let stake = stake::state::Stake { - delegation: stake::state::Delegation { - voter_pubkey: *voter_pubkey, - stake: stake_amount, - activation_epoch: FIRST_NORMAL_EPOCH, - deactivation_epoch: u64::MAX, - ..Default::default() - }, - credits_observed: 0, - }; - - let mut data = vec![0u8; std::mem::size_of::()]; - let stake_data = bincode::serialize(&stake::state::StakeStateV2::Stake( - meta, - stake, - stake::stake_flags::StakeFlags::empty(), - )) - .unwrap(); - data[..stake_data.len()].copy_from_slice(&stake_data); - let stake_account = SolanaAccount::create( - stake_amount + STAKE_ACCOUNT_RENT_EXEMPTION, - data, - stake::program::id(), - false, - Epoch::default(), - ); - - let raw_suffix = 0; - let validator_seed_suffix = NonZeroU32::new(raw_suffix); - let (stake_address, _) = find_stake_program_address( - &id(), - voter_pubkey, - stake_pool_pubkey, - validator_seed_suffix, - ); - program_test.add_account(stake_address, stake_account); - - let active_stake_lamports = stake_amount + STAKE_ACCOUNT_RENT_EXEMPTION; - - validator_list.validators.push(state::ValidatorStakeInfo { - status: status.into(), - vote_account_address: *voter_pubkey, - active_stake_lamports: active_stake_lamports.into(), - transient_stake_lamports: 0.into(), - last_update_epoch: FIRST_NORMAL_EPOCH.into(), - transient_seed_suffix: 0.into(), - unused: 0.into(), - validator_seed_suffix: raw_suffix.into(), - }); - - stake_pool.total_lamports += active_stake_lamports; - stake_pool.pool_token_supply += active_stake_lamports; -} - -pub fn add_reserve_stake_account( - program_test: &mut ProgramTest, - reserve_stake: &Pubkey, - withdraw_authority: &Pubkey, - stake_amount: u64, -) { - let meta = stake::state::Meta { - rent_exempt_reserve: STAKE_ACCOUNT_RENT_EXEMPTION, - authorized: stake::state::Authorized { - staker: *withdraw_authority, - withdrawer: *withdraw_authority, - }, - lockup: stake::state::Lockup::default(), - }; - let reserve_stake_account = SolanaAccount::create( - stake_amount + STAKE_ACCOUNT_RENT_EXEMPTION, - bincode::serialize::(&stake::state::StakeStateV2::Initialized( - meta, - )) - .unwrap(), - stake::program::id(), - false, - Epoch::default(), - ); - program_test.add_account(*reserve_stake, reserve_stake_account); -} - -pub fn add_stake_pool_account( - program_test: &mut ProgramTest, - stake_pool_pubkey: &Pubkey, - stake_pool: &state::StakePool, -) { - let mut stake_pool_bytes = borsh::to_vec(&stake_pool).unwrap(); - // more room for optionals - stake_pool_bytes.extend_from_slice(Pubkey::default().as_ref()); - stake_pool_bytes.extend_from_slice(Pubkey::default().as_ref()); - let stake_pool_account = SolanaAccount::create( - ACCOUNT_RENT_EXEMPTION, - stake_pool_bytes, - id(), - false, - Epoch::default(), - ); - program_test.add_account(*stake_pool_pubkey, stake_pool_account); -} - -pub fn add_validator_list_account( - program_test: &mut ProgramTest, - validator_list_pubkey: &Pubkey, - validator_list: &state::ValidatorList, - max_validators: u32, -) { - let mut validator_list_bytes = borsh::to_vec(&validator_list).unwrap(); - // add extra room if needed - for _ in validator_list.validators.len()..max_validators as usize { - validator_list_bytes - .append(&mut borsh::to_vec(&state::ValidatorStakeInfo::default()).unwrap()); - } - let validator_list_account = SolanaAccount::create( - ACCOUNT_RENT_EXEMPTION, - validator_list_bytes, - id(), - false, - Epoch::default(), - ); - program_test.add_account(*validator_list_pubkey, validator_list_account); -} - -pub fn add_mint_account( - program_test: &mut ProgramTest, - program_id: &Pubkey, - mint_key: &Pubkey, - mint_authority: &Pubkey, - supply: u64, -) { - let mut mint_vec = vec![0u8; Mint::LEN]; - let mint = Mint { - mint_authority: COption::Some(*mint_authority), - supply, - decimals: 9, - is_initialized: true, - freeze_authority: COption::None, - }; - Pack::pack(mint, &mut mint_vec).unwrap(); - let stake_pool_mint = SolanaAccount::create( - ACCOUNT_RENT_EXEMPTION, - mint_vec, - *program_id, - false, - Epoch::default(), - ); - program_test.add_account(*mint_key, stake_pool_mint); -} - -pub fn add_token_account( - program_test: &mut ProgramTest, - program_id: &Pubkey, - account_key: &Pubkey, - mint_key: &Pubkey, - owner: &Pubkey, -) { - let mut fee_account_vec = vec![0u8; Account::LEN]; - let fee_account_data = Account { - mint: *mint_key, - owner: *owner, - amount: 0, - delegate: COption::None, - state: spl_token_2022::state::AccountState::Initialized, - is_native: COption::None, - delegated_amount: 0, - close_authority: COption::None, - }; - Pack::pack(fee_account_data, &mut fee_account_vec).unwrap(); - let fee_account = SolanaAccount::create( - ACCOUNT_RENT_EXEMPTION, - fee_account_vec, - *program_id, - false, - Epoch::default(), - ); - program_test.add_account(*account_key, fee_account); -} - -pub async fn setup_for_withdraw( - token_program_id: Pubkey, - reserve_lamports: u64, -) -> ( - ProgramTestContext, - StakePoolAccounts, - ValidatorStakeAccount, - DepositStakeAccount, - Keypair, - Keypair, - u64, -) { - let mut context = program_test().start_with_context().await; - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - reserve_lamports, - ) - .await - .unwrap(); - - let validator_stake_account = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - let deposit_info = simple_deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - &validator_stake_account, - current_minimum_delegation * 3, - ) - .await - .unwrap(); - - let tokens_to_withdraw = deposit_info.pool_tokens; - - // Delegate tokens for withdrawing - let user_transfer_authority = Keypair::new(); - delegate_tokens( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &deposit_info.pool_account.pubkey(), - &deposit_info.authority, - &user_transfer_authority.pubkey(), - tokens_to_withdraw, - ) - .await; - - // Create stake account to withdraw to - let user_stake_recipient = Keypair::new(); - create_blank_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient, - ) - .await; - - ( - context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_withdraw, - ) -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum DecreaseInstruction { - Additional, - Reserve, - Deprecated, -} diff --git a/stake-pool/program/tests/huge_pool.rs b/stake-pool/program/tests/huge_pool.rs deleted file mode 100644 index 52662095bed..00000000000 --- a/stake-pool/program/tests/huge_pool.rs +++ /dev/null @@ -1,710 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{borsh1::try_from_slice_unchecked, pubkey::Pubkey, stake}, - solana_program_test::*, - solana_sdk::{ - native_token::LAMPORTS_PER_SOL, - signature::{Keypair, Signer}, - transaction::Transaction, - }, - spl_stake_pool::{ - find_stake_program_address, find_transient_stake_program_address, id, - instruction::{self, PreferredValidatorType}, - state::{StakePool, StakeStatus, ValidatorList}, - MAX_VALIDATORS_TO_UPDATE, - }, - test_case::test_case, -}; - -// Note: this is not the real max! The testing framework starts to blow out -// because the test require so many helper accounts. -// 20k is also a very safe number for the current upper bound of the network. -const MAX_POOL_SIZE_WITH_REQUESTED_COMPUTE_UNITS: u32 = 20_000; -const MAX_POOL_SIZE: u32 = 3_000; -const STAKE_AMOUNT: u64 = 200_000_000_000; - -async fn setup( - max_validators: u32, - num_validators: u32, - stake_amount: u64, -) -> ( - ProgramTestContext, - StakePoolAccounts, - Vec, - Pubkey, - Keypair, - Pubkey, - Pubkey, -) { - let mut program_test = program_test(); - let mut vote_account_pubkeys = vec![]; - let mut stake_pool_accounts = StakePoolAccounts { - max_validators, - ..Default::default() - }; - if max_validators > MAX_POOL_SIZE { - stake_pool_accounts.compute_unit_limit = Some(1_400_000); - } - - let stake_pool_pubkey = stake_pool_accounts.stake_pool.pubkey(); - let (mut stake_pool, mut validator_list) = stake_pool_accounts.state(); - stake_pool.last_update_epoch = FIRST_NORMAL_EPOCH; - - for _ in 0..max_validators { - vote_account_pubkeys.push(add_vote_account(&mut program_test)); - } - - for vote_account_address in vote_account_pubkeys.iter().take(num_validators as usize) { - add_validator_stake_account( - &mut program_test, - &mut stake_pool, - &mut validator_list, - &stake_pool_pubkey, - &stake_pool_accounts.withdraw_authority, - vote_account_address, - stake_amount, - StakeStatus::Active, - ); - } - - add_reserve_stake_account( - &mut program_test, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.withdraw_authority, - stake_amount, - ); - add_stake_pool_account( - &mut program_test, - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool, - ); - add_validator_list_account( - &mut program_test, - &stake_pool_accounts.validator_list.pubkey(), - &validator_list, - max_validators, - ); - - add_mint_account( - &mut program_test, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.withdraw_authority, - stake_pool.pool_token_supply, - ); - add_token_account( - &mut program_test, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.manager.pubkey(), - ); - - let mut context = program_test.start_with_context().await; - let epoch_schedule = &context.genesis_config().epoch_schedule; - let slot = epoch_schedule.first_normal_slot + epoch_schedule.slots_per_epoch + 1; - context.warp_to_slot(slot).unwrap(); - - let vote_pubkey = vote_account_pubkeys[max_validators as usize - 1]; - // make stake account - let user = Keypair::new(); - let deposit_stake = Keypair::new(); - let lockup = stake::state::Lockup::default(); - - let authorized = stake::state::Authorized { - staker: user.pubkey(), - withdrawer: user.pubkey(), - }; - - let _stake_lamports = create_independent_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake, - &authorized, - &lockup, - stake_amount, - ) - .await; - - delegate_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &deposit_stake.pubkey(), - &user, - &vote_pubkey, - ) - .await; - - // make pool token account - let pool_token_account = Keypair::new(); - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &pool_token_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - ( - context, - stake_pool_accounts, - vote_account_pubkeys, - vote_pubkey, - user, - deposit_stake.pubkey(), - pool_token_account.pubkey(), - ) -} - -#[test_case(MAX_POOL_SIZE_WITH_REQUESTED_COMPUTE_UNITS; "compute-budget")] -#[test_case(MAX_POOL_SIZE; "no-compute-budget")] -#[tokio::test] -async fn update(max_validators: u32) { - let (mut context, stake_pool_accounts, _, _, _, _, _) = - setup(max_validators, max_validators, STAKE_AMOUNT).await; - - let error = stake_pool_accounts - .update_validator_list_balance( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MAX_VALIDATORS_TO_UPDATE, - false, /* no_merge */ - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .update_stake_pool_balance( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .cleanup_removed_validator_entries( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -//#[test_case(MAX_POOL_SIZE_WITH_REQUESTED_COMPUTE_UNITS; "compute-budget")] -#[test_case(MAX_POOL_SIZE; "no-compute-budget")] -#[tokio::test] -async fn remove_validator_from_pool(max_validators: u32) { - let (mut context, stake_pool_accounts, vote_account_pubkeys, _, _, _, _) = - setup(max_validators, max_validators, LAMPORTS_PER_SOL).await; - - let first_vote = vote_account_pubkeys[0]; - let (stake_address, _) = find_stake_program_address( - &id(), - &first_vote, - &stake_pool_accounts.stake_pool.pubkey(), - None, - ); - let transient_stake_seed = u64::MAX; - let (transient_stake_address, _) = find_transient_stake_program_address( - &id(), - &first_vote, - &stake_pool_accounts.stake_pool.pubkey(), - transient_stake_seed, - ); - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_address, - &transient_stake_address, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let middle_index = max_validators as usize / 2; - let middle_vote = vote_account_pubkeys[middle_index]; - let (stake_address, _) = find_stake_program_address( - &id(), - &middle_vote, - &stake_pool_accounts.stake_pool.pubkey(), - None, - ); - let (transient_stake_address, _) = find_transient_stake_program_address( - &id(), - &middle_vote, - &stake_pool_accounts.stake_pool.pubkey(), - transient_stake_seed, - ); - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_address, - &transient_stake_address, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let last_index = max_validators as usize - 1; - let last_vote = vote_account_pubkeys[last_index]; - let (stake_address, _) = find_stake_program_address( - &id(), - &last_vote, - &stake_pool_accounts.stake_pool.pubkey(), - None, - ); - let (transient_stake_address, _) = find_transient_stake_program_address( - &id(), - &last_vote, - &stake_pool_accounts.stake_pool.pubkey(), - transient_stake_seed, - ); - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_address, - &transient_stake_address, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let first_element = &validator_list.validators[0]; - assert_eq!( - first_element.status, - StakeStatus::DeactivatingValidator.into() - ); - assert_eq!( - u64::from(first_element.active_stake_lamports), - LAMPORTS_PER_SOL + STAKE_ACCOUNT_RENT_EXEMPTION - ); - assert_eq!(u64::from(first_element.transient_stake_lamports), 0); - - let middle_element = &validator_list.validators[middle_index]; - assert_eq!( - middle_element.status, - StakeStatus::DeactivatingValidator.into() - ); - assert_eq!( - u64::from(middle_element.active_stake_lamports), - LAMPORTS_PER_SOL + STAKE_ACCOUNT_RENT_EXEMPTION - ); - assert_eq!(u64::from(middle_element.transient_stake_lamports), 0); - - let last_element = &validator_list.validators[last_index]; - assert_eq!( - last_element.status, - StakeStatus::DeactivatingValidator.into() - ); - assert_eq!( - u64::from(last_element.active_stake_lamports), - LAMPORTS_PER_SOL + STAKE_ACCOUNT_RENT_EXEMPTION - ); - assert_eq!(u64::from(last_element.transient_stake_lamports), 0); - - let error = stake_pool_accounts - .update_validator_list_balance( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - 1, - false, /* no_merge */ - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let mut instructions = vec![instruction::update_validator_list_balance_chunk( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &validator_list, - 1, - middle_index, - /* no_merge = */ false, - ) - .unwrap()]; - stake_pool_accounts.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err(); - assert!(error.is_none(), "{:?}", error); - - let mut instructions = vec![instruction::update_validator_list_balance_chunk( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &validator_list, - 1, - last_index, - /* no_merge = */ false, - ) - .unwrap()]; - stake_pool_accounts.maybe_add_compute_budget_instruction(&mut instructions); - let transaction = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err(); - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .cleanup_removed_validator_entries( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert_eq!(validator_list.validators.len() as u32, max_validators - 3); - // assert they're gone - assert!(!validator_list - .validators - .iter() - .any(|x| x.vote_account_address == first_vote)); - assert!(!validator_list - .validators - .iter() - .any(|x| x.vote_account_address == middle_vote)); - assert!(!validator_list - .validators - .iter() - .any(|x| x.vote_account_address == last_vote)); - - // but that we didn't remove too many - assert!(validator_list - .validators - .iter() - .any(|x| x.vote_account_address == vote_account_pubkeys[1])); - assert!(validator_list - .validators - .iter() - .any(|x| x.vote_account_address == vote_account_pubkeys[middle_index - 1])); - assert!(validator_list - .validators - .iter() - .any(|x| x.vote_account_address == vote_account_pubkeys[middle_index + 1])); - assert!(validator_list - .validators - .iter() - .any(|x| x.vote_account_address == vote_account_pubkeys[last_index - 1])); -} - -//#[test_case(MAX_POOL_SIZE_WITH_REQUESTED_COMPUTE_UNITS; "compute-budget")] -#[test_case(MAX_POOL_SIZE; "no-compute-budget")] -#[tokio::test] -async fn add_validator_to_pool(max_validators: u32) { - let (mut context, stake_pool_accounts, _, test_vote_address, _, _, _) = - setup(max_validators, max_validators - 1, STAKE_AMOUNT).await; - - let last_index = max_validators as usize - 1; - let stake_pool_pubkey = stake_pool_accounts.stake_pool.pubkey(); - let (stake_address, _) = - find_stake_program_address(&id(), &test_vote_address, &stake_pool_pubkey, None); - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_address, - &test_vote_address, - None, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert_eq!(validator_list.validators.len(), last_index + 1); - let last_element = validator_list.validators[last_index]; - assert_eq!(last_element.status, StakeStatus::Active.into()); - assert_eq!( - u64::from(last_element.active_stake_lamports), - LAMPORTS_PER_SOL + STAKE_ACCOUNT_RENT_EXEMPTION - ); - assert_eq!(u64::from(last_element.transient_stake_lamports), 0); - assert_eq!(last_element.vote_account_address, test_vote_address); - - let transient_stake_seed = u64::MAX; - let (transient_stake_address, _) = find_transient_stake_program_address( - &id(), - &test_vote_address, - &stake_pool_pubkey, - transient_stake_seed, - ); - let increase_amount = LAMPORTS_PER_SOL; - let error = stake_pool_accounts - .increase_validator_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &transient_stake_address, - &stake_address, - &test_vote_address, - increase_amount, - transient_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let last_element = validator_list.validators[last_index]; - assert_eq!(last_element.status, StakeStatus::Active.into()); - assert_eq!( - u64::from(last_element.active_stake_lamports), - LAMPORTS_PER_SOL + STAKE_ACCOUNT_RENT_EXEMPTION - ); - assert_eq!( - u64::from(last_element.transient_stake_lamports), - increase_amount + STAKE_ACCOUNT_RENT_EXEMPTION - ); - assert_eq!(last_element.vote_account_address, test_vote_address); -} - -//#[test_case(MAX_POOL_SIZE_WITH_REQUESTED_COMPUTE_UNITS; "compute-budget")] -#[test_case(MAX_POOL_SIZE; "no-compute-budget")] -#[tokio::test] -async fn set_preferred(max_validators: u32) { - let (mut context, stake_pool_accounts, _, vote_account_address, _, _, _) = - setup(max_validators, max_validators, STAKE_AMOUNT).await; - - let error = stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - PreferredValidatorType::Deposit, - Some(vote_account_address), - ) - .await; - assert!(error.is_none(), "{:?}", error); - let error = stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - PreferredValidatorType::Withdraw, - Some(vote_account_address), - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!( - stake_pool.preferred_deposit_validator_vote_address, - Some(vote_account_address) - ); - assert_eq!( - stake_pool.preferred_withdraw_validator_vote_address, - Some(vote_account_address) - ); -} - -#[test_case(MAX_POOL_SIZE_WITH_REQUESTED_COMPUTE_UNITS; "compute-budget")] -#[test_case(MAX_POOL_SIZE; "no-compute-budget")] -#[tokio::test] -async fn deposit_stake(max_validators: u32) { - let (mut context, stake_pool_accounts, _, vote_pubkey, user, stake_pubkey, pool_account_pubkey) = - setup(max_validators, max_validators, STAKE_AMOUNT).await; - - let (stake_address, _) = find_stake_program_address( - &id(), - &vote_pubkey, - &stake_pool_accounts.stake_pool.pubkey(), - None, - ); - - let error = stake_pool_accounts - .deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pubkey, - &pool_account_pubkey, - &stake_address, - &user, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -#[test_case(MAX_POOL_SIZE_WITH_REQUESTED_COMPUTE_UNITS; "compute-budget")] -#[test_case(MAX_POOL_SIZE; "no-compute-budget")] -#[tokio::test] -async fn withdraw(max_validators: u32) { - let (mut context, stake_pool_accounts, _, vote_pubkey, user, stake_pubkey, pool_account_pubkey) = - setup(max_validators, max_validators, STAKE_AMOUNT).await; - - let (stake_address, _) = find_stake_program_address( - &id(), - &vote_pubkey, - &stake_pool_accounts.stake_pool.pubkey(), - None, - ); - - let error = stake_pool_accounts - .deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pubkey, - &pool_account_pubkey, - &stake_address, - &user, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Create stake account to withdraw to - let user_stake_recipient = Keypair::new(); - create_blank_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient, - ) - .await; - - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user, - &pool_account_pubkey, - &stake_address, - &user.pubkey(), - TEST_STAKE_AMOUNT, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -//#[test_case(MAX_POOL_SIZE_WITH_REQUESTED_COMPUTE_UNITS; "compute-budget")] -#[test_case(MAX_POOL_SIZE; "no-compute-budget")] -#[tokio::test] -async fn cleanup_all(max_validators: u32) { - let mut program_test = program_test(); - let mut vote_account_pubkeys = vec![]; - let mut stake_pool_accounts = StakePoolAccounts { - max_validators, - ..Default::default() - }; - if max_validators > MAX_POOL_SIZE { - stake_pool_accounts.compute_unit_limit = Some(1_400_000); - } - - let stake_pool_pubkey = stake_pool_accounts.stake_pool.pubkey(); - let (mut stake_pool, mut validator_list) = stake_pool_accounts.state(); - - for _ in 0..max_validators { - vote_account_pubkeys.push(add_vote_account(&mut program_test)); - } - - for vote_account_address in vote_account_pubkeys.iter() { - add_validator_stake_account( - &mut program_test, - &mut stake_pool, - &mut validator_list, - &stake_pool_pubkey, - &stake_pool_accounts.withdraw_authority, - vote_account_address, - STAKE_AMOUNT, - StakeStatus::ReadyForRemoval, - ); - } - - add_stake_pool_account( - &mut program_test, - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool, - ); - add_validator_list_account( - &mut program_test, - &stake_pool_accounts.validator_list.pubkey(), - &validator_list, - max_validators, - ); - let mut context = program_test.start_with_context().await; - - let error = stake_pool_accounts - .cleanup_removed_validator_entries( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} diff --git a/stake-pool/program/tests/increase.rs b/stake-pool/program/tests/increase.rs deleted file mode 100644 index 0d6c4bda6a5..00000000000 --- a/stake-pool/program/tests/increase.rs +++ /dev/null @@ -1,595 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![allow(clippy::items_after_test_module)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - bincode::deserialize, - helpers::*, - solana_program::{clock::Epoch, instruction::InstructionError, pubkey::Pubkey, stake}, - solana_program_test::*, - solana_sdk::{ - signature::Signer, - stake::instruction::StakeError, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error::StakePoolError, find_ephemeral_stake_program_address, - find_transient_stake_program_address, id, instruction, MINIMUM_RESERVE_LAMPORTS, - }, - test_case::test_case, -}; - -async fn setup() -> ( - ProgramTestContext, - StakePoolAccounts, - ValidatorStakeAccount, - u64, -) { - let mut context = program_test().start_with_context().await; - let stake_pool_accounts = StakePoolAccounts::default(); - let reserve_lamports = 100_000_000_000 + MINIMUM_RESERVE_LAMPORTS; - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - reserve_lamports, - ) - .await - .unwrap(); - - let validator_stake_account = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - let _deposit_info = simple_deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - &validator_stake_account, - current_minimum_delegation * 2 + stake_rent, - ) - .await - .unwrap(); - - ( - context, - stake_pool_accounts, - validator_stake_account, - reserve_lamports, - ) -} - -#[test_case(true; "additional")] -#[test_case(false; "non-additional")] -#[tokio::test] -async fn success(use_additional_instruction: bool) { - let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; - - // Save reserve stake - let pre_reserve_stake_account = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await; - - // Check no transient stake - let transient_account = context - .banks_client - .get_account(validator_stake.transient_stake_account) - .await - .unwrap(); - assert!(transient_account.is_none()); - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let increase_amount = reserve_lamports - stake_rent - MINIMUM_RESERVE_LAMPORTS; - let error = stake_pool_accounts - .increase_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - increase_amount, - validator_stake.transient_stake_seed, - use_additional_instruction, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check reserve stake account balance - let reserve_stake_account = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await; - let reserve_stake_state = - deserialize::(&reserve_stake_account.data).unwrap(); - assert_eq!( - pre_reserve_stake_account.lamports - increase_amount - stake_rent, - reserve_stake_account.lamports - ); - assert!(reserve_stake_state.delegation().is_none()); - - // Check transient stake account state and balance - let transient_stake_account = get_account( - &mut context.banks_client, - &validator_stake.transient_stake_account, - ) - .await; - let transient_stake_state = - deserialize::(&transient_stake_account.data).unwrap(); - assert_eq!( - transient_stake_account.lamports, - increase_amount + stake_rent - ); - assert_ne!( - transient_stake_state.delegation().unwrap().activation_epoch, - Epoch::MAX - ); -} - -#[tokio::test] -async fn fail_with_wrong_withdraw_authority() { - let (context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; - - let wrong_authority = Pubkey::new_unique(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::increase_validator_stake( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &wrong_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - reserve_lamports / 2, - validator_stake.transient_stake_seed, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.staker], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = StakePoolError::InvalidProgramAddress as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error"), - } -} - -#[tokio::test] -async fn fail_with_wrong_validator_list() { - let (context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; - - let wrong_validator_list = Pubkey::new_unique(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::increase_validator_stake( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &wrong_validator_list, - &stake_pool_accounts.reserve_stake.pubkey(), - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - reserve_lamports / 2, - validator_stake.transient_stake_seed, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.staker], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = StakePoolError::InvalidValidatorStakeList as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error"), - } -} - -#[tokio::test] -async fn fail_with_unknown_validator() { - let (mut context, stake_pool_accounts, _validator_stake, reserve_lamports) = setup().await; - - let unknown_stake = create_unknown_validator_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.stake_pool.pubkey(), - 0, - ) - .await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::increase_validator_stake( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &unknown_stake.transient_stake_account, - &unknown_stake.stake_account, - &unknown_stake.vote.pubkey(), - reserve_lamports / 2, - unknown_stake.transient_stake_seed, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.staker], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::ValidatorNotFound as u32) - ) - ); -} - -#[test_case(true; "additional")] -#[test_case(false; "non-additional")] -#[tokio::test] -async fn fail_twice_diff_seed(use_additional_instruction: bool) { - let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; - - let first_increase = reserve_lamports / 3; - let second_increase = reserve_lamports / 4; - let error = stake_pool_accounts - .increase_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - first_increase, - validator_stake.transient_stake_seed, - use_additional_instruction, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let transient_stake_seed = validator_stake.transient_stake_seed * 100; - let transient_stake_address = find_transient_stake_program_address( - &id(), - &validator_stake.vote.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - transient_stake_seed, - ) - .0; - let error = stake_pool_accounts - .increase_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &transient_stake_address, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - second_increase, - transient_stake_seed, - use_additional_instruction, - ) - .await - .unwrap() - .unwrap(); - - if use_additional_instruction { - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::InvalidSeeds) - ); - } else { - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::TransientAccountInUse as u32) - ) - ); - } -} - -#[test_case(true, true, true; "success-all-additional")] -#[test_case(true, false, true; "success-with-additional")] -#[test_case(false, true, false; "fail-without-additional")] -#[test_case(false, false, false; "fail-no-additional")] -#[tokio::test] -async fn twice(success: bool, use_additional_first_time: bool, use_additional_second_time: bool) { - let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; - - let pre_reserve_stake_account = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await; - - let first_increase = reserve_lamports / 3; - let second_increase = reserve_lamports / 4; - let total_increase = first_increase + second_increase; - let error = stake_pool_accounts - .increase_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - first_increase, - validator_stake.transient_stake_seed, - use_additional_first_time, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .increase_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - second_increase, - validator_stake.transient_stake_seed, - use_additional_second_time, - ) - .await; - - if success { - assert!(error.is_none(), "{:?}", error); - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - // no ephemeral account - let ephemeral_stake = find_ephemeral_stake_program_address( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - 0, - ) - .0; - let ephemeral_account = context - .banks_client - .get_account(ephemeral_stake) - .await - .unwrap(); - assert!(ephemeral_account.is_none()); - // Check reserve stake account balance - let reserve_stake_account = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await; - let reserve_stake_state = - deserialize::(&reserve_stake_account.data).unwrap(); - assert_eq!( - pre_reserve_stake_account.lamports - total_increase - stake_rent * 2, - reserve_stake_account.lamports - ); - assert!(reserve_stake_state.delegation().is_none()); - - // Check transient stake account state and balance - let transient_stake_account = get_account( - &mut context.banks_client, - &validator_stake.transient_stake_account, - ) - .await; - let transient_stake_state = - deserialize::(&transient_stake_account.data).unwrap(); - assert_eq!( - transient_stake_account.lamports, - total_increase + stake_rent * 2 - ); - assert_ne!( - transient_stake_state.delegation().unwrap().activation_epoch, - Epoch::MAX - ); - - // marked correctly in the list - let validator_list = stake_pool_accounts - .get_validator_list(&mut context.banks_client) - .await; - let entry = validator_list.find(&validator_stake.vote.pubkey()).unwrap(); - assert_eq!( - u64::from(entry.transient_stake_lamports), - total_increase + stake_rent * 2 - ); - } else { - let error = error.unwrap().unwrap(); - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = StakePoolError::TransientAccountInUse as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error"), - } - } -} - -#[test_case(true; "additional")] -#[test_case(false; "non-additional")] -#[tokio::test] -async fn fail_with_small_lamport_amount(use_additional_instruction: bool) { - let (mut context, stake_pool_accounts, validator_stake, _reserve_lamports) = setup().await; - - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - let error = stake_pool_accounts - .increase_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - current_minimum_delegation - 1, - validator_stake.transient_stake_seed, - use_additional_instruction, - ) - .await - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = StakeError::InsufficientDelegation as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error"), - } -} - -#[test_case(true; "additional")] -#[test_case(false; "non-additional")] -#[tokio::test] -async fn fail_overdraw_reserve(use_additional_instruction: bool) { - let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; - - let error = stake_pool_accounts - .increase_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - reserve_lamports, - validator_stake.transient_stake_seed, - use_additional_instruction, - ) - .await - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::InsufficientFunds) => {} - _ => panic!("Wrong error occurs while overdrawing reserve stake"), - } -} - -#[tokio::test] -async fn fail_additional_with_decreasing() { - let (mut context, stake_pool_accounts, validator_stake, reserve_lamports) = setup().await; - - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - // warp forward to activation - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - current_minimum_delegation + stake_rent, - validator_stake.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .increase_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - reserve_lamports / 2, - validator_stake.transient_stake_seed, - true, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::WrongStakeStake as u32) - ) - ); -} - -#[tokio::test] -async fn fail_with_force_destaked_validator() {} diff --git a/stake-pool/program/tests/initialize.rs b/stake-pool/program/tests/initialize.rs deleted file mode 100644 index fa65c709908..00000000000 --- a/stake-pool/program/tests/initialize.rs +++ /dev/null @@ -1,1632 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![allow(clippy::items_after_test_module)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::{get_instance_packed_len, get_packed_len, try_from_slice_unchecked}, - hash::Hash, - instruction::{AccountMeta, Instruction}, - program_pack::Pack, - pubkey::Pubkey, - stake, system_instruction, sysvar, - }, - solana_program_test::*, - solana_sdk::{ - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_stake_pool::{error, id, instruction, state, MINIMUM_RESERVE_LAMPORTS}, - spl_token_2022::extension::ExtensionType, - test_case::test_case, -}; - -async fn create_required_accounts( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: &Hash, - stake_pool_accounts: &StakePoolAccounts, - mint_extensions: &[ExtensionType], -) { - create_mint( - banks_client, - payer, - recent_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint, - &stake_pool_accounts.withdraw_authority, - stake_pool_accounts.pool_decimals, - mint_extensions, - ) - .await - .unwrap(); - - let required_extensions = ExtensionType::get_required_init_account_extensions(mint_extensions); - create_token_account( - banks_client, - payer, - recent_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_fee_account, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.manager, - &required_extensions, - ) - .await - .unwrap(); - - create_independent_stake_account( - banks_client, - payer, - recent_blockhash, - &stake_pool_accounts.reserve_stake, - &stake::state::Authorized { - staker: stake_pool_accounts.withdraw_authority, - withdrawer: stake_pool_accounts.withdraw_authority, - }, - &stake::state::Lockup::default(), - MINIMUM_RESERVE_LAMPORTS, - ) - .await; -} - -#[test_case(spl_token::id(); "token")] -#[test_case(spl_token_2022::id(); "token-2022")] -#[tokio::test] -async fn success(token_program_id: Pubkey) { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - // Stake pool now exists - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - assert_eq!(stake_pool.data.len(), get_packed_len::()); - assert_eq!(stake_pool.owner, id()); - - // Validator stake list storage initialized - let validator_list = get_account( - &mut banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert!(validator_list.header.is_valid()); -} - -#[tokio::test] -async fn fail_double_initialize() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let latest_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - - let second_stake_pool_accounts = StakePoolAccounts { - stake_pool: stake_pool_accounts.stake_pool, - ..Default::default() - }; - - let transaction_error = second_stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &latest_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .err() - .unwrap(); - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::AlreadyInUse as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to initialize already initialized stake pool"), - } -} - -#[tokio::test] -async fn fail_with_already_initialized_validator_list() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let latest_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - - let second_stake_pool_accounts = StakePoolAccounts { - validator_list: stake_pool_accounts.validator_list, - ..Default::default() - }; - - let transaction_error = second_stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &latest_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .err() - .unwrap(); - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::AlreadyInUse as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to initialize stake pool with already initialized stake list storage"), - } -} - -#[tokio::test] -async fn fail_with_high_fee() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts { - epoch_fee: state::Fee { - numerator: 100_001, - denominator: 100_000, - }, - ..Default::default() - }; - - let transaction_error = stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .err() - .unwrap(); - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::FeeTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to initialize stake pool with high fee"), - } -} - -#[tokio::test] -async fn fail_with_high_withdrawal_fee() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts { - withdrawal_fee: state::Fee { - numerator: 100_001, - denominator: 100_000, - }, - ..Default::default() - }; - - let transaction_error = stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .err() - .unwrap(); - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::FeeTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => { - panic!("Wrong error occurs while try to initialize stake pool with high withdrawal fee") - } - } -} - -#[tokio::test] -async fn fail_with_wrong_max_validators() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &[], - ) - .await; - - let rent = banks_client.get_rent().await.unwrap(); - let rent_stake_pool = rent.minimum_balance(get_packed_len::()); - let validator_list_size = get_instance_packed_len(&state::ValidatorList::new( - stake_pool_accounts.max_validators - 1, - )) - .unwrap(); - let rent_validator_list = rent.minimum_balance(validator_list_size); - - let mut transaction = Transaction::new_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - rent_stake_pool, - get_packed_len::() as u64, - &id(), - ), - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - rent_validator_list, - validator_list_size as u64, - &id(), - ), - instruction::initialize( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &spl_token::id(), - None, - stake_pool_accounts.epoch_fee, - stake_pool_accounts.withdrawal_fee, - stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - stake_pool_accounts.max_validators, - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign( - &[ - &payer, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.manager, - ], - recent_blockhash, - ); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::UnexpectedValidatorListAccountSize as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to initialize stake pool with high fee"), - } -} - -#[tokio::test] -async fn fail_with_wrong_mint_authority() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - let wrong_mint = Keypair::new(); - - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &[], - ) - .await; - - // create wrong mint - create_mint( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &wrong_mint, - &stake_pool_accounts.withdraw_authority, - stake_pool_accounts.pool_decimals, - &[], - ) - .await - .unwrap(); - - let transaction_error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &wrong_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::InvalidFeeAccount as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to initialize stake pool with wrong mint authority of pool fee account"), - } -} - -#[tokio::test] -async fn fail_with_freeze_authority() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &[], - ) - .await; - - // create mint with freeze authority - let wrong_mint = Keypair::new(); - let rent = banks_client.get_rent().await.unwrap(); - let mint_rent = rent.minimum_balance(spl_token::state::Mint::LEN); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &wrong_mint.pubkey(), - mint_rent, - spl_token::state::Mint::LEN as u64, - &spl_token::id(), - ), - spl_token::instruction::initialize_mint( - &spl_token::id(), - &wrong_mint.pubkey(), - &stake_pool_accounts.withdraw_authority, - Some(&stake_pool_accounts.withdraw_authority), - stake_pool_accounts.pool_decimals, - ) - .unwrap(), - ], - Some(&payer.pubkey()), - &[&payer, &wrong_mint], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let pool_fee_account = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &pool_fee_account, - &wrong_mint.pubkey(), - &stake_pool_accounts.manager, - &[], - ) - .await - .unwrap(); - - let error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &wrong_mint.pubkey(), - &pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(error::StakePoolError::InvalidMintFreezeAuthority as u32), - ) - ); -} - -#[tokio::test] -async fn success_with_supported_extensions() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(spl_token_2022::id()); - - let mint_extensions = vec![ExtensionType::TransferFeeConfig]; - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &mint_extensions, - ) - .await; - - let mut account_extensions = - ExtensionType::get_required_init_account_extensions(&mint_extensions); - account_extensions.push(ExtensionType::CpiGuard); - let pool_fee_account = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &pool_fee_account, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.manager, - &account_extensions, - ) - .await - .unwrap(); - - create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn fail_with_unsupported_mint_extension() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(spl_token_2022::id()); - - let mint_extensions = vec![ExtensionType::NonTransferable]; - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &mint_extensions, - ) - .await; - - let required_extensions = ExtensionType::get_required_init_account_extensions(&mint_extensions); - let pool_fee_account = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &pool_fee_account, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.manager, - &required_extensions, - ) - .await - .unwrap(); - - let error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(error::StakePoolError::UnsupportedMintExtension as u32), - ) - ); -} - -#[tokio::test] -async fn fail_with_unsupported_account_extension() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(spl_token_2022::id()); - - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &[], - ) - .await; - - let extensions = vec![ExtensionType::MemoTransfer]; - let pool_fee_account = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &pool_fee_account, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.manager, - &extensions, - ) - .await - .unwrap(); - - let error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(error::StakePoolError::UnsupportedFeeAccountExtension as u32), - ) - ); -} - -#[tokio::test] -async fn fail_with_wrong_token_program_id() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - - let wrong_token_program = Keypair::new(); - - create_mint( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint, - &stake_pool_accounts.withdraw_authority, - stake_pool_accounts.pool_decimals, - &[], - ) - .await - .unwrap(); - - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_fee_account, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.manager, - &[], - ) - .await - .unwrap(); - - let rent = banks_client.get_rent().await.unwrap(); - let rent_stake_pool = rent.minimum_balance(get_packed_len::()); - let validator_list_size = get_instance_packed_len(&state::ValidatorList::new( - stake_pool_accounts.max_validators, - )) - .unwrap(); - let rent_validator_list = rent.minimum_balance(validator_list_size); - - let mut transaction = Transaction::new_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - rent_stake_pool, - get_packed_len::() as u64, - &id(), - ), - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - rent_validator_list, - validator_list_size as u64, - &id(), - ), - instruction::initialize( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &wrong_token_program.pubkey(), - None, - stake_pool_accounts.epoch_fee, - stake_pool_accounts.withdrawal_fee, - stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - stake_pool_accounts.max_validators, - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign( - &[ - &payer, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.manager, - ], - recent_blockhash, - ); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!( - "Wrong error occurs while try to initialize stake pool with wrong token program ID" - ), - } -} - -#[tokio::test] -async fn fail_with_fee_owned_by_wrong_token_program_id() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - - let wrong_token_program = Keypair::new(); - - create_mint( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint, - &stake_pool_accounts.withdraw_authority, - stake_pool_accounts.pool_decimals, - &[], - ) - .await - .unwrap(); - - let rent = banks_client.get_rent().await.unwrap(); - - let account_rent = rent.minimum_balance(spl_token::state::Account::LEN); - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - account_rent, - spl_token::state::Account::LEN as u64, - &wrong_token_program.pubkey(), - )], - Some(&payer.pubkey()), - &[&payer, &stake_pool_accounts.pool_fee_account], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let rent_stake_pool = rent.minimum_balance(get_packed_len::()); - let validator_list_size = get_instance_packed_len(&state::ValidatorList::new( - stake_pool_accounts.max_validators, - )) - .unwrap(); - let rent_validator_list = rent.minimum_balance(validator_list_size); - - let mut transaction = Transaction::new_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - rent_stake_pool, - get_packed_len::() as u64, - &id(), - ), - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - rent_validator_list, - validator_list_size as u64, - &id(), - ), - instruction::initialize( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &wrong_token_program.pubkey(), - None, - stake_pool_accounts.epoch_fee, - stake_pool_accounts.withdrawal_fee, - stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - stake_pool_accounts.max_validators, - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign( - &[ - &payer, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.manager, - ], - recent_blockhash, - ); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!( - "Wrong error occurs while try to initialize stake pool with wrong token program ID" - ), - } -} - -#[tokio::test] -async fn fail_with_wrong_fee_account() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - - create_mint( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint, - &stake_pool_accounts.withdraw_authority, - stake_pool_accounts.pool_decimals, - &[], - ) - .await - .unwrap(); - let rent = banks_client.get_rent().await.unwrap(); - let account_rent = rent.minimum_balance(spl_token::state::Account::LEN); - - let mut transaction = Transaction::new_with_payer( - &[system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - account_rent, - spl_token::state::Account::LEN as u64, - &Keypair::new().pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign( - &[&payer, &stake_pool_accounts.pool_fee_account], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let transaction_error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - transaction_error, - TransactionError::InstructionError(2, InstructionError::UninitializedAccount) - ); -} - -#[tokio::test] -async fn fail_with_wrong_withdraw_authority() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts { - withdraw_authority: Keypair::new().pubkey(), - ..Default::default() - }; - - let transaction_error = stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .err() - .unwrap(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::InvalidProgramAddress as u32; - assert_eq!(error_index, program_error); - } - _ => panic!( - "Wrong error occurs while try to initialize stake pool with wrong withdraw authority" - ), - } -} - -#[tokio::test] -async fn fail_with_not_rent_exempt_pool() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &[], - ) - .await; - - let rent = banks_client.get_rent().await.unwrap(); - let validator_list_size = get_instance_packed_len(&state::ValidatorList::new( - stake_pool_accounts.max_validators, - )) - .unwrap(); - let rent_validator_list = rent.minimum_balance(validator_list_size); - - let mut transaction = Transaction::new_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - 1, - get_packed_len::() as u64, - &id(), - ), - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - rent_validator_list, - validator_list_size as u64, - &id(), - ), - instruction::initialize( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &spl_token::id(), - None, - stake_pool_accounts.epoch_fee, - stake_pool_accounts.withdrawal_fee, - stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - stake_pool_accounts.max_validators, - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign( - &[ - &payer, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.manager, - ], - recent_blockhash, - ); - let result = banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert!( - result == TransactionError::InstructionError(2, InstructionError::InvalidError,) - || result - == TransactionError::InstructionError(2, InstructionError::AccountNotRentExempt,) - ); -} - -#[tokio::test] -async fn fail_with_not_rent_exempt_validator_list() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &[], - ) - .await; - - let rent = banks_client.get_rent().await.unwrap(); - let rent_stake_pool = rent.minimum_balance(get_packed_len::()); - let validator_list_size = get_instance_packed_len(&state::ValidatorList::new( - stake_pool_accounts.max_validators, - )) - .unwrap(); - - let mut transaction = Transaction::new_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - rent_stake_pool, - get_packed_len::() as u64, - &id(), - ), - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - 1, - validator_list_size as u64, - &id(), - ), - instruction::initialize( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &spl_token::id(), - None, - stake_pool_accounts.epoch_fee, - stake_pool_accounts.withdrawal_fee, - stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - stake_pool_accounts.max_validators, - ), - ], - Some(&payer.pubkey()), - ); - transaction.sign( - &[ - &payer, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.manager, - ], - recent_blockhash, - ); - - let result = banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - - assert!( - result == TransactionError::InstructionError(2, InstructionError::InvalidError,) - || result - == TransactionError::InstructionError(2, InstructionError::AccountNotRentExempt,) - ); -} - -#[tokio::test] -async fn fail_without_manager_signature() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &[], - ) - .await; - - let rent = banks_client.get_rent().await.unwrap(); - let rent_stake_pool = rent.minimum_balance(get_packed_len::()); - let validator_list_size = get_instance_packed_len(&state::ValidatorList::new( - stake_pool_accounts.max_validators, - )) - .unwrap(); - let rent_validator_list = rent.minimum_balance(validator_list_size); - - let init_data = instruction::StakePoolInstruction::Initialize { - fee: stake_pool_accounts.epoch_fee, - withdrawal_fee: stake_pool_accounts.withdrawal_fee, - deposit_fee: stake_pool_accounts.deposit_fee, - referral_fee: stake_pool_accounts.referral_fee, - max_validators: stake_pool_accounts.max_validators, - }; - let data = borsh::to_vec(&init_data).unwrap(); - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), true), - AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), false), - AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.reserve_stake.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.pool_mint.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.pool_fee_account.pubkey(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - ]; - let stake_pool_init_instruction = Instruction { - program_id: id(), - accounts, - data, - }; - - let mut transaction = Transaction::new_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - rent_stake_pool, - get_packed_len::() as u64, - &id(), - ), - system_instruction::create_account( - &payer.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - rent_validator_list, - validator_list_size as u64, - &id(), - ), - stake_pool_init_instruction, - ], - Some(&payer.pubkey()), - ); - transaction.sign( - &[ - &payer, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - ], - recent_blockhash, - ); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => panic!( - "Wrong error occurs while try to initialize stake pool without manager's signature" - ), - } -} - -#[tokio::test] -async fn fail_with_pre_minted_pool_tokens() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - let mint_authority = Keypair::new(); - - create_mint( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint, - &mint_authority.pubkey(), - stake_pool_accounts.pool_decimals, - &[], - ) - .await - .unwrap(); - - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_fee_account, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.manager, - &[], - ) - .await - .unwrap(); - - mint_tokens( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &mint_authority, - 1, - ) - .await - .unwrap(); - - let transaction_error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::NonZeroPoolTokenSupply as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to initialize stake pool with wrong mint authority of pool fee account"), - } -} - -#[tokio::test] -async fn fail_with_bad_reserve() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - let wrong_authority = Pubkey::new_unique(); - - create_required_accounts( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - &[], - ) - .await; - - { - let bad_stake = Keypair::new(); - create_independent_stake_account( - &mut banks_client, - &payer, - &recent_blockhash, - &bad_stake, - &stake::state::Authorized { - staker: wrong_authority, - withdrawer: stake_pool_accounts.withdraw_authority, - }, - &stake::state::Lockup::default(), - MINIMUM_RESERVE_LAMPORTS, - ) - .await; - - let error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &bad_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(error::StakePoolError::WrongStakeStake as u32), - ) - ); - } - - { - let bad_stake = Keypair::new(); - create_independent_stake_account( - &mut banks_client, - &payer, - &recent_blockhash, - &bad_stake, - &stake::state::Authorized { - staker: stake_pool_accounts.withdraw_authority, - withdrawer: wrong_authority, - }, - &stake::state::Lockup::default(), - MINIMUM_RESERVE_LAMPORTS, - ) - .await; - - let error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &bad_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(error::StakePoolError::WrongStakeStake as u32), - ) - ); - } - - { - let bad_stake = Keypair::new(); - create_independent_stake_account( - &mut banks_client, - &payer, - &recent_blockhash, - &bad_stake, - &stake::state::Authorized { - staker: stake_pool_accounts.withdraw_authority, - withdrawer: stake_pool_accounts.withdraw_authority, - }, - &stake::state::Lockup { - custodian: wrong_authority, - ..stake::state::Lockup::default() - }, - MINIMUM_RESERVE_LAMPORTS, - ) - .await; - - let error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &bad_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(error::StakePoolError::WrongStakeStake as u32), - ) - ); - } - - { - let bad_stake = Keypair::new(); - let rent = banks_client.get_rent().await.unwrap(); - let lamports = rent.minimum_balance(std::mem::size_of::()) - + MINIMUM_RESERVE_LAMPORTS; - - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::create_account( - &payer.pubkey(), - &bad_stake.pubkey(), - lamports, - std::mem::size_of::() as u64, - &stake::program::id(), - )], - Some(&payer.pubkey()), - &[&payer, &bad_stake], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let error = create_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.stake_pool, - &stake_pool_accounts.validator_list, - &bad_stake.pubkey(), - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &None, - &stake_pool_accounts.epoch_fee, - &stake_pool_accounts.withdrawal_fee, - &stake_pool_accounts.deposit_fee, - stake_pool_accounts.referral_fee, - &stake_pool_accounts.sol_deposit_fee, - stake_pool_accounts.sol_referral_fee, - stake_pool_accounts.max_validators, - ) - .await - .err() - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(error::StakePoolError::WrongStakeStake as u32), - ) - ); - } -} - -#[tokio::test] -async fn success_with_extra_reserve_lamports() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - let init_lamports = 1_000_000_000_000; - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS + init_lamports, - ) - .await - .unwrap(); - - let init_pool_tokens = get_token_balance( - &mut banks_client, - &stake_pool_accounts.pool_fee_account.pubkey(), - ) - .await; - assert_eq!(init_pool_tokens, init_lamports); -} - -#[tokio::test] -async fn fail_with_incorrect_mint_decimals() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts { - pool_decimals: 8, - ..Default::default() - }; - let error = stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap_err() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 2, - InstructionError::Custom(error::StakePoolError::IncorrectMintDecimals as u32), - ) - ); -} diff --git a/stake-pool/program/tests/set_deposit_fee.rs b/stake-pool/program/tests/set_deposit_fee.rs deleted file mode 100644 index 618228aa607..00000000000 --- a/stake-pool/program/tests/set_deposit_fee.rs +++ /dev/null @@ -1,279 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{ - borsh1::try_from_slice_unchecked, - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error, id, instruction, - state::{Fee, FeeType, StakePool}, - MINIMUM_RESERVE_LAMPORTS, - }, -}; - -async fn setup(fee: Option) -> (ProgramTestContext, StakePoolAccounts, Fee) { - let mut context = program_test().start_with_context().await; - let mut stake_pool_accounts = StakePoolAccounts::default(); - if let Some(fee) = fee { - stake_pool_accounts.deposit_fee = fee; - } - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - let new_deposit_fee = Fee { - numerator: 823, - denominator: 1000, - }; - - (context, stake_pool_accounts, new_deposit_fee) -} - -#[tokio::test] -async fn success_stake() { - let (mut context, stake_pool_accounts, new_deposit_fee) = setup(None).await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeDeposit(new_deposit_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_deposit_fee, new_deposit_fee); -} - -#[tokio::test] -async fn success_stake_increase_fee_from_0() { - let (mut context, stake_pool_accounts, _) = setup(Some(Fee { - numerator: 0, - denominator: 0, - })) - .await; - let new_deposit_fee = Fee { - numerator: 324, - denominator: 1234, - }; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeDeposit(new_deposit_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_deposit_fee, new_deposit_fee); -} - -#[tokio::test] -async fn fail_stake_wrong_manager() { - let (context, stake_pool_accounts, new_deposit_fee) = setup(None).await; - - let wrong_manager = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &wrong_manager.pubkey(), - FeeType::StakeDeposit(new_deposit_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while signing with the wrong manager"), - } -} - -#[tokio::test] -async fn fail_stake_high_deposit_fee() { - let (context, stake_pool_accounts, _new_deposit_fee) = setup(None).await; - - let new_deposit_fee = Fee { - numerator: 100001, - denominator: 100000, - }; - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeDeposit(new_deposit_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when setting fee too high"), - } -} - -#[tokio::test] -async fn success_sol() { - let (mut context, stake_pool_accounts, new_deposit_fee) = setup(None).await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolDeposit(new_deposit_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.sol_deposit_fee, new_deposit_fee); -} - -#[tokio::test] -async fn fail_sol_wrong_manager() { - let (context, stake_pool_accounts, new_deposit_fee) = setup(None).await; - - let wrong_manager = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &wrong_manager.pubkey(), - FeeType::SolDeposit(new_deposit_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while signing with the wrong manager"), - } -} - -#[tokio::test] -async fn fail_sol_high_deposit_fee() { - let (context, stake_pool_accounts, _new_deposit_fee) = setup(None).await; - - let new_deposit_fee = Fee { - numerator: 100001, - denominator: 100000, - }; - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolDeposit(new_deposit_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when setting fee too high"), - } -} diff --git a/stake-pool/program/tests/set_epoch_fee.rs b/stake-pool/program/tests/set_epoch_fee.rs deleted file mode 100644 index e7bb6cd57e1..00000000000 --- a/stake-pool/program/tests/set_epoch_fee.rs +++ /dev/null @@ -1,245 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{ - borsh1::try_from_slice_unchecked, - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error, id, instruction, - state::{Fee, FeeType, FutureEpoch, StakePool}, - MINIMUM_RESERVE_LAMPORTS, - }, -}; - -async fn setup() -> (ProgramTestContext, StakePoolAccounts, Fee) { - let mut context = program_test().start_with_context().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - let new_fee = Fee { - numerator: 10, - denominator: 10, - }; - - (context, stake_pool_accounts, new_fee) -} - -#[tokio::test] -async fn success() { - let (mut context, stake_pool_accounts, new_fee) = setup().await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - let old_fee = stake_pool.epoch_fee; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::Epoch(new_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.epoch_fee, old_fee); - assert_eq!(stake_pool.next_epoch_fee, FutureEpoch::Two(new_fee)); - - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - let slot = first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.epoch_fee, old_fee); - assert_eq!(stake_pool.next_epoch_fee, FutureEpoch::One(new_fee)); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - context.warp_to_slot(slot + slots_per_epoch).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.epoch_fee, new_fee); - assert_eq!(stake_pool.next_epoch_fee, FutureEpoch::None); -} - -#[tokio::test] -async fn fail_wrong_manager() { - let (context, stake_pool_accounts, new_fee) = setup().await; - - let wrong_manager = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &wrong_manager.pubkey(), - FeeType::Epoch(new_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while malicious try to set manager"), - } -} - -#[tokio::test] -async fn fail_high_fee() { - let (context, stake_pool_accounts, _new_fee) = setup().await; - - let new_fee = Fee { - numerator: 11, - denominator: 10, - }; - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::Epoch(new_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when setting fee too high"), - } -} - -#[tokio::test] -async fn fail_not_updated() { - let mut context = program_test().start_with_context().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - let new_fee = Fee { - numerator: 10, - denominator: 100, - }; - - // move forward so an update is required - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::Epoch(new_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::StakeListAndPoolOutOfDate as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when stake pool out of date"), - } -} diff --git a/stake-pool/program/tests/set_funding_authority.rs b/stake-pool/program/tests/set_funding_authority.rs deleted file mode 100644 index a3cdb619477..00000000000 --- a/stake-pool/program/tests/set_funding_authority.rs +++ /dev/null @@ -1,264 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, - hash::Hash, - instruction::{AccountMeta, Instruction}, - }, - solana_program_test::*, - solana_sdk::{ - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_stake_pool::{ - error, find_deposit_authority_program_address, id, - instruction::{self, FundingType}, - state, MINIMUM_RESERVE_LAMPORTS, - }, -}; - -async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, Keypair) { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let new_deposit_authority = Keypair::new(); - - ( - banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - new_deposit_authority, - ) -} - -#[tokio::test] -async fn success_set_stake_deposit_authority() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_authority) = - setup().await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - Some(&new_authority.pubkey()), - FundingType::StakeDeposit, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.stake_deposit_authority, new_authority.pubkey()); - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - None, - FundingType::StakeDeposit, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!( - stake_pool.stake_deposit_authority, - find_deposit_authority_program_address(&id(), &stake_pool_accounts.stake_pool.pubkey()).0 - ); -} - -#[tokio::test] -async fn fail_wrong_manager() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, new_authority) = setup().await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &new_authority.pubkey(), - Some(&new_authority.pubkey()), - FundingType::StakeDeposit, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &new_authority], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while malicious try to set manager"), - } -} - -#[tokio::test] -async fn fail_without_signature() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, new_authority) = setup().await; - - let data = borsh::to_vec(&instruction::StakePoolInstruction::SetFundingAuthority( - FundingType::StakeDeposit, - )) - .unwrap(); - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false), - AccountMeta::new_readonly(new_authority.pubkey(), false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data, - }; - - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to set new manager without signature"), - } -} - -#[tokio::test] -async fn success_set_sol_deposit_authority() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_sol_deposit_authority) = - setup().await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - Some(&new_sol_deposit_authority.pubkey()), - FundingType::SolDeposit, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!( - stake_pool.sol_deposit_authority, - Some(new_sol_deposit_authority.pubkey()) - ); - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - None, - FundingType::SolDeposit, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.sol_deposit_authority, None); -} - -#[tokio::test] -async fn success_set_withdraw_authority() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_authority) = - setup().await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - Some(&new_authority.pubkey()), - FundingType::SolWithdraw, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!( - stake_pool.sol_withdraw_authority, - Some(new_authority.pubkey()) - ); - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - None, - FundingType::SolWithdraw, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.sol_withdraw_authority, None); -} diff --git a/stake-pool/program/tests/set_manager.rs b/stake-pool/program/tests/set_manager.rs deleted file mode 100644 index e6f7afadb33..00000000000 --- a/stake-pool/program/tests/set_manager.rs +++ /dev/null @@ -1,288 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, - hash::Hash, - instruction::{AccountMeta, Instruction}, - }, - solana_program_test::*, - solana_sdk::{ - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_stake_pool::{error, id, instruction, state, MINIMUM_RESERVE_LAMPORTS}, -}; - -async fn setup() -> ( - BanksClient, - Keypair, - Hash, - StakePoolAccounts, - Keypair, - Keypair, -) { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let new_pool_fee = Keypair::new(); - let new_manager = Keypair::new(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &new_pool_fee, - &stake_pool_accounts.pool_mint.pubkey(), - &new_manager, - &[], - ) - .await - .unwrap(); - - ( - banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - new_pool_fee, - new_manager, - ) -} - -#[tokio::test] -async fn test_set_manager() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) = - setup().await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_manager( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &new_manager.pubkey(), - &new_pool_fee.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign( - &[&payer, &stake_pool_accounts.manager, &new_manager], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.manager, new_manager.pubkey()); -} - -#[tokio::test] -async fn test_set_manager_by_malicious() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) = - setup().await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_manager( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &new_manager.pubkey(), - &new_manager.pubkey(), - &new_pool_fee.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &new_manager], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while malicious try to set manager"), - } -} - -#[tokio::test] -async fn test_set_manager_without_existing_signature() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) = - setup().await; - - let data = borsh::to_vec(&instruction::StakePoolInstruction::SetManager).unwrap(); - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false), - AccountMeta::new_readonly(new_manager.pubkey(), true), - AccountMeta::new_readonly(new_pool_fee.pubkey(), false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data, - }; - - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer, &new_manager], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => panic!( - "Wrong error occurs while try to set new manager without existing manager signature" - ), - } -} - -#[tokio::test] -async fn test_set_manager_without_new_signature() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) = - setup().await; - - let data = borsh::to_vec(&instruction::StakePoolInstruction::SetManager).unwrap(); - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), true), - AccountMeta::new_readonly(new_manager.pubkey(), false), - AccountMeta::new_readonly(new_pool_fee.pubkey(), false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data, - }; - - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => { - panic!("Wrong error occurs while try to set new manager without new manager signature") - } - } -} - -#[tokio::test] -async fn test_set_manager_with_wrong_mint_for_pool_fee_acc() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let new_mint = Keypair::new(); - let new_withdraw_auth = Keypair::new(); - let new_pool_fee = Keypair::new(); - let new_manager = Keypair::new(); - - create_mint( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &new_mint, - &new_withdraw_auth.pubkey(), - 0, - &[], - ) - .await - .unwrap(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.token_program_id, - &new_pool_fee, - &new_mint.pubkey(), - &new_manager, - &[], - ) - .await - .unwrap(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_manager( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &new_manager.pubkey(), - &new_pool_fee.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign( - &[&payer, &stake_pool_accounts.manager, &new_manager], - recent_blockhash, - ); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::InvalidFeeAccount as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to set new manager with wrong mint"), - } -} diff --git a/stake-pool/program/tests/set_preferred.rs b/stake-pool/program/tests/set_preferred.rs deleted file mode 100644 index c18fd3df841..00000000000 --- a/stake-pool/program/tests/set_preferred.rs +++ /dev/null @@ -1,263 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::hash::Hash, - solana_program_test::*, - solana_sdk::{ - borsh1::try_from_slice_unchecked, - instruction::InstructionError, - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error, find_transient_stake_program_address, id, - instruction::{self, PreferredValidatorType}, - state::StakePool, - MINIMUM_RESERVE_LAMPORTS, - }, -}; - -async fn setup() -> ( - BanksClient, - Keypair, - Hash, - StakePoolAccounts, - ValidatorStakeAccount, -) { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let validator_stake_account = simple_add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - ( - banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake_account, - ) -} - -#[tokio::test] -async fn success_deposit() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) = - setup().await; - - let vote_account_address = validator_stake_account.vote.pubkey(); - let error = stake_pool_accounts - .set_preferred_validator( - &mut banks_client, - &payer, - &recent_blockhash, - PreferredValidatorType::Deposit, - Some(vote_account_address), - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!( - stake_pool.preferred_deposit_validator_vote_address, - Some(vote_account_address) - ); - assert_eq!(stake_pool.preferred_withdraw_validator_vote_address, None); -} - -#[tokio::test] -async fn success_withdraw() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) = - setup().await; - - let vote_account_address = validator_stake_account.vote.pubkey(); - - let error = stake_pool_accounts - .set_preferred_validator( - &mut banks_client, - &payer, - &recent_blockhash, - PreferredValidatorType::Withdraw, - Some(vote_account_address), - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.preferred_deposit_validator_vote_address, None); - assert_eq!( - stake_pool.preferred_withdraw_validator_vote_address, - Some(vote_account_address) - ); -} - -#[tokio::test] -async fn success_unset() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) = - setup().await; - - let vote_account_address = validator_stake_account.vote.pubkey(); - let error = stake_pool_accounts - .set_preferred_validator( - &mut banks_client, - &payer, - &recent_blockhash, - PreferredValidatorType::Withdraw, - Some(vote_account_address), - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!( - stake_pool.preferred_withdraw_validator_vote_address, - Some(vote_account_address) - ); - - let error = stake_pool_accounts - .set_preferred_validator( - &mut banks_client, - &payer, - &recent_blockhash, - PreferredValidatorType::Withdraw, - None, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.preferred_withdraw_validator_vote_address, None); -} - -#[tokio::test] -async fn fail_wrong_staker() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, _) = setup().await; - - let wrong_staker = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_preferred_validator( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &wrong_staker.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - PreferredValidatorType::Withdraw, - None, - )], - Some(&payer.pubkey()), - &[&payer, &wrong_staker], - recent_blockhash, - ); - let error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::WrongStaker as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while malicious try to set manager"), - } -} - -#[tokio::test] -async fn fail_not_present_validator() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, _) = setup().await; - - let validator_vote_address = Pubkey::new_unique(); - let error = stake_pool_accounts - .set_preferred_validator( - &mut banks_client, - &payer, - &recent_blockhash, - PreferredValidatorType::Withdraw, - Some(validator_vote_address), - ) - .await - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::ValidatorNotFound as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while malicious try to set manager"), - } -} - -#[tokio::test] -async fn fail_ready_for_removal() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) = - setup().await; - let validator_vote_address = validator_stake_account.vote.pubkey(); - - // Mark validator as ready for removal - let transient_stake_seed = 0; - let (transient_stake_address, _) = find_transient_stake_program_address( - &id(), - &validator_vote_address, - &stake_pool_accounts.stake_pool.pubkey(), - transient_stake_seed, - ); - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake_account.stake_account, - &transient_stake_address, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .set_preferred_validator( - &mut banks_client, - &payer, - &recent_blockhash, - PreferredValidatorType::Withdraw, - Some(validator_vote_address), - ) - .await - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::InvalidPreferredValidator as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while trying to set ReadyForRemoval validator"), - } -} diff --git a/stake-pool/program/tests/set_referral_fee.rs b/stake-pool/program/tests/set_referral_fee.rs deleted file mode 100644 index 19090820b7a..00000000000 --- a/stake-pool/program/tests/set_referral_fee.rs +++ /dev/null @@ -1,263 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{ - borsh1::try_from_slice_unchecked, - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error, id, instruction, - state::{FeeType, StakePool}, - MINIMUM_RESERVE_LAMPORTS, - }, -}; - -async fn setup(fee: Option) -> (ProgramTestContext, StakePoolAccounts, u8) { - let mut context = program_test().start_with_context().await; - let mut stake_pool_accounts = StakePoolAccounts::default(); - if let Some(fee) = fee { - stake_pool_accounts.referral_fee = fee; - } - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - let new_referral_fee = 15u8; - - (context, stake_pool_accounts, new_referral_fee) -} - -#[tokio::test] -async fn success_stake() { - let (mut context, stake_pool_accounts, new_referral_fee) = setup(None).await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeReferral(new_referral_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_referral_fee, new_referral_fee); -} - -#[tokio::test] -async fn success_stake_increase_fee_from_0() { - let (mut context, stake_pool_accounts, _) = setup(Some(0u8)).await; - let new_referral_fee = 30u8; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeReferral(new_referral_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_referral_fee, new_referral_fee); -} - -#[tokio::test] -async fn fail_stake_wrong_manager() { - let (context, stake_pool_accounts, new_referral_fee) = setup(None).await; - - let wrong_manager = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &wrong_manager.pubkey(), - FeeType::StakeReferral(new_referral_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while signing with the wrong manager"), - } -} - -#[tokio::test] -async fn fail_stake_high_referral_fee() { - let (context, stake_pool_accounts, _new_referral_fee) = setup(None).await; - - let new_referral_fee = 110u8; - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeReferral(new_referral_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when setting fee too high"), - } -} - -#[tokio::test] -async fn success_sol() { - let (mut context, stake_pool_accounts, new_referral_fee) = setup(None).await; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolReferral(new_referral_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.sol_referral_fee, new_referral_fee); -} - -#[tokio::test] -async fn fail_sol_wrong_manager() { - let (context, stake_pool_accounts, new_referral_fee) = setup(None).await; - - let wrong_manager = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &wrong_manager.pubkey(), - FeeType::SolReferral(new_referral_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while signing with the wrong manager"), - } -} - -#[tokio::test] -async fn fail_sol_high_referral_fee() { - let (context, stake_pool_accounts, _new_referral_fee) = setup(None).await; - - let new_referral_fee = 110u8; - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolReferral(new_referral_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when setting fee too high"), - } -} diff --git a/stake-pool/program/tests/set_staker.rs b/stake-pool/program/tests/set_staker.rs deleted file mode 100644 index d3dd61b4c24..00000000000 --- a/stake-pool/program/tests/set_staker.rs +++ /dev/null @@ -1,181 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, - hash::Hash, - instruction::{AccountMeta, Instruction}, - }, - solana_program_test::*, - solana_sdk::{ - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_stake_pool::{error, id, instruction, state, MINIMUM_RESERVE_LAMPORTS}, -}; - -async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, Keypair) { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let new_staker = Keypair::new(); - - ( - banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - new_staker, - ) -} - -#[tokio::test] -async fn success_set_staker_as_manager() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) = - setup().await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_staker( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &new_staker.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.staker, new_staker.pubkey()); -} - -#[tokio::test] -async fn success_set_staker_as_staker() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) = - setup().await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_staker( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &new_staker.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.staker, new_staker.pubkey()); - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_staker( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &new_staker.pubkey(), - &stake_pool_accounts.staker.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &new_staker], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); - - let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.staker, stake_pool_accounts.staker.pubkey()); -} - -#[tokio::test] -async fn fail_wrong_manager() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) = setup().await; - - let mut transaction = Transaction::new_with_payer( - &[instruction::set_staker( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &new_staker.pubkey(), - &new_staker.pubkey(), - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &new_staker], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while malicious try to set manager"), - } -} - -#[tokio::test] -async fn fail_set_staker_without_signature() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) = setup().await; - - let data = borsh::to_vec(&instruction::StakePoolInstruction::SetStaker).unwrap(); - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false), - AccountMeta::new_readonly(new_staker.pubkey(), false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data, - }; - - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = error::StakePoolError::SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to set new manager without signature"), - } -} diff --git a/stake-pool/program/tests/set_withdrawal_fee.rs b/stake-pool/program/tests/set_withdrawal_fee.rs deleted file mode 100644 index 4725685da95..00000000000 --- a/stake-pool/program/tests/set_withdrawal_fee.rs +++ /dev/null @@ -1,913 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program_test::*, - solana_sdk::{ - borsh1::try_from_slice_unchecked, - instruction::InstructionError, - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error, id, instruction, - state::{Fee, FeeType, FutureEpoch, StakePool}, - MINIMUM_RESERVE_LAMPORTS, - }, -}; - -async fn setup(fee: Option) -> (ProgramTestContext, StakePoolAccounts, Fee) { - let mut context = program_test().start_with_context().await; - let mut stake_pool_accounts = StakePoolAccounts::default(); - if let Some(fee) = fee { - stake_pool_accounts.withdrawal_fee = fee; - } - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - let new_withdrawal_fee = Fee { - numerator: 4, - denominator: 1000, - }; - - (context, stake_pool_accounts, new_withdrawal_fee) -} - -#[tokio::test] -async fn success() { - let (mut context, stake_pool_accounts, new_withdrawal_fee) = setup(None).await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - let old_stake_withdrawal_fee = stake_pool.stake_withdrawal_fee; - let old_sol_withdrawal_fee = stake_pool.sol_withdrawal_fee; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::Two(new_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::Two(new_withdrawal_fee) - ); - - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - let slot = first_normal_slot + 1; - - context.warp_to_slot(slot).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::One(new_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::One(new_withdrawal_fee) - ); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - context.warp_to_slot(slot + slots_per_epoch).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_stake_withdrawal_fee, FutureEpoch::None); - assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_sol_withdrawal_fee, FutureEpoch::None); -} - -#[tokio::test] -async fn success_fee_cannot_increase_more_than_once() { - let (mut context, stake_pool_accounts, new_withdrawal_fee) = setup(None).await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - let old_stake_withdrawal_fee = stake_pool.stake_withdrawal_fee; - let old_sol_withdrawal_fee = stake_pool.sol_withdrawal_fee; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::Two(new_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::Two(new_withdrawal_fee) - ); - - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - let slot = first_normal_slot + 1; - - context.warp_to_slot(slot).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::One(new_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::One(new_withdrawal_fee) - ); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - context.warp_to_slot(slot + slots_per_epoch).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_stake_withdrawal_fee, FutureEpoch::None); - assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_sol_withdrawal_fee, FutureEpoch::None); - - // try setting to the old fee in the same epoch - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(old_stake_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolWithdrawal(old_sol_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::Two(old_stake_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::Two(old_sol_withdrawal_fee) - ); - - let error = stake_pool_accounts - .update_stake_pool_balance( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check that nothing has changed after updating the stake pool - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::Two(old_stake_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::Two(old_sol_withdrawal_fee) - ); -} - -#[tokio::test] -async fn success_reset_fee_after_one_epoch() { - let (mut context, stake_pool_accounts, new_withdrawal_fee) = setup(None).await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - let old_stake_withdrawal_fee = stake_pool.stake_withdrawal_fee; - let old_sol_withdrawal_fee = stake_pool.sol_withdrawal_fee; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::Two(new_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::Two(new_withdrawal_fee) - ); - - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::One(new_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::One(new_withdrawal_fee) - ); - - // Flip the two fees, resets the counter to two future epochs - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(old_sol_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolWithdrawal(old_stake_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::Two(old_sol_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::Two(old_stake_withdrawal_fee) - ); -} - -#[tokio::test] -async fn success_increase_fee_from_0() { - let (mut context, stake_pool_accounts, _) = setup(Some(Fee { - numerator: 0, - denominator: 1, - })) - .await; - let new_withdrawal_fee = Fee { - numerator: 15, - denominator: 10000, - }; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - let old_stake_withdrawal_fee = stake_pool.stake_withdrawal_fee; - let old_sol_withdrawal_fee = stake_pool.sol_withdrawal_fee; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - - assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::Two(new_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::Two(new_withdrawal_fee) - ); - - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - let slot = first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); - assert_eq!( - stake_pool.next_stake_withdrawal_fee, - FutureEpoch::One(new_withdrawal_fee) - ); - assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!( - stake_pool.next_sol_withdrawal_fee, - FutureEpoch::One(new_withdrawal_fee) - ); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - context.warp_to_slot(slot + slots_per_epoch).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_stake_withdrawal_fee, FutureEpoch::None); - assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_sol_withdrawal_fee, FutureEpoch::None); -} - -#[tokio::test] -async fn fail_wrong_manager() { - let (context, stake_pool_accounts, new_stake_withdrawal_fee) = setup(None).await; - - let wrong_manager = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &wrong_manager.pubkey(), - FeeType::StakeWithdrawal(new_stake_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &wrong_manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while signing with the wrong manager"), - } -} - -#[tokio::test] -async fn fail_high_withdrawal_fee() { - let (context, stake_pool_accounts, _new_stake_withdrawal_fee) = setup(None).await; - - let new_stake_withdrawal_fee = Fee { - numerator: 11, - denominator: 10, - }; - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(new_stake_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when setting fee too high"), - } -} - -#[tokio::test] -async fn fail_high_stake_fee_increase() { - let (context, stake_pool_accounts, _new_stake_withdrawal_fee) = setup(None).await; - let new_withdrawal_fee = Fee { - numerator: 46, - denominator: 10_000, - }; - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeIncreaseTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when increasing fee by too large a factor"), - } -} - -#[tokio::test] -async fn fail_high_sol_fee_increase() { - let (context, stake_pool_accounts, _new_stake_withdrawal_fee) = setup(None).await; - let new_withdrawal_fee = Fee { - numerator: 46, - denominator: 10_000, - }; - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeIncreaseTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when increasing fee by too large a factor"), - } -} - -#[tokio::test] -async fn fail_high_stake_fee_increase_from_0() { - let (context, stake_pool_accounts, _new_stake_withdrawal_fee) = setup(Some(Fee { - numerator: 0, - denominator: 1, - })) - .await; - let new_withdrawal_fee = Fee { - numerator: 16, - denominator: 10_000, - }; - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeIncreaseTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when increasing fee by too large a factor"), - } -} - -#[tokio::test] -async fn fail_high_sol_fee_increase_from_0() { - let (context, stake_pool_accounts, _new_stake_withdrawal_fee) = setup(Some(Fee { - numerator: 0, - denominator: 1, - })) - .await; - let new_withdrawal_fee = Fee { - numerator: 16, - denominator: 10_000, - }; - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::SolWithdrawal(new_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::FeeIncreaseTooHigh as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when increasing fee by too large a factor"), - } -} - -#[tokio::test] -async fn fail_not_updated() { - let mut context = program_test().start_with_context().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - let new_stake_withdrawal_fee = Fee { - numerator: 11, - denominator: 100, - }; - - // move forward so an update is required - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - let slot = first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_fee( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - FeeType::StakeWithdrawal(new_stake_withdrawal_fee), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = error::StakePoolError::StakeListAndPoolOutOfDate as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs when stake pool out of date"), - } -} diff --git a/stake-pool/program/tests/update_pool_token_metadata.rs b/stake-pool/program/tests/update_pool_token_metadata.rs deleted file mode 100644 index 898366c5b6f..00000000000 --- a/stake-pool/program/tests/update_pool_token_metadata.rs +++ /dev/null @@ -1,191 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] -mod helpers; - -use { - helpers::*, - solana_program::instruction::InstructionError, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error::StakePoolError::{SignatureMissing, WrongManager}, - instruction, MINIMUM_RESERVE_LAMPORTS, - }, -}; - -async fn setup() -> (ProgramTestContext, StakePoolAccounts) { - let mut context = program_test_with_metadata_program() - .start_with_context() - .await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let name = "test_name"; - let symbol = "SYM"; - let uri = "test_uri"; - - let ix = instruction::create_token_metadata( - &spl_stake_pool::id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &context.payer.pubkey(), - name.to_string(), - symbol.to_string(), - uri.to_string(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - (context, stake_pool_accounts) -} - -#[tokio::test] -async fn success_update_pool_token_metadata() { - let (mut context, stake_pool_accounts) = setup().await; - - let updated_name = "updated_name"; - let updated_symbol = "USYM"; - let updated_uri = "updated_uri"; - - let ix = instruction::update_token_metadata( - &spl_stake_pool::id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - updated_name.to_string(), - updated_symbol.to_string(), - updated_uri.to_string(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let metadata = get_metadata_account( - &mut context.banks_client, - &stake_pool_accounts.pool_mint.pubkey(), - ) - .await; - - assert!(metadata.name.starts_with(updated_name)); - assert!(metadata.symbol.starts_with(updated_symbol)); - assert!(metadata.uri.starts_with(updated_uri)); -} - -#[tokio::test] -async fn fail_manager_did_not_sign() { - let (context, stake_pool_accounts) = setup().await; - - let updated_name = "updated_name"; - let updated_symbol = "USYM"; - let updated_uri = "updated_uri"; - - let mut ix = instruction::update_token_metadata( - &spl_stake_pool::id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - updated_name.to_string(), - updated_symbol.to_string(), - updated_uri.to_string(), - ); - ix.accounts[1].is_signer = false; - - let transaction = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while manager signature missing"), - } -} - -#[tokio::test] -async fn fail_wrong_manager_signed() { - let (context, stake_pool_accounts) = setup().await; - - let updated_name = "updated_name"; - let updated_symbol = "USYM"; - let updated_uri = "updated_uri"; - - let random_keypair = Keypair::new(); - let ix = instruction::update_token_metadata( - &spl_stake_pool::id(), - &stake_pool_accounts.stake_pool.pubkey(), - &random_keypair.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - updated_name.to_string(), - updated_symbol.to_string(), - updated_uri.to_string(), - ); - - let transaction = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer, &random_keypair], - context.last_blockhash, - ); - - let error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = WrongManager as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while signing with the wrong manager"), - } -} diff --git a/stake-pool/program/tests/update_stake_pool_balance.rs b/stake-pool/program/tests/update_stake_pool_balance.rs deleted file mode 100644 index f242e14382f..00000000000 --- a/stake-pool/program/tests/update_stake_pool_balance.rs +++ /dev/null @@ -1,413 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{borsh1::try_from_slice_unchecked, instruction::InstructionError}, - solana_program_test::*, - solana_sdk::{ - hash::Hash, - signature::{Keypair, Signer}, - stake, - transaction::TransactionError, - }, - spl_stake_pool::{error::StakePoolError, state::StakePool, MINIMUM_RESERVE_LAMPORTS}, - std::num::NonZeroU32, -}; - -const NUM_VALIDATORS: u64 = 3; - -async fn setup( - num_validators: u64, -) -> ( - ProgramTestContext, - Hash, - StakePoolAccounts, - Vec, -) { - let mut context = program_test().start_with_context().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - let error = stake_pool_accounts - .deposit_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.pool_fee_account.pubkey(), - (stake_rent + current_minimum_delegation) * num_validators, - None, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let mut last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - // Add several accounts - let mut stake_accounts: Vec = vec![]; - for i in 0..num_validators { - let stake_account = ValidatorStakeAccount::new( - &stake_pool_accounts.stake_pool.pubkey(), - NonZeroU32::new(i as u32), - u64::MAX, - ); - create_vote( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.validator, - &stake_account.vote, - ) - .await; - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.stake_account, - &stake_account.vote.pubkey(), - stake_account.validator_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let mut deposit_account = DepositStakeAccount::new_with_vote( - stake_account.vote.pubkey(), - stake_account.stake_account, - TEST_STAKE_AMOUNT, - ); - deposit_account - .create_and_delegate(&mut context.banks_client, &context.payer, &last_blockhash) - .await; - - deposit_account - .deposit_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - ) - .await; - - last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - stake_accounts.push(stake_account); - } - - (context, last_blockhash, stake_pool_accounts, stake_accounts) -} - -#[tokio::test] -async fn success() { - let (mut context, last_blockhash, stake_pool_accounts, stake_accounts) = - setup(NUM_VALIDATORS).await; - - let pre_fee = get_token_balance( - &mut context.banks_client, - &stake_pool_accounts.pool_fee_account.pubkey(), - ) - .await; - - let pre_balance = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(pre_balance, stake_pool.total_lamports); - - let pre_token_supply = get_token_supply( - &mut context.banks_client, - &stake_pool_accounts.pool_mint.pubkey(), - ) - .await; - - // Increment vote credits to earn rewards - const VOTE_CREDITS: u64 = 1_000; - for stake_account in &stake_accounts { - context.increment_vote_account_credits(&stake_account.vote.pubkey(), VOTE_CREDITS); - } - - // Update epoch - let slot = context.genesis_config().epoch_schedule.first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - // Update list and pool - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check fee - let post_balance = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(post_balance, stake_pool.total_lamports); - - let post_fee = get_token_balance( - &mut context.banks_client, - &stake_pool_accounts.pool_fee_account.pubkey(), - ) - .await; - let pool_token_supply = get_token_supply( - &mut context.banks_client, - &stake_pool_accounts.pool_mint.pubkey(), - ) - .await; - let actual_fee = post_fee - pre_fee; - assert_eq!(pool_token_supply - pre_token_supply, actual_fee); - - let expected_fee_lamports = (post_balance - pre_balance) * stake_pool.epoch_fee.numerator - / stake_pool.epoch_fee.denominator; - let actual_fee_lamports = stake_pool.calc_pool_tokens_for_deposit(actual_fee).unwrap(); - assert_eq!(actual_fee_lamports, expected_fee_lamports); - - let expected_fee = expected_fee_lamports * pool_token_supply / post_balance; - assert_eq!(expected_fee, actual_fee); - - assert_eq!(pool_token_supply, stake_pool.pool_token_supply); - assert_eq!(pre_token_supply, stake_pool.last_epoch_pool_token_supply); - assert_eq!(pre_balance, stake_pool.last_epoch_total_lamports); -} - -#[tokio::test] -async fn success_absorbing_extra_lamports() { - let (mut context, mut last_blockhash, stake_pool_accounts, stake_accounts) = - setup(NUM_VALIDATORS).await; - - let pre_balance = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - assert_eq!(pre_balance, stake_pool.total_lamports); - - let pre_token_supply = get_token_supply( - &mut context.banks_client, - &stake_pool_accounts.pool_mint.pubkey(), - ) - .await; - - // Transfer extra funds, will be absorbed during update - const EXTRA_STAKE_AMOUNT: u64 = 1_000_000; - for stake_account in &stake_accounts { - transfer( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.stake_account, - EXTRA_STAKE_AMOUNT, - ) - .await; - - last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - } - - let extra_lamports = EXTRA_STAKE_AMOUNT * stake_accounts.len() as u64; - let expected_fee = stake_pool.calc_epoch_fee_amount(extra_lamports).unwrap(); - - // Update epoch - let slot = context.genesis_config().epoch_schedule.first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - // Update list and pool - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check extra lamports are absorbed and fee'd as rewards - let post_balance = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - assert_eq!(post_balance, pre_balance + extra_lamports); - let pool_token_supply = get_token_supply( - &mut context.banks_client, - &stake_pool_accounts.pool_mint.pubkey(), - ) - .await; - assert_eq!(pool_token_supply, pre_token_supply + expected_fee); -} - -#[tokio::test] -async fn fail_with_wrong_validator_list() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let mut stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let wrong_validator_list = Keypair::new(); - stake_pool_accounts.validator_list = wrong_validator_list; - let error = stake_pool_accounts - .update_stake_pool_balance(&mut banks_client, &payer, &recent_blockhash) - .await - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - ) => { - let program_error = StakePoolError::InvalidValidatorStakeList as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to update pool balance with wrong validator stake list account"), - } -} - -#[tokio::test] -async fn fail_with_wrong_pool_fee_account() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let mut stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let wrong_fee_account = Keypair::new(); - stake_pool_accounts.pool_fee_account = wrong_fee_account; - let error = stake_pool_accounts - .update_stake_pool_balance(&mut banks_client, &payer, &recent_blockhash) - .await - .unwrap() - .unwrap(); - - match error { - TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - ) => { - let program_error = StakePoolError::InvalidFeeAccount as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to update pool balance with wrong validator stake list account"), - } -} - -#[tokio::test] -async fn fail_with_wrong_reserve() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let mut stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let wrong_reserve_stake = Keypair::new(); - stake_pool_accounts.reserve_stake = wrong_reserve_stake; - let error = stake_pool_accounts - .update_stake_pool_balance(&mut banks_client, &payer, &recent_blockhash) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::InvalidProgramAddress as u32), - ) - ); -} - -#[tokio::test] -async fn test_update_stake_pool_balance_with_uninitialized_validator_list() {} // TODO - -#[tokio::test] -async fn test_update_stake_pool_balance_with_out_of_dated_validators_balances() {} // TODO diff --git a/stake-pool/program/tests/update_validator_list_balance.rs b/stake-pool/program/tests/update_validator_list_balance.rs deleted file mode 100644 index 7d5d431a16c..00000000000 --- a/stake-pool/program/tests/update_validator_list_balance.rs +++ /dev/null @@ -1,737 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{borsh1::try_from_slice_unchecked, program_pack::Pack}, - solana_program_test::*, - solana_sdk::{hash::Hash, signature::Signer, stake::state::StakeStateV2}, - spl_stake_pool::{ - state::{StakePool, StakeStatus, ValidatorList}, - MAX_VALIDATORS_TO_UPDATE, MINIMUM_RESERVE_LAMPORTS, - }, - spl_token::state::Mint, - std::num::NonZeroU32, -}; - -async fn setup( - num_validators: usize, -) -> ( - ProgramTestContext, - Hash, - StakePoolAccounts, - Vec, - Vec, - u64, - u64, - u64, -) { - let mut context = program_test().start_with_context().await; - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - let mut slot = first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - - let reserve_stake_amount = TEST_STAKE_AMOUNT * 2 * num_validators as u64; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - reserve_stake_amount + MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - // Add several accounts with some stake - let mut stake_accounts: Vec = vec![]; - let mut deposit_accounts: Vec = vec![]; - for i in 0..num_validators { - let stake_account = ValidatorStakeAccount::new( - &stake_pool_accounts.stake_pool.pubkey(), - NonZeroU32::new(i as u32), - u64::MAX, - ); - create_vote( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_account.validator, - &stake_account.vote, - ) - .await; - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_account.stake_account, - &stake_account.vote.pubkey(), - stake_account.validator_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let deposit_account = DepositStakeAccount::new_with_vote( - stake_account.vote.pubkey(), - stake_account.stake_account, - TEST_STAKE_AMOUNT, - ); - deposit_account - .create_and_delegate( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - stake_accounts.push(stake_account); - deposit_accounts.push(deposit_account); - } - - // Warp forward so the stakes properly activate, and deposit - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - for deposit_account in &mut deposit_accounts { - deposit_account - .deposit_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - ) - .await; - } - - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - ( - context, - last_blockhash, - stake_pool_accounts, - stake_accounts, - deposit_accounts, - TEST_STAKE_AMOUNT, - reserve_stake_amount, - slot, - ) -} - -#[tokio::test] -async fn success_with_normal() { - let num_validators = 5; - let ( - mut context, - last_blockhash, - stake_pool_accounts, - stake_accounts, - _, - validator_lamports, - reserve_lamports, - mut slot, - ) = setup(num_validators).await; - - // Check current balance in the list - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - let validator_list_sum = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - assert_eq!(stake_pool.total_lamports, validator_list_sum); - // initially, have all of the deposits plus their rent, and the reserve stake - let initial_lamports = - (validator_lamports + stake_rent) * num_validators as u64 + reserve_lamports; - assert_eq!(validator_list_sum, initial_lamports); - - // Simulate rewards - for stake_account in &stake_accounts { - context.increment_vote_account_credits(&stake_account.vote.pubkey(), 100); - } - - // Warp one more epoch so the rewards are paid out - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - let new_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - assert!(new_lamports > initial_lamports); - - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - assert_eq!(new_lamports, stake_pool.total_lamports); -} - -#[tokio::test] -async fn merge_into_reserve() { - let ( - mut context, - last_blockhash, - stake_pool_accounts, - stake_accounts, - _, - lamports, - _, - mut slot, - ) = setup(MAX_VALIDATORS_TO_UPDATE).await; - - let pre_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - - let reserve_stake = context - .banks_client - .get_account(stake_pool_accounts.reserve_stake.pubkey()) - .await - .unwrap() - .unwrap(); - let pre_reserve_lamports = reserve_stake.lamports; - - println!("Decrease from all validators"); - for stake_account in &stake_accounts { - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.stake_account, - &stake_account.transient_stake_account, - lamports, - stake_account.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - } - - println!("Update, should not change, no merges yet"); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let expected_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - assert_eq!(pre_lamports, expected_lamports); - - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - assert_eq!(expected_lamports, stake_pool.total_lamports); - - println!("Warp one more epoch so the stakes deactivate"); - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - let expected_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - assert_eq!(pre_lamports, expected_lamports); - - let reserve_stake = context - .banks_client - .get_account(stake_pool_accounts.reserve_stake.pubkey()) - .await - .unwrap() - .unwrap(); - let post_reserve_lamports = reserve_stake.lamports; - assert!(post_reserve_lamports > pre_reserve_lamports); - - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - assert_eq!(expected_lamports, stake_pool.total_lamports); -} - -#[tokio::test] -async fn merge_into_validator_stake() { - let ( - mut context, - last_blockhash, - stake_pool_accounts, - stake_accounts, - _, - lamports, - reserve_lamports, - mut slot, - ) = setup(MAX_VALIDATORS_TO_UPDATE).await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let pre_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - - // Increase stake to all validators - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &last_blockhash, - ) - .await; - let available_lamports = - reserve_lamports - (stake_rent + current_minimum_delegation) * stake_accounts.len() as u64; - let increase_amount = available_lamports / stake_accounts.len() as u64; - for stake_account in &stake_accounts { - let error = stake_pool_accounts - .increase_validator_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.transient_stake_account, - &stake_account.stake_account, - &stake_account.vote.pubkey(), - increase_amount, - stake_account.transient_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - } - - // Warp just a little bit to get a new blockhash and update again - context.warp_to_slot(slot + 10).unwrap(); - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - // Update, should not change, no merges yet - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let expected_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - assert_eq!(pre_lamports, expected_lamports); - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - assert_eq!(expected_lamports, stake_pool.total_lamports); - - // Warp one more epoch so the stakes activate, ready to merge - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - let current_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - assert_eq!(current_lamports, stake_pool.total_lamports); - - // Check that transient accounts are gone - for stake_account in &stake_accounts { - assert!(context - .banks_client - .get_account(stake_account.transient_stake_account) - .await - .unwrap() - .is_none()); - } - - // Check validator stake accounts have the expected balance now: - // validator stake account minimum + deposited lamports + rents + increased - // lamports - let expected_lamports = current_minimum_delegation + lamports + increase_amount + stake_rent; - for stake_account in &stake_accounts { - let validator_stake = - get_account(&mut context.banks_client, &stake_account.stake_account).await; - assert_eq!(validator_stake.lamports, expected_lamports); - } - - // Check reserve stake accounts for expected balance: - // own rent, other account rents, and 1 extra lamport - let reserve_stake = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await; - assert_eq!( - reserve_stake.lamports, - MINIMUM_RESERVE_LAMPORTS + stake_rent * (1 + stake_accounts.len() as u64) - ); -} - -#[tokio::test] -async fn merge_transient_stake_after_remove() { - let ( - mut context, - last_blockhash, - stake_pool_accounts, - stake_accounts, - _, - lamports, - reserve_lamports, - mut slot, - ) = setup(1).await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &last_blockhash, - ) - .await; - let deactivated_lamports = lamports; - // Decrease and remove all validators - for stake_account in &stake_accounts { - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.stake_account, - &stake_account.transient_stake_account, - deactivated_lamports, - stake_account.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.stake_account, - &stake_account.transient_stake_account, - ) - .await; - assert!(error.is_none(), "{:?}", error); - } - - // Warp forward to merge time - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - // Update without merge, status should be DeactivatingTransient - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - true, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert_eq!(validator_list.validators.len(), 1); - assert_eq!( - validator_list.validators[0].status, - StakeStatus::DeactivatingAll.into() - ); - assert_eq!( - u64::from(validator_list.validators[0].active_stake_lamports), - stake_rent + current_minimum_delegation - ); - assert_eq!( - u64::from(validator_list.validators[0].transient_stake_lamports), - deactivated_lamports + stake_rent - ); - - // Update with merge, status should be ReadyForRemoval and no lamports - let error = stake_pool_accounts - .update_validator_list_balance( - &mut context.banks_client, - &context.payer, - &last_blockhash, - validator_list.validators.len(), - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // stake accounts were merged in, none exist anymore - for stake_account in &stake_accounts { - let not_found_account = context - .banks_client - .get_account(stake_account.stake_account) - .await - .unwrap(); - assert!(not_found_account.is_none()); - let not_found_account = context - .banks_client - .get_account(stake_account.transient_stake_account) - .await - .unwrap(); - assert!(not_found_account.is_none()); - } - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert_eq!(validator_list.validators.len(), 1); - assert_eq!( - validator_list.validators[0].status, - StakeStatus::ReadyForRemoval.into() - ); - assert_eq!(validator_list.validators[0].stake_lamports().unwrap(), 0); - - let reserve_stake = context - .banks_client - .get_account(stake_pool_accounts.reserve_stake.pubkey()) - .await - .unwrap() - .unwrap(); - assert_eq!( - reserve_stake.lamports, - reserve_lamports + deactivated_lamports + stake_rent * 2 + MINIMUM_RESERVE_LAMPORTS - ); - - // Update stake pool balance and cleanup, should be gone - let error = stake_pool_accounts - .update_stake_pool_balance(&mut context.banks_client, &context.payer, &last_blockhash) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .cleanup_removed_validator_entries( - &mut context.banks_client, - &context.payer, - &last_blockhash, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert_eq!(validator_list.validators.len(), 0); -} - -#[tokio::test] -async fn success_with_burned_tokens() { - let num_validators = 1; - let (mut context, last_blockhash, stake_pool_accounts, _, deposit_accounts, _, _, mut slot) = - setup(num_validators).await; - - let mint_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.pool_mint.pubkey(), - ) - .await; - let mint = Mint::unpack(&mint_info.data).unwrap(); - - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - assert_eq!(mint.supply, stake_pool.pool_token_supply); - - burn_tokens( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_mint.pubkey(), - &deposit_accounts[0].pool_account.pubkey(), - &deposit_accounts[0].authority, - deposit_accounts[0].pool_tokens, - ) - .await - .unwrap(); - - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - let mint_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.pool_mint.pubkey(), - ) - .await; - let mint = Mint::unpack(&mint_info.data).unwrap(); - assert_ne!(mint.supply, stake_pool.pool_token_supply); - - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - - assert_eq!(mint.supply, stake_pool.pool_token_supply); -} - -#[tokio::test] -async fn fail_with_uninitialized_validator_list() {} // TODO - -#[tokio::test] -async fn success_with_force_destaked_validator() {} diff --git a/stake-pool/program/tests/update_validator_list_balance_hijack.rs b/stake-pool/program/tests/update_validator_list_balance_hijack.rs deleted file mode 100644 index 66a2ab36d40..00000000000 --- a/stake-pool/program/tests/update_validator_list_balance_hijack.rs +++ /dev/null @@ -1,547 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -use spl_stake_pool::instruction; - -mod helpers; - -use { - helpers::*, - solana_program::{borsh1::try_from_slice_unchecked, pubkey::Pubkey, stake}, - solana_program_test::*, - solana_sdk::{ - hash::Hash, - instruction::InstructionError, - signature::Signer, - stake::state::{Authorized, Lockup, StakeStateV2}, - system_instruction, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error::StakePoolError, find_stake_program_address, find_transient_stake_program_address, - find_withdraw_authority_program_address, id, state::StakePool, MINIMUM_RESERVE_LAMPORTS, - }, - std::num::NonZeroU32, -}; - -async fn setup( - num_validators: usize, -) -> ( - ProgramTestContext, - Hash, - StakePoolAccounts, - Vec, - Vec, - u64, - u64, - u64, -) { - let mut context = program_test().start_with_context().await; - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - let mut slot = first_normal_slot + 1; - context.warp_to_slot(slot).unwrap(); - - let reserve_stake_amount = TEST_STAKE_AMOUNT * 2 * num_validators as u64; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - reserve_stake_amount + MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - // Add several accounts with some stake - let mut stake_accounts: Vec = vec![]; - let mut deposit_accounts: Vec = vec![]; - for i in 0..num_validators { - let stake_account = ValidatorStakeAccount::new( - &stake_pool_accounts.stake_pool.pubkey(), - NonZeroU32::new(i as u32), - u64::MAX, - ); - create_vote( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_account.validator, - &stake_account.vote, - ) - .await; - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_account.stake_account, - &stake_account.vote.pubkey(), - stake_account.validator_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let deposit_account = DepositStakeAccount::new_with_vote( - stake_account.vote.pubkey(), - stake_account.stake_account, - TEST_STAKE_AMOUNT, - ); - deposit_account - .create_and_delegate( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - stake_accounts.push(stake_account); - deposit_accounts.push(deposit_account); - } - - // Warp forward so the stakes properly activate, and deposit - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - for deposit_account in &mut deposit_accounts { - deposit_account - .deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - ) - .await; - } - - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - ( - context, - last_blockhash, - stake_pool_accounts, - stake_accounts, - deposit_accounts, - TEST_STAKE_AMOUNT, - reserve_stake_amount, - slot, - ) -} - -#[tokio::test] -async fn success_ignoring_hijacked_transient_stake_with_authorized() { - let hijacker = Pubkey::new_unique(); - check_ignored_hijacked_transient_stake(Some(&Authorized::auto(&hijacker)), None).await; -} - -#[tokio::test] -async fn success_ignoring_hijacked_transient_stake_with_lockup() { - let hijacker = Pubkey::new_unique(); - check_ignored_hijacked_transient_stake( - None, - Some(&Lockup { - custodian: hijacker, - ..Lockup::default() - }), - ) - .await; -} - -async fn check_ignored_hijacked_transient_stake( - hijack_authorized: Option<&Authorized>, - hijack_lockup: Option<&Lockup>, -) { - let num_validators = 1; - let ( - mut context, - last_blockhash, - stake_pool_accounts, - stake_accounts, - _, - lamports, - _, - mut slot, - ) = setup(num_validators).await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - let pre_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let (withdraw_authority, _) = - find_withdraw_authority_program_address(&id(), &stake_pool_accounts.stake_pool.pubkey()); - - println!("Decrease from all validators"); - let stake_account = &stake_accounts[0]; - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.stake_account, - &stake_account.transient_stake_account, - lamports, - stake_account.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - println!("Warp one epoch so the stakes deactivate and merge"); - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - println!("During update, hijack the transient stake account"); - let validator_list = stake_pool_accounts - .get_validator_list(&mut context.banks_client) - .await; - let transient_stake_address = find_transient_stake_program_address( - &id(), - &stake_account.vote.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - stake_account.transient_stake_seed, - ) - .0; - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::update_validator_list_balance_chunk( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &validator_list, - 1, - 0, - /* no_merge = */ false, - ) - .unwrap(), - system_instruction::transfer( - &context.payer.pubkey(), - &transient_stake_address, - stake_rent + MINIMUM_RESERVE_LAMPORTS, - ), - stake::instruction::initialize( - &transient_stake_address, - hijack_authorized.unwrap_or(&Authorized::auto(&withdraw_authority)), - hijack_lockup.unwrap_or(&Lockup::default()), - ), - instruction::update_stake_pool_balance( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &spl_token::id(), - ), - instruction::cleanup_removed_validator_entries( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer], - last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err(); - assert!(error.is_none(), "{:?}", error); - - println!("Update again normally, should be no change in the lamports"); - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let expected_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - assert_eq!(pre_lamports, expected_lamports); - - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - assert_eq!(pre_lamports, stake_pool.total_lamports); -} - -#[tokio::test] -async fn success_ignoring_hijacked_validator_stake_with_authorized() { - let hijacker = Pubkey::new_unique(); - check_ignored_hijacked_transient_stake(Some(&Authorized::auto(&hijacker)), None).await; -} - -#[tokio::test] -async fn success_ignoring_hijacked_validator_stake_with_lockup() { - let hijacker = Pubkey::new_unique(); - check_ignored_hijacked_validator_stake( - None, - Some(&Lockup { - custodian: hijacker, - ..Lockup::default() - }), - ) - .await; -} - -async fn check_ignored_hijacked_validator_stake( - hijack_authorized: Option<&Authorized>, - hijack_lockup: Option<&Lockup>, -) { - let num_validators = 1; - let ( - mut context, - last_blockhash, - stake_pool_accounts, - stake_accounts, - _, - lamports, - _, - mut slot, - ) = setup(num_validators).await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - let pre_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let (withdraw_authority, _) = - find_withdraw_authority_program_address(&id(), &stake_pool_accounts.stake_pool.pubkey()); - - let stake_account = &stake_accounts[0]; - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.stake_account, - &stake_account.transient_stake_account, - lamports, - stake_account.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.stake_account, - &stake_account.transient_stake_account, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - println!("Warp one epoch so the stakes deactivate and merge"); - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - println!("During update, hijack the validator stake account"); - let validator_list = stake_pool_accounts - .get_validator_list(&mut context.banks_client) - .await; - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::update_validator_list_balance_chunk( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &validator_list, - 1, - 0, - /* no_merge = */ false, - ) - .unwrap(), - system_instruction::transfer( - &context.payer.pubkey(), - &stake_account.stake_account, - stake_rent + MINIMUM_RESERVE_LAMPORTS, - ), - stake::instruction::initialize( - &stake_account.stake_account, - hijack_authorized.unwrap_or(&Authorized::auto(&withdraw_authority)), - hijack_lockup.unwrap_or(&Lockup::default()), - ), - instruction::update_stake_pool_balance( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &spl_token::id(), - ), - instruction::cleanup_removed_validator_entries( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer], - last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err(); - assert!(error.is_none(), "{:?}", error); - - println!("Update again normally, should be no change in the lamports"); - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let expected_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - assert_eq!(pre_lamports, expected_lamports); - - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - assert_eq!(pre_lamports, stake_pool.total_lamports); - - println!("Fail adding validator back in with first seed"); - let error = stake_pool_accounts - .add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account.stake_account, - &stake_account.vote.pubkey(), - stake_account.validator_stake_seed, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::AlreadyInUse as u32), - ) - ); - - println!("Succeed adding validator back in with new seed"); - let seed = NonZeroU32::new(1); - let validator = stake_account.vote.pubkey(); - let (stake_account, _) = find_stake_program_address( - &id(), - &validator, - &stake_pool_accounts.stake_pool.pubkey(), - seed, - ); - let error = stake_pool_accounts - .add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_account, - &validator, - seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let stake_pool_info = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data).unwrap(); - assert_eq!(pre_lamports, stake_pool.total_lamports); - - let expected_lamports = get_validator_list_sum( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - assert_eq!(pre_lamports, expected_lamports); -} diff --git a/stake-pool/program/tests/vsa_add.rs b/stake-pool/program/tests/vsa_add.rs deleted file mode 100644 index af6bd42ecc2..00000000000 --- a/stake-pool/program/tests/vsa_add.rs +++ /dev/null @@ -1,720 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - bincode::deserialize, - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, - hash::Hash, - instruction::{AccountMeta, Instruction, InstructionError}, - pubkey::Pubkey, - stake, system_program, sysvar, - }, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_stake_pool::{ - error::StakePoolError, find_stake_program_address, id, instruction, state, - MINIMUM_RESERVE_LAMPORTS, - }, -}; - -async fn setup( - num_validators: u64, -) -> ( - BanksClient, - Keypair, - Hash, - StakePoolAccounts, - ValidatorStakeAccount, -) { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let rent = banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = - stake_pool_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; - let minimum_for_validator = stake_rent + current_minimum_delegation; - - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS + num_validators * minimum_for_validator, - ) - .await - .unwrap(); - - let validator_stake = - ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey(), None, 0); - create_vote( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.validator, - &validator_stake.vote, - ) - .await; - - ( - banks_client, - payer, - recent_blockhash, - stake_pool_accounts, - validator_stake, - ) -} - -#[tokio::test] -async fn success() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(1).await; - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check if validator account was added to the list - let validator_list = get_account( - &mut banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let rent = banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = - stake_pool_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; - assert_eq!( - validator_list, - state::ValidatorList { - header: state::ValidatorListHeader { - account_type: state::AccountType::ValidatorList, - max_validators: stake_pool_accounts.max_validators, - }, - validators: vec![state::ValidatorStakeInfo { - status: state::StakeStatus::Active.into(), - vote_account_address: validator_stake.vote.pubkey(), - last_update_epoch: 0.into(), - active_stake_lamports: (stake_rent + current_minimum_delegation).into(), - transient_stake_lamports: 0.into(), - transient_seed_suffix: 0.into(), - unused: 0.into(), - validator_seed_suffix: validator_stake - .validator_stake_seed - .map(|s| s.get()) - .unwrap_or(0) - .into(), - }] - } - ); - - // Check stake account existence and authority - let stake = get_account(&mut banks_client, &validator_stake.stake_account).await; - let stake_state = deserialize::(&stake.data).unwrap(); - match stake_state { - stake::state::StakeStateV2::Stake(meta, _, _) => { - assert_eq!( - &meta.authorized.staker, - &stake_pool_accounts.withdraw_authority - ); - assert_eq!( - &meta.authorized.withdrawer, - &stake_pool_accounts.withdraw_authority - ); - } - _ => panic!(), - } -} - -#[tokio::test] -async fn fail_with_wrong_validator_list_account() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(1).await; - - let wrong_validator_list = Keypair::new(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::add_validator_to_pool( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.withdraw_authority, - &wrong_validator_list.pubkey(), - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::InvalidValidatorStakeList as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to add validator stake address with wrong validator stake list account"), - } -} - -#[tokio::test] -async fn fail_double_add() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(2).await; - - stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await; - - let latest_blockhash = banks_client.get_latest_blockhash().await.unwrap(); - - let transaction_error = stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &latest_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await - .unwrap(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::ValidatorAlreadyAdded as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to add already added validator stake account"), - } -} - -#[tokio::test] -async fn fail_wrong_staker() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(1).await; - - let malicious = Keypair::new(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::add_validator_to_pool( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &malicious.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &malicious], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::WrongStaker as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while malicious try to add validator stake account"), - } -} - -#[tokio::test] -async fn fail_without_signature() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(1).await; - - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), false), - AccountMeta::new(payer.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), - AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), - AccountMeta::new(validator_stake.stake_account, false), - AccountMeta::new(validator_stake.vote.pubkey(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - #[allow(deprecated)] - AccountMeta::new_readonly(stake::config::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data: borsh::to_vec(&instruction::StakePoolInstruction::AddValidatorToPool( - validator_stake - .validator_stake_seed - .map(|s| s.get()) - .unwrap_or(0), - )) - .unwrap(), - }; - - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while malicious try to add validator stake account without signing transaction"), - } -} - -#[tokio::test] -async fn fail_with_wrong_stake_program_id() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(1).await; - - let wrong_stake_program = Pubkey::new_unique(); - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true), - AccountMeta::new(payer.pubkey(), true), - AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), - AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), - AccountMeta::new(validator_stake.stake_account, false), - AccountMeta::new(validator_stake.vote.pubkey(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - #[allow(deprecated)] - AccountMeta::new_readonly(stake::config::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(wrong_stake_program, false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data: borsh::to_vec(&instruction::StakePoolInstruction::AddValidatorToPool( - validator_stake - .validator_stake_seed - .map(|s| s.get()) - .unwrap_or(0), - )) - .unwrap(), - }; - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!( - "Wrong error occurs while try to add validator stake account with wrong stake program ID" - ), - } -} - -#[tokio::test] -async fn fail_with_wrong_system_program_id() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(1).await; - - let wrong_system_program = Pubkey::new_unique(); - - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true), - AccountMeta::new(payer.pubkey(), true), - AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), - AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), - AccountMeta::new(validator_stake.stake_account, false), - AccountMeta::new(validator_stake.vote.pubkey(), false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(sysvar::stake_history::id(), false), - #[allow(deprecated)] - AccountMeta::new_readonly(stake::config::id(), false), - AccountMeta::new_readonly(wrong_system_program, false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data: borsh::to_vec(&instruction::StakePoolInstruction::AddValidatorToPool( - validator_stake - .validator_stake_seed - .map(|s| s.get()) - .unwrap_or(0), - )) - .unwrap(), - }; - let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); - transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!( - "Wrong error occurs while try to add validator stake account with wrong stake program ID" - ), - } -} - -#[tokio::test] -async fn fail_add_too_many_validator_stake_accounts() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let rent = banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = - stake_pool_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; - let minimum_for_validator = stake_rent + current_minimum_delegation; - - let stake_pool_accounts = StakePoolAccounts { - max_validators: 1, - ..Default::default() - }; - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - MINIMUM_RESERVE_LAMPORTS + 2 * minimum_for_validator, - ) - .await - .unwrap(); - - let validator_stake = - ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey(), None, 0); - create_vote( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.validator, - &validator_stake.vote, - ) - .await; - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let validator_stake = - ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey(), None, 0); - create_vote( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.validator, - &validator_stake.vote, - ) - .await; - let error = stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::AccountDataTooSmall), - ); -} - -#[tokio::test] -async fn fail_with_unupdated_stake_pool() {} // TODO - -#[tokio::test] -async fn fail_with_uninitialized_validator_list_account() {} // TODO - -#[tokio::test] -async fn fail_on_non_vote_account() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, _) = setup(1).await; - - let validator = Pubkey::new_unique(); - let (stake_account, _) = find_stake_program_address( - &id(), - &validator, - &stake_pool_accounts.stake_pool.pubkey(), - None, - ); - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_account, - &validator, - None, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::IncorrectProgramId,) - ); -} - -#[tokio::test] -async fn fail_on_incorrectly_derived_stake_account() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(1).await; - - let bad_stake_account = Pubkey::new_unique(); - let error = stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &bad_stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::InvalidStakeAccountAddress as u32), - ) - ); -} - -#[tokio::test] -async fn success_with_lamports_in_account() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(1).await; - - transfer( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.stake_account, - 1_000_000, - ) - .await; - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check stake account existence and authority - let stake = get_account(&mut banks_client, &validator_stake.stake_account).await; - let stake_state = deserialize::(&stake.data).unwrap(); - match stake_state { - stake::state::StakeStateV2::Stake(meta, _, _) => { - assert_eq!( - &meta.authorized.staker, - &stake_pool_accounts.withdraw_authority - ); - assert_eq!( - &meta.authorized.withdrawer, - &stake_pool_accounts.withdraw_authority - ); - } - _ => panic!(), - } -} - -#[tokio::test] -async fn fail_with_not_enough_reserve_lamports() { - let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(0).await; - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::InsufficientFunds) - ); -} - -#[tokio::test] -async fn fail_with_wrong_reserve() { - let (banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) = - setup(1).await; - - let wrong_reserve = Pubkey::new_unique(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::add_validator_to_pool( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &wrong_reserve, - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - )], - Some(&payer.pubkey()), - ); - transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash); - let transaction_error = banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::InvalidProgramAddress as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to add validator stake address with wrong validator stake list account"), - } -} - -#[tokio::test] -async fn fail_with_draining_reserve() { - let (mut banks_client, payer, recent_blockhash) = program_test().start().await; - let current_minimum_delegation = - stake_pool_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; - - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut banks_client, - &payer, - &recent_blockhash, - current_minimum_delegation, // add exactly enough for a validator - ) - .await - .unwrap(); - - let validator_stake = - ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey(), None, 0); - create_vote( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.validator, - &validator_stake.vote, - ) - .await; - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut banks_client, - &payer, - &recent_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::InsufficientFunds), - ); -} diff --git a/stake-pool/program/tests/vsa_remove.rs b/stake-pool/program/tests/vsa_remove.rs deleted file mode 100644 index 9cfb5bdeefb..00000000000 --- a/stake-pool/program/tests/vsa_remove.rs +++ /dev/null @@ -1,845 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - bincode::deserialize, - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, - instruction::{AccountMeta, Instruction, InstructionError}, - pubkey::Pubkey, - stake, system_instruction, sysvar, - }, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_stake_pool::{ - error::StakePoolError, find_transient_stake_program_address, id, instruction, state, - MINIMUM_RESERVE_LAMPORTS, - }, - std::num::NonZeroU32, -}; - -async fn setup() -> (ProgramTestContext, StakePoolAccounts, ValidatorStakeAccount) { - let mut context = program_test().start_with_context().await; - let stake_pool_accounts = StakePoolAccounts::default(); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - 10_000_000_000 + MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let validator_stake = ValidatorStakeAccount::new( - &stake_pool_accounts.stake_pool.pubkey(), - NonZeroU32::new(u32::MAX), - u64::MAX, - ); - create_vote( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.validator, - &validator_stake.vote, - ) - .await; - - let error = stake_pool_accounts - .add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - validator_stake.validator_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - (context, stake_pool_accounts, validator_stake) -} - -#[tokio::test] -async fn success() { - let (mut context, stake_pool_accounts, validator_stake) = setup().await; - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check if account was removed from the list of stake accounts - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert_eq!( - validator_list, - state::ValidatorList { - header: state::ValidatorListHeader { - account_type: state::AccountType::ValidatorList, - max_validators: stake_pool_accounts.max_validators, - }, - validators: vec![] - } - ); - - // Check stake account no longer exists - let account = context - .banks_client - .get_account(validator_stake.stake_account) - .await - .unwrap(); - assert!(account.is_none()); -} - -#[tokio::test] -async fn fail_with_wrong_stake_program_id() { - let (context, stake_pool_accounts, validator_stake) = setup().await; - - let wrong_stake_program = Pubkey::new_unique(); - - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true), - AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), - AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), - AccountMeta::new(validator_stake.stake_account, false), - AccountMeta::new_readonly(validator_stake.transient_stake_account, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(wrong_stake_program, false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data: borsh::to_vec(&instruction::StakePoolInstruction::RemoveValidatorFromPool).unwrap(), - }; - - let mut transaction = - Transaction::new_with_payer(&[instruction], Some(&context.payer.pubkey())); - transaction.sign( - &[&context.payer, &stake_pool_accounts.staker], - context.last_blockhash, - ); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - error, - )) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!("Wrong error occurs while try to remove validator stake address with wrong stake program ID"), - } -} - -#[tokio::test] -async fn fail_with_wrong_validator_list_account() { - let (context, stake_pool_accounts, validator_stake) = setup().await; - - let wrong_validator_list = Keypair::new(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::remove_validator_from_pool( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.staker.pubkey(), - &stake_pool_accounts.withdraw_authority, - &wrong_validator_list.pubkey(), - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - )], - Some(&context.payer.pubkey()), - ); - transaction.sign( - &[&context.payer, &stake_pool_accounts.staker], - context.last_blockhash, - ); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::InvalidValidatorStakeList as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to remove validator stake address with wrong validator stake list account"), - } -} - -#[tokio::test] -async fn success_at_large_value() { - let (mut context, stake_pool_accounts, validator_stake) = setup().await; - - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - - let threshold_amount = current_minimum_delegation * 1_000; - let _ = simple_deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - &validator_stake, - threshold_amount, - ) - .await - .unwrap(); - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -#[tokio::test] -async fn fail_double_remove() { - let (mut context, stake_pool_accounts, validator_stake) = setup().await; - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::BorshIoError("Unknown".to_string()) - ) - ); -} - -#[tokio::test] -async fn fail_wrong_staker() { - let (context, stake_pool_accounts, validator_stake) = setup().await; - - let malicious = Keypair::new(); - - let mut transaction = Transaction::new_with_payer( - &[instruction::remove_validator_from_pool( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &malicious.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - )], - Some(&context.payer.pubkey()), - ); - transaction.sign(&[&context.payer, &malicious], context.last_blockhash); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::WrongStaker as u32; - assert_eq!(error_index, program_error); - } - _ => { - panic!("Wrong error occurs while not an staker try to remove validator stake address") - } - } -} - -#[tokio::test] -async fn fail_no_signature() { - let (context, stake_pool_accounts, validator_stake) = setup().await; - - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), - AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), - AccountMeta::new(validator_stake.stake_account, false), - AccountMeta::new_readonly(validator_stake.transient_stake_account, false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(stake::program::id(), false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data: borsh::to_vec(&instruction::StakePoolInstruction::RemoveValidatorFromPool).unwrap(), - }; - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::SignatureMissing as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while malicious try to remove validator stake account without signing transaction"), - } -} - -#[tokio::test] -async fn success_with_activating_transient_stake() { - let (mut context, stake_pool_accounts, validator_stake) = setup().await; - - // increase the validator stake - let error = stake_pool_accounts - .increase_validator_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - 2_000_000_000, - validator_stake.transient_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // transient stake should be inactive now - let stake = get_account( - &mut context.banks_client, - &validator_stake.transient_stake_account, - ) - .await; - let stake_state = deserialize::(&stake.data).unwrap(); - assert_ne!( - stake_state.stake().unwrap().delegation.deactivation_epoch, - u64::MAX - ); -} - -#[tokio::test] -async fn success_with_deactivating_transient_stake() { - let (mut context, stake_pool_accounts, validator_stake) = setup().await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - let deposit_info = simple_deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - &validator_stake, - TEST_STAKE_AMOUNT, - ) - .await - .unwrap(); - - // increase the validator stake - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - TEST_STAKE_AMOUNT + stake_rent, - validator_stake.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // fail deposit - let maybe_deposit = simple_deposit_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - &validator_stake, - TEST_STAKE_AMOUNT, - ) - .await; - assert!(maybe_deposit.is_none()); - - // fail withdraw - let user_stake_recipient = Keypair::new(); - create_blank_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient, - ) - .await; - - let user_transfer_authority = Keypair::new(); - let new_authority = Pubkey::new_unique(); - delegate_tokens( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &deposit_info.pool_account.pubkey(), - &deposit_info.authority, - &user_transfer_authority.pubkey(), - 1, - ) - .await; - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake.stake_account, - &new_authority, - 1, - ) - .await; - assert!(error.is_some()); - - // check validator has changed - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let expected_list = state::ValidatorList { - header: state::ValidatorListHeader { - account_type: state::AccountType::ValidatorList, - max_validators: stake_pool_accounts.max_validators, - }, - validators: vec![state::ValidatorStakeInfo { - status: state::StakeStatus::DeactivatingAll.into(), - vote_account_address: validator_stake.vote.pubkey(), - last_update_epoch: 0.into(), - active_stake_lamports: (stake_rent + current_minimum_delegation).into(), - transient_stake_lamports: (TEST_STAKE_AMOUNT + stake_rent * 2).into(), - transient_seed_suffix: validator_stake.transient_stake_seed.into(), - unused: 0.into(), - validator_seed_suffix: validator_stake - .validator_stake_seed - .map(|s| s.get()) - .unwrap_or(0) - .into(), - }], - }; - assert_eq!(validator_list, expected_list); - - // Update will merge since activation and deactivation were in the same epoch - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let expected_list = state::ValidatorList { - header: state::ValidatorListHeader { - account_type: state::AccountType::ValidatorList, - max_validators: stake_pool_accounts.max_validators, - }, - validators: vec![], - }; - assert_eq!(validator_list, expected_list); -} - -#[tokio::test] -async fn success_resets_preferred_validator() { - let (mut context, stake_pool_accounts, validator_stake) = setup().await; - - stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - instruction::PreferredValidatorType::Deposit, - Some(validator_stake.vote.pubkey()), - ) - .await; - stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - instruction::PreferredValidatorType::Withdraw, - Some(validator_stake.vote.pubkey()), - ) - .await; - - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check if account was removed from the list of stake accounts - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert_eq!( - validator_list, - state::ValidatorList { - header: state::ValidatorListHeader { - account_type: state::AccountType::ValidatorList, - max_validators: stake_pool_accounts.max_validators, - }, - validators: vec![] - } - ); - - // Check stake account no longer exists - let account = context - .banks_client - .get_account(validator_stake.stake_account) - .await - .unwrap(); - assert!(account.is_none()); -} - -#[tokio::test] -async fn success_with_hijacked_transient_account() { - let (mut context, stake_pool_accounts, validator_stake) = setup().await; - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let current_minimum_delegation = stake_pool_get_minimum_delegation( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - ) - .await; - let increase_amount = current_minimum_delegation + stake_rent; - - // increase stake on validator - let error = stake_pool_accounts - .increase_validator_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - increase_amount, - validator_stake.transient_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // warp forward to merge - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; - let mut slot = first_normal_slot + slots_per_epoch + 1; - context.warp_to_slot(slot).unwrap(); - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - // decrease - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - increase_amount, - validator_stake.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // warp forward to merge - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - // hijack - let validator_list = stake_pool_accounts - .get_validator_list(&mut context.banks_client) - .await; - let hijacker = Keypair::new(); - let transient_stake_address = find_transient_stake_program_address( - &id(), - &validator_stake.vote.pubkey(), - &stake_pool_accounts.stake_pool.pubkey(), - validator_stake.transient_stake_seed, - ) - .0; - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::update_validator_list_balance_chunk( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &validator_list, - 1, - 0, - /* no_merge = */ false, - ) - .unwrap(), - system_instruction::transfer( - &context.payer.pubkey(), - &transient_stake_address, - current_minimum_delegation + stake_rent, - ), - stake::instruction::initialize( - &transient_stake_address, - &stake::state::Authorized { - staker: hijacker.pubkey(), - withdrawer: hijacker.pubkey(), - }, - &stake::state::Lockup::default(), - ), - instruction::update_stake_pool_balance( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.withdraw_authority, - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &spl_token::id(), - ), - instruction::cleanup_removed_validator_entries( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .err(); - assert!(error.is_none(), "{:?}", error); - - // activate transient stake account - delegate_stake_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &transient_stake_address, - &hijacker, - &validator_stake.vote.pubkey(), - ) - .await; - - // Remove works even though transient account is activating - let error = stake_pool_accounts - .remove_validator_from_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // warp forward to merge - slot += slots_per_epoch; - context.warp_to_slot(slot).unwrap(); - - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check if account was removed from the list of stake accounts - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - assert_eq!( - validator_list, - state::ValidatorList { - header: state::ValidatorListHeader { - account_type: state::AccountType::ValidatorList, - max_validators: stake_pool_accounts.max_validators, - }, - validators: vec![] - } - ); -} - -#[tokio::test] -async fn fail_not_updated_stake_pool() {} // TODO - -#[tokio::test] -async fn fail_with_uninitialized_validator_list_account() {} // TODO diff --git a/stake-pool/program/tests/withdraw.rs b/stake-pool/program/tests/withdraw.rs deleted file mode 100644 index 3420a0394b9..00000000000 --- a/stake-pool/program/tests/withdraw.rs +++ /dev/null @@ -1,840 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, - instruction::{AccountMeta, Instruction, InstructionError}, - pubkey::Pubkey, - sysvar, - }, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_stake_pool::{error::StakePoolError, id, instruction, state}, - spl_token::error::TokenError, - test_case::test_case, -}; - -#[test_case(spl_token::id(); "token")] -#[test_case(spl_token_2022::id(); "token-2022")] -#[tokio::test] -async fn success(token_program_id: Pubkey) { - _success(token_program_id, SuccessTestType::Success).await; -} - -#[tokio::test] -async fn success_with_closed_manager_fee_account() { - _success(spl_token::id(), SuccessTestType::UninitializedManagerFee).await; -} - -enum SuccessTestType { - Success, - UninitializedManagerFee, -} - -async fn _success(token_program_id: Pubkey, test_type: SuccessTestType) { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_withdraw, - ) = setup_for_withdraw(token_program_id, 0).await; - - // Save stake pool state before withdrawal - let stake_pool_before = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool_before = - try_from_slice_unchecked::(stake_pool_before.data.as_slice()).unwrap(); - - // Check user recipient stake account balance - let initial_stake_lamports = - get_account(&mut context.banks_client, &user_stake_recipient.pubkey()) - .await - .lamports; - - // Save validator stake account record before withdrawal - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let validator_stake_item_before = validator_list - .find(&validator_stake_account.vote.pubkey()) - .unwrap(); - - // Save user token balance - let user_token_balance_before = get_token_balance( - &mut context.banks_client, - &deposit_info.pool_account.pubkey(), - ) - .await; - - // Save pool fee token balance - let pool_fee_balance_before = get_token_balance( - &mut context.banks_client, - &stake_pool_accounts.pool_fee_account.pubkey(), - ) - .await; - - let destination_keypair = Keypair::new(); - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &destination_keypair, - &stake_pool_accounts.pool_mint.pubkey(), - &Keypair::new(), - &[], - ) - .await - .unwrap(); - - if let SuccessTestType::UninitializedManagerFee = test_type { - transfer_spl_tokens( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &destination_keypair.pubkey(), - &stake_pool_accounts.manager, - pool_fee_balance_before, - stake_pool_accounts.pool_decimals, - ) - .await; - // Check that the account cannot be frozen due to lack of - // freeze authority. - let transaction_error = freeze_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.manager, - ) - .await - .unwrap_err(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::Custom(0x10)); - } - _ => panic!("Wrong error occurs while try to withdraw with wrong stake program ID"), - } - close_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_fee_account.pubkey(), - &destination_keypair.pubkey(), - &stake_pool_accounts.manager, - ) - .await - .unwrap(); - } - - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - tokens_to_withdraw, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check pool stats - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - // first and only deposit, lamports:pool 1:1 - let tokens_withdrawal_fee = match test_type { - SuccessTestType::Success => { - stake_pool_accounts.calculate_withdrawal_fee(tokens_to_withdraw) - } - _ => 0, - }; - let tokens_burnt = tokens_to_withdraw - tokens_withdrawal_fee; - assert_eq!( - stake_pool.total_lamports, - stake_pool_before.total_lamports - tokens_burnt - ); - assert_eq!( - stake_pool.pool_token_supply, - stake_pool_before.pool_token_supply - tokens_burnt - ); - - if let SuccessTestType::Success = test_type { - // Check manager received withdrawal fee - let pool_fee_balance = get_token_balance( - &mut context.banks_client, - &stake_pool_accounts.pool_fee_account.pubkey(), - ) - .await; - assert_eq!( - pool_fee_balance, - pool_fee_balance_before + tokens_withdrawal_fee, - ); - } - - // Check validator stake list storage - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let validator_stake_item = validator_list - .find(&validator_stake_account.vote.pubkey()) - .unwrap(); - assert_eq!( - validator_stake_item.stake_lamports().unwrap(), - validator_stake_item_before.stake_lamports().unwrap() - tokens_burnt - ); - assert_eq!( - u64::from(validator_stake_item.active_stake_lamports), - validator_stake_item.stake_lamports().unwrap(), - ); - - // Check tokens used - let user_token_balance = get_token_balance( - &mut context.banks_client, - &deposit_info.pool_account.pubkey(), - ) - .await; - assert_eq!( - user_token_balance, - user_token_balance_before - tokens_to_withdraw - ); - - // Check validator stake account balance - let validator_stake_account = get_account( - &mut context.banks_client, - &validator_stake_account.stake_account, - ) - .await; - assert_eq!( - validator_stake_account.lamports, - u64::from(validator_stake_item.active_stake_lamports) - ); - - // Check user recipient stake account balance - let user_stake_recipient_account = - get_account(&mut context.banks_client, &user_stake_recipient.pubkey()).await; - assert_eq!( - user_stake_recipient_account.lamports, - initial_stake_lamports + tokens_burnt - ); -} - -#[tokio::test] -async fn fail_with_wrong_stake_program() { - let ( - context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let new_authority = Pubkey::new_unique(); - let wrong_stake_program = Pubkey::new_unique(); - - let accounts = vec![ - AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false), - AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false), - AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false), - AccountMeta::new(validator_stake_account.stake_account, false), - AccountMeta::new(user_stake_recipient.pubkey(), false), - AccountMeta::new_readonly(new_authority, false), - AccountMeta::new_readonly(user_transfer_authority.pubkey(), true), - AccountMeta::new(deposit_info.pool_account.pubkey(), false), - AccountMeta::new(stake_pool_accounts.pool_fee_account.pubkey(), false), - AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(wrong_stake_program, false), - ]; - let instruction = Instruction { - program_id: id(), - accounts, - data: borsh::to_vec(&instruction::StakePoolInstruction::WithdrawStake( - tokens_to_burn, - )) - .unwrap(), - }; - - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer, &user_transfer_authority], - context.last_blockhash, - ); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!("Wrong error occurs while try to withdraw with wrong stake program ID"), - } -} - -#[tokio::test] -async fn fail_with_wrong_withdraw_authority() { - let ( - mut context, - mut stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let new_authority = Pubkey::new_unique(); - stake_pool_accounts.withdraw_authority = Keypair::new().pubkey(); - - let transaction_error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - tokens_to_burn, - ) - .await - .unwrap(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::InvalidProgramAddress as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while try to withdraw with wrong withdraw authority"), - } -} - -#[tokio::test] -async fn fail_with_wrong_token_program_id() { - let ( - context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let new_authority = Pubkey::new_unique(); - let wrong_token_program = Keypair::new(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::withdraw_stake( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.validator_list.pubkey(), - &stake_pool_accounts.withdraw_authority, - &validator_stake_account.stake_account, - &user_stake_recipient.pubkey(), - &new_authority, - &user_transfer_authority.pubkey(), - &deposit_info.pool_account.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &wrong_token_program.pubkey(), - tokens_to_burn, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &user_transfer_authority], - context.last_blockhash, - ); - let transaction_error = context - .banks_client - .process_transaction(transaction) - .await - .err() - .unwrap() - .into(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::IncorrectProgramId); - } - _ => panic!("Wrong error occurs while try to withdraw with wrong token program ID"), - } -} - -#[tokio::test] -async fn fail_with_wrong_validator_list() { - let ( - mut context, - mut stake_pool_accounts, - validator_stake, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let new_authority = Pubkey::new_unique(); - stake_pool_accounts.validator_list = Keypair::new(); - - let transaction_error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake.stake_account, - &new_authority, - tokens_to_burn, - ) - .await - .unwrap(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = StakePoolError::InvalidValidatorStakeList as u32; - assert_eq!(error_index, program_error); - } - _ => panic!( - "Wrong error occurs while try to withdraw with wrong validator stake list account" - ), - } -} - -#[tokio::test] -async fn fail_with_unknown_validator() { - let ( - mut context, - stake_pool_accounts, - _, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let unknown_stake = create_unknown_validator_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.stake_pool.pubkey(), - 0, - ) - .await; - - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &unknown_stake.stake_account, - &new_authority, - tokens_to_withdraw, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::ValidatorNotFound as u32) - ) - ); -} - -#[tokio::test] -async fn fail_double_withdraw_to_the_same_account() { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - tokens_to_burn / 2, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let latest_blockhash = context.banks_client.get_latest_blockhash().await.unwrap(); - - // Delegate tokens for burning - delegate_tokens( - &mut context.banks_client, - &context.payer, - &latest_blockhash, - &stake_pool_accounts.token_program_id, - &deposit_info.pool_account.pubkey(), - &deposit_info.authority, - &user_transfer_authority.pubkey(), - tokens_to_burn / 2, - ) - .await; - - let transaction_error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &latest_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - tokens_to_burn / 2, - ) - .await - .unwrap(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError(_, error)) => { - assert_eq!(error, InstructionError::InvalidAccountData); - } - _ => panic!("Wrong error occurs while try to do double withdraw"), - } -} - -#[tokio::test] -async fn fail_without_token_approval() { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - revoke_tokens( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &deposit_info.pool_account.pubkey(), - &deposit_info.authority, - ) - .await; - - let new_authority = Pubkey::new_unique(); - let transaction_error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - tokens_to_burn, - ) - .await - .unwrap(); - - match transaction_error { - TransportError::TransactionError(TransactionError::InstructionError( - _, - InstructionError::Custom(error_index), - )) => { - let program_error = TokenError::OwnerMismatch as u32; - assert_eq!(error_index, program_error); - } - _ => panic!( - "Wrong error occurs while try to do withdraw without token delegation for burn before" - ), - } -} - -#[tokio::test] -async fn fail_with_not_enough_tokens() { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - // Empty validator stake account - let empty_stake_account = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &empty_stake_account.stake_account, - &new_authority, - tokens_to_burn, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32) - ), - ); - - // revoked delegation - revoke_tokens( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts.token_program_id, - &deposit_info.pool_account.pubkey(), - &deposit_info.authority, - ) - .await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - // generate a new authority each time to make each transaction unique - let new_authority = Pubkey::new_unique(); - let transaction_error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - tokens_to_burn, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - transaction_error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32), - ) - ); - - // Delegate few tokens for burning - delegate_tokens( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts.token_program_id, - &deposit_info.pool_account.pubkey(), - &deposit_info.authority, - &user_transfer_authority.pubkey(), - 1, - ) - .await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - // generate a new authority each time to make each transaction unique - let new_authority = Pubkey::new_unique(); - let transaction_error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - tokens_to_burn, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - transaction_error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InsufficientFunds as u32), - ) - ); -} - -#[test_case(spl_token::id(); "token")] -#[test_case(spl_token_2022::id(); "token-2022")] -#[tokio::test] -async fn success_with_slippage(token_program_id: Pubkey) { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_withdraw, - ) = setup_for_withdraw(token_program_id, 0).await; - - // Save user token balance - let user_token_balance_before = get_token_balance( - &mut context.banks_client, - &deposit_info.pool_account.pubkey(), - ) - .await; - - // first and only deposit, lamports:pool 1:1 - let tokens_withdrawal_fee = stake_pool_accounts.calculate_withdrawal_fee(tokens_to_withdraw); - let received_lamports = tokens_to_withdraw - tokens_withdrawal_fee; - - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake_with_slippage( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - tokens_to_withdraw, - received_lamports + 1, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::ExceededSlippage as u32) - ) - ); - - let error = stake_pool_accounts - .withdraw_stake_with_slippage( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - tokens_to_withdraw, - received_lamports, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check tokens used - let user_token_balance = get_token_balance( - &mut context.banks_client, - &deposit_info.pool_account.pubkey(), - ) - .await; - assert_eq!( - user_token_balance, - user_token_balance_before - tokens_to_withdraw - ); -} diff --git a/stake-pool/program/tests/withdraw_edge_cases.rs b/stake-pool/program/tests/withdraw_edge_cases.rs deleted file mode 100644 index 8abdc2f87a3..00000000000 --- a/stake-pool/program/tests/withdraw_edge_cases.rs +++ /dev/null @@ -1,877 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![allow(clippy::items_after_test_module)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - bincode::deserialize, - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, instruction::InstructionError, pubkey::Pubkey, stake, - }, - solana_program_test::*, - solana_sdk::{signature::Signer, transaction::TransactionError}, - spl_stake_pool::{error::StakePoolError, instruction, state}, - test_case::test_case, -}; - -#[tokio::test] -async fn fail_remove_validator() { - let ( - mut context, - stake_pool_accounts, - validator_stake, - deposit_info, - user_transfer_authority, - user_stake_recipient, - _, - ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; - - // decrease a little stake, not all - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - deposit_info.stake_lamports / 2, - validator_stake.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // warp forward to deactivation - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - - // update to merge deactivated stake into reserve - let error = stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Withdraw entire account, fail because some stake left - let validator_stake_account = - get_account(&mut context.banks_client, &validator_stake.stake_account).await; - let remaining_lamports = validator_stake_account.lamports; - let new_user_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake.stake_account, - &new_user_authority, - remaining_lamports, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32) - ) - ); -} - -#[test_case(0; "equal")] -#[test_case(5; "big")] -#[test_case(11; "bigger")] -#[test_case(29; "biggest")] -#[tokio::test] -async fn success_remove_validator(multiple: u64) { - let ( - mut context, - stake_pool_accounts, - validator_stake, - deposit_info, - user_transfer_authority, - user_stake_recipient, - _, - ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; - - // make pool tokens very valuable, so it isn't possible to exactly get down to - // the minimum - transfer( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.reserve_stake.pubkey(), - deposit_info.stake_lamports * multiple, // each pool token is worth more than one lamport - ) - .await; - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let stake_pool = stake_pool_accounts - .get_stake_pool(&mut context.banks_client) - .await; - let lamports_per_pool_token = stake_pool.get_lamports_per_pool_token().unwrap(); - - // decrease all of stake except for lamports_per_pool_token lamports, must be - // withdrawable - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - deposit_info.stake_lamports + stake_rent - lamports_per_pool_token, - validator_stake.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // warp forward to deactivation - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - // update to merge deactivated stake into reserve - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let validator_stake_account = - get_account(&mut context.banks_client, &validator_stake.stake_account).await; - let remaining_lamports = validator_stake_account.lamports; - let stake_minimum_delegation = - stake_get_minimum_delegation(&mut context.banks_client, &context.payer, &last_blockhash) - .await; - // make sure it's actually more than the minimum - assert!(remaining_lamports > stake_rent + stake_minimum_delegation); - - // round up to force one more pool token if needed - let pool_tokens_post_fee = - (remaining_lamports * stake_pool.pool_token_supply + stake_pool.total_lamports - 1) - / stake_pool.total_lamports; - let new_user_authority = Pubkey::new_unique(); - let pool_tokens = stake_pool_accounts.calculate_inverse_withdrawal_fee(pool_tokens_post_fee); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake.stake_account, - &new_user_authority, - pool_tokens, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check validator stake account gone - let validator_stake_account = context - .banks_client - .get_account(validator_stake.stake_account) - .await - .unwrap(); - assert!(validator_stake_account.is_none()); - - // Check user recipient stake account balance - let user_stake_recipient_account = - get_account(&mut context.banks_client, &user_stake_recipient.pubkey()).await; - assert_eq!( - user_stake_recipient_account.lamports, - remaining_lamports + stake_rent - ); - - // Check that cleanup happens correctly - stake_pool_accounts - .cleanup_removed_validator_entries( - &mut context.banks_client, - &context.payer, - &last_blockhash, - ) - .await; - - let validator_list = get_account( - &mut context.banks_client, - &stake_pool_accounts.validator_list.pubkey(), - ) - .await; - let validator_list = - try_from_slice_unchecked::(validator_list.data.as_slice()).unwrap(); - let validator_stake_item = validator_list.find(&validator_stake.vote.pubkey()); - assert!(validator_stake_item.is_none()); -} - -#[tokio::test] -async fn fail_with_reserve() { - let ( - mut context, - stake_pool_accounts, - validator_stake, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; - - // decrease a little stake, not all - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - deposit_info.stake_lamports / 2, - validator_stake.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // warp forward to deactivation - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - - // update to merge deactivated stake into reserve - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - // Withdraw directly from reserve, fail because some stake left - let new_user_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &new_user_authority, - tokens_to_burn, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32) - ) - ); -} - -#[tokio::test] -async fn success_with_reserve() { - let ( - mut context, - stake_pool_accounts, - validator_stake, - deposit_info, - user_transfer_authority, - user_stake_recipient, - _, - ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - // decrease all of stake - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.stake_account, - &validator_stake.transient_stake_account, - deposit_info.stake_lamports + stake_rent, - validator_stake.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // warp forward to deactivation - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - - // update to merge deactivated stake into reserve - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - false, - ) - .await; - - // now it works - let new_user_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &stake_pool_accounts.reserve_stake.pubkey(), - &new_user_authority, - deposit_info.pool_tokens, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // first and only deposit, lamports:pool 1:1 - let stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let stake_pool = - try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); - // the entire deposit is actually stake since it isn't activated, so only - // the stake deposit fee is charged - let deposit_fee = stake_pool - .calc_pool_tokens_stake_deposit_fee(stake_rent + deposit_info.stake_lamports) - .unwrap(); - assert_eq!( - deposit_info.stake_lamports + stake_rent - deposit_fee, - deposit_info.pool_tokens, - "stake {} rent {} deposit fee {} pool tokens {}", - deposit_info.stake_lamports, - stake_rent, - deposit_fee, - deposit_info.pool_tokens - ); - - let withdrawal_fee = stake_pool_accounts.calculate_withdrawal_fee(deposit_info.pool_tokens); - - // Check tokens used - let user_token_balance = get_token_balance( - &mut context.banks_client, - &deposit_info.pool_account.pubkey(), - ) - .await; - assert_eq!(user_token_balance, 0); - - // Check reserve stake account balance - let reserve_stake_account = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await; - let stake_state = - deserialize::(&reserve_stake_account.data).unwrap(); - let meta = stake_state.meta().unwrap(); - assert_eq!( - meta.rent_exempt_reserve + withdrawal_fee + deposit_fee + stake_rent, - reserve_stake_account.lamports - ); - - // Check user recipient stake account balance - let user_stake_recipient_account = - get_account(&mut context.banks_client, &user_stake_recipient.pubkey()).await; - assert_eq!( - user_stake_recipient_account.lamports, - deposit_info.stake_lamports + stake_rent * 2 - withdrawal_fee - deposit_fee - ); -} - -#[tokio::test] -async fn success_with_empty_preferred_withdraw() { - let ( - mut context, - stake_pool_accounts, - validator_stake, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let preferred_validator = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - instruction::PreferredValidatorType::Withdraw, - Some(preferred_validator.vote.pubkey()), - ) - .await; - - // preferred is empty, withdrawing from non-preferred works - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake.stake_account, - &new_authority, - tokens_to_burn / 2, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -#[tokio::test] -async fn success_and_fail_with_preferred_withdraw() { - let ( - mut context, - stake_pool_accounts, - validator_stake, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - let preferred_validator = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &last_blockhash, - instruction::PreferredValidatorType::Withdraw, - Some(preferred_validator.vote.pubkey()), - ) - .await; - - let _preferred_deposit = simple_deposit_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - &preferred_validator, - TEST_STAKE_AMOUNT, - ) - .await - .unwrap(); - - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake.stake_account, - &new_authority, - tokens_to_burn / 2 + 1, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::IncorrectWithdrawVoteAddress as u32) - ) - ); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - // success from preferred - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &preferred_validator.stake_account, - &new_authority, - tokens_to_burn / 2, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -#[tokio::test] -async fn fail_withdraw_from_transient() { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - // add a preferred withdraw validator, keep it empty, to be sure that this works - let preferred_validator = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &last_blockhash, - instruction::PreferredValidatorType::Withdraw, - Some(preferred_validator.vote.pubkey()), - ) - .await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - // decrease to minimum stake + 2 lamports - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &validator_stake_account.stake_account, - &validator_stake_account.transient_stake_account, - deposit_info.stake_lamports + stake_rent - 2, - validator_stake_account.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // fail withdrawing from transient, still a lamport in the validator stake - // account - let new_user_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.transient_stake_account, - &new_user_authority, - tokens_to_withdraw, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::InvalidStakeAccountAddress as u32) - ) - ); -} - -#[tokio::test] -async fn success_withdraw_from_transient() { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - // add a preferred withdraw validator, keep it empty, to be sure that this works - let preferred_validator = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - instruction::PreferredValidatorType::Withdraw, - Some(preferred_validator.vote.pubkey()), - ) - .await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - // decrease all of stake - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &validator_stake_account.stake_account, - &validator_stake_account.transient_stake_account, - deposit_info.stake_lamports + stake_rent, - validator_stake_account.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // nothing left in the validator stake account (or any others), so withdrawing - // from the transient account is ok! - let new_user_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake_account.transient_stake_account, - &new_user_authority, - tokens_to_withdraw / 2, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -#[tokio::test] -async fn success_with_small_preferred_withdraw() { - let ( - mut context, - stake_pool_accounts, - validator_stake, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_burn, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - // make pool tokens very valuable, so it isn't possible to exactly get down to - // the minimum - transfer( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts.reserve_stake.pubkey(), - deposit_info.stake_lamports * 5, // each pool token is worth more than one lamport - ) - .await; - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - let preferred_validator = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - stake_pool_accounts - .set_preferred_validator( - &mut context.banks_client, - &context.payer, - &last_blockhash, - instruction::PreferredValidatorType::Withdraw, - Some(preferred_validator.vote.pubkey()), - ) - .await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - // add a tiny bit of stake, less than lamports per pool token to preferred - // validator - let rent = context.banks_client.get_rent().await.unwrap(); - let rent_exempt = rent.minimum_balance(std::mem::size_of::()); - let stake_minimum_delegation = - stake_get_minimum_delegation(&mut context.banks_client, &context.payer, &last_blockhash) - .await; - let minimum_lamports = stake_minimum_delegation + rent_exempt; - - simple_deposit_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - &preferred_validator, - stake_minimum_delegation + 1, // stake_rent gets deposited too - ) - .await - .unwrap(); - - // decrease all stake except for 1 lamport - let error = stake_pool_accounts - .decrease_validator_stake_either( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &preferred_validator.stake_account, - &preferred_validator.transient_stake_account, - minimum_lamports, - preferred_validator.transient_stake_seed, - DecreaseInstruction::Reserve, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // warp forward to deactivation - let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; - context.warp_to_slot(first_normal_slot + 1).unwrap(); - - // update to merge deactivated stake into reserve - stake_pool_accounts - .update_all( - &mut context.banks_client, - &context.payer, - &last_blockhash, - false, - ) - .await; - - // withdraw from preferred fails - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &preferred_validator.stake_account, - &new_authority, - 1, - ) - .await; - assert!(error.is_some()); - - // preferred is empty, withdrawing from non-preferred works - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &deposit_info.pool_account.pubkey(), - &validator_stake.stake_account, - &new_authority, - tokens_to_burn / 6, - ) - .await; - assert!(error.is_none(), "{:?}", error); -} diff --git a/stake-pool/program/tests/withdraw_sol.rs b/stake-pool/program/tests/withdraw_sol.rs deleted file mode 100644 index 623aa82cf0a..00000000000 --- a/stake-pool/program/tests/withdraw_sol.rs +++ /dev/null @@ -1,394 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - helpers::*, - solana_program::{ - borsh1::try_from_slice_unchecked, instruction::InstructionError, pubkey::Pubkey, stake, - }, - solana_program_test::*, - solana_sdk::{ - signature::{Keypair, Signer}, - transaction::{Transaction, TransactionError}, - }, - spl_stake_pool::{ - error::StakePoolError, - id, - instruction::{self, FundingType}, - state, MINIMUM_RESERVE_LAMPORTS, - }, - test_case::test_case, -}; - -async fn setup( - token_program_id: Pubkey, -) -> (ProgramTestContext, StakePoolAccounts, Keypair, Pubkey, u64) { - let mut context = program_test().start_with_context().await; - - let stake_pool_accounts = StakePoolAccounts::new_with_token_program(token_program_id); - stake_pool_accounts - .initialize_stake_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, - ) - .await - .unwrap(); - - let user = Keypair::new(); - - // make pool token account for user - let pool_token_account = Keypair::new(); - create_token_account( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts.token_program_id, - &pool_token_account, - &stake_pool_accounts.pool_mint.pubkey(), - &user, - &[], - ) - .await - .unwrap(); - - let error = stake_pool_accounts - .deposit_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &pool_token_account.pubkey(), - TEST_STAKE_AMOUNT, - None, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - let tokens_issued = - get_token_balance(&mut context.banks_client, &pool_token_account.pubkey()).await; - - ( - context, - stake_pool_accounts, - user, - pool_token_account.pubkey(), - tokens_issued, - ) -} - -#[test_case(spl_token::id(); "token")] -#[test_case(spl_token_2022::id(); "token-2022")] -#[tokio::test] -async fn success(token_program_id: Pubkey) { - let (mut context, stake_pool_accounts, user, pool_token_account, pool_tokens) = - setup(token_program_id).await; - - // Save stake pool state before withdrawing - let pre_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let pre_stake_pool = - try_from_slice_unchecked::(pre_stake_pool.data.as_slice()).unwrap(); - - // Save reserve state before withdrawing - let pre_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - - let error = stake_pool_accounts - .withdraw_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user, - &pool_token_account, - pool_tokens, - None, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Stake pool should add its balance to the pool balance - let post_stake_pool = get_account( - &mut context.banks_client, - &stake_pool_accounts.stake_pool.pubkey(), - ) - .await; - let post_stake_pool = - try_from_slice_unchecked::(post_stake_pool.data.as_slice()).unwrap(); - let amount_withdrawn_minus_fee = - pool_tokens - stake_pool_accounts.calculate_withdrawal_fee(pool_tokens); - assert_eq!( - post_stake_pool.total_lamports, - pre_stake_pool.total_lamports - amount_withdrawn_minus_fee - ); - assert_eq!( - post_stake_pool.pool_token_supply, - pre_stake_pool.pool_token_supply - amount_withdrawn_minus_fee - ); - - // Check minted tokens - let user_token_balance = - get_token_balance(&mut context.banks_client, &pool_token_account).await; - assert_eq!(user_token_balance, 0); - - // Check reserve - let post_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - assert_eq!( - post_reserve_lamports, - pre_reserve_lamports - amount_withdrawn_minus_fee - ); -} - -#[tokio::test] -async fn fail_with_wrong_withdraw_authority() { - let (mut context, mut stake_pool_accounts, user, pool_token_account, pool_tokens) = - setup(spl_token::id()).await; - - stake_pool_accounts.withdraw_authority = Pubkey::new_unique(); - - let error = stake_pool_accounts - .withdraw_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user, - &pool_token_account, - pool_tokens, - None, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::InvalidProgramAddress as u32) - ) - ); -} - -#[tokio::test] -async fn fail_overdraw_reserve() { - let (mut context, stake_pool_accounts, user, pool_token_account, _) = - setup(spl_token::id()).await; - - // add a validator and increase stake to drain the reserve - let validator_stake = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let error = stake_pool_accounts - .increase_validator_stake( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &validator_stake.transient_stake_account, - &validator_stake.stake_account, - &validator_stake.vote.pubkey(), - TEST_STAKE_AMOUNT - stake_rent, - validator_stake.transient_stake_seed, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // try to withdraw one lamport after fees, will overdraw - let error = stake_pool_accounts - .withdraw_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user, - &pool_token_account, - 2, - None, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::SolWithdrawalTooLarge as u32) - ) - ); -} - -#[tokio::test] -async fn success_with_sol_withdraw_authority() { - let (mut context, stake_pool_accounts, user, pool_token_account, pool_tokens) = - setup(spl_token::id()).await; - let sol_withdraw_authority = Keypair::new(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - Some(&sol_withdraw_authority.pubkey()), - FundingType::SolWithdraw, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let error = stake_pool_accounts - .withdraw_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user, - &pool_token_account, - pool_tokens, - Some(&sol_withdraw_authority), - ) - .await; - assert!(error.is_none(), "{:?}", error); -} - -#[tokio::test] -async fn fail_without_sol_withdraw_authority_signature() { - let (mut context, stake_pool_accounts, user, pool_token_account, pool_tokens) = - setup(spl_token::id()).await; - let sol_withdraw_authority = Keypair::new(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::set_funding_authority( - &id(), - &stake_pool_accounts.stake_pool.pubkey(), - &stake_pool_accounts.manager.pubkey(), - Some(&sol_withdraw_authority.pubkey()), - FundingType::SolWithdraw, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let wrong_withdrawer = Keypair::new(); - let error = stake_pool_accounts - .withdraw_sol( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user, - &pool_token_account, - pool_tokens, - Some(&wrong_withdrawer), - ) - .await - .unwrap() - .unwrap(); - - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::InvalidSolWithdrawAuthority as u32) - ) - ); -} - -#[test_case(spl_token::id(); "token")] -#[test_case(spl_token_2022::id(); "token-2022")] -#[tokio::test] -async fn success_with_slippage(token_program_id: Pubkey) { - let (mut context, stake_pool_accounts, user, pool_token_account, pool_tokens) = - setup(token_program_id).await; - - let amount_received = pool_tokens - stake_pool_accounts.calculate_withdrawal_fee(pool_tokens); - - // Save reserve state before withdrawing - let pre_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - - let error = stake_pool_accounts - .withdraw_sol_with_slippage( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user, - &pool_token_account, - pool_tokens, - amount_received + 1, - ) - .await - .unwrap() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::ExceededSlippage as u32) - ) - ); - - let error = stake_pool_accounts - .withdraw_sol_with_slippage( - &mut context.banks_client, - &context.payer, - &context.last_blockhash, - &user, - &pool_token_account, - pool_tokens, - amount_received, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check burned tokens - let user_token_balance = - get_token_balance(&mut context.banks_client, &pool_token_account).await; - assert_eq!(user_token_balance, 0); - - // Check reserve - let post_reserve_lamports = get_account( - &mut context.banks_client, - &stake_pool_accounts.reserve_stake.pubkey(), - ) - .await - .lamports; - assert_eq!( - post_reserve_lamports, - pre_reserve_lamports - amount_received - ); -} diff --git a/stake-pool/program/tests/withdraw_with_fee.rs b/stake-pool/program/tests/withdraw_with_fee.rs deleted file mode 100644 index 7a8e000c1cb..00000000000 --- a/stake-pool/program/tests/withdraw_with_fee.rs +++ /dev/null @@ -1,223 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![cfg(feature = "test-sbf")] - -mod helpers; - -use { - bincode::deserialize, - helpers::*, - solana_program::{pubkey::Pubkey, stake}, - solana_program_test::*, - solana_sdk::signature::{Keypair, Signer}, - spl_stake_pool::minimum_stake_lamports, -}; - -#[tokio::test] -async fn success_withdraw_all_fee_tokens() { - let ( - mut context, - stake_pool_accounts, - validator_stake_account, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - // move tokens to fee account - transfer_spl_tokens( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts.token_program_id, - &deposit_info.pool_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.pool_fee_account.pubkey(), - &user_transfer_authority, - tokens_to_withdraw / 2, - stake_pool_accounts.pool_decimals, - ) - .await; - - let fee_tokens = get_token_balance( - &mut context.banks_client, - &stake_pool_accounts.pool_fee_account.pubkey(), - ) - .await; - - let user_transfer_authority = Keypair::new(); - delegate_tokens( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts.token_program_id, - &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.manager, - &user_transfer_authority.pubkey(), - fee_tokens, - ) - .await; - - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &stake_pool_accounts.pool_fee_account.pubkey(), - &validator_stake_account.stake_account, - &new_authority, - fee_tokens, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check balance is 0 - let fee_tokens = get_token_balance( - &mut context.banks_client, - &stake_pool_accounts.pool_fee_account.pubkey(), - ) - .await; - assert_eq!(fee_tokens, 0); -} - -#[tokio::test] -async fn success_empty_out_stake_with_fee() { - let ( - mut context, - stake_pool_accounts, - _, - deposit_info, - user_transfer_authority, - user_stake_recipient, - tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id(), 0).await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&context.last_blockhash) - .await - .unwrap(); - - // add another validator and deposit into it - let other_validator_stake_account = simple_add_validator_to_pool( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - None, - ) - .await; - - let other_deposit_info = simple_deposit_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts, - &other_validator_stake_account, - TEST_STAKE_AMOUNT, - ) - .await - .unwrap(); - - // move tokens to new account - transfer_spl_tokens( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts.token_program_id, - &deposit_info.pool_account.pubkey(), - &stake_pool_accounts.pool_mint.pubkey(), - &other_deposit_info.pool_account.pubkey(), - &user_transfer_authority, - tokens_to_withdraw, - stake_pool_accounts.pool_decimals, - ) - .await; - - let user_tokens = get_token_balance( - &mut context.banks_client, - &other_deposit_info.pool_account.pubkey(), - ) - .await; - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - let user_transfer_authority = Keypair::new(); - delegate_tokens( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &stake_pool_accounts.token_program_id, - &other_deposit_info.pool_account.pubkey(), - &other_deposit_info.authority, - &user_transfer_authority.pubkey(), - user_tokens, - ) - .await; - - // calculate exactly how much to withdraw, given the fee, to get the account - // down to 0, using an inverse fee calculation - let validator_stake_account = get_account( - &mut context.banks_client, - &other_validator_stake_account.stake_account, - ) - .await; - let stake_state = - deserialize::(&validator_stake_account.data).unwrap(); - let meta = stake_state.meta().unwrap(); - let stake_minimum_delegation = - stake_get_minimum_delegation(&mut context.banks_client, &context.payer, &last_blockhash) - .await; - let lamports_to_withdraw = - validator_stake_account.lamports - minimum_stake_lamports(&meta, stake_minimum_delegation); - let pool_tokens_to_withdraw = - stake_pool_accounts.calculate_inverse_withdrawal_fee(lamports_to_withdraw); - - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - let new_authority = Pubkey::new_unique(); - let error = stake_pool_accounts - .withdraw_stake( - &mut context.banks_client, - &context.payer, - &last_blockhash, - &user_stake_recipient.pubkey(), - &user_transfer_authority, - &other_deposit_info.pool_account.pubkey(), - &other_validator_stake_account.stake_account, - &new_authority, - pool_tokens_to_withdraw, - ) - .await; - assert!(error.is_none(), "{:?}", error); - - // Check balance of validator stake account is MINIMUM + rent-exemption - let validator_stake_account = get_account( - &mut context.banks_client, - &other_validator_stake_account.stake_account, - ) - .await; - let stake_state = - deserialize::(&validator_stake_account.data).unwrap(); - let meta = stake_state.meta().unwrap(); - assert_eq!( - validator_stake_account.lamports, - minimum_stake_lamports(&meta, stake_minimum_delegation) - ); -} diff --git a/stake-pool/py/.flake8 b/stake-pool/py/.flake8 deleted file mode 100644 index aa079ec57f8..00000000000 --- a/stake-pool/py/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length=120 diff --git a/stake-pool/py/.gitignore b/stake-pool/py/.gitignore deleted file mode 100644 index ad0e7548ff2..00000000000 --- a/stake-pool/py/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# python cache files -*__pycache__* -.pytest_cache -.mypy_cache - -# venv -venv/ diff --git a/stake-pool/py/LICENSE b/stake-pool/py/LICENSE deleted file mode 100644 index 7e71f6e9a88..00000000000 --- a/stake-pool/py/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2022 Solana Foundation. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/stake-pool/py/README.md b/stake-pool/py/README.md deleted file mode 100644 index cda7beea17b..00000000000 --- a/stake-pool/py/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Stake-Pool Python Bindings - -Preliminary Python bindings to interact with the stake pool program, enabling -simple stake delegation bots. - -## To do - -* More reference bot implementations -* Add bindings for all stake pool instructions, see `TODO`s in `stake_pool/instructions.py` -* Finish bindings for vote and stake program -* Upstream vote and stake program bindings to https://github.com/michaelhly/solana-py - -## Development - -### Environment Setup - -1. Ensure that Python 3 is installed with `venv`: https://www.python.org/downloads/ -2. (Optional, but highly recommended) Setup and activate a virtual environment: - -``` -$ python3 -m venv venv -$ source venv/bin/activate -``` - -3. Install build and dev requirements - -``` -$ pip install -r requirements.txt -$ pip install -r optional-requirements.txt -``` - -4. Install the Solana tool suite: https://docs.solana.com/cli/install-solana-cli-tools - -### Test - -Testing through `pytest`: - -``` -$ python3 -m pytest -``` - -Note: the tests all run against a `solana-test-validator` with short epochs of 64 -slots (25.6 seconds exactly). Some tests wait for epoch changes, so they take -time, roughly 90 seconds total at the time of this writing. - -### Formatting - -``` -$ flake8 bot spl_token stake stake_pool system tests vote -``` - -### Type Checker - -``` -$ mypy bot stake stake_pool tests vote spl_token system -``` - -## Delegation Bots - -The `./bot` directory contains sample stake pool delegation bot implementations: - -* `rebalance`: simple bot to make the amount delegated to each validator -uniform, while also maintaining some SOL in the reserve if desired. Can be run -with the stake pool address, staker keypair, and SOL to leave in the reserve: - -``` -$ python3 bot/rebalance.py Zg5YBPAk8RqBR9kaLLSoN5C8Uv7nErBz1WC63HTsCPR staker.json 10.5 -``` diff --git a/stake-pool/py/bot/__init__.py b/stake-pool/py/bot/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/stake-pool/py/bot/rebalance.py b/stake-pool/py/bot/rebalance.py deleted file mode 100644 index e12a6a9a678..00000000000 --- a/stake-pool/py/bot/rebalance.py +++ /dev/null @@ -1,132 +0,0 @@ -import argparse -import asyncio -import json - -from solders.keypair import Keypair -from solders.pubkey import Pubkey -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed - -from stake.constants import STAKE_LEN, LAMPORTS_PER_SOL -from stake_pool.actions import decrease_validator_stake, increase_validator_stake, update_stake_pool -from stake_pool.constants import MINIMUM_ACTIVE_STAKE -from stake_pool.state import StakePool, ValidatorList - - -async def get_client(endpoint: str) -> AsyncClient: - print(f'Connecting to network at {endpoint}') - async_client = AsyncClient(endpoint=endpoint, commitment=Confirmed) - total_attempts = 10 - current_attempt = 0 - while not await async_client.is_connected(): - if current_attempt == total_attempts: - raise Exception("Could not connect to test validator") - else: - current_attempt += 1 - await asyncio.sleep(1) - return async_client - - -async def rebalance(endpoint: str, stake_pool_address: Pubkey, staker: Keypair, retained_reserve_amount: float): - async_client = await get_client(endpoint) - - epoch_resp = await async_client.get_epoch_info(commitment=Confirmed) - epoch = epoch_resp.value.epoch - resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - print(f'Stake pool last update epoch {stake_pool.last_update_epoch}, current epoch {epoch}') - if stake_pool.last_update_epoch != epoch: - print('Updating stake pool') - await update_stake_pool(async_client, staker, stake_pool_address) - resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - rent_resp = await async_client.get_minimum_balance_for_rent_exemption(STAKE_LEN) - stake_rent_exemption = rent_resp.value - retained_reserve_lamports = int(retained_reserve_amount * LAMPORTS_PER_SOL) - - val_resp = await async_client.get_account_info(stake_pool.validator_list, commitment=Confirmed) - data = val_resp.value.data if val_resp.value else bytes() - validator_list = ValidatorList.decode(data) - - print('Stake pool stats:') - print(f'* {stake_pool.total_lamports} total lamports') - num_validators = len(validator_list.validators) - print(f'* {num_validators} validators') - print(f'* Retaining {retained_reserve_lamports} lamports in the reserve') - lamports_per_validator = (stake_pool.total_lamports - retained_reserve_lamports) // num_validators - num_increases = sum([ - 1 for validator in validator_list.validators - if validator.transient_stake_lamports == 0 and validator.active_stake_lamports < lamports_per_validator - ]) - total_usable_lamports = stake_pool.total_lamports - retained_reserve_lamports - num_increases * stake_rent_exemption - lamports_per_validator = total_usable_lamports // num_validators - print(f'* {lamports_per_validator} lamports desired per validator') - - futures = [] - for validator in validator_list.validators: - if validator.transient_stake_lamports != 0: - print(f'Skipping {validator.vote_account_address}: {validator.transient_stake_lamports} transient lamports') - else: - if validator.active_stake_lamports > lamports_per_validator: - lamports_to_decrease = validator.active_stake_lamports - lamports_per_validator - if lamports_to_decrease <= stake_rent_exemption: - print(f'Skipping decrease on {validator.vote_account_address}, \ -currently at {validator.active_stake_lamports} lamports, \ -decrease of {lamports_to_decrease} below the rent exmption') - else: - futures.append(decrease_validator_stake( - async_client, staker, staker, stake_pool_address, - validator.vote_account_address, lamports_to_decrease - )) - elif validator.active_stake_lamports < lamports_per_validator: - lamports_to_increase = lamports_per_validator - validator.active_stake_lamports - if lamports_to_increase < MINIMUM_ACTIVE_STAKE: - print(f'Skipping increase on {validator.vote_account_address}, \ -currently at {validator.active_stake_lamports} lamports, \ -increase of {lamports_to_increase} less than the minimum of {MINIMUM_ACTIVE_STAKE}') - else: - futures.append(increase_validator_stake( - async_client, staker, staker, stake_pool_address, - validator.vote_account_address, lamports_to_increase - )) - else: - print(f'{validator.vote_account_address}: already at {lamports_per_validator}') - - print('Executing strategy') - await asyncio.gather(*futures) - print('Done') - await async_client.close() - - -def keypair_from_file(keyfile_name: str) -> Keypair: - with open(keyfile_name, 'r') as keyfile: - data = keyfile.read() - int_list = json.loads(data) - bytes_list = [value.to_bytes(1, 'little') for value in int_list] - return Keypair.from_seed(b''.join(bytes_list)) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Rebalance stake evenly between all the validators in a stake pool.') - parser.add_argument('stake_pool', metavar='STAKE_POOL_ADDRESS', type=str, - help='Stake pool to rebalance, given by a public key in base-58,\ - e.g. Zg5YBPAk8RqBR9kaLLSoN5C8Uv7nErBz1WC63HTsCPR') - parser.add_argument('staker', metavar='STAKER_KEYPAIR', type=str, - help='Staker for the stake pool, given by a keypair file, e.g. staker.json') - parser.add_argument('reserve_amount', metavar='RESERVE_AMOUNT', type=float, - help='Amount of SOL to keep in the reserve, e.g. 10.5') - parser.add_argument('--endpoint', metavar='ENDPOINT_URL', type=str, - default='https://api.mainnet-beta.solana.com', - help='RPC endpoint to use, e.g. https://api.mainnet-beta.solana.com') - - args = parser.parse_args() - stake_pool = Pubkey(args.stake_pool) - staker = keypair_from_file(args.staker) - print(f'Rebalancing stake pool {stake_pool}') - print(f'Staker public key: {staker.pubkey()}') - print(f'Amount to leave in the reserve: {args.reserve_amount} SOL') - asyncio.run(rebalance(args.endpoint, stake_pool, staker, args.reserve_amount)) diff --git a/stake-pool/py/optional-requirements.txt b/stake-pool/py/optional-requirements.txt deleted file mode 100644 index 5f08be95a62..00000000000 --- a/stake-pool/py/optional-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -flake8==7.1.0 -iniconfig==2.0.0 -mccabe==0.7.0 -mypy==1.11.0 -mypy-extensions==1.0.0 -packaging==24.1 -pluggy==1.5.0 -pycodestyle==2.12.0 -pyflakes==3.2.0 -pytest==8.3.1 -pytest-asyncio==0.23.8 diff --git a/stake-pool/py/requirements.txt b/stake-pool/py/requirements.txt deleted file mode 100644 index 9b0f39a3f60..00000000000 --- a/stake-pool/py/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -anyio==4.4.0 -certifi==2024.7.4 -construct==2.10.68 -construct-typing==0.5.6 -h11==0.14.0 -httpcore==1.0.5 -httpx==0.27.0 -idna==3.7 -jsonalias==0.1.1 -sniffio==1.3.1 -solana==0.34.2 -solders==0.21.0 -typing_extensions==4.12.2 -websockets==11.0.3 diff --git a/stake-pool/py/spl_token/__init__.py b/stake-pool/py/spl_token/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/stake-pool/py/spl_token/actions.py b/stake-pool/py/spl_token/actions.py deleted file mode 100644 index 99a03fd0d9e..00000000000 --- a/stake-pool/py/spl_token/actions.py +++ /dev/null @@ -1,62 +0,0 @@ -from solders.pubkey import Pubkey -from solders.keypair import Keypair -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed -from solana.rpc.types import TxOpts -from solana.transaction import Transaction -import solders.system_program as sys - -from spl.token.constants import TOKEN_PROGRAM_ID -from spl.token.async_client import AsyncToken -from spl.token._layouts import MINT_LAYOUT -import spl.token.instructions as spl_token - - -OPTS = TxOpts(skip_confirmation=False, preflight_commitment=Confirmed) - - -async def create_associated_token_account( - client: AsyncClient, - payer: Keypair, - owner: Pubkey, - mint: Pubkey -) -> Pubkey: - txn = Transaction(fee_payer=payer.pubkey()) - create_txn = spl_token.create_associated_token_account( - payer=payer.pubkey(), owner=owner, mint=mint - ) - txn.add(create_txn) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, payer, recent_blockhash=recent_blockhash, opts=OPTS) - return create_txn.accounts[1].pubkey - - -async def create_mint(client: AsyncClient, payer: Keypair, mint: Keypair, mint_authority: Pubkey): - mint_balance = await AsyncToken.get_min_balance_rent_for_exempt_for_mint(client) - print(f"Creating pool token mint {mint.pubkey()}") - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( - sys.create_account( - sys.CreateAccountParams( - from_pubkey=payer.pubkey(), - to_pubkey=mint.pubkey(), - lamports=mint_balance, - space=MINT_LAYOUT.sizeof(), - owner=TOKEN_PROGRAM_ID, - ) - ) - ) - txn.add( - spl_token.initialize_mint( - spl_token.InitializeMintParams( - program_id=TOKEN_PROGRAM_ID, - mint=mint.pubkey(), - decimals=9, - mint_authority=mint_authority, - freeze_authority=None, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction( - txn, payer, mint, recent_blockhash=recent_blockhash, opts=OPTS) diff --git a/stake-pool/py/stake/__init__.py b/stake-pool/py/stake/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/stake-pool/py/stake/actions.py b/stake-pool/py/stake/actions.py deleted file mode 100644 index 1147a56db03..00000000000 --- a/stake-pool/py/stake/actions.py +++ /dev/null @@ -1,91 +0,0 @@ -from solders.pubkey import Pubkey -from solders.keypair import Keypair -import solders.system_program as sys -from solana.constants import SYSTEM_PROGRAM_ID -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed -from solana.rpc.types import TxOpts -from solders.sysvar import CLOCK, STAKE_HISTORY -from solana.transaction import Transaction - -from stake.constants import STAKE_LEN, STAKE_PROGRAM_ID, SYSVAR_STAKE_CONFIG_ID -from stake.state import Authorized, Lockup, StakeAuthorize -import stake.instructions as st - - -OPTS = TxOpts(skip_confirmation=False, preflight_commitment=Confirmed) - - -async def create_stake(client: AsyncClient, payer: Keypair, stake: Keypair, authority: Pubkey, lamports: int): - print(f"Creating stake {stake.pubkey()}") - resp = await client.get_minimum_balance_for_rent_exemption(STAKE_LEN) - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( - sys.create_account( - sys.CreateAccountParams( - from_pubkey=payer.pubkey(), - to_pubkey=stake.pubkey(), - lamports=resp.value + lamports, - space=STAKE_LEN, - owner=STAKE_PROGRAM_ID, - ) - ) - ) - txn.add( - st.initialize( - st.InitializeParams( - stake=stake.pubkey(), - authorized=Authorized( - staker=authority, - withdrawer=authority, - ), - lockup=Lockup( - unix_timestamp=0, - epoch=0, - custodian=SYSTEM_PROGRAM_ID, - ) - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, payer, stake, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def delegate_stake(client: AsyncClient, payer: Keypair, staker: Keypair, stake: Pubkey, vote: Pubkey): - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( - st.delegate_stake( - st.DelegateStakeParams( - stake=stake, - vote=vote, - clock_sysvar=CLOCK, - stake_history_sysvar=STAKE_HISTORY, - stake_config_id=SYSVAR_STAKE_CONFIG_ID, - staker=staker.pubkey(), - ) - ) - ) - signers = [payer, staker] if payer.pubkey() != staker.pubkey() else [payer] - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, *signers, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def authorize( - client: AsyncClient, payer: Keypair, authority: Keypair, stake: Pubkey, - new_authority: Pubkey, stake_authorize: StakeAuthorize -): - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( - st.authorize( - st.AuthorizeParams( - stake=stake, - clock_sysvar=CLOCK, - authority=authority.pubkey(), - new_authority=new_authority, - stake_authorize=stake_authorize, - ) - ) - ) - signers = [payer, authority] if payer.pubkey() != authority.pubkey() else [payer] - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, *signers, recent_blockhash=recent_blockhash, opts=OPTS) diff --git a/stake-pool/py/stake/constants.py b/stake-pool/py/stake/constants.py deleted file mode 100644 index da1c644c75d..00000000000 --- a/stake-pool/py/stake/constants.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Stake Program Constants.""" - -from solders.pubkey import Pubkey - -STAKE_PROGRAM_ID = Pubkey.from_string("Stake11111111111111111111111111111111111111") -"""Public key that identifies the Stake program.""" - -SYSVAR_STAKE_CONFIG_ID = Pubkey.from_string("StakeConfig11111111111111111111111111111111") -"""Public key that identifies the Stake config sysvar.""" - -STAKE_LEN: int = 200 -"""Size of stake account.""" - -LAMPORTS_PER_SOL: int = 1_000_000_000 -"""Number of lamports per SOL""" - -MINIMUM_DELEGATION: int = LAMPORTS_PER_SOL -"""Minimum delegation allowed by the stake program""" diff --git a/stake-pool/py/stake/instructions.py b/stake-pool/py/stake/instructions.py deleted file mode 100644 index 91e7e7dc651..00000000000 --- a/stake-pool/py/stake/instructions.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Stake Program Instructions.""" - -from enum import IntEnum -from typing import NamedTuple - -from construct import Switch # type: ignore -from construct import Int32ul, Pass # type: ignore -from construct import Bytes, Struct - -from solders.pubkey import Pubkey -from solders.sysvar import RENT -from solders.instruction import AccountMeta, Instruction - -from stake.constants import STAKE_PROGRAM_ID -from stake.state import AUTHORIZED_LAYOUT, LOCKUP_LAYOUT, Authorized, Lockup, StakeAuthorize - -PUBLIC_KEY_LAYOUT = Bytes(32) - - -class InitializeParams(NamedTuple): - """Initialize stake transaction params.""" - - stake: Pubkey - """`[w]` Uninitialized stake account.""" - authorized: Authorized - """Information about the staker and withdrawer keys.""" - lockup: Lockup - """Stake lockup, if any.""" - - -class DelegateStakeParams(NamedTuple): - """Initialize stake transaction params.""" - - stake: Pubkey - """`[w]` Uninitialized stake account.""" - vote: Pubkey - """`[]` Vote account to which this stake will be delegated.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - stake_history_sysvar: Pubkey - """`[]` Stake history sysvar that carries stake warmup/cooldown history.""" - stake_config_id: Pubkey - """`[]` Address of config account that carries stake config.""" - staker: Pubkey - """`[s]` Stake authority.""" - - -class AuthorizeParams(NamedTuple): - """Authorize stake transaction params.""" - - stake: Pubkey - """`[w]` Initialized stake account to modify.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - authority: Pubkey - """`[s]` Current stake authority.""" - - # Params - new_authority: Pubkey - """New authority's public key.""" - stake_authorize: StakeAuthorize - """Type of authority to modify, staker or withdrawer.""" - - -class InstructionType(IntEnum): - """Stake Instruction Types.""" - - INITIALIZE = 0 - AUTHORIZE = 1 - DELEGATE_STAKE = 2 - SPLIT = 3 - WITHDRAW = 4 - DEACTIVATE = 5 - SET_LOCKUP = 6 - MERGE = 7 - AUTHORIZE_WITH_SEED = 8 - INITIALIZE_CHECKED = 9 - AUTHORIZED_CHECKED = 10 - AUTHORIZED_CHECKED_WITH_SEED = 11 - SET_LOCKUP_CHECKED = 12 - - -INITIALIZE_LAYOUT = Struct( - "authorized" / AUTHORIZED_LAYOUT, - "lockup" / LOCKUP_LAYOUT, -) - - -AUTHORIZE_LAYOUT = Struct( - "new_authority" / PUBLIC_KEY_LAYOUT, - "stake_authorize" / Int32ul, -) - - -INSTRUCTIONS_LAYOUT = Struct( - "instruction_type" / Int32ul, - "args" - / Switch( - lambda this: this.instruction_type, - { - InstructionType.INITIALIZE: INITIALIZE_LAYOUT, - InstructionType.AUTHORIZE: AUTHORIZE_LAYOUT, - InstructionType.DELEGATE_STAKE: Pass, - InstructionType.SPLIT: Pass, - InstructionType.WITHDRAW: Pass, - InstructionType.DEACTIVATE: Pass, - InstructionType.SET_LOCKUP: Pass, - InstructionType.MERGE: Pass, - InstructionType.AUTHORIZE_WITH_SEED: Pass, - InstructionType.INITIALIZE_CHECKED: Pass, - InstructionType.AUTHORIZED_CHECKED: Pass, - InstructionType.AUTHORIZED_CHECKED_WITH_SEED: Pass, - InstructionType.SET_LOCKUP_CHECKED: Pass, - }, - ), -) - - -def initialize(params: InitializeParams) -> Instruction: - """Creates a transaction instruction to initialize a new stake.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=RENT, is_signer=False, is_writable=False), - ], - program_id=STAKE_PROGRAM_ID, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.INITIALIZE, - args=dict( - authorized=params.authorized.as_bytes_dict(), - lockup=params.lockup.as_bytes_dict(), - ), - ) - ) - ) - - -def delegate_stake(params: DelegateStakeParams) -> Instruction: - """Creates an instruction to delegate a stake account.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.vote, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_config_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), - ], - program_id=STAKE_PROGRAM_ID, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.DELEGATE_STAKE, - args=None, - ) - ) - ) - - -def authorize(params: AuthorizeParams) -> Instruction: - """Creates an instruction to change the authority on a stake account.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.authority, is_signer=True, is_writable=False), - ], - program_id=STAKE_PROGRAM_ID, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.AUTHORIZE, - args={ - 'new_authority': bytes(params.new_authority), - 'stake_authorize': params.stake_authorize, - }, - ) - ) - ) diff --git a/stake-pool/py/stake/state.py b/stake-pool/py/stake/state.py deleted file mode 100644 index 45d1942ab40..00000000000 --- a/stake-pool/py/stake/state.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Stake State.""" - -from enum import IntEnum -from typing import NamedTuple, Dict -from construct import Bytes, Container, Struct, Float64l, Int32ul, Int64ul # type: ignore - -from solders.pubkey import Pubkey - -PUBLIC_KEY_LAYOUT = Bytes(32) - - -class Lockup(NamedTuple): - """Lockup for a stake account.""" - unix_timestamp: int - epoch: int - custodian: Pubkey - - @classmethod - def decode_container(cls, container: Container): - return Lockup( - unix_timestamp=container['unix_timestamp'], - epoch=container['epoch'], - custodian=Pubkey(container['custodian']), - ) - - def as_bytes_dict(self) -> Dict: - self_dict = self._asdict() - self_dict['custodian'] = bytes(self_dict['custodian']) - return self_dict - - -class Authorized(NamedTuple): - """Define who is authorized to change a stake.""" - staker: Pubkey - withdrawer: Pubkey - - def as_bytes_dict(self) -> Dict: - return { - 'staker': bytes(self.staker), - 'withdrawer': bytes(self.withdrawer), - } - - -class StakeAuthorize(IntEnum): - """Stake Authorization Types.""" - STAKER = 0 - WITHDRAWER = 1 - - -class StakeStakeType(IntEnum): - """Stake State Types.""" - UNINITIALIZED = 0 - INITIALIZED = 1 - STAKE = 2 - REWARDS_POOL = 3 - - -class StakeStake(NamedTuple): - state_type: StakeStakeType - state: Container - - """Stake state.""" - @classmethod - def decode(cls, data: bytes): - parsed = STAKE_STATE_LAYOUT.parse(data) - return StakeStake( - state_type=parsed['state_type'], - state=parsed['state'], - ) - - -LOCKUP_LAYOUT = Struct( - "unix_timestamp" / Int64ul, - "epoch" / Int64ul, - "custodian" / PUBLIC_KEY_LAYOUT, -) - - -AUTHORIZED_LAYOUT = Struct( - "staker" / PUBLIC_KEY_LAYOUT, - "withdrawer" / PUBLIC_KEY_LAYOUT, -) - -META_LAYOUT = Struct( - "rent_exempt_reserve" / Int64ul, - "authorized" / AUTHORIZED_LAYOUT, - "lockup" / LOCKUP_LAYOUT, -) - -META_LAYOUT = Struct( - "rent_exempt_reserve" / Int64ul, - "authorized" / AUTHORIZED_LAYOUT, - "lockup" / LOCKUP_LAYOUT, -) - -DELEGATION_LAYOUT = Struct( - "voter_pubkey" / PUBLIC_KEY_LAYOUT, - "stake" / Int64ul, - "activation_epoch" / Int64ul, - "deactivation_epoch" / Int64ul, - "warmup_cooldown_rate" / Float64l, -) - -STAKE_LAYOUT = Struct( - "delegation" / DELEGATION_LAYOUT, - "credits_observed" / Int64ul, -) - -STAKE_AND_META_LAYOUT = Struct( - "meta" / META_LAYOUT, - "stake" / STAKE_LAYOUT, -) - -STAKE_STATE_LAYOUT = Struct( - "state_type" / Int32ul, - "state" / STAKE_AND_META_LAYOUT, - # NOTE: This can be done better, but was mainly needed for testing. Ideally, - # we would have something like: - # - # Switch( - # lambda this: this.state, - # { - # StakeStakeType.UNINITIALIZED: Pass, - # StakeStakeType.INITIALIZED: META_LAYOUT, - # StakeStakeType.STAKE: STAKE_AND_META_LAYOUT, - # } - # ), - # - # Unfortunately, it didn't work. -) diff --git a/stake-pool/py/stake_pool/__init__.py b/stake-pool/py/stake_pool/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/stake-pool/py/stake_pool/actions.py b/stake-pool/py/stake_pool/actions.py deleted file mode 100644 index 400f9252447..00000000000 --- a/stake-pool/py/stake_pool/actions.py +++ /dev/null @@ -1,753 +0,0 @@ -from typing import Optional, Tuple - -from solders.keypair import Keypair -from solders.pubkey import Pubkey -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed -from solana.rpc.types import TxOpts -from solders.sysvar import CLOCK, RENT, STAKE_HISTORY -from solana.transaction import Transaction -import solders.system_program as sys - -from spl.token.constants import TOKEN_PROGRAM_ID - -from stake.constants import STAKE_PROGRAM_ID, STAKE_LEN, SYSVAR_STAKE_CONFIG_ID -import stake.instructions as st -from stake.state import StakeAuthorize -from stake_pool.constants import \ - MAX_VALIDATORS_TO_UPDATE, \ - MINIMUM_RESERVE_LAMPORTS, \ - STAKE_POOL_PROGRAM_ID, \ - METADATA_PROGRAM_ID, \ - find_stake_program_address, \ - find_transient_stake_program_address, \ - find_withdraw_authority_program_address, \ - find_metadata_account, \ - find_ephemeral_stake_program_address -from stake_pool.state import STAKE_POOL_LAYOUT, ValidatorList, Fee, StakePool -import stake_pool.instructions as sp - -from stake.actions import create_stake -from spl_token.actions import create_mint, create_associated_token_account - - -OPTS = TxOpts(skip_confirmation=False, preflight_commitment=Confirmed) - - -async def create(client: AsyncClient, manager: Keypair, - stake_pool: Keypair, validator_list: Keypair, - pool_mint: Pubkey, reserve_stake: Pubkey, - manager_fee_account: Pubkey, fee: Fee, referral_fee: int): - resp = await client.get_minimum_balance_for_rent_exemption(STAKE_POOL_LAYOUT.sizeof()) - pool_balance = resp.value - txn = Transaction(fee_payer=manager.pubkey()) - txn.add( - sys.create_account( - sys.CreateAccountParams( - from_pubkey=manager.pubkey(), - to_pubkey=stake_pool.pubkey(), - lamports=pool_balance, - space=STAKE_POOL_LAYOUT.sizeof(), - owner=STAKE_POOL_PROGRAM_ID, - ) - ) - ) - max_validators = 2950 # current supported max by the program, go big! - validator_list_size = ValidatorList.calculate_validator_list_size(max_validators) - resp = await client.get_minimum_balance_for_rent_exemption(validator_list_size) - validator_list_balance = resp.value - txn.add( - sys.create_account( - sys.CreateAccountParams( - from_pubkey=manager.pubkey(), - to_pubkey=validator_list.pubkey(), - lamports=validator_list_balance, - space=validator_list_size, - owner=STAKE_POOL_PROGRAM_ID, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction( - txn, manager, stake_pool, validator_list, recent_blockhash=recent_blockhash, opts=OPTS) - - (withdraw_authority, seed) = find_withdraw_authority_program_address( - STAKE_POOL_PROGRAM_ID, stake_pool.pubkey()) - txn = Transaction(fee_payer=manager.pubkey()) - txn.add( - sp.initialize( - sp.InitializeParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool.pubkey(), - manager=manager.pubkey(), - staker=manager.pubkey(), - withdraw_authority=withdraw_authority, - validator_list=validator_list.pubkey(), - reserve_stake=reserve_stake, - pool_mint=pool_mint, - manager_fee_account=manager_fee_account, - token_program_id=TOKEN_PROGRAM_ID, - epoch_fee=fee, - withdrawal_fee=fee, - deposit_fee=fee, - referral_fee=referral_fee, - max_validators=max_validators, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, manager, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def create_all( - client: AsyncClient, manager: Keypair, fee: Fee, referral_fee: int -) -> Tuple[Pubkey, Pubkey, Pubkey]: - stake_pool = Keypair() - validator_list = Keypair() - (pool_withdraw_authority, seed) = find_withdraw_authority_program_address( - STAKE_POOL_PROGRAM_ID, stake_pool.pubkey()) - - reserve_stake = Keypair() - await create_stake(client, manager, reserve_stake, pool_withdraw_authority, MINIMUM_RESERVE_LAMPORTS) - - pool_mint = Keypair() - await create_mint(client, manager, pool_mint, pool_withdraw_authority) - - manager_fee_account = await create_associated_token_account( - client, - manager, - manager.pubkey(), - pool_mint.pubkey(), - ) - - fee = Fee(numerator=1, denominator=1000) - referral_fee = 20 - await create( - client, manager, stake_pool, validator_list, pool_mint.pubkey(), - reserve_stake.pubkey(), manager_fee_account, fee, referral_fee) - return (stake_pool.pubkey(), validator_list.pubkey(), pool_mint.pubkey()) - - -async def add_validator_to_pool( - client: AsyncClient, staker: Keypair, - stake_pool_address: Pubkey, validator: Pubkey -): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - txn = Transaction(fee_payer=staker.pubkey()) - txn.add( - sp.add_validator_to_pool_with_vote( - STAKE_POOL_PROGRAM_ID, - stake_pool_address, - stake_pool.staker, - stake_pool.validator_list, - stake_pool.reserve_stake, - validator, - None, - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, staker, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def remove_validator_from_pool( - client: AsyncClient, staker: Keypair, - stake_pool_address: Pubkey, validator: Pubkey -): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - validator_info = next(x for x in validator_list.validators if x.vote_account_address == validator) - txn = Transaction(fee_payer=staker.pubkey()) - txn.add( - sp.remove_validator_from_pool_with_vote( - STAKE_POOL_PROGRAM_ID, - stake_pool_address, - stake_pool.staker, - stake_pool.validator_list, - validator, - validator_info.validator_seed_suffix or None, - validator_info.transient_seed_suffix, - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, staker, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def deposit_sol( - client: AsyncClient, funder: Keypair, stake_pool_address: Pubkey, - destination_token_account: Pubkey, amount: int, -): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) - - txn = Transaction(fee_payer=funder.pubkey()) - txn.add( - sp.deposit_sol( - sp.DepositSolParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - withdraw_authority=withdraw_authority, - reserve_stake=stake_pool.reserve_stake, - funding_account=funder.pubkey(), - destination_pool_account=destination_token_account, - manager_fee_account=stake_pool.manager_fee_account, - referral_pool_account=destination_token_account, - pool_mint=stake_pool.pool_mint, - system_program_id=sys.ID, - token_program_id=stake_pool.token_program_id, - amount=amount, - deposit_authority=None, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, funder, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def withdraw_sol( - client: AsyncClient, owner: Keypair, source_token_account: Pubkey, - stake_pool_address: Pubkey, destination_system_account: Pubkey, amount: int, -): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) - - txn = Transaction(fee_payer=owner.pubkey()) - txn.add( - sp.withdraw_sol( - sp.WithdrawSolParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - withdraw_authority=withdraw_authority, - source_transfer_authority=owner.pubkey(), - source_pool_account=source_token_account, - reserve_stake=stake_pool.reserve_stake, - destination_system_account=destination_system_account, - manager_fee_account=stake_pool.manager_fee_account, - pool_mint=stake_pool.pool_mint, - clock_sysvar=CLOCK, - stake_history_sysvar=STAKE_HISTORY, - stake_program_id=STAKE_PROGRAM_ID, - token_program_id=stake_pool.token_program_id, - amount=amount, - sol_withdraw_authority=None, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, owner, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def deposit_stake( - client: AsyncClient, - deposit_stake_authority: Keypair, - stake_pool_address: Pubkey, - validator_vote: Pubkey, - deposit_stake: Pubkey, - destination_pool_account: Pubkey, -): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - - validator_info = next(x for x in validator_list.validators if x.vote_account_address == validator_vote) - - (withdraw_authority, _) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) - (validator_stake, _) = find_stake_program_address( - STAKE_POOL_PROGRAM_ID, - validator_vote, - stake_pool_address, - validator_info.validator_seed_suffix or None, - ) - - txn = Transaction(fee_payer=deposit_stake_authority.pubkey()) - txn.add( - st.authorize( - st.AuthorizeParams( - stake=deposit_stake, - clock_sysvar=CLOCK, - authority=deposit_stake_authority.pubkey(), - new_authority=stake_pool.stake_deposit_authority, - stake_authorize=StakeAuthorize.STAKER, - ) - ) - ) - txn.add( - st.authorize( - st.AuthorizeParams( - stake=deposit_stake, - clock_sysvar=CLOCK, - authority=deposit_stake_authority.pubkey(), - new_authority=stake_pool.stake_deposit_authority, - stake_authorize=StakeAuthorize.WITHDRAWER, - ) - ) - ) - txn.add( - sp.deposit_stake( - sp.DepositStakeParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - validator_list=stake_pool.validator_list, - deposit_authority=stake_pool.stake_deposit_authority, - withdraw_authority=withdraw_authority, - deposit_stake=deposit_stake, - validator_stake=validator_stake, - reserve_stake=stake_pool.reserve_stake, - destination_pool_account=destination_pool_account, - manager_fee_account=stake_pool.manager_fee_account, - referral_pool_account=destination_pool_account, - pool_mint=stake_pool.pool_mint, - clock_sysvar=CLOCK, - stake_history_sysvar=STAKE_HISTORY, - token_program_id=stake_pool.token_program_id, - stake_program_id=STAKE_PROGRAM_ID, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, deposit_stake_authority, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def withdraw_stake( - client: AsyncClient, - payer: Keypair, - source_transfer_authority: Keypair, - destination_stake: Keypair, - stake_pool_address: Pubkey, - validator_vote: Pubkey, - destination_stake_authority: Pubkey, - source_pool_account: Pubkey, - amount: int, -): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - - validator_info = next(x for x in validator_list.validators if x.vote_account_address == validator_vote) - - (withdraw_authority, _) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) - (validator_stake, _) = find_stake_program_address( - STAKE_POOL_PROGRAM_ID, - validator_vote, - stake_pool_address, - validator_info.validator_seed_suffix or None, - ) - - rent_resp = await client.get_minimum_balance_for_rent_exemption(STAKE_LEN) - stake_rent_exemption = rent_resp.value - - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( - sys.create_account( - sys.CreateAccountParams( - from_pubkey=payer.pubkey(), - to_pubkey=destination_stake.pubkey(), - lamports=stake_rent_exemption, - space=STAKE_LEN, - owner=STAKE_PROGRAM_ID, - ) - ) - ) - txn.add( - sp.withdraw_stake( - sp.WithdrawStakeParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - validator_list=stake_pool.validator_list, - withdraw_authority=withdraw_authority, - validator_stake=validator_stake, - destination_stake=destination_stake.pubkey(), - destination_stake_authority=destination_stake_authority, - source_transfer_authority=source_transfer_authority.pubkey(), - source_pool_account=source_pool_account, - manager_fee_account=stake_pool.manager_fee_account, - pool_mint=stake_pool.pool_mint, - clock_sysvar=CLOCK, - token_program_id=stake_pool.token_program_id, - stake_program_id=STAKE_PROGRAM_ID, - amount=amount, - ) - ) - ) - signers = [payer, source_transfer_authority, destination_stake] \ - if payer != source_transfer_authority else [payer, destination_stake] - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, *signers, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def update_stake_pool(client: AsyncClient, payer: Keypair, stake_pool_address: Pubkey): - """Create and send all instructions to completely update a stake pool after epoch change.""" - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) - update_list_instructions = [] - validator_chunks = [ - validator_list.validators[i:i+MAX_VALIDATORS_TO_UPDATE] - for i in range(0, len(validator_list.validators), MAX_VALIDATORS_TO_UPDATE) - ] - start_index = 0 - for validator_chunk in validator_chunks: - validator_and_transient_stake_pairs = [] - for validator in validator_chunk: - (validator_stake_address, _) = find_stake_program_address( - STAKE_POOL_PROGRAM_ID, - validator.vote_account_address, - stake_pool_address, - validator.validator_seed_suffix or None, - ) - validator_and_transient_stake_pairs.append(validator_stake_address) - (transient_stake_address, _) = find_transient_stake_program_address( - STAKE_POOL_PROGRAM_ID, - validator.vote_account_address, - stake_pool_address, - validator.transient_seed_suffix, - ) - validator_and_transient_stake_pairs.append(transient_stake_address) - update_list_instructions.append( - sp.update_validator_list_balance( - sp.UpdateValidatorListBalanceParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - withdraw_authority=withdraw_authority, - validator_list=stake_pool.validator_list, - reserve_stake=stake_pool.reserve_stake, - clock_sysvar=CLOCK, - stake_history_sysvar=STAKE_HISTORY, - stake_program_id=STAKE_PROGRAM_ID, - validator_and_transient_stake_pairs=validator_and_transient_stake_pairs, - start_index=start_index, - no_merge=False, - ) - ) - ) - start_index += MAX_VALIDATORS_TO_UPDATE - if update_list_instructions: - last_instruction = update_list_instructions.pop() - for update_list_instruction in update_list_instructions: - txn = Transaction(fee_payer=payer.pubkey()) - txn.add(update_list_instruction) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, payer, recent_blockhash=recent_blockhash, - opts=TxOpts(skip_confirmation=True, preflight_commitment=Confirmed)) - txn = Transaction(fee_payer=payer.pubkey()) - txn.add(last_instruction) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, payer, recent_blockhash=recent_blockhash, opts=OPTS) - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( - sp.update_stake_pool_balance( - sp.UpdateStakePoolBalanceParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - withdraw_authority=withdraw_authority, - validator_list=stake_pool.validator_list, - reserve_stake=stake_pool.reserve_stake, - manager_fee_account=stake_pool.manager_fee_account, - pool_mint=stake_pool.pool_mint, - token_program_id=stake_pool.token_program_id, - ) - ) - ) - txn.add( - sp.cleanup_removed_validator_entries( - sp.CleanupRemovedValidatorEntriesParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - validator_list=stake_pool.validator_list, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, payer, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def increase_validator_stake( - client: AsyncClient, - payer: Keypair, - staker: Keypair, - stake_pool_address: Pubkey, - validator_vote: Pubkey, - lamports: int, - ephemeral_stake_seed: Optional[int] = None -): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) - - validator_info = next(x for x in validator_list.validators if x.vote_account_address == validator_vote) - - if ephemeral_stake_seed is None: - transient_stake_seed = validator_info.transient_seed_suffix + 1 # bump up by one to avoid reuse - else: - # we are updating an existing transient stake account, so we must use the same seed - transient_stake_seed = validator_info.transient_seed_suffix - - validator_stake_seed = validator_info.validator_seed_suffix or None - (transient_stake, _) = find_transient_stake_program_address( - STAKE_POOL_PROGRAM_ID, - validator_info.vote_account_address, - stake_pool_address, - transient_stake_seed, - ) - (validator_stake, _) = find_stake_program_address( - STAKE_POOL_PROGRAM_ID, - validator_info.vote_account_address, - stake_pool_address, - validator_stake_seed - ) - - txn = Transaction(fee_payer=payer.pubkey()) - if ephemeral_stake_seed is not None: - - # We assume there is an existing transient account that we will update - (ephemeral_stake, _) = find_ephemeral_stake_program_address( - STAKE_POOL_PROGRAM_ID, - stake_pool_address, - ephemeral_stake_seed) - - txn.add( - sp.increase_additional_validator_stake( - sp.IncreaseAdditionalValidatorStakeParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - staker=staker.pubkey(), - withdraw_authority=withdraw_authority, - validator_list=stake_pool.validator_list, - reserve_stake=stake_pool.reserve_stake, - transient_stake=transient_stake, - validator_stake=validator_stake, - validator_vote=validator_vote, - clock_sysvar=CLOCK, - rent_sysvar=RENT, - stake_history_sysvar=STAKE_HISTORY, - stake_config_sysvar=SYSVAR_STAKE_CONFIG_ID, - system_program_id=sys.ID, - stake_program_id=STAKE_PROGRAM_ID, - lamports=lamports, - transient_stake_seed=transient_stake_seed, - ephemeral_stake=ephemeral_stake, - ephemeral_stake_seed=ephemeral_stake_seed - ) - ) - ) - - else: - txn.add( - sp.increase_validator_stake( - sp.IncreaseValidatorStakeParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - staker=staker.pubkey(), - withdraw_authority=withdraw_authority, - validator_list=stake_pool.validator_list, - reserve_stake=stake_pool.reserve_stake, - transient_stake=transient_stake, - validator_stake=validator_stake, - validator_vote=validator_vote, - clock_sysvar=CLOCK, - rent_sysvar=RENT, - stake_history_sysvar=STAKE_HISTORY, - stake_config_sysvar=SYSVAR_STAKE_CONFIG_ID, - system_program_id=sys.ID, - stake_program_id=STAKE_PROGRAM_ID, - lamports=lamports, - transient_stake_seed=transient_stake_seed, - ) - ) - ) - - signers = [payer, staker] if payer != staker else [payer] - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, *signers, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def decrease_validator_stake( - client: AsyncClient, - payer: Keypair, - staker: Keypair, - stake_pool_address: Pubkey, - validator_vote: Pubkey, - lamports: int, - ephemeral_stake_seed: Optional[int] = None -): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - resp = await client.get_account_info(stake_pool.validator_list, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) - - validator_info = next(x for x in validator_list.validators if x.vote_account_address == validator_vote) - validator_stake_seed = validator_info.validator_seed_suffix or None - (validator_stake, _) = find_stake_program_address( - STAKE_POOL_PROGRAM_ID, - validator_info.vote_account_address, - stake_pool_address, - validator_stake_seed, - ) - - if ephemeral_stake_seed is None: - transient_stake_seed = validator_info.transient_seed_suffix + 1 # bump up by one to avoid reuse - else: - # we are updating an existing transient stake account, so we must use the same seed - transient_stake_seed = validator_info.transient_seed_suffix - - (transient_stake, _) = find_transient_stake_program_address( - STAKE_POOL_PROGRAM_ID, - validator_info.vote_account_address, - stake_pool_address, - transient_stake_seed, - ) - - txn = Transaction(fee_payer=payer.pubkey()) - - if ephemeral_stake_seed is not None: - - # We assume there is an existing transient account that we will update - (ephemeral_stake, _) = find_ephemeral_stake_program_address( - STAKE_POOL_PROGRAM_ID, - stake_pool_address, - ephemeral_stake_seed) - - txn.add( - sp.decrease_additional_validator_stake( - sp.DecreaseAdditionalValidatorStakeParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - staker=staker.pubkey(), - withdraw_authority=withdraw_authority, - validator_list=stake_pool.validator_list, - reserve_stake=stake_pool.reserve_stake, - validator_stake=validator_stake, - transient_stake=transient_stake, - clock_sysvar=CLOCK, - rent_sysvar=RENT, - stake_history_sysvar=STAKE_HISTORY, - system_program_id=sys.ID, - stake_program_id=STAKE_PROGRAM_ID, - lamports=lamports, - transient_stake_seed=transient_stake_seed, - ephemeral_stake=ephemeral_stake, - ephemeral_stake_seed=ephemeral_stake_seed - ) - ) - ) - - else: - - txn.add( - sp.decrease_validator_stake_with_reserve( - sp.DecreaseValidatorStakeWithReserveParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - staker=staker.pubkey(), - withdraw_authority=withdraw_authority, - validator_list=stake_pool.validator_list, - reserve_stake=stake_pool.reserve_stake, - validator_stake=validator_stake, - transient_stake=transient_stake, - clock_sysvar=CLOCK, - stake_history_sysvar=STAKE_HISTORY, - system_program_id=sys.ID, - stake_program_id=STAKE_PROGRAM_ID, - lamports=lamports, - transient_stake_seed=transient_stake_seed, - ) - ) - ) - - signers = [payer, staker] if payer != staker else [payer] - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, *signers, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def create_token_metadata(client: AsyncClient, payer: Keypair, stake_pool_address: Pubkey, - name: str, symbol: str, uri: str): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - (withdraw_authority, _seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) - (token_metadata, _seed) = find_metadata_account(stake_pool.pool_mint) - - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( - sp.create_token_metadata( - sp.CreateTokenMetadataParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - manager=stake_pool.manager, - pool_mint=stake_pool.pool_mint, - payer=payer.pubkey(), - name=name, - symbol=symbol, - uri=uri, - withdraw_authority=withdraw_authority, - token_metadata=token_metadata, - metadata_program_id=METADATA_PROGRAM_ID, - system_program_id=sys.ID, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, payer, recent_blockhash=recent_blockhash, opts=OPTS) - - -async def update_token_metadata(client: AsyncClient, payer: Keypair, stake_pool_address: Pubkey, - name: str, symbol: str, uri: str): - resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - (withdraw_authority, _seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) - (token_metadata, _seed) = find_metadata_account(stake_pool.pool_mint) - - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( - sp.update_token_metadata( - sp.UpdateTokenMetadataParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - manager=stake_pool.manager, - pool_mint=stake_pool.pool_mint, - name=name, - symbol=symbol, - uri=uri, - withdraw_authority=withdraw_authority, - token_metadata=token_metadata, - metadata_program_id=METADATA_PROGRAM_ID, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction(txn, payer, recent_blockhash=recent_blockhash, opts=OPTS) diff --git a/stake-pool/py/stake_pool/constants.py b/stake-pool/py/stake_pool/constants.py deleted file mode 100644 index 8a09dfbb20d..00000000000 --- a/stake-pool/py/stake_pool/constants.py +++ /dev/null @@ -1,121 +0,0 @@ -"""SPL Stake Pool Constants.""" - -from typing import Optional, Tuple - -from solders.pubkey import Pubkey -from stake.constants import MINIMUM_DELEGATION - -STAKE_POOL_PROGRAM_ID = Pubkey.from_string("SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy") -"""Public key that identifies the SPL Stake Pool program.""" - -MAX_VALIDATORS_TO_UPDATE: int = 5 -"""Maximum number of validators to update during UpdateValidatorListBalance.""" - -MINIMUM_RESERVE_LAMPORTS: int = 0 -"""Minimum balance required in the stake pool reserve""" - -MINIMUM_ACTIVE_STAKE: int = MINIMUM_DELEGATION -"""Minimum active delegated staked required in a stake account""" - -METADATA_PROGRAM_ID = Pubkey.from_string("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s") -"""Public key that identifies the Metaplex Token Metadata program.""" - - -def find_deposit_authority_program_address( - program_id: Pubkey, - stake_pool_address: Pubkey, -) -> Tuple[Pubkey, int]: - """Generates the deposit authority program address for the stake pool""" - return Pubkey.find_program_address( - [bytes(stake_pool_address), AUTHORITY_DEPOSIT], - program_id, - ) - - -def find_withdraw_authority_program_address( - program_id: Pubkey, - stake_pool_address: Pubkey, -) -> Tuple[Pubkey, int]: - """Generates the withdraw authority program address for the stake pool""" - return Pubkey.find_program_address( - [bytes(stake_pool_address), AUTHORITY_WITHDRAW], - program_id, - ) - - -def find_stake_program_address( - program_id: Pubkey, - vote_account_address: Pubkey, - stake_pool_address: Pubkey, - seed: Optional[int] -) -> Tuple[Pubkey, int]: - """Generates the stake program address for a validator's vote account""" - return Pubkey.find_program_address( - [ - bytes(vote_account_address), - bytes(stake_pool_address), - seed.to_bytes(4, 'little') if seed else bytes(), - ], - program_id, - ) - - -def find_transient_stake_program_address( - program_id: Pubkey, - vote_account_address: Pubkey, - stake_pool_address: Pubkey, - seed: int, -) -> Tuple[Pubkey, int]: - """Generates the stake program address for a validator's vote account""" - return Pubkey.find_program_address( - [ - TRANSIENT_STAKE_SEED_PREFIX, - bytes(vote_account_address), - bytes(stake_pool_address), - seed.to_bytes(8, 'little'), - ], - program_id, - ) - - -def find_ephemeral_stake_program_address( - program_id: Pubkey, - stake_pool_address: Pubkey, - seed: int -) -> Tuple[Pubkey, int]: - - """Generates the ephemeral program address for stake pool redelegation""" - return Pubkey.find_program_address( - [ - EPHEMERAL_STAKE_SEED_PREFIX, - bytes(stake_pool_address), - seed.to_bytes(8, 'little'), - ], - program_id, - ) - - -def find_metadata_account( - mint_key: Pubkey -) -> Tuple[Pubkey, int]: - """Generates the metadata account program address""" - return Pubkey.find_program_address( - [ - METADATA_SEED_PREFIX, - bytes(METADATA_PROGRAM_ID), - bytes(mint_key) - ], - METADATA_PROGRAM_ID - ) - - -AUTHORITY_DEPOSIT = b"deposit" -"""Seed used to derive the default stake pool deposit authority.""" -AUTHORITY_WITHDRAW = b"withdraw" -"""Seed used to derive the stake pool withdraw authority.""" -TRANSIENT_STAKE_SEED_PREFIX = b"transient" -"""Seed used to derive transient stake accounts.""" -METADATA_SEED_PREFIX = b"metadata" -"""Seed used to avoid certain collision attacks.""" -EPHEMERAL_STAKE_SEED_PREFIX = b'ephemeral' -"""Seed for ephemeral stake account""" diff --git a/stake-pool/py/stake_pool/instructions.py b/stake-pool/py/stake_pool/instructions.py deleted file mode 100644 index 493d2251977..00000000000 --- a/stake-pool/py/stake_pool/instructions.py +++ /dev/null @@ -1,1277 +0,0 @@ -"""SPL Stake Pool Instructions.""" - -from enum import IntEnum -from typing import List, NamedTuple, Optional -from construct import Prefixed, GreedyString, Struct, Switch, Int8ul, Int32ul, Int64ul, Pass # type: ignore - -from solana.constants import SYSTEM_PROGRAM_ID -from solders.pubkey import Pubkey -from solders.instruction import AccountMeta, Instruction -from solders.sysvar import CLOCK, RENT, STAKE_HISTORY -from spl.token.constants import TOKEN_PROGRAM_ID - -from stake.constants import STAKE_PROGRAM_ID, SYSVAR_STAKE_CONFIG_ID -from stake_pool.constants import find_stake_program_address, find_transient_stake_program_address -from stake_pool.constants import find_withdraw_authority_program_address -from stake_pool.constants import STAKE_POOL_PROGRAM_ID -from stake_pool.state import Fee, FEE_LAYOUT - - -class PreferredValidatorType(IntEnum): - """Specifies the validator type for SetPreferredValidator instruction.""" - - DEPOSIT = 0 - """Specifies the preferred deposit validator.""" - WITHDRAW = 1 - """Specifies the preferred withdraw validator.""" - - -class FundingType(IntEnum): - """Defines which authority to update in the `SetFundingAuthority` instruction.""" - - STAKE_DEPOSIT = 0 - """Sets the stake deposit authority.""" - SOL_DEPOSIT = 1 - """Sets the SOL deposit authority.""" - SOL_WITHDRAW = 2 - """Sets the SOL withdraw authority.""" - - -class InitializeParams(NamedTuple): - """Initialize token mint transaction params.""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """[w] Stake Pool account to initialize.""" - manager: Pubkey - """[s] Manager for new stake pool.""" - staker: Pubkey - """[] Staker for the new stake pool.""" - withdraw_authority: Pubkey - """[] Withdraw authority for the new stake pool.""" - validator_list: Pubkey - """[w] Uninitialized validator list account for the new stake pool.""" - reserve_stake: Pubkey - """[] Reserve stake account.""" - pool_mint: Pubkey - """[w] Pool token mint account.""" - manager_fee_account: Pubkey - """[w] Manager's fee account""" - token_program_id: Pubkey - """[] SPL Token program id.""" - - # Params - epoch_fee: Fee - """Fee assessed as percentage of rewards.""" - withdrawal_fee: Fee - """Fee charged per withdrawal.""" - deposit_fee: Fee - """Fee charged per deposit.""" - referral_fee: int - """Percentage [0-100] of deposit fee that goes to referrer.""" - max_validators: int - """Maximum number of possible validators in the pool.""" - - # Optional - deposit_authority: Optional[Pubkey] = None - """[] Optional deposit authority that must sign all deposits.""" - - -class AddValidatorToPoolParams(NamedTuple): - """(Staker only) Adds stake account delegated to validator to the pool's list of managed validators.""" - - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[w]` Stake pool.""" - staker: Pubkey - """`[s]` Staker.""" - reserve_stake: Pubkey - """`[w]` Reserve stake account.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - validator_stake: Pubkey - """`[w]` Stake account to add to the pool.""" - validator_vote: Pubkey - """`[]` Validator this stake account will be delegated to.""" - rent_sysvar: Pubkey - """`[]` Rent sysvar.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - stake_history_sysvar: Pubkey - """'[]' Stake history sysvar.""" - stake_config_sysvar: Pubkey - """'[]' Stake config sysvar.""" - system_program_id: Pubkey - """`[]` System program.""" - stake_program_id: Pubkey - """`[]` Stake program.""" - - # Params - seed: Optional[int] - """Seed to used to create the validator stake account.""" - - -class RemoveValidatorFromPoolParams(NamedTuple): - """(Staker only) Removes validator from the pool.""" - - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[w]` Stake pool.""" - staker: Pubkey - """`[s]` Staker.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - validator_stake: Pubkey - """`[w]` Stake account to remove from the pool.""" - transient_stake: Pubkey - """`[]` Transient stake account, to check that there's no activation ongoing.""" - clock_sysvar: Pubkey - """'[]' Stake config sysvar.""" - stake_program_id: Pubkey - """`[]` Stake program.""" - - -class DecreaseValidatorStakeParams(NamedTuple): - """(Staker only) Decrease active stake on a validator, eventually moving it to the reserve""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[]` Stake pool.""" - staker: Pubkey - """`[s]` Staker.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - validator_stake: Pubkey - """`[w]` Canonical stake to split from.""" - transient_stake: Pubkey - """`[w]` Transient stake account to receive split.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - rent_sysvar: Pubkey - """`[]` Rent sysvar.""" - system_program_id: Pubkey - """`[]` System program.""" - stake_program_id: Pubkey - """`[]` Stake program.""" - - # Params - lamports: int - """Amount of lamports to split into the transient stake account.""" - transient_stake_seed: int - """Seed to used to create the transient stake account.""" - - -class DecreaseValidatorStakeWithReserveParams(NamedTuple): - """(Staker only) Decrease active stake on a validator, eventually moving it to the reserve""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[]` Stake pool.""" - staker: Pubkey - """`[s]` Staker.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - reserve_stake: Pubkey - """`[w]` Stake pool's reserve.""" - validator_stake: Pubkey - """`[w]` Canonical stake to split from.""" - transient_stake: Pubkey - """`[w]` Transient stake account to receive split.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - stake_history_sysvar: Pubkey - """'[]' Stake history sysvar.""" - system_program_id: Pubkey - """`[]` System program.""" - stake_program_id: Pubkey - """`[]` Stake program.""" - - # Params - lamports: int - """Amount of lamports to split into the transient stake account.""" - transient_stake_seed: int - """Seed to used to create the transient stake account.""" - - -class IncreaseValidatorStakeParams(NamedTuple): - """(Staker only) Increase stake on a validator from the reserve account.""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[]` Stake pool.""" - staker: Pubkey - """`[s]` Staker.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - reserve_stake: Pubkey - """`[w]` Stake pool's reserve.""" - transient_stake: Pubkey - """`[w]` Transient stake account to receive split.""" - validator_stake: Pubkey - """`[]` Canonical stake account to check.""" - validator_vote: Pubkey - """`[]` Validator vote account to delegate to.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - rent_sysvar: Pubkey - """`[]` Rent sysvar.""" - stake_history_sysvar: Pubkey - """'[]' Stake history sysvar.""" - stake_config_sysvar: Pubkey - """'[]' Stake config sysvar.""" - system_program_id: Pubkey - """`[]` System program.""" - stake_program_id: Pubkey - """`[]` Stake program.""" - - # Params - lamports: int - """Amount of lamports to split into the transient stake account.""" - transient_stake_seed: int - """Seed to used to create the transient stake account.""" - - -class SetPreferredValidatorParams(NamedTuple): - pass - - -class UpdateValidatorListBalanceParams(NamedTuple): - """Updates balances of validator and transient stake accounts in the pool.""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[]` Stake pool.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - reserve_stake: Pubkey - """`[w]` Stake pool's reserve.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - stake_history_sysvar: Pubkey - """'[]' Stake history sysvar.""" - stake_program_id: Pubkey - """`[]` Stake program.""" - validator_and_transient_stake_pairs: List[Pubkey] - """[] N pairs of validator and transient stake accounts""" - - # Params - start_index: int - """Index to start updating on the validator list.""" - no_merge: bool - """If true, don't try merging transient stake accounts.""" - - -class UpdateStakePoolBalanceParams(NamedTuple): - """Updates total pool balance based on balances in the reserve and validator list.""" - - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[w]` Stake pool.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - reserve_stake: Pubkey - """`[w]` Stake pool's reserve.""" - manager_fee_account: Pubkey - """`[w]` Account to receive pool fee tokens.""" - pool_mint: Pubkey - """`[w]` Pool mint account.""" - token_program_id: Pubkey - """`[]` Pool token program.""" - - -class CleanupRemovedValidatorEntriesParams(NamedTuple): - """Cleans up validator stake account entries marked as `ReadyForRemoval`""" - - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[w]` Stake pool.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - - -class DepositStakeParams(NamedTuple): - """Deposits a stake account into the pool in exchange for pool tokens""" - - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[w]` Stake pool""" - validator_list: Pubkey - """`[w]` Validator stake list storage account""" - deposit_authority: Pubkey - """`[s]/[]` Stake pool deposit authority""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority""" - deposit_stake: Pubkey - """`[w]` Stake account to join the pool (stake's withdraw authority set to the stake pool deposit authority)""" - validator_stake: Pubkey - """`[w]` Validator stake account for the stake account to be merged with""" - reserve_stake: Pubkey - """`[w]` Reserve stake account, to withdraw rent exempt reserve""" - destination_pool_account: Pubkey - """`[w]` User account to receive pool tokens""" - manager_fee_account: Pubkey - """`[w]` Account to receive pool fee tokens""" - referral_pool_account: Pubkey - """`[w]` Account to receive a portion of pool fee tokens as referral fees""" - pool_mint: Pubkey - """`[w]` Pool token mint account""" - clock_sysvar: Pubkey - """`[]` Sysvar clock account""" - stake_history_sysvar: Pubkey - """`[]` Sysvar stake history account""" - token_program_id: Pubkey - """`[]` Pool token program id""" - stake_program_id: Pubkey - """`[]` Stake program id""" - - -class WithdrawStakeParams(NamedTuple): - """Withdraws a stake account from the pool in exchange for pool tokens""" - - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[w]` Stake pool""" - validator_list: Pubkey - """`[w]` Validator stake list storage account""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority""" - validator_stake: Pubkey - """`[w]` Validator or reserve stake account to split""" - destination_stake: Pubkey - """`[w]` Uninitialized stake account to receive withdrawal""" - destination_stake_authority: Pubkey - """`[]` User account to set as a new withdraw authority""" - source_transfer_authority: Pubkey - """`[s]` User transfer authority, for pool token account""" - source_pool_account: Pubkey - """`[w]` User account with pool tokens to burn from""" - manager_fee_account: Pubkey - """`[w]` Account to receive pool fee tokens""" - pool_mint: Pubkey - """`[w]` Pool token mint account""" - clock_sysvar: Pubkey - """`[]` Sysvar clock account""" - token_program_id: Pubkey - """`[]` Pool token program id""" - stake_program_id: Pubkey - """`[]` Stake program id""" - - # Params - amount: int - """Amount of pool tokens to burn in exchange for stake""" - - -class SetManagerParams(NamedTuple): - pass - - -class SetFeeParams(NamedTuple): - pass - - -class SetStakerParams(NamedTuple): - pass - - -class DepositSolParams(NamedTuple): - """Deposit SOL directly into the pool's reserve account. The output is a "pool" token - representing ownership into the pool. Inputs are converted to the current ratio.""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[w]` Stake pool.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - reserve_stake: Pubkey - """`[w]` Stake pool's reserve.""" - funding_account: Pubkey - """`[ws]` Funding account (must be a system account).""" - destination_pool_account: Pubkey - """`[w]` User account to receive pool tokens.""" - manager_fee_account: Pubkey - """`[w]` Manager's pool token account to receive deposit fee.""" - referral_pool_account: Pubkey - """`[w]` Referrer pool token account to receive referral fee.""" - pool_mint: Pubkey - """`[w]` Pool token mint.""" - system_program_id: Pubkey - """`[]` System program.""" - token_program_id: Pubkey - """`[]` Token program.""" - - # Params - amount: int - """Amount of SOL to deposit""" - - # Optional - deposit_authority: Optional[Pubkey] = None - """`[s]` (Optional) Stake pool sol deposit authority.""" - - -class SetFundingAuthorityParams(NamedTuple): - pass - - -class WithdrawSolParams(NamedTuple): - """Withdraw SOL directly from the pool's reserve account.""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[w]` Stake pool.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - source_transfer_authority: Pubkey - """`[s]` Transfer authority for user pool token account.""" - source_pool_account: Pubkey - """`[w]` User's pool token account to burn pool tokens.""" - reserve_stake: Pubkey - """`[w]` Stake pool's reserve.""" - destination_system_account: Pubkey - """`[w]` Destination system account to receive lamports from the reserve.""" - manager_fee_account: Pubkey - """`[w]` Manager's pool token account to receive fee.""" - pool_mint: Pubkey - """`[w]` Pool token mint.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - stake_history_sysvar: Pubkey - """'[]' Stake history sysvar.""" - stake_program_id: Pubkey - """`[]` Stake program.""" - token_program_id: Pubkey - """`[]` Token program.""" - - # Params - amount: int - """Amount of pool tokens to burn""" - - # Optional - sol_withdraw_authority: Optional[Pubkey] = None - """`[s]` (Optional) Stake pool sol withdraw authority.""" - - -class CreateTokenMetadataParams(NamedTuple): - """Create token metadata for the stake-pool token in the metaplex-token program.""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[]` Stake pool.""" - manager: Pubkey - """`[s]` Manager.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - pool_mint: Pubkey - """`[]` Pool token mint account.""" - payer: Pubkey - """`[s, w]` Payer for creation of token metadata account.""" - token_metadata: Pubkey - """`[w]` Token metadata program account.""" - metadata_program_id: Pubkey - """`[]` Metadata program id""" - system_program_id: Pubkey - """`[]` System program id""" - - # Params - name: str - """Token name.""" - symbol: str - """Token symbol e.g. stkSOL.""" - uri: str - """URI of the uploaded metadata of the spl-token.""" - - -class UpdateTokenMetadataParams(NamedTuple): - """Update token metadata for the stake-pool token in the metaplex-token program.""" - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[]` Stake pool.""" - manager: Pubkey - """`[s]` Manager.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - pool_mint: Pubkey - """`[]` Pool token mint account.""" - token_metadata: Pubkey - """`[w]` Token metadata program account.""" - metadata_program_id: Pubkey - """`[]` Metadata program id""" - - # Params - name: str - """Token name.""" - symbol: str - """Token symbol e.g. stkSOL.""" - uri: str - """URI of the uploaded metadata of the spl-token.""" - - -class IncreaseAdditionalValidatorStakeParams(NamedTuple): - """(Staker only) Increase stake on a validator from the reserve account.""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[]` Stake pool.""" - staker: Pubkey - """`[s]` Staker.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - reserve_stake: Pubkey - """`[w]` Stake pool's reserve.""" - ephemeral_stake: Pubkey - """The ephemeral stake account used during the operation.""" - transient_stake: Pubkey - """`[w]` Transient stake account to receive split.""" - validator_stake: Pubkey - """`[]` Canonical stake account to check.""" - validator_vote: Pubkey - """`[]` Validator vote account to delegate to.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - rent_sysvar: Pubkey - """`[]` Rent sysvar.""" - stake_history_sysvar: Pubkey - """'[]' Stake history sysvar.""" - stake_config_sysvar: Pubkey - """'[]' Stake config sysvar.""" - system_program_id: Pubkey - """`[]` System program.""" - stake_program_id: Pubkey - """`[]` Stake program.""" - - # Params - lamports: int - """Amount of lamports to increase on the given validator.""" - transient_stake_seed: int - """Seed to used to create the transient stake account.""" - ephemeral_stake_seed: int - """The seed used to generate the ephemeral stake account""" - - -class DecreaseAdditionalValidatorStakeParams(NamedTuple): - """(Staker only) Decrease active stake on a validator, eventually moving it to the reserve""" - - # Accounts - program_id: Pubkey - """SPL Stake Pool program account.""" - stake_pool: Pubkey - """`[]` Stake pool.""" - staker: Pubkey - """`[s]` Staker.""" - withdraw_authority: Pubkey - """`[]` Stake pool withdraw authority.""" - validator_list: Pubkey - """`[w]` Validator stake list storage account.""" - reserve_stake: Pubkey - """The reserve stake account to move the stake to.""" - validator_stake: Pubkey - """`[w]` Canonical stake to split from.""" - ephemeral_stake: Pubkey - """The ephemeral stake account used during the operation.""" - transient_stake: Pubkey - """`[w]` Transient stake account to receive split.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - rent_sysvar: Pubkey - """`[]` Rent sysvar.""" - stake_history_sysvar: Pubkey - """'[]' Stake history sysvar.""" - system_program_id: Pubkey - """`[]` System program.""" - stake_program_id: Pubkey - """`[]` Stake program.""" - - # Params - lamports: int - """Amount of lamports to split into the transient stake account.""" - transient_stake_seed: int - """Seed to used to create the transient stake account.""" - ephemeral_stake_seed: int - """The seed used to generate the ephemeral stake account""" - - -class InstructionType(IntEnum): - """Stake Pool Instruction Types.""" - - INITIALIZE = 0 - ADD_VALIDATOR_TO_POOL = 1 - REMOVE_VALIDATOR_FROM_POOL = 2 - DECREASE_VALIDATOR_STAKE = 3 - INCREASE_VALIDATOR_STAKE = 4 - SET_PREFERRED_VALIDATOR = 5 - UPDATE_VALIDATOR_LIST_BALANCE = 6 - UPDATE_STAKE_POOL_BALANCE = 7 - CLEANUP_REMOVED_VALIDATOR_ENTRIES = 8 - DEPOSIT_STAKE = 9 - WITHDRAW_STAKE = 10 - SET_MANAGER = 11 - SET_FEE = 12 - SET_STAKER = 13 - DEPOSIT_SOL = 14 - SET_FUNDING_AUTHORITY = 15 - WITHDRAW_SOL = 16 - CREATE_TOKEN_METADATA = 17 - UPDATE_TOKEN_METADATA = 18 - INCREASE_ADDITIONAL_VALIDATOR_STAKE = 19 - DECREASE_ADDITIONAL_VALIDATOR_STAKE = 20 - DECREASE_VALIDATOR_STAKE_WITH_RESERVE = 21 - REDELEGATE = 22 - - -INITIALIZE_LAYOUT = Struct( - "epoch_fee" / FEE_LAYOUT, - "withdrawal_fee" / FEE_LAYOUT, - "deposit_fee" / FEE_LAYOUT, - "referral_fee" / Int8ul, - "max_validators" / Int32ul, -) - -MOVE_STAKE_LAYOUT = Struct( - "lamports" / Int64ul, - "transient_stake_seed" / Int64ul, -) - -MOVE_STAKE_LAYOUT_WITH_EPHEMERAL_STAKE = Struct( - "lamports" / Int64ul, - "transient_stake_seed" / Int64ul, - "ephemeral_stake_seed" / Int64ul, -) - -UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT = Struct( - "start_index" / Int32ul, - "no_merge" / Int8ul, -) - -AMOUNT_LAYOUT = Struct( - "amount" / Int64ul -) - -SEED_LAYOUT = Struct( - "seed" / Int32ul -) - -TOKEN_METADATA_LAYOUT = Struct( - "name" / Prefixed(Int32ul, GreedyString("utf8")), - "symbol" / Prefixed(Int32ul, GreedyString("utf8")), - "uri" / Prefixed(Int32ul, GreedyString("utf8")) -) - -INSTRUCTIONS_LAYOUT = Struct( - "instruction_type" / Int8ul, - "args" - / Switch( - lambda this: this.instruction_type, - { - InstructionType.INITIALIZE: INITIALIZE_LAYOUT, - InstructionType.ADD_VALIDATOR_TO_POOL: SEED_LAYOUT, - InstructionType.REMOVE_VALIDATOR_FROM_POOL: Pass, - InstructionType.DECREASE_VALIDATOR_STAKE: MOVE_STAKE_LAYOUT, - InstructionType.INCREASE_VALIDATOR_STAKE: MOVE_STAKE_LAYOUT, - InstructionType.SET_PREFERRED_VALIDATOR: Pass, # TODO - InstructionType.UPDATE_VALIDATOR_LIST_BALANCE: UPDATE_VALIDATOR_LIST_BALANCE_LAYOUT, - InstructionType.UPDATE_STAKE_POOL_BALANCE: Pass, - InstructionType.CLEANUP_REMOVED_VALIDATOR_ENTRIES: Pass, - InstructionType.DEPOSIT_STAKE: Pass, - InstructionType.WITHDRAW_STAKE: AMOUNT_LAYOUT, - InstructionType.SET_MANAGER: Pass, # TODO - InstructionType.SET_FEE: Pass, # TODO - InstructionType.SET_STAKER: Pass, # TODO - InstructionType.DEPOSIT_SOL: AMOUNT_LAYOUT, - InstructionType.SET_FUNDING_AUTHORITY: Pass, # TODO - InstructionType.WITHDRAW_SOL: AMOUNT_LAYOUT, - InstructionType.CREATE_TOKEN_METADATA: TOKEN_METADATA_LAYOUT, - InstructionType.UPDATE_TOKEN_METADATA: TOKEN_METADATA_LAYOUT, - InstructionType.DECREASE_ADDITIONAL_VALIDATOR_STAKE: MOVE_STAKE_LAYOUT_WITH_EPHEMERAL_STAKE, - InstructionType.INCREASE_ADDITIONAL_VALIDATOR_STAKE: MOVE_STAKE_LAYOUT_WITH_EPHEMERAL_STAKE, - InstructionType.DECREASE_VALIDATOR_STAKE_WITH_RESERVE: MOVE_STAKE_LAYOUT, - }, - ), -) - - -def initialize(params: InitializeParams) -> Instruction: - """Creates a transaction instruction to initialize a new stake pool.""" - - data = INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.INITIALIZE, - args=dict( - epoch_fee=params.epoch_fee._asdict(), - withdrawal_fee=params.withdrawal_fee._asdict(), - deposit_fee=params.deposit_fee._asdict(), - referral_fee=params.referral_fee, - max_validators=params.max_validators - ), - ) - ) - accounts = [ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.manager, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.staker, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), - ] - if params.deposit_authority: - accounts.append( - AccountMeta(pubkey=params.deposit_authority, is_signer=True, is_writable=False), - ) - return Instruction( - accounts=accounts, - program_id=params.program_id, - data=data, - ) - - -def add_validator_to_pool(params: AddValidatorToPoolParams) -> Instruction: - """Creates instruction to add a validator to the pool.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_vote, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.rent_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_config_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.ADD_VALIDATOR_TO_POOL, - args={'seed': params.seed or 0} - ) - ) - ) - - -def add_validator_to_pool_with_vote( - program_id: Pubkey, - stake_pool: Pubkey, - staker: Pubkey, - validator_list: Pubkey, - reserve_stake: Pubkey, - validator: Pubkey, - validator_stake_seed: Optional[int], -) -> Instruction: - """Creates instruction to add a validator based on their vote account address.""" - (withdraw_authority, _seed) = find_withdraw_authority_program_address(program_id, stake_pool) - (validator_stake, _seed) = find_stake_program_address(program_id, validator, stake_pool, validator_stake_seed) - return add_validator_to_pool( - AddValidatorToPoolParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool, - staker=staker, - reserve_stake=reserve_stake, - withdraw_authority=withdraw_authority, - validator_list=validator_list, - validator_stake=validator_stake, - validator_vote=validator, - rent_sysvar=RENT, - clock_sysvar=CLOCK, - stake_history_sysvar=STAKE_HISTORY, - stake_config_sysvar=SYSVAR_STAKE_CONFIG_ID, - system_program_id=SYSTEM_PROGRAM_ID, - stake_program_id=STAKE_PROGRAM_ID, - seed=validator_stake_seed, - ) - ) - - -def remove_validator_from_pool(params: RemoveValidatorFromPoolParams) -> Instruction: - """Creates instruction to remove a validator from the pool.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.REMOVE_VALIDATOR_FROM_POOL, - args=None - ) - ) - ) - - -def remove_validator_from_pool_with_vote( - program_id: Pubkey, - stake_pool: Pubkey, - staker: Pubkey, - validator_list: Pubkey, - validator: Pubkey, - validator_stake_seed: Optional[int], - transient_stake_seed: int, -) -> Instruction: - """Creates instruction to remove a validator based on their vote account address.""" - (withdraw_authority, seed) = find_withdraw_authority_program_address(program_id, stake_pool) - (validator_stake, seed) = find_stake_program_address(program_id, validator, stake_pool, validator_stake_seed) - (transient_stake, seed) = find_transient_stake_program_address( - program_id, validator, stake_pool, transient_stake_seed) - return remove_validator_from_pool( - RemoveValidatorFromPoolParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool, - staker=staker, - withdraw_authority=withdraw_authority, - validator_list=validator_list, - validator_stake=validator_stake, - transient_stake=transient_stake, - clock_sysvar=CLOCK, - stake_program_id=STAKE_PROGRAM_ID, - ) - ) - - -def deposit_stake(params: DepositStakeParams) -> Instruction: - """Creates a transaction instruction to deposit a stake account into a stake pool.""" - accounts = [ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.deposit_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.deposit_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.destination_pool_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.referral_pool_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ] - return Instruction( - accounts=accounts, - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.DEPOSIT_STAKE, - args=None, - ) - ) - ) - - -def withdraw_stake(params: WithdrawStakeParams) -> Instruction: - """Creates a transaction instruction to withdraw active stake from a stake pool.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.destination_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.destination_stake_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.source_transfer_authority, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.source_pool_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.WITHDRAW_STAKE, - args={'amount': params.amount} - ) - ) - ) - - -def deposit_sol(params: DepositSolParams) -> Instruction: - """Creates a transaction instruction to deposit SOL into a stake pool.""" - accounts = [ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.funding_account, is_signer=True, is_writable=True), - AccountMeta(pubkey=params.destination_pool_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.referral_pool_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), - ] - if params.deposit_authority: - accounts.append(AccountMeta(pubkey=params.deposit_authority, is_signer=True, is_writable=False)) - return Instruction( - accounts=accounts, - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.DEPOSIT_SOL, - args={'amount': params.amount} - ) - ) - ) - - -def withdraw_sol(params: WithdrawSolParams) -> Instruction: - """Creates a transaction instruction to withdraw SOL from a stake pool.""" - accounts = [ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.source_transfer_authority, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.source_pool_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.destination_system_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), - ] - - if params.sol_withdraw_authority: - AccountMeta(pubkey=params.sol_withdraw_authority, is_signer=True, is_writable=False) - - return Instruction( - accounts=accounts, - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.WITHDRAW_SOL, - args={'amount': params.amount} - ) - ) - ) - - -def update_validator_list_balance(params: UpdateValidatorListBalanceParams) -> Instruction: - """Creates instruction to update a set of validators in the stake pool.""" - accounts = [ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ] - accounts.extend([ - AccountMeta(pubkey=pubkey, is_signer=False, is_writable=True) - for pubkey in params.validator_and_transient_stake_pairs - ]) - return Instruction( - accounts=accounts, - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.UPDATE_VALIDATOR_LIST_BALANCE, - args={'start_index': params.start_index, 'no_merge': params.no_merge} - ) - ) - ) - - -def update_stake_pool_balance(params: UpdateStakePoolBalanceParams) -> Instruction: - """Creates instruction to update the overall stake pool balance.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.manager_fee_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.token_program_id, is_signer=False, is_writable=False), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.UPDATE_STAKE_POOL_BALANCE, - args=None, - ) - ) - ) - - -def cleanup_removed_validator_entries(params: CleanupRemovedValidatorEntriesParams) -> Instruction: - """Creates instruction to cleanup removed validator entries.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.CLEANUP_REMOVED_VALIDATOR_ENTRIES, - args=None, - ) - ) - ) - - -def increase_validator_stake(params: IncreaseValidatorStakeParams) -> Instruction: - """Creates instruction to increase the stake on a validator.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_vote, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.rent_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_config_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.INCREASE_VALIDATOR_STAKE, - args={ - 'lamports': params.lamports, - 'transient_stake_seed': params.transient_stake_seed - } - ) - ) - ) - - -def increase_additional_validator_stake( - params: IncreaseAdditionalValidatorStakeParams, - ) -> Instruction: - - """Creates `IncreaseAdditionalValidatorStake` instruction (rebalance from reserve account to transient account)""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.ephemeral_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_vote, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_config_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.INCREASE_ADDITIONAL_VALIDATOR_STAKE, - args={ - 'lamports': params.lamports, - 'transient_stake_seed': params.transient_stake_seed, - 'ephemeral_stake_seed': params.ephemeral_stake_seed - } - ) - ) - ) - - -def decrease_validator_stake(params: DecreaseValidatorStakeParams) -> Instruction: - """Creates instruction to decrease the stake on a validator.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.rent_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.DECREASE_VALIDATOR_STAKE, - args={ - 'lamports': params.lamports, - 'transient_stake_seed': params.transient_stake_seed - } - ) - ) - ) - - -def decrease_additional_validator_stake(params: DecreaseAdditionalValidatorStakeParams) -> Instruction: - """ Creates `DecreaseAdditionalValidatorStake` instruction (rebalance from validator account to - transient account).""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.ephemeral_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.DECREASE_ADDITIONAL_VALIDATOR_STAKE, - args={ - 'lamports': params.lamports, - 'transient_stake_seed': params.transient_stake_seed, - 'ephemeral_stake_seed': params.ephemeral_stake_seed - } - ) - ) - ) - - -def decrease_validator_stake_with_reserve(params: DecreaseValidatorStakeWithReserveParams) -> Instruction: - """Creates instruction to decrease the stake on a validator.""" - return Instruction( - accounts=[ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), - ], - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.DECREASE_VALIDATOR_STAKE_WITH_RESERVE, - args={ - 'lamports': params.lamports, - 'transient_stake_seed': params.transient_stake_seed - } - ) - ) - ) - - -def create_token_metadata(params: CreateTokenMetadataParams) -> Instruction: - """Creates an instruction to create metadata using the mpl token metadata program for the pool token.""" - - accounts = [ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.manager, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.pool_mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.payer, is_signer=True, is_writable=True), - AccountMeta(pubkey=params.token_metadata, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.metadata_program_id, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), - ] - return Instruction( - accounts=accounts, - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.CREATE_TOKEN_METADATA, - args={ - 'name': params.name, - 'symbol': params.symbol, - 'uri': params.uri - } - ) - ) - ) - - -def update_token_metadata(params: UpdateTokenMetadataParams) -> Instruction: - """Creates an instruction to update metadata in the mpl token metadata program account for the pool token.""" - - accounts = [ - AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.manager, is_signer=True, is_writable=False), - AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.token_metadata, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.metadata_program_id, is_signer=False, is_writable=False) - ] - return Instruction( - accounts=accounts, - program_id=params.program_id, - data=INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.UPDATE_TOKEN_METADATA, - args={ - "name": params.name, - "symbol": params.symbol, - "uri": params.uri - } - ) - ) - ) diff --git a/stake-pool/py/stake_pool/state.py b/stake-pool/py/stake_pool/state.py deleted file mode 100644 index b0bc5cbcb5c..00000000000 --- a/stake-pool/py/stake_pool/state.py +++ /dev/null @@ -1,327 +0,0 @@ -"""SPL Stake Pool State.""" - -from enum import IntEnum -from typing import List, NamedTuple, Optional -from construct import Bytes, Container, Struct, Switch, Int8ul, Int32ul, Int64ul, Pass # type: ignore - -from solders.pubkey import Pubkey -from stake.state import Lockup, LOCKUP_LAYOUT - -PUBLIC_KEY_LAYOUT = Bytes(32) - - -def decode_optional_publickey(container: Container) -> Optional[Pubkey]: - if container: - return Pubkey(container.popitem()[1]) - else: - return None - - -class Fee(NamedTuple): - """Fee assessed by the stake pool, expressed as numerator / denominator.""" - numerator: int - denominator: int - - @classmethod - def decode_container(cls, container: Container): - return Fee( - numerator=container['numerator'], - denominator=container['denominator'], - ) - - @classmethod - def decode_optional_container(cls, container: Container): - if container: - return cls.decode_container(container) - else: - return None - - -class StakePool(NamedTuple): - """Stake pool and all its data.""" - manager: Pubkey - staker: Pubkey - stake_deposit_authority: Pubkey - stake_withdraw_bump_seed: int - validator_list: Pubkey - reserve_stake: Pubkey - pool_mint: Pubkey - manager_fee_account: Pubkey - token_program_id: Pubkey - total_lamports: int - pool_token_supply: int - last_update_epoch: int - lockup: Lockup - epoch_fee: Fee - next_epoch_fee: Optional[Fee] - preferred_deposit_validator: Optional[Pubkey] - preferred_withdraw_validator: Optional[Pubkey] - stake_deposit_fee: Fee - stake_withdrawal_fee: Fee - next_stake_withdrawal_fee: Optional[Fee] - stake_referral_fee: int - sol_deposit_authority: Optional[Pubkey] - sol_deposit_fee: Fee - sol_referral_fee: int - sol_withdraw_authority: Optional[Pubkey] - sol_withdrawal_fee: Fee - next_sol_withdrawal_fee: Optional[Fee] - last_epoch_pool_token_supply: int - last_epoch_total_lamports: int - - @classmethod - def decode(cls, data: bytes): - parsed = DECODE_STAKE_POOL_LAYOUT.parse(data) - return StakePool( - manager=Pubkey(parsed['manager']), - staker=Pubkey(parsed['staker']), - stake_deposit_authority=Pubkey(parsed['stake_deposit_authority']), - stake_withdraw_bump_seed=parsed['stake_withdraw_bump_seed'], - validator_list=Pubkey(parsed['validator_list']), - reserve_stake=Pubkey(parsed['reserve_stake']), - pool_mint=Pubkey(parsed['pool_mint']), - manager_fee_account=Pubkey(parsed['manager_fee_account']), - token_program_id=Pubkey(parsed['token_program_id']), - total_lamports=parsed['total_lamports'], - pool_token_supply=parsed['pool_token_supply'], - last_update_epoch=parsed['last_update_epoch'], - lockup=Lockup.decode_container(parsed['lockup']), - epoch_fee=Fee.decode_container(parsed['epoch_fee']), - next_epoch_fee=Fee.decode_optional_container(parsed['next_epoch_fee']), - preferred_deposit_validator=decode_optional_publickey(parsed['preferred_deposit_validator']), - preferred_withdraw_validator=decode_optional_publickey(parsed['preferred_withdraw_validator']), - stake_deposit_fee=Fee.decode_container(parsed['stake_deposit_fee']), - stake_withdrawal_fee=Fee.decode_container(parsed['stake_withdrawal_fee']), - next_stake_withdrawal_fee=Fee.decode_optional_container(parsed['next_stake_withdrawal_fee']), - stake_referral_fee=parsed['stake_referral_fee'], - sol_deposit_authority=decode_optional_publickey(parsed['sol_deposit_authority']), - sol_deposit_fee=Fee.decode_container(parsed['sol_deposit_fee']), - sol_referral_fee=parsed['sol_referral_fee'], - sol_withdraw_authority=decode_optional_publickey(parsed['sol_withdraw_authority']), - sol_withdrawal_fee=Fee.decode_container(parsed['sol_withdrawal_fee']), - next_sol_withdrawal_fee=Fee.decode_optional_container(parsed['next_sol_withdrawal_fee']), - last_epoch_pool_token_supply=parsed['last_epoch_pool_token_supply'], - last_epoch_total_lamports=parsed['last_epoch_total_lamports'], - ) - - -class StakeStatus(IntEnum): - """Specifies the status of a stake on a validator in a stake pool.""" - - ACTIVE = 0 - """Stake is active and normal.""" - DEACTIVATING_TRANSIENT = 1 - """Stake has been removed, but a deactivating transient stake still exists.""" - READY_FOR_REMOVAL = 2 - """No more validator stake accounts exist, entry ready for removal.""" - DEACTIVATING_VALIDATOR = 3 - """Validator stake account is deactivating to be merged into the reserve next epoch.""" - DEACTIVATING_ALL = 3 - """All alidator stake accounts are deactivating to be merged into the reserve next epoch.""" - - -class ValidatorStakeInfo(NamedTuple): - active_stake_lamports: int - """Amount of active stake delegated to this validator.""" - - transient_stake_lamports: int - """Amount of transient stake delegated to this validator.""" - - last_update_epoch: int - """Last epoch the active and transient stake lamports fields were updated.""" - - transient_seed_suffix: int - """Transient account seed suffix.""" - - unused: int - """Unused space, initially meant to specify the range of transient stake account suffixes.""" - - validator_seed_suffix: int - """Validator account seed suffix.""" - - status: StakeStatus - """Status of the validator stake account.""" - - vote_account_address: Pubkey - """Validator vote account address.""" - - @classmethod - def decode_container(cls, container: Container): - return ValidatorStakeInfo( - active_stake_lamports=container['active_stake_lamports'], - transient_stake_lamports=container['transient_stake_lamports'], - last_update_epoch=container['last_update_epoch'], - transient_seed_suffix=container['transient_seed_suffix'], - unused=container['unused'], - validator_seed_suffix=container['validator_seed_suffix'], - status=container['status'], - vote_account_address=Pubkey(container['vote_account_address']), - ) - - -class ValidatorList(NamedTuple): - """List of validators and amount staked, associated to a stake pool.""" - - max_validators: int - """Maximum number of validators possible in the list.""" - - validators: List[ValidatorStakeInfo] - """Info for each validator in the stake pool.""" - - @staticmethod - def calculate_validator_list_size(max_validators: int) -> int: - layout = VALIDATOR_LIST_LAYOUT + VALIDATOR_INFO_LAYOUT[max_validators] - return layout.sizeof() - - @classmethod - def decode(cls, data: bytes): - parsed = DECODE_VALIDATOR_LIST_LAYOUT.parse(data) - return ValidatorList( - max_validators=parsed['max_validators'], - validators=[ValidatorStakeInfo.decode_container(container) for container in parsed['validators']], - ) - - -FEE_LAYOUT = Struct( - "denominator" / Int64ul, - "numerator" / Int64ul, -) - -STAKE_POOL_LAYOUT = Struct( - "account_type" / Int8ul, - "manager" / PUBLIC_KEY_LAYOUT, - "staker" / PUBLIC_KEY_LAYOUT, - "stake_deposit_authority" / PUBLIC_KEY_LAYOUT, - "stake_withdraw_bump_seed" / Int8ul, - "validator_list" / PUBLIC_KEY_LAYOUT, - "reserve_stake" / PUBLIC_KEY_LAYOUT, - "pool_mint" / PUBLIC_KEY_LAYOUT, - "manager_fee_account" / PUBLIC_KEY_LAYOUT, - "token_program_id" / PUBLIC_KEY_LAYOUT, - "total_lamports" / Int64ul, - "pool_token_supply" / Int64ul, - "last_update_epoch" / Int64ul, - "lockup" / LOCKUP_LAYOUT, - "epoch_fee" / FEE_LAYOUT, - "next_epoch_fee_option" / Int8ul, - "next_epoch_fee" / FEE_LAYOUT, - "preferred_deposit_validator_option" / Int8ul, - "preferred_deposit_validator" / PUBLIC_KEY_LAYOUT, - "preferred_withdraw_validator_option" / Int8ul, - "preferred_withdraw_validator" / PUBLIC_KEY_LAYOUT, - "stake_deposit_fee" / FEE_LAYOUT, - "stake_withdrawal_fee" / FEE_LAYOUT, - "next_stake_withdrawal_fee_option" / Int8ul, - "next_stake_withdrawal_fee" / FEE_LAYOUT, - "stake_referral_fee" / Int8ul, - "sol_deposit_authority_option" / Int8ul, - "sol_deposit_authority" / PUBLIC_KEY_LAYOUT, - "sol_deposit_fee" / FEE_LAYOUT, - "sol_referral_fee" / Int8ul, - "sol_withdraw_authority_option" / Int8ul, - "sol_withdraw_authority" / PUBLIC_KEY_LAYOUT, - "sol_withdrawal_fee" / FEE_LAYOUT, - "next_sol_withdrawal_fee_option" / Int8ul, - "next_sol_withdrawal_fee" / FEE_LAYOUT, - "last_epoch_pool_token_supply" / Int64ul, - "last_epoch_total_lamports" / Int64ul, -) - -DECODE_STAKE_POOL_LAYOUT = Struct( - "account_type" / Int8ul, - "manager" / PUBLIC_KEY_LAYOUT, - "staker" / PUBLIC_KEY_LAYOUT, - "stake_deposit_authority" / PUBLIC_KEY_LAYOUT, - "stake_withdraw_bump_seed" / Int8ul, - "validator_list" / PUBLIC_KEY_LAYOUT, - "reserve_stake" / PUBLIC_KEY_LAYOUT, - "pool_mint" / PUBLIC_KEY_LAYOUT, - "manager_fee_account" / PUBLIC_KEY_LAYOUT, - "token_program_id" / PUBLIC_KEY_LAYOUT, - "total_lamports" / Int64ul, - "pool_token_supply" / Int64ul, - "last_update_epoch" / Int64ul, - "lockup" / LOCKUP_LAYOUT, - "epoch_fee" / FEE_LAYOUT, - "next_epoch_fee_option" / Int8ul, - "next_epoch_fee" / Switch( - lambda this: this.next_epoch_fee_option, - { - 0: Pass, - 1: FEE_LAYOUT, - }), - "preferred_deposit_validator_option" / Int8ul, - "preferred_deposit_validator" / Switch( - lambda this: this.preferred_deposit_validator_option, - { - 0: Pass, - 1: PUBLIC_KEY_LAYOUT, - }), - "preferred_withdraw_validator_option" / Int8ul, - "preferred_withdraw_validator" / Switch( - lambda this: this.preferred_withdraw_validator_option, - { - 0: Pass, - 1: PUBLIC_KEY_LAYOUT, - }), - "stake_deposit_fee" / FEE_LAYOUT, - "stake_withdrawal_fee" / FEE_LAYOUT, - "next_stake_withdrawal_fee_option" / Int8ul, - "next_stake_withdrawal_fee" / Switch( - lambda this: this.next_stake_withdrawal_fee_option, - { - 0: Pass, - 1: FEE_LAYOUT, - }), - "stake_referral_fee" / Int8ul, - "sol_deposit_authority_option" / Int8ul, - "sol_deposit_authority" / Switch( - lambda this: this.sol_deposit_authority_option, - { - 0: Pass, - 1: PUBLIC_KEY_LAYOUT, - }), - "sol_deposit_fee" / FEE_LAYOUT, - "sol_referral_fee" / Int8ul, - "sol_withdraw_authority_option" / Int8ul, - "sol_withdraw_authority" / Switch( - lambda this: this.sol_withdraw_authority_option, - { - 0: Pass, - 1: PUBLIC_KEY_LAYOUT, - }), - "sol_withdrawal_fee" / FEE_LAYOUT, - "next_sol_withdrawal_fee_option" / Int8ul, - "next_sol_withdrawal_fee" / Switch( - lambda this: this.next_sol_withdrawal_fee_option, - { - 0: Pass, - 1: FEE_LAYOUT, - }), - "last_epoch_pool_token_supply" / Int64ul, - "last_epoch_total_lamports" / Int64ul, -) - -VALIDATOR_INFO_LAYOUT = Struct( - "active_stake_lamports" / Int64ul, - "transient_stake_lamports" / Int64ul, - "last_update_epoch" / Int64ul, - "transient_seed_suffix" / Int64ul, - "unused" / Int32ul, - "validator_seed_suffix" / Int32ul, - "status" / Int8ul, - "vote_account_address" / PUBLIC_KEY_LAYOUT, -) - -VALIDATOR_LIST_LAYOUT = Struct( - "account_type" / Int8ul, - "max_validators" / Int32ul, - "validators_len" / Int32ul, -) - -DECODE_VALIDATOR_LIST_LAYOUT = Struct( - "account_type" / Int8ul, - "max_validators" / Int32ul, - "validators_len" / Int32ul, - "validators" / VALIDATOR_INFO_LAYOUT[lambda this: this.validators_len], -) diff --git a/stake-pool/py/system/__init__.py b/stake-pool/py/system/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/stake-pool/py/system/actions.py b/stake-pool/py/system/actions.py deleted file mode 100644 index c4c025494b7..00000000000 --- a/stake-pool/py/system/actions.py +++ /dev/null @@ -1,8 +0,0 @@ -from solders.pubkey import Pubkey -from solana.rpc.async_api import AsyncClient - - -async def airdrop(client: AsyncClient, receiver: Pubkey, lamports: int): - print(f"Airdropping {lamports} lamports to {receiver}...") - resp = await client.request_airdrop(receiver, lamports) - await client.confirm_transaction(resp.value) diff --git a/stake-pool/py/tests/conftest.py b/stake-pool/py/tests/conftest.py deleted file mode 100644 index 647e9594f2a..00000000000 --- a/stake-pool/py/tests/conftest.py +++ /dev/null @@ -1,121 +0,0 @@ -import asyncio -import pytest -import pytest_asyncio -import os -import shutil -import tempfile -from typing import AsyncIterator, List, Tuple -from subprocess import Popen - -from solders.keypair import Keypair -from solders.pubkey import Pubkey -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed - -from spl.token.instructions import get_associated_token_address - -from vote.actions import create_vote -from system.actions import airdrop -from stake_pool.actions import deposit_sol, create_all, add_validator_to_pool -from stake_pool.state import Fee - -NUM_SLOTS_PER_EPOCH: int = 32 -AIRDROP_LAMPORTS: int = 30_000_000_000 - - -@pytest.fixture(scope="session") -def solana_test_validator(): - old_cwd = os.getcwd() - newpath = tempfile.mkdtemp() - os.chdir(newpath) - validator = Popen([ - "solana-test-validator", - "--reset", "--quiet", - "--bpf-program", "SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy", - f"{old_cwd}/../../target/deploy/spl_stake_pool.so", - "--bpf-program", "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", - f"{old_cwd}/../program/tests/fixtures/mpl_token_metadata.so", - "--slots-per-epoch", str(NUM_SLOTS_PER_EPOCH), - ],) - yield - validator.kill() - os.chdir(old_cwd) - shutil.rmtree(newpath) - - -@pytest_asyncio.fixture -async def validators(async_client, payer) -> List[Pubkey]: - num_validators = 3 - validators = [] - for i in range(num_validators): - vote = Keypair() - node = Keypair() - await create_vote(async_client, payer, vote, node, payer.pubkey(), payer.pubkey(), 10) - validators.append(vote.pubkey()) - return validators - - -@pytest_asyncio.fixture -async def stake_pool_addresses( - async_client, payer, validators, waiter -) -> Tuple[Pubkey, Pubkey, Pubkey]: - fee = Fee(numerator=1, denominator=1000) - referral_fee = 20 - await waiter.wait_for_next_epoch_if_soon(async_client) - stake_pool_addresses = await create_all(async_client, payer, fee, referral_fee) - stake_pool = stake_pool_addresses[0] - pool_mint = stake_pool_addresses[2] - token_account = get_associated_token_address(payer.pubkey(), pool_mint) - await deposit_sol(async_client, payer, stake_pool, token_account, AIRDROP_LAMPORTS // 2) - for validator in validators: - await add_validator_to_pool(async_client, payer, stake_pool, validator) - return stake_pool_addresses - - -@pytest_asyncio.fixture -async def async_client(solana_test_validator) -> AsyncIterator[AsyncClient]: - async_client = AsyncClient(commitment=Confirmed) - total_attempts = 20 - current_attempt = 0 - while not await async_client.is_connected(): - if current_attempt == total_attempts: - raise Exception("Could not connect to test validator") - else: - current_attempt += 1 - await asyncio.sleep(1.0) - yield async_client - await async_client.close() - - -@pytest_asyncio.fixture -async def payer(async_client) -> Keypair: - payer = Keypair() - await airdrop(async_client, payer.pubkey(), AIRDROP_LAMPORTS) - return payer - - -class Waiter: - @staticmethod - async def wait_for_next_epoch(async_client: AsyncClient): - resp = await async_client.get_epoch_info(commitment=Confirmed) - current_epoch = resp.value.epoch - next_epoch = current_epoch - while current_epoch == next_epoch: - await asyncio.sleep(1.0) - resp = await async_client.get_epoch_info(commitment=Confirmed) - next_epoch = resp.value.epoch - await asyncio.sleep(0.4) # wait one more block to avoid reward payout time - - @staticmethod - async def wait_for_next_epoch_if_soon(async_client: AsyncClient): - resp = await async_client.get_epoch_info(commitment=Confirmed) - if resp.value.slots_in_epoch - resp.value.slot_index < NUM_SLOTS_PER_EPOCH // 2: - await Waiter.wait_for_next_epoch(async_client) - return True - else: - return False - - -@pytest.fixture -def waiter() -> Waiter: - return Waiter() diff --git a/stake-pool/py/tests/test_a_time_sensitive.py b/stake-pool/py/tests/test_a_time_sensitive.py deleted file mode 100644 index 59e59e9e89f..00000000000 --- a/stake-pool/py/tests/test_a_time_sensitive.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Time sensitive test, so run it first out of the bunch.""" -import asyncio -import pytest -from solana.rpc.commitment import Confirmed -from spl.token.instructions import get_associated_token_address - -from stake.constants import STAKE_LEN -from stake_pool.actions import deposit_sol, decrease_validator_stake, increase_validator_stake, update_stake_pool -from stake_pool.constants import MINIMUM_ACTIVE_STAKE -from stake_pool.state import StakePool, ValidatorList - - -@pytest.mark.asyncio -async def test_increase_decrease_this_is_very_slow(async_client, validators, payer, stake_pool_addresses, waiter): - (stake_pool_address, validator_list_address, _) = stake_pool_addresses - - resp = await async_client.get_minimum_balance_for_rent_exemption(STAKE_LEN) - stake_rent_exemption = resp.value - minimum_amount = MINIMUM_ACTIVE_STAKE + stake_rent_exemption - increase_amount = MINIMUM_ACTIVE_STAKE * 4 - decrease_amount = increase_amount // 2 - deposit_amount = (increase_amount + stake_rent_exemption) * len(validators) - - resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - token_account = get_associated_token_address(payer.pubkey(), stake_pool.pool_mint) - await deposit_sol(async_client, payer, stake_pool_address, token_account, deposit_amount) - - # increase to all - futures = [ - increase_validator_stake(async_client, payer, payer, stake_pool_address, validator, increase_amount // 2) - for validator in validators - ] - await asyncio.gather(*futures) - - # validate the increase is now on the transient account - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - for validator in validator_list.validators: - assert validator.transient_stake_lamports == increase_amount // 2 + stake_rent_exemption - assert validator.active_stake_lamports == minimum_amount - - # increase the same amount to test the increase additional instruction - futures = [ - increase_validator_stake(async_client, payer, payer, stake_pool_address, validator, increase_amount // 2, - ephemeral_stake_seed=0) - for validator in validators - ] - await asyncio.gather(*futures) - - # validate the additional increase is now on the transient account - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - for validator in validator_list.validators: - assert validator.transient_stake_lamports == increase_amount + stake_rent_exemption * 2 - assert validator.active_stake_lamports == minimum_amount - - print("Waiting for epoch to roll over") - await waiter.wait_for_next_epoch(async_client) - await update_stake_pool(async_client, payer, stake_pool_address) - - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - for validator in validator_list.validators: - assert validator.last_update_epoch != 0 - assert validator.transient_stake_lamports == 0 - assert validator.active_stake_lamports == increase_amount + minimum_amount + stake_rent_exemption - - # decrease from all - futures = [ - decrease_validator_stake(async_client, payer, payer, stake_pool_address, validator, decrease_amount) - for validator in validators - ] - await asyncio.gather(*futures) - - # validate the decrease is now on the transient account - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - for validator in validator_list.validators: - assert validator.transient_stake_lamports == decrease_amount + stake_rent_exemption - assert validator.active_stake_lamports == increase_amount - decrease_amount + minimum_amount + \ - stake_rent_exemption - - # DO NOT test decrese additional instruction as it is confirmed NOT to be working as advertised - - # roll over one epoch and verify we have the balances that we expect - expected_active_stake_lamports = increase_amount - decrease_amount + minimum_amount + stake_rent_exemption - - print("Waiting for epoch to roll over") - await waiter.wait_for_next_epoch(async_client) - await update_stake_pool(async_client, payer, stake_pool_address) - - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - for validator in validator_list.validators: - assert validator.transient_stake_lamports == 0 - assert validator.active_stake_lamports == expected_active_stake_lamports diff --git a/stake-pool/py/tests/test_add_remove.py b/stake-pool/py/tests/test_add_remove.py deleted file mode 100644 index abf47871ef8..00000000000 --- a/stake-pool/py/tests/test_add_remove.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncio -import pytest -from solana.rpc.commitment import Confirmed - -from stake.constants import STAKE_LEN -from stake_pool.actions import remove_validator_from_pool -from stake_pool.constants import MINIMUM_ACTIVE_STAKE -from stake_pool.state import ValidatorList, StakeStatus - - -@pytest.mark.asyncio -async def test_add_remove_validators(async_client, validators, payer, stake_pool_addresses): - (stake_pool_address, validator_list_address, _) = stake_pool_addresses - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - assert len(validator_list.validators) == len(validators) - resp = await async_client.get_minimum_balance_for_rent_exemption(STAKE_LEN) - stake_rent_exemption = resp.value - futures = [] - for validator_info in validator_list.validators: - assert validator_info.vote_account_address in validators - assert validator_info.active_stake_lamports == stake_rent_exemption + MINIMUM_ACTIVE_STAKE - assert validator_info.transient_stake_lamports == 0 - assert validator_info.status == StakeStatus.ACTIVE - futures.append( - remove_validator_from_pool(async_client, payer, stake_pool_address, validator_info.vote_account_address) - ) - await asyncio.gather(*futures) - - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - for validator_info in validator_list.validators: - assert validator_info.status == StakeStatus.DEACTIVATING_VALIDATOR diff --git a/stake-pool/py/tests/test_bot_rebalance.py b/stake-pool/py/tests/test_bot_rebalance.py deleted file mode 100644 index c7da3f82586..00000000000 --- a/stake-pool/py/tests/test_bot_rebalance.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Time sensitive test, so run it first out of the bunch.""" -import pytest -from solana.rpc.commitment import Confirmed -from spl.token.instructions import get_associated_token_address - -from stake.constants import STAKE_LEN, LAMPORTS_PER_SOL -from stake_pool.actions import deposit_sol -from stake_pool.constants import MINIMUM_ACTIVE_STAKE, MINIMUM_RESERVE_LAMPORTS -from stake_pool.state import StakePool, ValidatorList - -from bot.rebalance import rebalance - - -ENDPOINT: str = "http://127.0.0.1:8899" - - -@pytest.mark.asyncio -async def test_rebalance_this_is_very_slow(async_client, validators, payer, stake_pool_addresses, waiter): - (stake_pool_address, validator_list_address, _) = stake_pool_addresses - resp = await async_client.get_minimum_balance_for_rent_exemption(STAKE_LEN) - stake_rent_exemption = resp.value - # With minimum delegation at MINIMUM_DELEGATION + rent-exemption, when - # decreasing, we'll need rent exemption + minimum delegation delegated to - # cover all movements - minimum_amount = MINIMUM_ACTIVE_STAKE + stake_rent_exemption - increase_amount = MINIMUM_ACTIVE_STAKE + stake_rent_exemption - deposit_amount = (increase_amount + stake_rent_exemption) * len(validators) - - resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - total_lamports = stake_pool.total_lamports + deposit_amount - token_account = get_associated_token_address(payer.pubkey(), stake_pool.pool_mint) - await deposit_sol(async_client, payer, stake_pool_address, token_account, deposit_amount) - - # Test case 1: Increase everywhere - await rebalance(ENDPOINT, stake_pool_address, payer, 0.0) - - # should only have minimum left - resp = await async_client.get_account_info(stake_pool.reserve_stake, commitment=Confirmed) - assert resp.value.lamports == stake_rent_exemption + MINIMUM_RESERVE_LAMPORTS - - # should all be the same - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - for validator in validator_list.validators: - assert validator.active_stake_lamports == minimum_amount - assert validator.transient_stake_lamports == total_lamports / len(validators) - minimum_amount - - # Test case 2: Decrease everything back to reserve - print('Waiting for next epoch') - await waiter.wait_for_next_epoch(async_client) - max_in_reserve = total_lamports - minimum_amount * len(validators) - await rebalance(ENDPOINT, stake_pool_address, payer, max_in_reserve / LAMPORTS_PER_SOL) - - # should still only have minimum left - resp = await async_client.get_account_info(stake_pool.reserve_stake, commitment=Confirmed) - reserve_lamports = resp.value.lamports - assert reserve_lamports == stake_rent_exemption + MINIMUM_RESERVE_LAMPORTS - - # should all be decreasing now - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - for validator in validator_list.validators: - assert validator.active_stake_lamports == minimum_amount - assert validator.transient_stake_lamports == max_in_reserve / len(validators) - - # Test case 3: Do nothing - print('Waiting for next epoch') - await waiter.wait_for_next_epoch(async_client) - await rebalance(ENDPOINT, stake_pool_address, payer, max_in_reserve / LAMPORTS_PER_SOL) - - # should still only have minimum left + rent exemptions from increase - resp = await async_client.get_account_info(stake_pool.reserve_stake, commitment=Confirmed) - reserve_lamports = resp.value.lamports - assert reserve_lamports == stake_rent_exemption + max_in_reserve + MINIMUM_RESERVE_LAMPORTS - - # should all be decreased now - resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - validator_list = ValidatorList.decode(data) - for validator in validator_list.validators: - assert validator.active_stake_lamports == minimum_amount - assert validator.transient_stake_lamports == 0 diff --git a/stake-pool/py/tests/test_create.py b/stake-pool/py/tests/test_create.py deleted file mode 100644 index 3b5a42a18c0..00000000000 --- a/stake-pool/py/tests/test_create.py +++ /dev/null @@ -1,71 +0,0 @@ -import pytest -from solders.keypair import Keypair -from solana.rpc.commitment import Confirmed -from spl.token.constants import TOKEN_PROGRAM_ID - -from stake_pool.constants import \ - find_withdraw_authority_program_address, \ - MINIMUM_RESERVE_LAMPORTS, \ - STAKE_POOL_PROGRAM_ID -from stake_pool.state import StakePool, Fee - -from stake.actions import create_stake -from stake_pool.actions import create -from spl_token.actions import create_mint, create_associated_token_account - - -@pytest.mark.asyncio -async def test_create_stake_pool(async_client, payer): - stake_pool = Keypair() - validator_list = Keypair() - (pool_withdraw_authority, seed) = find_withdraw_authority_program_address( - STAKE_POOL_PROGRAM_ID, stake_pool.pubkey()) - - reserve_stake = Keypair() - await create_stake(async_client, payer, reserve_stake, pool_withdraw_authority, MINIMUM_RESERVE_LAMPORTS) - - pool_mint = Keypair() - await create_mint(async_client, payer, pool_mint, pool_withdraw_authority) - - manager_fee_account = await create_associated_token_account( - async_client, - payer, - payer.pubkey(), - pool_mint.pubkey(), - ) - - fee = Fee(numerator=1, denominator=1000) - referral_fee = 20 - await create( - async_client, payer, stake_pool, validator_list, pool_mint.pubkey(), - reserve_stake.pubkey(), manager_fee_account, fee, referral_fee) - resp = await async_client.get_account_info(stake_pool.pubkey(), commitment=Confirmed) - assert resp.value.owner == STAKE_POOL_PROGRAM_ID - data = resp.value.data if resp.value else bytes() - pool_data = StakePool.decode(data) - assert pool_data.manager == payer.pubkey() - assert pool_data.staker == payer.pubkey() - assert pool_data.stake_withdraw_bump_seed == seed - assert pool_data.validator_list == validator_list.pubkey() - assert pool_data.reserve_stake == reserve_stake.pubkey() - assert pool_data.pool_mint == pool_mint.pubkey() - assert pool_data.manager_fee_account == manager_fee_account - assert pool_data.token_program_id == TOKEN_PROGRAM_ID - assert pool_data.total_lamports == 0 - assert pool_data.pool_token_supply == 0 - assert pool_data.epoch_fee == fee - assert pool_data.next_epoch_fee is None - assert pool_data.preferred_deposit_validator is None - assert pool_data.preferred_withdraw_validator is None - assert pool_data.stake_deposit_fee == fee - assert pool_data.stake_withdrawal_fee == fee - assert pool_data.next_stake_withdrawal_fee is None - assert pool_data.stake_referral_fee == referral_fee - assert pool_data.sol_deposit_authority is None - assert pool_data.sol_deposit_fee == fee - assert pool_data.sol_referral_fee == referral_fee - assert pool_data.sol_withdraw_authority is None - assert pool_data.sol_withdrawal_fee == fee - assert pool_data.next_sol_withdrawal_fee is None - assert pool_data.last_epoch_pool_token_supply == 0 - assert pool_data.last_epoch_total_lamports == 0 diff --git a/stake-pool/py/tests/test_create_update_token_metadata.py b/stake-pool/py/tests/test_create_update_token_metadata.py deleted file mode 100644 index 2ee0a7db226..00000000000 --- a/stake-pool/py/tests/test_create_update_token_metadata.py +++ /dev/null @@ -1,63 +0,0 @@ -import pytest -from stake_pool.actions import create_all, create_token_metadata, update_token_metadata -from stake_pool.state import Fee, StakePool -from solana.rpc.commitment import Confirmed -from stake_pool.constants import find_metadata_account - - -@pytest.mark.asyncio -async def test_create_metadata_success(async_client, waiter, payer): - fee = Fee(numerator=1, denominator=1000) - referral_fee = 20 - await waiter.wait_for_next_epoch_if_soon(async_client) - (stake_pool_address, _validator_list_address, _) = await create_all(async_client, payer, fee, referral_fee) - resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - name = "test_name" - symbol = "SYM" - uri = "test_uri" - await create_token_metadata(async_client, payer, stake_pool_address, name, symbol, uri) - - (metadata_program_address, _seed) = find_metadata_account(stake_pool.pool_mint) - resp = await async_client.get_account_info(metadata_program_address, commitment=Confirmed) - raw_data = resp.value.data if resp.value else bytes() - assert name == str(raw_data[69:101], "utf-8")[:len(name)] - assert symbol == str(raw_data[105:115], "utf-8")[:len(symbol)] - assert uri == str(raw_data[119:319], "utf-8")[:len(uri)] - - -@pytest.mark.asyncio -async def test_update_metadata_success(async_client, waiter, payer): - fee = Fee(numerator=1, denominator=1000) - referral_fee = 20 - await waiter.wait_for_next_epoch_if_soon(async_client) - (stake_pool_address, _validator_list_address, _) = await create_all(async_client, payer, fee, referral_fee) - resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - - name = "test_name" - symbol = "SYM" - uri = "test_uri" - await create_token_metadata(async_client, payer, stake_pool_address, name, symbol, uri) - - (metadata_program_address, _seed) = find_metadata_account(stake_pool.pool_mint) - resp = await async_client.get_account_info(metadata_program_address, commitment=Confirmed) - raw_data = resp.value.data if resp.value else bytes() - assert name == str(raw_data[69:101], "utf-8")[:len(name)] - assert symbol == str(raw_data[105:115], "utf-8")[:len(symbol)] - assert uri == str(raw_data[119:319], "utf-8")[:len(uri)] - - updated_name = "updated_name" - updated_symbol = "USM" - updated_uri = "updated_uri" - await update_token_metadata(async_client, payer, stake_pool_address, updated_name, updated_symbol, updated_uri) - - (metadata_program_address, _seed) = find_metadata_account(stake_pool.pool_mint) - resp = await async_client.get_account_info(metadata_program_address, commitment=Confirmed) - raw_data = resp.value.data if resp.value else bytes() - assert updated_name == str(raw_data[69:101], "utf-8")[:len(updated_name)] - assert updated_symbol == str(raw_data[105:115], "utf-8")[:len(updated_symbol)] - assert updated_uri == str(raw_data[119:319], "utf-8")[:len(updated_uri)] diff --git a/stake-pool/py/tests/test_deposit_withdraw_sol.py b/stake-pool/py/tests/test_deposit_withdraw_sol.py deleted file mode 100644 index 438c4afd293..00000000000 --- a/stake-pool/py/tests/test_deposit_withdraw_sol.py +++ /dev/null @@ -1,28 +0,0 @@ -import pytest -from solana.rpc.commitment import Confirmed, Processed -from solders.keypair import Keypair -from spl.token.instructions import get_associated_token_address - -from stake_pool.state import Fee, StakePool -from stake_pool.actions import create_all, deposit_sol, withdraw_sol - - -@pytest.mark.asyncio -async def test_deposit_withdraw_sol(async_client, waiter, payer): - fee = Fee(numerator=1, denominator=1000) - referral_fee = 20 - await waiter.wait_for_next_epoch(async_client) - (stake_pool_address, validator_list_address, _) = await create_all(async_client, payer, fee, referral_fee) - resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - token_account = get_associated_token_address(payer.pubkey(), stake_pool.pool_mint) - deposit_amount = 100_000_000 - await deposit_sol(async_client, payer, stake_pool_address, token_account, deposit_amount) - pool_token_balance = await async_client.get_token_account_balance(token_account, Confirmed) - assert pool_token_balance.value.amount == str(deposit_amount) - recipient = Keypair() - await withdraw_sol(async_client, payer, token_account, stake_pool_address, recipient.pubkey(), deposit_amount) - # for some reason, this is not always in sync when running all tests - pool_token_balance = await async_client.get_token_account_balance(token_account, Processed) - assert pool_token_balance.value.amount == str('0') diff --git a/stake-pool/py/tests/test_deposit_withdraw_stake.py b/stake-pool/py/tests/test_deposit_withdraw_stake.py deleted file mode 100644 index 63b9e07dbc1..00000000000 --- a/stake-pool/py/tests/test_deposit_withdraw_stake.py +++ /dev/null @@ -1,52 +0,0 @@ -import pytest -from solana.rpc.commitment import Confirmed -from solders.keypair import Keypair -from spl.token.instructions import get_associated_token_address - -from stake.actions import create_stake, delegate_stake -from stake.constants import STAKE_LEN -from stake.state import StakeStake -from stake_pool.actions import deposit_stake, withdraw_stake, update_stake_pool -from stake_pool.constants import MINIMUM_ACTIVE_STAKE -from stake_pool.state import StakePool - - -@pytest.mark.asyncio -async def test_deposit_withdraw_stake(async_client, validators, payer, stake_pool_addresses, waiter): - (stake_pool_address, validator_list_address, _) = stake_pool_addresses - resp = await async_client.get_account_info(stake_pool_address, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_pool = StakePool.decode(data) - validator = next(iter(validators)) - stake_amount = MINIMUM_ACTIVE_STAKE - stake = Keypair() - await create_stake(async_client, payer, stake, payer.pubkey(), stake_amount) - stake = stake.pubkey() - await delegate_stake(async_client, payer, payer, stake, validator) - resp = await async_client.get_account_info(stake, commitment=Confirmed) - data = resp.value.data if resp.value else bytes() - stake_state = StakeStake.decode(data) - token_account = get_associated_token_address(payer.pubkey(), stake_pool.pool_mint) - pre_pool_token_balance = await async_client.get_token_account_balance(token_account, Confirmed) - pre_pool_token_balance = int(pre_pool_token_balance.value.amount) - print(stake_state) - - await waiter.wait_for_next_epoch(async_client) - - await update_stake_pool(async_client, payer, stake_pool_address) - await deposit_stake(async_client, payer, stake_pool_address, validator, stake, token_account) - pool_token_balance = await async_client.get_token_account_balance(token_account, Confirmed) - pool_token_balance = pool_token_balance.value.amount - resp = await async_client.get_minimum_balance_for_rent_exemption(STAKE_LEN) - stake_rent_exemption = resp.value - assert pool_token_balance == str(stake_amount + stake_rent_exemption + pre_pool_token_balance) - - destination_stake = Keypair() - await withdraw_stake( - async_client, payer, payer, destination_stake, stake_pool_address, validator, - payer.pubkey(), token_account, stake_amount - ) - - pool_token_balance = await async_client.get_token_account_balance(token_account, Confirmed) - pool_token_balance = pool_token_balance.value.amount - assert pool_token_balance == str(stake_rent_exemption + pre_pool_token_balance) diff --git a/stake-pool/py/tests/test_stake.py b/stake-pool/py/tests/test_stake.py deleted file mode 100644 index 7f89b8a75cf..00000000000 --- a/stake-pool/py/tests/test_stake.py +++ /dev/null @@ -1,33 +0,0 @@ -import asyncio -import pytest -from solders.keypair import Keypair - -from stake.actions import authorize, create_stake, delegate_stake -from stake.constants import MINIMUM_DELEGATION -from stake.state import StakeAuthorize - - -@pytest.mark.asyncio -async def test_create_stake(async_client, payer): - stake = Keypair() - await create_stake(async_client, payer, stake, payer.pubkey(), 1) - - -@pytest.mark.asyncio -async def test_delegate_stake(async_client, validators, payer): - validator = validators[0] - stake = Keypair() - await create_stake(async_client, payer, stake, payer.pubkey(), MINIMUM_DELEGATION) - await delegate_stake(async_client, payer, payer, stake.pubkey(), validator) - - -@pytest.mark.asyncio -async def test_authorize_stake(async_client, payer): - stake = Keypair() - new_authority = Keypair() - await create_stake(async_client, payer, stake, payer.pubkey(), MINIMUM_DELEGATION) - await asyncio.gather( - authorize(async_client, payer, payer, stake.pubkey(), new_authority.pubkey(), StakeAuthorize.STAKER), - authorize(async_client, payer, payer, stake.pubkey(), new_authority.pubkey(), StakeAuthorize.WITHDRAWER) - ) - await authorize(async_client, payer, new_authority, stake.pubkey(), payer.pubkey(), StakeAuthorize.WITHDRAWER) diff --git a/stake-pool/py/tests/test_system.py b/stake-pool/py/tests/test_system.py deleted file mode 100644 index f3b578ae2f1..00000000000 --- a/stake-pool/py/tests/test_system.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest -from solders.keypair import Keypair -from solana.rpc.commitment import Confirmed - -import system.actions - - -@pytest.mark.asyncio -async def test_airdrop(async_client): - manager = Keypair() - airdrop_lamports = 1_000_000 - await system.actions.airdrop(async_client, manager.pubkey(), airdrop_lamports) - resp = await async_client.get_balance(manager.pubkey(), commitment=Confirmed) - assert resp.value == airdrop_lamports diff --git a/stake-pool/py/tests/test_token.py b/stake-pool/py/tests/test_token.py deleted file mode 100644 index 835feb15f73..00000000000 --- a/stake-pool/py/tests/test_token.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest -from solders.keypair import Keypair - -from spl_token.actions import create_mint, create_associated_token_account - - -@pytest.mark.asyncio -async def test_create_mint(async_client, payer): - pool_mint = Keypair() - await create_mint(async_client, payer, pool_mint, payer.pubkey()) - await create_associated_token_account( - async_client, - payer, - payer.pubkey(), - pool_mint.pubkey(), - ) diff --git a/stake-pool/py/tests/test_vote.py b/stake-pool/py/tests/test_vote.py deleted file mode 100644 index 3b3ff460fbb..00000000000 --- a/stake-pool/py/tests/test_vote.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest -from solders.keypair import Keypair -from solana.rpc.commitment import Confirmed - -from vote.actions import create_vote -from vote.constants import VOTE_PROGRAM_ID - - -@pytest.mark.asyncio -async def test_create_vote(async_client, payer): - vote = Keypair() - node = Keypair() - await create_vote(async_client, payer, vote, node, payer.pubkey(), payer.pubkey(), 10) - resp = await async_client.get_account_info(vote.pubkey(), commitment=Confirmed) - assert resp.value.owner == VOTE_PROGRAM_ID diff --git a/stake-pool/py/vote/__init__.py b/stake-pool/py/vote/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/stake-pool/py/vote/actions.py b/stake-pool/py/vote/actions.py deleted file mode 100644 index 305d79a4c26..00000000000 --- a/stake-pool/py/vote/actions.py +++ /dev/null @@ -1,47 +0,0 @@ -from solders.pubkey import Pubkey -from solders.keypair import Keypair -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed -from solana.rpc.types import TxOpts -from solders.sysvar import CLOCK, RENT -from solana.transaction import Transaction -import solders.system_program as sys - -from vote.constants import VOTE_PROGRAM_ID, VOTE_STATE_LEN -from vote.instructions import initialize, InitializeParams - - -async def create_vote( - client: AsyncClient, payer: Keypair, vote: Keypair, node: Keypair, - voter: Pubkey, withdrawer: Pubkey, commission: int): - print(f"Creating vote account {vote.pubkey()}") - resp = await client.get_minimum_balance_for_rent_exemption(VOTE_STATE_LEN) - txn = Transaction(fee_payer=payer.pubkey()) - txn.add( - sys.create_account( - sys.CreateAccountParams( - from_pubkey=payer.pubkey(), - to_pubkey=vote.pubkey(), - lamports=resp.value, - space=VOTE_STATE_LEN, - owner=VOTE_PROGRAM_ID, - ) - ) - ) - txn.add( - initialize( - InitializeParams( - vote=vote.pubkey(), - rent_sysvar=RENT, - clock_sysvar=CLOCK, - node=node.pubkey(), - authorized_voter=voter, - authorized_withdrawer=withdrawer, - commission=commission, - ) - ) - ) - recent_blockhash = (await client.get_latest_blockhash()).value.blockhash - await client.send_transaction( - txn, payer, vote, node, recent_blockhash=recent_blockhash, - opts=TxOpts(skip_confirmation=False, preflight_commitment=Confirmed)) diff --git a/stake-pool/py/vote/constants.py b/stake-pool/py/vote/constants.py deleted file mode 100644 index 0da3846bea3..00000000000 --- a/stake-pool/py/vote/constants.py +++ /dev/null @@ -1,8 +0,0 @@ -from solders.pubkey import Pubkey - - -VOTE_PROGRAM_ID = Pubkey.from_string("Vote111111111111111111111111111111111111111") -"""Program id for the native vote program.""" - -VOTE_STATE_LEN: int = 3762 -"""Size of vote account.""" diff --git a/stake-pool/py/vote/instructions.py b/stake-pool/py/vote/instructions.py deleted file mode 100644 index c8a0a69d803..00000000000 --- a/stake-pool/py/vote/instructions.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Vote Program Instructions.""" - -from enum import IntEnum -from typing import NamedTuple - -from construct import Bytes, Struct, Switch, Int8ul, Int32ul, Pass # type: ignore - -from solders.pubkey import Pubkey -from solders.sysvar import CLOCK, RENT -from solders.instruction import AccountMeta, Instruction - -from vote.constants import VOTE_PROGRAM_ID - -PUBLIC_KEY_LAYOUT = Bytes(32) - - -class InitializeParams(NamedTuple): - """Initialize vote account params.""" - - vote: Pubkey - """`[w]` Uninitialized vote account""" - rent_sysvar: Pubkey - """`[]` Rent sysvar.""" - clock_sysvar: Pubkey - """`[]` Clock sysvar.""" - node: Pubkey - """`[s]` New validator identity.""" - - authorized_voter: Pubkey - """The authorized voter for this vote account.""" - authorized_withdrawer: Pubkey - """The authorized withdrawer for this vote account.""" - commission: int - """Commission, represented as a percentage""" - - -class InstructionType(IntEnum): - """Vote Instruction Types.""" - - INITIALIZE = 0 - AUTHORIZE = 1 - VOTE = 2 - WITHDRAW = 3 - UPDATE_VALIDATOR_IDENTITY = 4 - UPDATE_COMMISSION = 5 - VOTE_SWITCH = 6 - AUTHORIZE_CHECKED = 7 - - -INITIALIZE_LAYOUT = Struct( - "node" / PUBLIC_KEY_LAYOUT, - "authorized_voter" / PUBLIC_KEY_LAYOUT, - "authorized_withdrawer" / PUBLIC_KEY_LAYOUT, - "commission" / Int8ul, -) - -INSTRUCTIONS_LAYOUT = Struct( - "instruction_type" / Int32ul, - "args" - / Switch( - lambda this: this.instruction_type, - { - InstructionType.INITIALIZE: INITIALIZE_LAYOUT, - InstructionType.AUTHORIZE: Pass, # TODO - InstructionType.VOTE: Pass, # TODO - InstructionType.WITHDRAW: Pass, # TODO - InstructionType.UPDATE_VALIDATOR_IDENTITY: Pass, # TODO - InstructionType.UPDATE_COMMISSION: Pass, # TODO - InstructionType.VOTE_SWITCH: Pass, # TODO - InstructionType.AUTHORIZE_CHECKED: Pass, # TODO - }, - ), -) - - -def initialize(params: InitializeParams) -> Instruction: - """Creates a transaction instruction to initialize a new stake.""" - data = INSTRUCTIONS_LAYOUT.build( - dict( - instruction_type=InstructionType.INITIALIZE, - args=dict( - node=bytes(params.node), - authorized_voter=bytes(params.authorized_voter), - authorized_withdrawer=bytes(params.authorized_withdrawer), - commission=params.commission, - ), - ) - ) - return Instruction( - program_id=VOTE_PROGRAM_ID, - accounts=[ - AccountMeta(pubkey=params.vote, is_signer=False, is_writable=True), - AccountMeta(pubkey=params.rent_sysvar or RENT, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.clock_sysvar or CLOCK, is_signer=False, is_writable=False), - AccountMeta(pubkey=params.node, is_signer=True, is_writable=False), - ], - data=data, - ) diff --git a/stateless-asks/program/Cargo.toml b/stateless-asks/program/Cargo.toml index be6be89ee78..2d4288f74c0 100644 --- a/stateless-asks/program/Cargo.toml +++ b/stateless-asks/program/Cargo.toml @@ -13,10 +13,10 @@ test-sbf = [] [dependencies] borsh = "1.5.3" solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ +spl-token = { version = "7.0", features = [ "no-entrypoint", ] } -spl-associated-token-account-client = { version = "2.0.0", path = "../../associated-token-account/client" } +spl-associated-token-account-client = { version = "2.0.0" } thiserror = "2.0" [dev-dependencies] diff --git a/token-collection/program/Cargo.toml b/token-collection/program/Cargo.toml index c26d5f3a7d9..535315a2ae3 100644 --- a/token-collection/program/Cargo.toml +++ b/token-collection/program/Cargo.toml @@ -13,19 +13,19 @@ test-sbf = [] [dependencies] solana-program = "2.1.0" -spl-pod = { version = "0.5.0", path = "../../libraries/pod" } -spl-program-error = { version = "0.6.0", path = "../../libraries/program-error" } -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = ["no-entrypoint"] } -spl-token-group-example = { version = "0.2", path = "../../token-group/example", features = ["no-entrypoint"] } -spl-token-group-interface = { version = "0.5.0", path = "../../token-group/interface" } -spl-token-metadata-interface = { version = "0.6.0", path = "../../token-metadata/interface" } -spl-type-length-value = { version = "0.7.0", path = "../../libraries/type-length-value" } +spl-pod = { version = "0.5.0" } +spl-program-error = { version = "0.6.0" } +spl-token-2022 = { version = "6.0.0", features = ["no-entrypoint"] } +spl-token-group-example = { version = "0.2", features = ["no-entrypoint"] } +spl-token-group-interface = { version = "0.5.0" } +spl-token-metadata-interface = { version = "0.6.0" } +spl-type-length-value = { version = "0.7.0" } [dev-dependencies] solana-program-test = "2.1.0" solana-sdk = "2.1.0" -spl-discriminator = { version = "0.4.0", path = "../../libraries/discriminator" } -spl-token-client = { version = "0.13.0", path = "../../token/client" } +spl-discriminator = { version = "0.4.0" } +spl-token-client = { version = "0.13.0" } [lib] crate-type = ["cdylib", "lib"] diff --git a/token-group/README.md b/token-group/README.md new file mode 100644 index 00000000000..cd23e7d1c8e --- /dev/null +++ b/token-group/README.md @@ -0,0 +1,2 @@ +NOTE: The token-group interface, program, and clients are now maintained at +[solana-program/token-group](https://github.com/solana-program/token-group). diff --git a/token-group/example/Cargo.toml b/token-group/example/Cargo.toml deleted file mode 100644 index 669a310d514..00000000000 --- a/token-group/example/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "spl-token-group-example" -version = "0.2.1" -description = "Solana Program Library Token Group Example" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -solana-program = "2.1.0" -spl-pod = { version = "0.5.0", path = "../../libraries/pod" } -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = ["no-entrypoint"] } -spl-token-group-interface = { version = "0.5.0", path = "../interface" } -spl-type-length-value = { version = "0.7.0", path = "../../libraries/type-length-value" } - -[dev-dependencies] -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" -spl-discriminator = { version = "0.4.0", path = "../../libraries/discriminator" } -spl-token-client = { version = "0.13.0", path = "../../token/client" } -spl-token-metadata-interface = { version = "0.6.0", path = "../../token-metadata/interface" } - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token-group/example/src/entrypoint.rs b/token-group/example/src/entrypoint.rs deleted file mode 100644 index 0336a5f1c99..00000000000 --- a/token-group/example/src/entrypoint.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Program entrypoint - -use { - crate::processor, - solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError, - pubkey::Pubkey, - }, - spl_token_group_interface::error::TokenGroupError, -}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - if let Err(error) = processor::process(program_id, accounts, instruction_data) { - error.print::(); - return Err(error); - } - Ok(()) -} diff --git a/token-group/example/src/lib.rs b/token-group/example/src/lib.rs deleted file mode 100644 index bd0da38115d..00000000000 --- a/token-group/example/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Crate defining an example program for creating SPL token groups -//! using the SPL Token Group interface. - -#![deny(missing_docs)] -#![forbid(unsafe_code)] - -pub mod processor; - -#[cfg(not(feature = "no-entrypoint"))] -mod entrypoint; diff --git a/token-group/example/src/processor.rs b/token-group/example/src/processor.rs deleted file mode 100644 index 4b86defc781..00000000000 --- a/token-group/example/src/processor.rs +++ /dev/null @@ -1,226 +0,0 @@ -//! Program state processor - -use { - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program_error::ProgramError, - program_option::COption, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_2022::{extension::StateWithExtensions, state::Mint}, - spl_token_group_interface::{ - error::TokenGroupError, - instruction::{ - InitializeGroup, TokenGroupInstruction, UpdateGroupAuthority, UpdateGroupMaxSize, - }, - state::{TokenGroup, TokenGroupMember}, - }, - spl_type_length_value::state::TlvStateMut, -}; - -fn check_update_authority( - update_authority_info: &AccountInfo, - expected_update_authority: &OptionalNonZeroPubkey, -) -> Result<(), ProgramError> { - if !update_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - let update_authority = Option::::from(*expected_update_authority) - .ok_or(TokenGroupError::ImmutableGroup)?; - if update_authority != *update_authority_info.key { - return Err(TokenGroupError::IncorrectUpdateAuthority.into()); - } - Ok(()) -} - -/// Processes an [InitializeGroup](enum.GroupInterfaceInstruction.html) -/// instruction -pub fn process_initialize_group( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: InitializeGroup, -) -> ProgramResult { - // Assumes one has already created a mint for the group. - let account_info_iter = &mut accounts.iter(); - - // Accounts expected by this instruction: - // - // 0. `[w]` Group - // 1. `[]` Mint - // 2. `[s]` Mint authority - let group_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let mint_authority_info = next_account_info(account_info_iter)?; - - { - // IMPORTANT: this example program is designed to work with any - // program that implements the SPL token interface, so there is no - // ownership check on the mint account. - let mint_data = mint_info.try_borrow_data()?; - let mint = StateWithExtensions::::unpack(&mint_data)?; - - if !mint_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - if mint.base.mint_authority.as_ref() != COption::Some(mint_authority_info.key) { - return Err(TokenGroupError::IncorrectMintAuthority.into()); - } - } - - // Allocate a TLV entry for the space and write it in - let mut buffer = group_info.try_borrow_mut_data()?; - let mut state = TlvStateMut::unpack(&mut buffer)?; - let (group, _) = state.init_value::(false)?; - *group = TokenGroup::new(mint_info.key, data.update_authority, data.max_size.into()); - - Ok(()) -} - -/// Processes an -/// [UpdateGroupMaxSize](enum.GroupInterfaceInstruction.html) -/// instruction -pub fn process_update_group_max_size( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: UpdateGroupMaxSize, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - // Accounts expected by this instruction: - // - // 0. `[w]` Group - // 1. `[s]` Update authority - let group_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - let mut buffer = group_info.try_borrow_mut_data()?; - let mut state = TlvStateMut::unpack(&mut buffer)?; - let group = state.get_first_value_mut::()?; - - check_update_authority(update_authority_info, &group.update_authority)?; - - // Update the max size (zero-copy) - group.update_max_size(data.max_size.into())?; - - Ok(()) -} - -/// Processes an -/// [UpdateGroupAuthority](enum.GroupInterfaceInstruction.html) -/// instruction -pub fn process_update_group_authority( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: UpdateGroupAuthority, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - // Accounts expected by this instruction: - // - // 0. `[w]` Group - // 1. `[s]` Current update authority - let group_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - let mut buffer = group_info.try_borrow_mut_data()?; - let mut state = TlvStateMut::unpack(&mut buffer)?; - let group = state.get_first_value_mut::()?; - - check_update_authority(update_authority_info, &group.update_authority)?; - - // Update the authority (zero-copy) - group.update_authority = data.new_authority; - - Ok(()) -} - -/// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html) -/// instruction -pub fn process_initialize_member(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - // For this group, we are going to assume the group has been - // initialized, and we're also assuming a mint has been created for the - // member. - // Group members in this example can have their own separate - // metadata that differs from the metadata of the group, since - // metadata is not involved here. - let account_info_iter = &mut accounts.iter(); - - // Accounts expected by this instruction: - // - // 0. `[w]` Member - // 1. `[]` Member Mint - // 2. `[s]` Member Mint authority - // 3. `[w]` Group - // 4. `[s]` Group update authority - let member_info = next_account_info(account_info_iter)?; - let member_mint_info = next_account_info(account_info_iter)?; - let member_mint_authority_info = next_account_info(account_info_iter)?; - let group_info = next_account_info(account_info_iter)?; - let group_update_authority_info = next_account_info(account_info_iter)?; - - // Mint checks on the member - { - // IMPORTANT: this example program is designed to work with any - // program that implements the SPL token interface, so there is no - // ownership check on the mint account. - let member_mint_data = member_mint_info.try_borrow_data()?; - let member_mint = StateWithExtensions::::unpack(&member_mint_data)?; - - if !member_mint_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - if member_mint.base.mint_authority.as_ref() != COption::Some(member_mint_authority_info.key) - { - return Err(TokenGroupError::IncorrectMintAuthority.into()); - } - } - - // Make sure the member account is not the same as the group account - if member_info.key == group_info.key { - return Err(TokenGroupError::MemberAccountIsGroupAccount.into()); - } - - // Increment the size of the group - let mut buffer = group_info.try_borrow_mut_data()?; - let mut state = TlvStateMut::unpack(&mut buffer)?; - let group = state.get_first_value_mut::()?; - - check_update_authority(group_update_authority_info, &group.update_authority)?; - let member_number = group.increment_size()?; - - // Allocate a TLV entry for the space and write it in - let mut buffer = member_info.try_borrow_mut_data()?; - let mut state = TlvStateMut::unpack(&mut buffer)?; - // Note if `allow_repetition: true` is instead used here, one can initialize - // the same token as a member of multiple groups! - let (member, _) = state.init_value::(false)?; - *member = TokenGroupMember::new(member_mint_info.key, group_info.key, member_number); - - Ok(()) -} - -/// Processes an `SplTokenGroupInstruction` -pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { - let instruction = TokenGroupInstruction::unpack(input)?; - match instruction { - TokenGroupInstruction::InitializeGroup(data) => { - msg!("Instruction: InitializeGroup"); - process_initialize_group(program_id, accounts, data) - } - TokenGroupInstruction::UpdateGroupMaxSize(data) => { - msg!("Instruction: UpdateGroupMaxSize"); - process_update_group_max_size(program_id, accounts, data) - } - TokenGroupInstruction::UpdateGroupAuthority(data) => { - msg!("Instruction: UpdateGroupAuthority"); - process_update_group_authority(program_id, accounts, data) - } - TokenGroupInstruction::InitializeMember(_) => { - msg!("Instruction: InitializeMember"); - process_initialize_member(program_id, accounts) - } - } -} diff --git a/token-group/example/tests/initialize_group.rs b/token-group/example/tests/initialize_group.rs deleted file mode 100644 index 7a78e58a0d0..00000000000 --- a/token-group/example/tests/initialize_group.rs +++ /dev/null @@ -1,149 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod setup; - -use { - setup::{setup_mint, setup_program_test}, - solana_program::{instruction::InstructionError, pubkey::Pubkey, system_instruction}, - solana_program_test::tokio, - solana_sdk::{ - signature::Keypair, - signer::Signer, - transaction::{Transaction, TransactionError}, - }, - spl_token_client::token::Token, - spl_token_group_interface::{instruction::initialize_group, state::TokenGroup}, - spl_type_length_value::{ - error::TlvError, - state::{TlvState, TlvStateBorrowed}, - }, -}; - -#[tokio::test] -async fn test_initialize_group() { - let program_id = Pubkey::new_unique(); - let group = Keypair::new(); - let group_mint = Keypair::new(); - let group_mint_authority = Keypair::new(); - - let group_state = TokenGroup::new(&group_mint.pubkey(), None.try_into().unwrap(), 50); - - let (context, client, payer) = setup_program_test(&program_id).await; - - let token_client = Token::new( - client, - &spl_token_2022::id(), - &group_mint.pubkey(), - Some(0), - payer.clone(), - ); - setup_mint(&token_client, &group_mint, &group_mint_authority).await; - - let context = context.lock().await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let space = TlvStateBorrowed::get_base_len() + std::mem::size_of::(); - let rent_lamports = rent.minimum_balance(space); - - // Fail: mint authority not signer - let mut init_group_ix = initialize_group( - &program_id, - &group.pubkey(), - &group_mint.pubkey(), - &group_mint_authority.pubkey(), - group_state.update_authority.into(), - group_state.max_size.into(), - ); - init_group_ix.accounts[2].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &group.pubkey(), - rent_lamports, - space.try_into().unwrap(), - &program_id, - ), - init_group_ix, - ], - Some(&context.payer.pubkey()), - &[&context.payer, &group], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature) - ); - - // Success: create the group - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &group.pubkey(), - rent_lamports, - space.try_into().unwrap(), - &program_id, - ), - initialize_group( - &program_id, - &group.pubkey(), - &group_mint.pubkey(), - &group_mint_authority.pubkey(), - group_state.update_authority.into(), - group_state.max_size.into(), - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &group_mint_authority, &group], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // Fetch the group account and ensure it matches our state - let fetched_group_account = context - .banks_client - .get_account(group.pubkey()) - .await - .unwrap() - .unwrap(); - let fetched_meta = TlvStateBorrowed::unpack(&fetched_group_account.data).unwrap(); - let fetched_group_state = fetched_meta.get_first_value::().unwrap(); - assert_eq!(fetched_group_state, &group_state); - - // Fail: can't initialize twice - let transaction = Transaction::new_signed_with_payer( - &[initialize_group( - &program_id, - &group.pubkey(), - &group_mint.pubkey(), - &group_mint_authority.pubkey(), - Pubkey::new_unique().into(), // Intentionally changed - group_state.max_size.into(), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &group_mint_authority], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(TlvError::TypeAlreadyExists as u32) - ) - ); -} diff --git a/token-group/example/tests/initialize_member.rs b/token-group/example/tests/initialize_member.rs deleted file mode 100644 index d87608ff8f5..00000000000 --- a/token-group/example/tests/initialize_member.rs +++ /dev/null @@ -1,260 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod setup; - -use { - setup::{setup_mint, setup_program_test}, - solana_program::{instruction::InstructionError, pubkey::Pubkey, system_instruction}, - solana_program_test::tokio, - solana_sdk::{ - signature::Keypair, - signer::Signer, - transaction::{Transaction, TransactionError}, - }, - spl_token_client::token::Token, - spl_token_group_interface::{ - error::TokenGroupError, - instruction::{initialize_group, initialize_member}, - state::{TokenGroup, TokenGroupMember}, - }, - spl_type_length_value::state::{TlvState, TlvStateBorrowed}, -}; - -#[tokio::test] -async fn test_initialize_group_member() { - let program_id = Pubkey::new_unique(); - let group = Keypair::new(); - let group_mint = Keypair::new(); - let group_mint_authority = Keypair::new(); - let group_update_authority = Keypair::new(); - let member = Keypair::new(); - let member_mint = Keypair::new(); - let member_mint_authority = Keypair::new(); - - let group_state = TokenGroup::new( - &group_mint.pubkey(), - Some(group_update_authority.pubkey()).try_into().unwrap(), - 50, - ); - - let (context, client, payer) = setup_program_test(&program_id).await; - - setup_mint( - &Token::new( - client.clone(), - &spl_token_2022::id(), - &group_mint.pubkey(), - Some(0), - payer.clone(), - ), - &group_mint, - &group_mint_authority, - ) - .await; - setup_mint( - &Token::new( - client.clone(), - &spl_token_2022::id(), - &member_mint.pubkey(), - Some(0), - payer.clone(), - ), - &member_mint, - &member_mint_authority, - ) - .await; - - let context = context.lock().await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let space = TlvStateBorrowed::get_base_len() + std::mem::size_of::(); - let rent_lamports = rent.minimum_balance(space); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &group.pubkey(), - rent_lamports, - space.try_into().unwrap(), - &program_id, - ), - initialize_group( - &program_id, - &group.pubkey(), - &group_mint.pubkey(), - &group_mint_authority.pubkey(), - group_state.update_authority.into(), - group_state.max_size.into(), - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &group_mint_authority, &group], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let member_space = TlvStateBorrowed::get_base_len() + std::mem::size_of::(); - let member_rent_lamports = rent.minimum_balance(member_space); - - // Fail: member mint authority not signer - let mut init_member_ix = initialize_member( - &program_id, - &member.pubkey(), - &member_mint.pubkey(), - &member_mint_authority.pubkey(), - &group.pubkey(), - &group_update_authority.pubkey(), - ); - init_member_ix.accounts[2].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &member.pubkey(), - member_rent_lamports, - member_space.try_into().unwrap(), - &program_id, - ), - init_member_ix, - ], - Some(&context.payer.pubkey()), - &[&context.payer, &member, &group_update_authority], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature) - ); - - // Fail: group update authority not signer - let mut init_member_ix = initialize_member( - &program_id, - &member.pubkey(), - &member_mint.pubkey(), - &member_mint_authority.pubkey(), - &group.pubkey(), - &group_update_authority.pubkey(), - ); - init_member_ix.accounts[4].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &member.pubkey(), - member_rent_lamports, - member_space.try_into().unwrap(), - &program_id, - ), - init_member_ix, - ], - Some(&context.payer.pubkey()), - &[&context.payer, &member, &member_mint_authority], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature) - ); - - // Fail: member account is group account - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &member.pubkey(), - member_rent_lamports, - member_space.try_into().unwrap(), - &program_id, - ), - initialize_member( - &program_id, - &member.pubkey(), - &member_mint.pubkey(), - &member_mint_authority.pubkey(), - &member.pubkey(), - &group_update_authority.pubkey(), - ), - ], - Some(&context.payer.pubkey()), - &[ - &context.payer, - &member, - &member_mint_authority, - &group_update_authority, - ], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenGroupError::MemberAccountIsGroupAccount as u32) - ) - ); - - // Success: initialize member - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &member.pubkey(), - member_rent_lamports, - member_space.try_into().unwrap(), - &program_id, - ), - initialize_member( - &program_id, - &member.pubkey(), - &member_mint.pubkey(), - &member_mint_authority.pubkey(), - &group.pubkey(), - &group_update_authority.pubkey(), - ), - ], - Some(&context.payer.pubkey()), - &[ - &context.payer, - &member, - &member_mint_authority, - &group_update_authority, - ], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // Fetch the member account and ensure it matches our state - let member_account = context - .banks_client - .get_account(member.pubkey()) - .await - .unwrap() - .unwrap(); - let fetched_meta = TlvStateBorrowed::unpack(&member_account.data).unwrap(); - let fetched_group_member_state = fetched_meta.get_first_value::().unwrap(); - assert_eq!(fetched_group_member_state.group, group.pubkey()); - assert_eq!(u64::from(fetched_group_member_state.member_number), 1); -} diff --git a/token-group/example/tests/setup.rs b/token-group/example/tests/setup.rs deleted file mode 100644 index 14cb091830c..00000000000 --- a/token-group/example/tests/setup.rs +++ /dev/null @@ -1,61 +0,0 @@ -#![cfg(feature = "test-sbf")] - -use { - solana_program_test::{processor, tokio::sync::Mutex, ProgramTest, ProgramTestContext}, - solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}, - spl_token_client::{ - client::{ - ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient, - SendTransaction, SimulateTransaction, - }, - token::Token, - }, - std::sync::Arc, -}; - -/// Set up a program test -pub async fn setup_program_test( - program_id: &Pubkey, -) -> ( - Arc>, - Arc>, - Arc, -) { - let mut program_test = ProgramTest::new( - "spl_token_group_example", - *program_id, - processor!(spl_token_group_example::processor::process), - ); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(spl_token_2022::processor::Processor::process), - ); - let context = program_test.start_with_context().await; - let payer = Arc::new(context.payer.insecure_clone()); - let context = Arc::new(Mutex::new(context)); - let client: Arc> = - Arc::new(ProgramBanksClient::new_from_context( - Arc::clone(&context), - ProgramBanksClientProcessTransaction, - )); - (context, client, payer) -} - -/// Set up a Token-2022 mint -pub async fn setup_mint( - token_client: &Token, - mint_keypair: &Keypair, - mint_authority_keypair: &Keypair, -) { - token_client - .create_mint( - &mint_authority_keypair.pubkey(), - None, - vec![], - &[mint_keypair], - ) - .await - .unwrap(); -} diff --git a/token-group/example/tests/update_group_authority.rs b/token-group/example/tests/update_group_authority.rs deleted file mode 100644 index 12aa6779dcb..00000000000 --- a/token-group/example/tests/update_group_authority.rs +++ /dev/null @@ -1,187 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod setup; - -use { - setup::{setup_mint, setup_program_test}, - solana_program::{instruction::InstructionError, pubkey::Pubkey, system_instruction}, - solana_program_test::tokio, - solana_sdk::{ - signature::Keypair, - signer::Signer, - transaction::{Transaction, TransactionError}, - }, - spl_token_client::token::Token, - spl_token_group_interface::{ - error::TokenGroupError, - instruction::{initialize_group, update_group_authority}, - state::TokenGroup, - }, - spl_type_length_value::state::{TlvState, TlvStateBorrowed}, -}; - -#[tokio::test] -async fn test_update_group_authority() { - let program_id = Pubkey::new_unique(); - let group = Keypair::new(); - let group_mint = Keypair::new(); - let group_mint_authority = Keypair::new(); - let group_update_authority = Keypair::new(); - - let group_state = TokenGroup::new( - &group_mint.pubkey(), - Some(group_update_authority.pubkey()).try_into().unwrap(), - 50, - ); - - let (context, client, payer) = setup_program_test(&program_id).await; - - let token_client = Token::new( - client, - &spl_token_2022::id(), - &group_mint.pubkey(), - Some(0), - payer.clone(), - ); - setup_mint(&token_client, &group_mint, &group_mint_authority).await; - - let context = context.lock().await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let space = TlvStateBorrowed::get_base_len() + std::mem::size_of::(); - let rent_lamports = rent.minimum_balance(space); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &group.pubkey(), - rent_lamports, - space.try_into().unwrap(), - &program_id, - ), - initialize_group( - &program_id, - &group.pubkey(), - &group_mint.pubkey(), - &group_mint_authority.pubkey(), - group_state.update_authority.into(), - group_state.max_size.into(), - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &group_mint_authority, &group], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // Fail: update authority not signer - let mut update_ix = update_group_authority( - &program_id, - &group.pubkey(), - &group_update_authority.pubkey(), - None, - ); - update_ix.accounts[1].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[update_ix], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ); - - // Fail: incorrect update authority - let transaction = Transaction::new_signed_with_payer( - &[update_group_authority( - &program_id, - &group.pubkey(), - &group.pubkey(), - None, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &group], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenGroupError::IncorrectUpdateAuthority as u32) - ) - ); - - // Success: update authority - let transaction = Transaction::new_signed_with_payer( - &[update_group_authority( - &program_id, - &group.pubkey(), - &group_update_authority.pubkey(), - None, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &group_update_authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // Fetch the account and assert the new authority - let fetched_group_account = context - .banks_client - .get_account(group.pubkey()) - .await - .unwrap() - .unwrap(); - let fetched_meta = TlvStateBorrowed::unpack(&fetched_group_account.data).unwrap(); - let fetched_group_state = fetched_meta.get_first_value::().unwrap(); - assert_eq!( - fetched_group_state.update_authority, - None.try_into().unwrap(), - ); - - // Fail: immutable group - let transaction = Transaction::new_signed_with_payer( - &[update_group_authority( - &program_id, - &group.pubkey(), - &group_update_authority.pubkey(), - Some(group_update_authority.pubkey()), - )], - Some(&context.payer.pubkey()), - &[&context.payer, &group_update_authority], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenGroupError::ImmutableGroup as u32) - ) - ); -} diff --git a/token-group/example/tests/update_group_max_size.rs b/token-group/example/tests/update_group_max_size.rs deleted file mode 100644 index 2969044880a..00000000000 --- a/token-group/example/tests/update_group_max_size.rs +++ /dev/null @@ -1,283 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod setup; - -use { - setup::{setup_mint, setup_program_test}, - solana_program::{instruction::InstructionError, pubkey::Pubkey, system_instruction}, - solana_program_test::tokio, - solana_sdk::{ - account::Account as SolanaAccount, - signature::Keypair, - signer::Signer, - transaction::{Transaction, TransactionError}, - }, - spl_token_client::token::Token, - spl_token_group_interface::{ - error::TokenGroupError, - instruction::{initialize_group, update_group_max_size}, - state::TokenGroup, - }, - spl_type_length_value::state::{TlvState, TlvStateBorrowed, TlvStateMut}, -}; - -#[tokio::test] -async fn test_update_group_max_size() { - let program_id = Pubkey::new_unique(); - let group = Keypair::new(); - let group_mint = Keypair::new(); - let group_mint_authority = Keypair::new(); - let group_update_authority = Keypair::new(); - - let group_state = TokenGroup::new( - &group_mint.pubkey(), - Some(group_update_authority.pubkey()).try_into().unwrap(), - 50, - ); - - let (context, client, payer) = setup_program_test(&program_id).await; - - let token_client = Token::new( - client, - &spl_token_2022::id(), - &group_mint.pubkey(), - Some(0), - payer.clone(), - ); - setup_mint(&token_client, &group_mint, &group_mint_authority).await; - - let mut context = context.lock().await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let space = TlvStateBorrowed::get_base_len() + std::mem::size_of::(); - let rent_lamports = rent.minimum_balance(space); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &group.pubkey(), - rent_lamports, - space.try_into().unwrap(), - &program_id, - ), - initialize_group( - &program_id, - &group.pubkey(), - &group_mint.pubkey(), - &group_mint_authority.pubkey(), - group_state.update_authority.into(), - group_state.max_size.into(), - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &group_mint_authority, &group], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // Fail: update authority not signer - let mut update_ix = update_group_max_size(&program_id, &group.pubkey(), &group.pubkey(), 100); - update_ix.accounts[1].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[update_ix], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ); - - // Fail: incorrect update authority - let transaction = Transaction::new_signed_with_payer( - &[update_group_max_size( - &program_id, - &group.pubkey(), - &group.pubkey(), - 100, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &group], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenGroupError::IncorrectUpdateAuthority as u32) - ) - ); - - // Fail: size exceeds new max size - let fetched_group_account = context - .banks_client - .get_account(group.pubkey()) - .await - .unwrap() - .unwrap(); - let mut data = fetched_group_account.data; - let mut state = TlvStateMut::unpack(&mut data).unwrap(); - let group_data = state.get_first_value_mut::().unwrap(); - group_data.size = 30.into(); - context.set_account( - &group.pubkey(), - &SolanaAccount { - data, - ..fetched_group_account - } - .into(), - ); - let transaction = Transaction::new_signed_with_payer( - &[update_group_max_size( - &program_id, - &group.pubkey(), - &group_update_authority.pubkey(), - 20, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &group_update_authority], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenGroupError::SizeExceedsNewMaxSize as u32) - ) - ); - - // Success: update max size - let transaction = Transaction::new_signed_with_payer( - &[update_group_max_size( - &program_id, - &group.pubkey(), - &group_update_authority.pubkey(), - 100, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &group_update_authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // Fetch the account and assert the new max size - let fetched_group_account = context - .banks_client - .get_account(group.pubkey()) - .await - .unwrap() - .unwrap(); - let fetched_meta = TlvStateBorrowed::unpack(&fetched_group_account.data).unwrap(); - let fetched_group_state = fetched_meta.get_first_value::().unwrap(); - assert_eq!(fetched_group_state.max_size, 100.into()); -} - -// Fail: immutable group -#[tokio::test] -async fn test_update_group_max_size_fail_immutable_group() { - let program_id = Pubkey::new_unique(); - let group = Keypair::new(); - let group_mint = Keypair::new(); - let group_mint_authority = Keypair::new(); - let group_update_authority = Keypair::new(); - - let group_state = TokenGroup::new( - &group_mint.pubkey(), - Some(group_update_authority.pubkey()).try_into().unwrap(), - 50, - ); - - let (context, client, payer) = setup_program_test(&program_id).await; - - let token_client = Token::new( - client, - &spl_token_2022::id(), - &group_mint.pubkey(), - Some(0), - payer.clone(), - ); - setup_mint(&token_client, &group_mint, &group_mint_authority).await; - - let context = context.lock().await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let space = TlvStateBorrowed::get_base_len() + std::mem::size_of::(); - let rent_lamports = rent.minimum_balance(space); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &group.pubkey(), - rent_lamports, - space.try_into().unwrap(), - &program_id, - ), - initialize_group( - &program_id, - &group.pubkey(), - &group_mint.pubkey(), - &group_mint_authority.pubkey(), - None, - group_state.max_size.into(), - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &group_mint_authority, &group], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[update_group_max_size( - &program_id, - &group.pubkey(), - &group_update_authority.pubkey(), - 100, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &group_update_authority], - context.last_blockhash, - ); - assert_eq!( - context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(), - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenGroupError::ImmutableGroup as u32) - ) - ); -} diff --git a/token-group/interface/Cargo.toml b/token-group/interface/Cargo.toml deleted file mode 100644 index 49a7c6332d0..00000000000 --- a/token-group/interface/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "spl-token-group-interface" -version = "0.5.0" -description = "Solana Program Library Token Group Interface" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -bytemuck = "1.21.0" -num-derive = "0.4" -num-traits = "0.2" -solana-decode-error = "2.1.0" -solana-instruction = "2.1.0" -solana-msg = "2.1.0" -solana-program-error = "2.1.0" -solana-pubkey = "2.1.0" -spl-discriminator = { version = "0.4.0", path = "../../libraries/discriminator" } -spl-pod = { version = "0.5.0", path = "../../libraries/pod", features = ["borsh"] } -thiserror = "2.0" - -[dev-dependencies] -solana-sha256-hasher = "2.1.0" -spl-type-length-value = { version = "0.7.0", path = "../../libraries/type-length-value", features = ["derive"] } - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token-group/interface/README.md b/token-group/interface/README.md deleted file mode 100644 index 04925742cbc..00000000000 --- a/token-group/interface/README.md +++ /dev/null @@ -1,138 +0,0 @@ -## Token-Group Interface - -An interface describing the instructions required for a program to implement -to be considered a "token-group" program for SPL token mints. The interface can -be implemented by any program. - -With a common interface, any wallet, dapp, or on-chain program can read the -group or member configurations, and any tool that creates or modifies group -or member configurations will just work with any program that implements the -interface. - -This interface is compatible with any program that implements the SPL Token -interface. However, other program implementations that are not SPL Token -programs may still be compatible with an SPL Token Group program should that -program's token standard support the proper components such as mint and mint -authority accounts (see [Required Instructions](#required-instructions)). - -There are also structs for `TokenGroup` and `TokenGroupMember` that may -optionally be implemented, but are not required. - -### Example program - -An example program demonstrating how to implement the SPL Token-Group Interface -can be found in the -[example](https://github.com/solana-labs/solana-program-library/tree/master/token-group/example) -directory alongside this interface's directory. - -In addition to demonstrating what a token-group program might look like, it -also provides some reference examples for using the SPL Type Length Value -library to manage TLV-encoded data within account data. - -For more information on SPL Type Length Value you can reference the library's -[source code](https://github.com/solana-labs/solana-program-library/tree/master/libraries/type-length-value). - -### Motivation - -As developers have engineered more creative ways to customize tokens and use -them to power applications, communities, and more, the reliance on tokens that -are intrinsically related through on-chain mapping has continued to strengthen. - -Token-group provides developers with the minimum necessary interface components -required to create these relational mappings, allowing for reliable -composability as well as the freedom to customize these groups of tokens -however one might please. - -By implementing token-group, on-chain programs can build brand-new kinds of -token groups, which can all overlap with each other and share common tooling. - -### Required Instructions - -All of the following instructions are listed in greater detail in the source code. - -- [`InitializeGroup`](https://github.com/solana-labs/solana-program-library/blob/master/token-group/interface/src/instruction.rs#L22) -- [`UpdateGroupMaxSize`](https://github.com/solana-labs/solana-program-library/blob/master/token-group/interface/src/instruction.rs#L33) -- [`UpdateGroupAuthority`](https://github.com/solana-labs/solana-program-library/blob/master/token-group/interface/src/instruction.rs#L42) -- [`InitializeMember`](https://github.com/solana-labs/solana-program-library/blob/master/token-group/interface/src/instruction.rs#L51) - -#### Initialize Group - -Initializes a token-group TLV entry in an account for group configurations with -a provided maximum group size and update authority. - -Must provide an SPL token mint and be signed by the mint authority. - -#### Update Group Max Size - -Updates the maximum size limit of a group. - -Must be signed by the update authority. - -#### Update Group Authority - -Sets or unsets the token-group update authority, which signs any future updates -to the group configurations. - -Must be signed by the update authority. - -#### Initialize Member - -Initializes a token-group TLV entry in an account for group member -configurations. - -Must provide an SPL token mint for both the group and the group member. - -Must be signed by the member mint's mint authority _and_ the group's update -authority. - -### (Optional) State - -A program that implements the interface may write the following data fields -into a type-length-value entry into an account. Note the type discriminants -for each. - -For a group: - -```rust -type OptionalNonZeroPubkey = Pubkey; // if all zeroes, interpreted as `None` -type PodU64 = [u8; 8]; -type Pubkey = [u8; 32]; - -/// Type discriminant: [214, 15, 63, 132, 49, 119, 209, 40] -/// First 8 bytes of `hash("spl_token_group_interface:group")` -pub struct TokenGroup { - /// The authority that can sign to update the group - pub update_authority: OptionalNonZeroPubkey, - /// The associated mint, used to counter spoofing to be sure that group - /// belongs to a particular mint - pub mint: Pubkey, - /// The current number of group members - pub size: PodU64, - /// The maximum number of group members - pub max_size: PodU64, -} -``` - -For a group member: - -```rust -/// Type discriminant: [254, 50, 168, 134, 88, 126, 100, 186] -/// First 8 bytes of `hash("spl_token_group_interface:member")` -pub struct TokenGroupMember { - /// The associated mint, used to counter spoofing to be sure that member - /// belongs to a particular mint - pub mint: Pubkey, - /// The pubkey of the `TokenGroup` - pub group: Pubkey, - /// The member number - pub member_number: PodU64, -} -``` - -By storing the configurations for either groups or group members in a TLV -structure, a developer who implements this interface can freely add any other -data fields in a different TLV entry. - -As mentioned previously, you can find more information about -TLV / type-length-value structures at the -[spl-type-length-value repo](https://github.com/solana-labs/solana-program-library/tree/master/libraries/type-length-value). \ No newline at end of file diff --git a/token-group/interface/src/error.rs b/token-group/interface/src/error.rs deleted file mode 100644 index 1e3aa5311d7..00000000000 --- a/token-group/interface/src/error.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Interface error types - -use { - solana_decode_error::DecodeError, - solana_msg::msg, - solana_program_error::{PrintProgramError, ProgramError}, -}; - -/// Errors that may be returned by the interface. -#[repr(u32)] -#[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] -pub enum TokenGroupError { - /// Size is greater than proposed max size - #[error("Size is greater than proposed max size")] - SizeExceedsNewMaxSize = 3_406_457_176, - /// Size is greater than max size - #[error("Size is greater than max size")] - SizeExceedsMaxSize, - /// Group is immutable - #[error("Group is immutable")] - ImmutableGroup, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, - /// Incorrect update authority has signed the instruction - #[error("Incorrect update authority has signed the instruction")] - IncorrectUpdateAuthority, - /// Member account should not be the same as the group account - #[error("Member account should not be the same as the group account")] - MemberAccountIsGroupAccount, -} - -impl From for ProgramError { - fn from(e: TokenGroupError) -> Self { - ProgramError::Custom(e as u32) - } -} - -impl DecodeError for TokenGroupError { - fn type_of() -> &'static str { - "TokenGroupError" - } -} - -impl PrintProgramError for TokenGroupError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - TokenGroupError::SizeExceedsNewMaxSize => { - msg!("Size is greater than proposed max size") - } - TokenGroupError::SizeExceedsMaxSize => { - msg!("Size is greater than max size") - } - TokenGroupError::ImmutableGroup => { - msg!("Group is immutable") - } - TokenGroupError::IncorrectMintAuthority => { - msg!("Incorrect mint authority has signed the instruction",) - } - TokenGroupError::IncorrectUpdateAuthority => { - msg!("Incorrect update authority has signed the instruction",) - } - TokenGroupError::MemberAccountIsGroupAccount => { - msg!("Member account should not be the same as the group account",) - } - } - } -} diff --git a/token-group/interface/src/instruction.rs b/token-group/interface/src/instruction.rs deleted file mode 100644 index f0de6f32615..00000000000 --- a/token-group/interface/src/instruction.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Instruction types - -use { - bytemuck::{Pod, Zeroable}, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, - spl_pod::{ - bytemuck::{pod_bytes_of, pod_from_bytes}, - optional_keys::OptionalNonZeroPubkey, - primitives::PodU64, - }, -}; - -/// Instruction data for initializing a new `Group` -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] -#[discriminator_hash_input("spl_token_group_interface:initialize_token_group")] -pub struct InitializeGroup { - /// Update authority for the group - pub update_authority: OptionalNonZeroPubkey, - /// The maximum number of group members - pub max_size: PodU64, -} - -/// Instruction data for updating the max size of a `Group` -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] -#[discriminator_hash_input("spl_token_group_interface:update_group_max_size")] -pub struct UpdateGroupMaxSize { - /// New max size for the group - pub max_size: PodU64, -} - -/// Instruction data for updating the authority of a `Group` -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] -#[discriminator_hash_input("spl_token_group_interface:update_authority")] -pub struct UpdateGroupAuthority { - /// New authority for the group, or unset if `None` - pub new_authority: OptionalNonZeroPubkey, -} - -/// Instruction data for initializing a new `Member` of a `Group` -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] -#[discriminator_hash_input("spl_token_group_interface:initialize_member")] -pub struct InitializeMember; - -/// All instructions that must be implemented in the SPL Token Group Interface -#[derive(Clone, Debug, PartialEq)] -pub enum TokenGroupInstruction { - /// Initialize a new `Group` - /// - /// Assumes one has already initialized a mint for the - /// group. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Group - /// 1. `[]` Mint - /// 2. `[s]` Mint authority - InitializeGroup(InitializeGroup), - - /// Update the max size of a `Group` - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Group - /// 1. `[s]` Update authority - UpdateGroupMaxSize(UpdateGroupMaxSize), - - /// Update the authority of a `Group` - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Group - /// 1. `[s]` Current update authority - UpdateGroupAuthority(UpdateGroupAuthority), - - /// Initialize a new `Member` of a `Group` - /// - /// Assumes the `Group` has already been initialized, - /// as well as the mint for the member. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Member - /// 1. `[]` Member mint - /// 1. `[s]` Member mint authority - /// 2. `[w]` Group - /// 3. `[s]` Group update authority - InitializeMember(InitializeMember), -} -impl TokenGroupInstruction { - /// Unpacks a byte buffer into a `TokenGroupInstruction` - pub fn unpack(input: &[u8]) -> Result { - if input.len() < ArrayDiscriminator::LENGTH { - return Err(ProgramError::InvalidInstructionData); - } - let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH); - Ok(match discriminator { - InitializeGroup::SPL_DISCRIMINATOR_SLICE => { - let data = pod_from_bytes::(rest)?; - Self::InitializeGroup(*data) - } - UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE => { - let data = pod_from_bytes::(rest)?; - Self::UpdateGroupMaxSize(*data) - } - UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE => { - let data = pod_from_bytes::(rest)?; - Self::UpdateGroupAuthority(*data) - } - InitializeMember::SPL_DISCRIMINATOR_SLICE => { - let data = pod_from_bytes::(rest)?; - Self::InitializeMember(*data) - } - _ => return Err(ProgramError::InvalidInstructionData), - }) - } - - /// Packs a `TokenGroupInstruction` into a byte buffer. - pub fn pack(&self) -> Vec { - let mut buf = vec![]; - match self { - Self::InitializeGroup(data) => { - buf.extend_from_slice(InitializeGroup::SPL_DISCRIMINATOR_SLICE); - buf.extend_from_slice(pod_bytes_of(data)); - } - Self::UpdateGroupMaxSize(data) => { - buf.extend_from_slice(UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE); - buf.extend_from_slice(pod_bytes_of(data)); - } - Self::UpdateGroupAuthority(data) => { - buf.extend_from_slice(UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE); - buf.extend_from_slice(pod_bytes_of(data)); - } - Self::InitializeMember(data) => { - buf.extend_from_slice(InitializeMember::SPL_DISCRIMINATOR_SLICE); - buf.extend_from_slice(pod_bytes_of(data)); - } - }; - buf - } -} - -/// Creates a `InitializeGroup` instruction -pub fn initialize_group( - program_id: &Pubkey, - group: &Pubkey, - mint: &Pubkey, - mint_authority: &Pubkey, - update_authority: Option, - max_size: u64, -) -> Instruction { - let update_authority = OptionalNonZeroPubkey::try_from(update_authority) - .expect("Failed to deserialize `Option`"); - let data = TokenGroupInstruction::InitializeGroup(InitializeGroup { - update_authority, - max_size: max_size.into(), - }) - .pack(); - Instruction { - program_id: *program_id, - accounts: vec![ - AccountMeta::new(*group, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new_readonly(*mint_authority, true), - ], - data, - } -} - -/// Creates a `UpdateGroupMaxSize` instruction -pub fn update_group_max_size( - program_id: &Pubkey, - group: &Pubkey, - update_authority: &Pubkey, - max_size: u64, -) -> Instruction { - let data = TokenGroupInstruction::UpdateGroupMaxSize(UpdateGroupMaxSize { - max_size: max_size.into(), - }) - .pack(); - Instruction { - program_id: *program_id, - accounts: vec![ - AccountMeta::new(*group, false), - AccountMeta::new_readonly(*update_authority, true), - ], - data, - } -} - -/// Creates a `UpdateGroupAuthority` instruction -pub fn update_group_authority( - program_id: &Pubkey, - group: &Pubkey, - current_authority: &Pubkey, - new_authority: Option, -) -> Instruction { - let new_authority = OptionalNonZeroPubkey::try_from(new_authority) - .expect("Failed to deserialize `Option`"); - let data = - TokenGroupInstruction::UpdateGroupAuthority(UpdateGroupAuthority { new_authority }).pack(); - Instruction { - program_id: *program_id, - accounts: vec![ - AccountMeta::new(*group, false), - AccountMeta::new_readonly(*current_authority, true), - ], - data, - } -} - -/// Creates a `InitializeMember` instruction -#[allow(clippy::too_many_arguments)] -pub fn initialize_member( - program_id: &Pubkey, - member: &Pubkey, - member_mint: &Pubkey, - member_mint_authority: &Pubkey, - group: &Pubkey, - group_update_authority: &Pubkey, -) -> Instruction { - let data = TokenGroupInstruction::InitializeMember(InitializeMember {}).pack(); - Instruction { - program_id: *program_id, - accounts: vec![ - AccountMeta::new(*member, false), - AccountMeta::new_readonly(*member_mint, false), - AccountMeta::new_readonly(*member_mint_authority, true), - AccountMeta::new(*group, false), - AccountMeta::new_readonly(*group_update_authority, true), - ], - data, - } -} - -#[cfg(test)] -mod test { - use {super::*, crate::NAMESPACE, solana_sha256_hasher::hashv}; - - fn instruction_pack_unpack(instruction: TokenGroupInstruction, discriminator: &[u8], data: I) - where - I: core::fmt::Debug + PartialEq + Pod + Zeroable + SplDiscriminate, - { - let mut expect = vec![]; - expect.extend_from_slice(discriminator.as_ref()); - expect.extend_from_slice(pod_bytes_of(&data)); - let packed = instruction.pack(); - assert_eq!(packed, expect); - let unpacked = TokenGroupInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, instruction); - } - - #[test] - fn initialize_group_pack() { - let data = InitializeGroup { - update_authority: OptionalNonZeroPubkey::default(), - max_size: 100.into(), - }; - let instruction = TokenGroupInstruction::InitializeGroup(data); - let preimage = hashv(&[format!("{NAMESPACE}:initialize_token_group").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - instruction_pack_unpack::(instruction, discriminator, data); - } - - #[test] - fn update_group_max_size_pack() { - let data = UpdateGroupMaxSize { - max_size: 200.into(), - }; - let instruction = TokenGroupInstruction::UpdateGroupMaxSize(data); - let preimage = hashv(&[format!("{NAMESPACE}:update_group_max_size").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - instruction_pack_unpack::(instruction, discriminator, data); - } - - #[test] - fn update_authority_pack() { - let data = UpdateGroupAuthority { - new_authority: OptionalNonZeroPubkey::default(), - }; - let instruction = TokenGroupInstruction::UpdateGroupAuthority(data); - let preimage = hashv(&[format!("{NAMESPACE}:update_authority").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - instruction_pack_unpack::(instruction, discriminator, data); - } - - #[test] - fn initialize_member_pack() { - let data = InitializeMember {}; - let instruction = TokenGroupInstruction::InitializeMember(data); - let preimage = hashv(&[format!("{NAMESPACE}:initialize_member").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - instruction_pack_unpack::(instruction, discriminator, data); - } -} diff --git a/token-group/interface/src/lib.rs b/token-group/interface/src/lib.rs deleted file mode 100644 index 6867b86acf8..00000000000 --- a/token-group/interface/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Crate defining the SPL Token Group Interface - -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -pub mod error; -pub mod instruction; -pub mod state; - -/// Namespace for all programs implementing spl-token-group -pub const NAMESPACE: &str = "spl_token_group_interface"; diff --git a/token-group/interface/src/state.rs b/token-group/interface/src/state.rs deleted file mode 100644 index 14d73ab2c5c..00000000000 --- a/token-group/interface/src/state.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Interface state types - -use { - crate::error::TokenGroupError, - bytemuck::{Pod, Zeroable}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - spl_discriminator::SplDiscriminate, - spl_pod::{error::PodSliceError, optional_keys::OptionalNonZeroPubkey, primitives::PodU64}, -}; - -/// Data struct for a `TokenGroup` -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] -#[discriminator_hash_input("spl_token_group_interface:group")] -pub struct TokenGroup { - /// The authority that can sign to update the group - pub update_authority: OptionalNonZeroPubkey, - /// The associated mint, used to counter spoofing to be sure that group - /// belongs to a particular mint - pub mint: Pubkey, - /// The current number of group members - pub size: PodU64, - /// The maximum number of group members - pub max_size: PodU64, -} - -impl TokenGroup { - /// Creates a new `TokenGroup` state - pub fn new(mint: &Pubkey, update_authority: OptionalNonZeroPubkey, max_size: u64) -> Self { - Self { - mint: *mint, - update_authority, - size: PodU64::default(), // [0, 0, 0, 0, 0, 0, 0, 0] - max_size: max_size.into(), - } - } - - /// Updates the max size for a group - pub fn update_max_size(&mut self, new_max_size: u64) -> Result<(), ProgramError> { - // The new max size cannot be less than the current size - if new_max_size < u64::from(self.size) { - return Err(TokenGroupError::SizeExceedsNewMaxSize.into()); - } - self.max_size = new_max_size.into(); - Ok(()) - } - - /// Increment the size for a group, returning the new size - pub fn increment_size(&mut self) -> Result { - // The new size cannot be greater than the max size - let new_size = u64::from(self.size) - .checked_add(1) - .ok_or::(PodSliceError::CalculationFailure.into())?; - if new_size > u64::from(self.max_size) { - return Err(TokenGroupError::SizeExceedsMaxSize.into()); - } - self.size = new_size.into(); - Ok(new_size) - } -} - -/// Data struct for a `TokenGroupMember` -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] -#[discriminator_hash_input("spl_token_group_interface:member")] -pub struct TokenGroupMember { - /// The associated mint, used to counter spoofing to be sure that member - /// belongs to a particular mint - pub mint: Pubkey, - /// The pubkey of the `TokenGroup` - pub group: Pubkey, - /// The member number - pub member_number: PodU64, -} -impl TokenGroupMember { - /// Creates a new `TokenGroupMember` state - pub fn new(mint: &Pubkey, group: &Pubkey, member_number: u64) -> Self { - Self { - mint: *mint, - group: *group, - member_number: member_number.into(), - } - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::NAMESPACE, - solana_sha256_hasher::hashv, - spl_discriminator::ArrayDiscriminator, - spl_type_length_value::state::{TlvState, TlvStateBorrowed, TlvStateMut}, - std::mem::size_of, - }; - - #[test] - fn discriminators() { - let preimage = hashv(&[format!("{NAMESPACE}:group").as_bytes()]); - let discriminator = - ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); - assert_eq!(TokenGroup::SPL_DISCRIMINATOR, discriminator); - - let preimage = hashv(&[format!("{NAMESPACE}:member").as_bytes()]); - let discriminator = - ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); - assert_eq!(TokenGroupMember::SPL_DISCRIMINATOR, discriminator); - } - - #[test] - fn tlv_state_pack() { - // Make sure we can pack more than one instance of each type - let group = TokenGroup { - mint: Pubkey::new_unique(), - update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), - size: 10.into(), - max_size: 20.into(), - }; - - let member = TokenGroupMember { - mint: Pubkey::new_unique(), - group: Pubkey::new_unique(), - member_number: 0.into(), - }; - - let account_size = TlvStateBorrowed::get_base_len() - + size_of::() - + TlvStateBorrowed::get_base_len() - + size_of::(); - let mut buffer = vec![0; account_size]; - let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); - - let group_data = state.init_value::(false).unwrap().0; - *group_data = group; - - let member_data = state.init_value::(false).unwrap().0; - *member_data = member; - - assert_eq!(state.get_first_value::().unwrap(), &group); - assert_eq!( - state.get_first_value::().unwrap(), - &member - ); - } - - #[test] - fn update_max_size() { - // Test with a `Some` max size - let max_size = 10; - let mut group = TokenGroup { - mint: Pubkey::new_unique(), - update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), - size: 0.into(), - max_size: max_size.into(), - }; - - let new_max_size = 30; - group.update_max_size(new_max_size).unwrap(); - assert_eq!(u64::from(group.max_size), new_max_size); - - // Change the current size to 30 - group.size = 30.into(); - - // Try to set the max size to 20, which is less than the current size - let new_max_size = 20; - assert_eq!( - group.update_max_size(new_max_size), - Err(ProgramError::from(TokenGroupError::SizeExceedsNewMaxSize)) - ); - - let new_max_size = 30; - group.update_max_size(new_max_size).unwrap(); - assert_eq!(u64::from(group.max_size), new_max_size); - } - - #[test] - fn increment_current_size() { - let mut group = TokenGroup { - mint: Pubkey::new_unique(), - update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), - size: 0.into(), - max_size: 1.into(), - }; - - group.increment_size().unwrap(); - assert_eq!(u64::from(group.size), 1); - - // Try to increase the current size to 2, which is greater than the max size - assert_eq!( - group.increment_size(), - Err(ProgramError::from(TokenGroupError::SizeExceedsMaxSize)) - ); - } -} diff --git a/token-group/js/.eslintignore b/token-group/js/.eslintignore deleted file mode 100644 index 6da325effab..00000000000 --- a/token-group/js/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -docs -lib -test-ledger - -package-lock.json diff --git a/token-group/js/.eslintrc b/token-group/js/.eslintrc deleted file mode 100644 index 5aef10a4729..00000000000 --- a/token-group/js/.eslintrc +++ /dev/null @@ -1,34 +0,0 @@ -{ - "root": true, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - "plugin:require-extensions/recommended" - ], - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "prettier", - "require-extensions" - ], - "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/consistent-type-imports": "error" - }, - "overrides": [ - { - "files": [ - "examples/**/*", - "test/**/*" - ], - "rules": { - "require-extensions/require-extensions": "off", - "require-extensions/require-index": "off" - } - } - ] -} diff --git a/token-group/js/.gitignore b/token-group/js/.gitignore deleted file mode 100644 index 21f33db819c..00000000000 --- a/token-group/js/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -.idea -.vscode -.DS_Store - -node_modules - -pnpm-lock.yaml -yarn.lock - -docs -lib -test-ledger -*.tsbuildinfo diff --git a/token-group/js/.mocharc.json b/token-group/js/.mocharc.json deleted file mode 100644 index 451c14c3016..00000000000 --- a/token-group/js/.mocharc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extension": ["ts"], - "node-option": ["experimental-specifier-resolution=node", "loader=ts-node/esm"], - "timeout": 5000 -} diff --git a/token-group/js/.nojekyll b/token-group/js/.nojekyll deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/token-group/js/LICENSE b/token-group/js/LICENSE deleted file mode 100644 index d6456956733..00000000000 --- a/token-group/js/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/token-group/js/README.md b/token-group/js/README.md deleted file mode 100644 index 8b00c0894bd..00000000000 --- a/token-group/js/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# `@solana/spl-token-group` - -A TypeScript interface describing the instructions required for a program to implement to be considered a "token-group" program for SPL token mints. The interface can be implemented by any program. - -## Links - -- [TypeScript Docs](https://solana-labs.github.io/solana-program-library/token-group/js/) -- [FAQs (Frequently Asked Questions)](#faqs) -- [Install](#install) -- [Build from Source](#build-from-source) - -## FAQs - -### How can I get support? - -Please ask questions in the Solana Stack Exchange: https://solana.stackexchange.com/ - -If you've found a bug or you'd like to request a feature, please -[open an issue](https://github.com/solana-labs/solana-program-library/issues/new). - -## Install - -```shell -npm install --save @solana/spl-token-group @solana/web3.js@1 -``` -_OR_ -```shell -yarn add @solana/spl-token-group @solana/web3.js@1 -``` - -## Build from Source - -0. Prerequisites - -* Node 16+ -* NPM 8+ - -1. Clone the project: -```shell -git clone https://github.com/solana-labs/solana-program-library.git -``` - -2. Navigate to the library: -```shell -cd solana-program-library/token-group/js -``` - -3. Install the dependencies: -```shell -npm install -``` - -4. Build the library: -```shell -npm run build -``` - -5. Build the on-chain programs: -```shell -npm run test:build-programs -``` diff --git a/token-group/js/package.json b/token-group/js/package.json deleted file mode 100644 index b310514f060..00000000000 --- a/token-group/js/package.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "@solana/spl-token-group", - "description": "SPL Token Group Interface JS API", - "version": "0.0.7", - "author": "Solana Labs Maintainers ", - "repository": "https://github.com/solana-labs/solana-program-library", - "license": "Apache-2.0", - "type": "module", - "sideEffects": false, - "engines": { - "node": ">=16" - }, - "files": [ - "lib", - "src", - "LICENSE", - "README.md" - ], - "publishConfig": { - "access": "public" - }, - "main": "./lib/cjs/index.js", - "module": "./lib/esm/index.js", - "types": "./lib/types/index.d.ts", - "exports": { - "types": "./lib/types/index.d.ts", - "require": "./lib/cjs/index.js", - "import": "./lib/esm/index.js" - }, - "scripts": { - "build": "tsc --build --verbose tsconfig.all.json", - "clean": "shx rm -rf lib **/*.tsbuildinfo || true", - "deploy": "npm run deploy:docs", - "deploy:docs": "npm run docs && gh-pages --dest token-group/js --dist docs --dotfiles", - "docs": "shx rm -rf docs && typedoc && shx cp .nojekyll docs/", - "lint": "eslint --max-warnings 0 .", - "lint:fix": "eslint --fix .", - "nuke": "shx rm -rf node_modules package-lock.json || true", - "postbuild": "shx echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", - "reinstall": "npm run nuke && npm install", - "release": "npm run clean && npm run build", - "test": "mocha test", - "watch": "tsc --build --verbose --watch tsconfig.all.json" - }, - "peerDependencies": { - "@solana/web3.js": "^1.95.5" - }, - "dependencies": { - "@solana/codecs": "2.0.0" - }, - "devDependencies": { - "@solana/spl-type-length-value": "0.2.0", - "@solana/web3.js": "^1.95.5", - "@types/chai": "^5.0.1", - "@types/mocha": "^10.0.10", - "@types/node": "^22.10.5", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "@typescript-eslint/parser": "^8.4.0", - "chai": "^5.1.2", - "eslint": "^8.57.0", - "eslint-plugin-require-extensions": "^0.1.1", - "gh-pages": "^6.3.0", - "mocha": "^11.0.1", - "shx": "^0.3.4", - "ts-node": "^10.9.2", - "tslib": "^2.8.1", - "typedoc": "^0.27.6", - "typescript": "^5.7.2" - } -} diff --git a/token-group/js/src/errors.ts b/token-group/js/src/errors.ts deleted file mode 100644 index fc93a33960d..00000000000 --- a/token-group/js/src/errors.ts +++ /dev/null @@ -1,35 +0,0 @@ -export class TokenGroupError extends Error { - constructor(message?: string) { - super(message); - } -} - -/** Thrown if size is greater than proposed max size */ -export class SizeExceedsNewMaxSizeError extends TokenGroupError { - name = 'SizeExceedsNewMaxSizeError'; -} - -/** Thrown if size is greater than max size */ -export class SizeExceedsMaxSizeError extends TokenGroupError { - name = 'SizeExceedsMaxSizeError'; -} - -/** Thrown if group is immutable */ -export class ImmutableGroupError extends TokenGroupError { - name = 'ImmutableGroupError'; -} - -/** Thrown if incorrect mint authority has signed the instruction */ -export class IncorrectMintAuthorityError extends TokenGroupError { - name = 'IncorrectMintAuthorityError'; -} - -/** Thrown if incorrect update authority has signed the instruction */ -export class IncorrectUpdateAuthorityError extends TokenGroupError { - name = 'IncorrectUpdateAuthorityError'; -} - -/** Thrown if member account is the same as the group account */ -export class MemberAccountIsGroupAccountError extends TokenGroupError { - name = 'MemberAccountIsGroupAccountError'; -} diff --git a/token-group/js/src/index.ts b/token-group/js/src/index.ts deleted file mode 100644 index 5bc9f496131..00000000000 --- a/token-group/js/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './errors.js'; -export * from './instruction.js'; -export * from './state/index.js'; diff --git a/token-group/js/src/instruction.ts b/token-group/js/src/instruction.ts deleted file mode 100644 index b51dd03cd18..00000000000 --- a/token-group/js/src/instruction.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { Encoder } from '@solana/codecs'; -import type { PublicKey } from '@solana/web3.js'; -import { - fixEncoderSize, - getBytesEncoder, - getStructEncoder, - getTupleEncoder, - getU64Encoder, - transformEncoder, -} from '@solana/codecs'; -import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; - -function getInstructionEncoder(discriminator: Uint8Array, dataEncoder: Encoder): Encoder { - return transformEncoder(getTupleEncoder([getBytesEncoder(), dataEncoder]), (data: T): [Uint8Array, T] => [ - discriminator, - data, - ]); -} - -function getPublicKeyEncoder(): Encoder { - return transformEncoder(fixEncoderSize(getBytesEncoder(), 32), (publicKey: PublicKey) => publicKey.toBytes()); -} - -export interface InitializeGroupInstruction { - programId: PublicKey; - group: PublicKey; - mint: PublicKey; - mintAuthority: PublicKey; - updateAuthority: PublicKey | null; - maxSize: bigint; -} - -export function createInitializeGroupInstruction(args: InitializeGroupInstruction): TransactionInstruction { - const { programId, group, mint, mintAuthority, updateAuthority, maxSize } = args; - - return new TransactionInstruction({ - programId, - keys: [ - { isSigner: false, isWritable: true, pubkey: group }, - { isSigner: false, isWritable: false, pubkey: mint }, - { isSigner: true, isWritable: false, pubkey: mintAuthority }, - ], - data: Buffer.from( - getInstructionEncoder( - new Uint8Array([ - /* await splDiscriminate('spl_token_group_interface:initialize_token_group') */ - 121, 113, 108, 39, 54, 51, 0, 4, - ]), - getStructEncoder([ - ['updateAuthority', getPublicKeyEncoder()], - ['maxSize', getU64Encoder()], - ]), - ).encode({ updateAuthority: updateAuthority ?? SystemProgram.programId, maxSize }), - ), - }); -} - -export interface UpdateGroupMaxSize { - programId: PublicKey; - group: PublicKey; - updateAuthority: PublicKey; - maxSize: bigint; -} - -export function createUpdateGroupMaxSizeInstruction(args: UpdateGroupMaxSize): TransactionInstruction { - const { programId, group, updateAuthority, maxSize } = args; - return new TransactionInstruction({ - programId, - keys: [ - { isSigner: false, isWritable: true, pubkey: group }, - { isSigner: true, isWritable: false, pubkey: updateAuthority }, - ], - data: Buffer.from( - getInstructionEncoder( - new Uint8Array([ - /* await splDiscriminate('spl_token_group_interface:update_group_max_size') */ - 108, 37, 171, 143, 248, 30, 18, 110, - ]), - getStructEncoder([['maxSize', getU64Encoder()]]), - ).encode({ maxSize }), - ), - }); -} - -export interface UpdateGroupAuthority { - programId: PublicKey; - group: PublicKey; - currentAuthority: PublicKey; - newAuthority: PublicKey | null; -} - -export function createUpdateGroupAuthorityInstruction(args: UpdateGroupAuthority): TransactionInstruction { - const { programId, group, currentAuthority, newAuthority } = args; - - return new TransactionInstruction({ - programId, - keys: [ - { isSigner: false, isWritable: true, pubkey: group }, - { isSigner: true, isWritable: false, pubkey: currentAuthority }, - ], - data: Buffer.from( - getInstructionEncoder( - new Uint8Array([ - /* await splDiscriminate('spl_token_group_interface:update_authority') */ - 161, 105, 88, 1, 237, 221, 216, 203, - ]), - getStructEncoder([['newAuthority', getPublicKeyEncoder()]]), - ).encode({ newAuthority: newAuthority ?? SystemProgram.programId }), - ), - }); -} - -export interface InitializeMember { - programId: PublicKey; - member: PublicKey; - memberMint: PublicKey; - memberMintAuthority: PublicKey; - group: PublicKey; - groupUpdateAuthority: PublicKey; -} - -export function createInitializeMemberInstruction(args: InitializeMember): TransactionInstruction { - const { programId, member, memberMint, memberMintAuthority, group, groupUpdateAuthority } = args; - - return new TransactionInstruction({ - programId, - keys: [ - { isSigner: false, isWritable: true, pubkey: member }, - { isSigner: false, isWritable: false, pubkey: memberMint }, - { isSigner: true, isWritable: false, pubkey: memberMintAuthority }, - { isSigner: false, isWritable: true, pubkey: group }, - { isSigner: true, isWritable: false, pubkey: groupUpdateAuthority }, - ], - data: Buffer.from( - getInstructionEncoder( - new Uint8Array([ - /* await splDiscriminate('spl_token_group_interface:initialize_member') */ - 152, 32, 222, 176, 223, 237, 116, 134, - ]), - getStructEncoder([]), - ).encode({}), - ), - }); -} diff --git a/token-group/js/src/state/index.ts b/token-group/js/src/state/index.ts deleted file mode 100644 index e5db1ace95d..00000000000 --- a/token-group/js/src/state/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './tokenGroup.js'; -export * from './tokenGroupMember.js'; diff --git a/token-group/js/src/state/tokenGroup.ts b/token-group/js/src/state/tokenGroup.ts deleted file mode 100644 index c0895e28507..00000000000 --- a/token-group/js/src/state/tokenGroup.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import type { ReadonlyUint8Array } from '@solana/codecs'; -import { fixCodecSize, getBytesCodec, getStructCodec, getU64Codec } from '@solana/codecs'; - -const tokenGroupCodec = getStructCodec([ - ['updateAuthority', fixCodecSize(getBytesCodec(), 32)], - ['mint', fixCodecSize(getBytesCodec(), 32)], - ['size', getU64Codec()], - ['maxSize', getU64Codec()], -]); - -export const TOKEN_GROUP_SIZE = tokenGroupCodec.fixedSize; - -export interface TokenGroup { - /** The authority that can sign to update the group */ - updateAuthority?: PublicKey; - /** The associated mint, used to counter spoofing to be sure that group belongs to a particular mint */ - mint: PublicKey; - /** The current number of group members */ - size: bigint; - /** The maximum number of group members */ - maxSize: bigint; -} - -// Checks if all elements in the array are 0 -function isNonePubkey(buffer: ReadonlyUint8Array): boolean { - for (let i = 0; i < buffer.length; i++) { - if (buffer[i] !== 0) { - return false; - } - } - return true; -} - -// Pack TokenGroup into byte slab -export function packTokenGroup(group: TokenGroup): ReadonlyUint8Array { - // If no updateAuthority given, set it to the None/Zero PublicKey for encoding - const updateAuthority = group.updateAuthority ?? PublicKey.default; - return tokenGroupCodec.encode({ - updateAuthority: updateAuthority.toBuffer(), - mint: group.mint.toBuffer(), - size: group.size, - maxSize: group.maxSize, - }); -} - -// unpack byte slab into TokenGroup -export function unpackTokenGroup(buffer: Buffer | Uint8Array | ReadonlyUint8Array): TokenGroup { - const data = tokenGroupCodec.decode(buffer); - - return isNonePubkey(data.updateAuthority) - ? { - mint: new PublicKey(data.mint), - size: data.size, - maxSize: data.maxSize, - } - : { - updateAuthority: new PublicKey(data.updateAuthority), - mint: new PublicKey(data.mint), - size: data.size, - maxSize: data.maxSize, - }; -} diff --git a/token-group/js/src/state/tokenGroupMember.ts b/token-group/js/src/state/tokenGroupMember.ts deleted file mode 100644 index c952c4faaa1..00000000000 --- a/token-group/js/src/state/tokenGroupMember.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import type { ReadonlyUint8Array } from '@solana/codecs'; -import { fixCodecSize, getBytesCodec, getStructCodec, getU64Codec } from '@solana/codecs'; - -const tokenGroupMemberCodec = getStructCodec([ - ['mint', fixCodecSize(getBytesCodec(), 32)], - ['group', fixCodecSize(getBytesCodec(), 32)], - ['memberNumber', getU64Codec()], -]); - -export const TOKEN_GROUP_MEMBER_SIZE = tokenGroupMemberCodec.fixedSize; - -export interface TokenGroupMember { - /** The associated mint, used to counter spoofing to be sure that member belongs to a particular mint */ - mint: PublicKey; - /** The pubkey of the `TokenGroup` */ - group: PublicKey; - /** The member number */ - memberNumber: bigint; -} - -// Pack TokenGroupMember into byte slab -export function packTokenGroupMember(member: TokenGroupMember): ReadonlyUint8Array { - return tokenGroupMemberCodec.encode({ - mint: member.mint.toBuffer(), - group: member.group.toBuffer(), - memberNumber: member.memberNumber, - }); -} - -// unpack byte slab into TokenGroupMember -export function unpackTokenGroupMember(buffer: Buffer | Uint8Array | ReadonlyUint8Array): TokenGroupMember { - const data = tokenGroupMemberCodec.decode(buffer); - return { - mint: new PublicKey(data.mint), - group: new PublicKey(data.group), - memberNumber: data.memberNumber, - }; -} diff --git a/token-group/js/test/instruction.test.ts b/token-group/js/test/instruction.test.ts deleted file mode 100644 index 8f4dcac0a82..00000000000 --- a/token-group/js/test/instruction.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { expect } from 'chai'; -import type { Decoder } from '@solana/codecs'; -import { fixDecoderSize, getBytesDecoder, getStructDecoder, getU64Decoder } from '@solana/codecs'; -import { splDiscriminate } from '@solana/spl-type-length-value'; -import { PublicKey, type TransactionInstruction } from '@solana/web3.js'; - -import { - createInitializeGroupInstruction, - createInitializeMemberInstruction, - createUpdateGroupMaxSizeInstruction, - createUpdateGroupAuthorityInstruction, -} from '../src'; - -function checkPackUnpack( - instruction: TransactionInstruction, - discriminator: Uint8Array, - decoder: Decoder, - values: T, -) { - expect(instruction.data.subarray(0, 8)).to.deep.equal(discriminator); - const unpacked = decoder.decode(instruction.data.subarray(8)); - expect(unpacked).to.deep.equal(values); -} - -describe('Token Group Instructions', () => { - const programId = new PublicKey('22222222222222222222222222222222222222222222'); - const group = new PublicKey('33333333333333333333333333333333333333333333'); - const updateAuthority = new PublicKey('44444444444444444444444444444444444444444444'); - const mint = new PublicKey('55555555555555555555555555555555555555555555'); - const mintAuthority = new PublicKey('66666666666666666666666666666666666666666666'); - const maxSize = BigInt(100); - - it('Can create InitializeGroup Instruction', async () => { - checkPackUnpack( - createInitializeGroupInstruction({ - programId, - group, - mint, - mintAuthority, - updateAuthority, - maxSize, - }), - await splDiscriminate('spl_token_group_interface:initialize_token_group'), - getStructDecoder([ - ['updateAuthority', fixDecoderSize(getBytesDecoder(), 32)], - ['maxSize', getU64Decoder()], - ]), - { updateAuthority: Uint8Array.from(updateAuthority.toBuffer()), maxSize }, - ); - }); - - it('Can create UpdateGroupMaxSize Instruction', async () => { - checkPackUnpack( - createUpdateGroupMaxSizeInstruction({ - programId, - group, - updateAuthority, - maxSize, - }), - await splDiscriminate('spl_token_group_interface:update_group_max_size'), - getStructDecoder([['maxSize', getU64Decoder()]]), - { maxSize }, - ); - }); - - it('Can create UpdateGroupAuthority Instruction', async () => { - checkPackUnpack( - createUpdateGroupAuthorityInstruction({ - programId, - group, - currentAuthority: updateAuthority, - newAuthority: PublicKey.default, - }), - await splDiscriminate('spl_token_group_interface:update_authority'), - getStructDecoder([['newAuthority', fixDecoderSize(getBytesDecoder(), 32)]]), - { newAuthority: Uint8Array.from(PublicKey.default.toBuffer()) }, - ); - }); - - it('Can create InitializeMember Instruction', async () => { - const member = new PublicKey('22222222222222222222222222222222222222222222'); - const memberMint = new PublicKey('33333333333333333333333333333333333333333333'); - const memberMintAuthority = new PublicKey('44444444444444444444444444444444444444444444'); - const group = new PublicKey('55555555555555555555555555555555555555555555'); - const groupUpdateAuthority = new PublicKey('66666666666666666666666666666666666666666666'); - - checkPackUnpack( - createInitializeMemberInstruction({ - programId, - member, - memberMint, - memberMintAuthority, - group, - groupUpdateAuthority, - }), - await splDiscriminate('spl_token_group_interface:initialize_member'), - getStructDecoder([]), - {}, - ); - }); -}); diff --git a/token-group/js/test/state.test.ts b/token-group/js/test/state.test.ts deleted file mode 100644 index f071de87dc1..00000000000 --- a/token-group/js/test/state.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import { expect } from 'chai'; - -import type { TokenGroup, TokenGroupMember } from '../src/state'; -import { unpackTokenGroupMember, packTokenGroupMember, unpackTokenGroup, packTokenGroup } from '../src'; - -describe('Token Group State', () => { - describe('Token Group', () => { - function checkPackUnpack(tokenGroup: TokenGroup) { - const packed = packTokenGroup(tokenGroup); - const unpacked = unpackTokenGroup(packed); - expect(unpacked).to.deep.equal(tokenGroup); - } - - it('Can pack and unpack TokenGroup with updateAuthoritygroup', () => { - checkPackUnpack({ - mint: new PublicKey('44444444444444444444444444444444444444444444'), - updateAuthority: new PublicKey('55555555555555555555555555555555555555555555'), - size: BigInt(10), - maxSize: BigInt(20), - }); - }); - - it('Can pack and unpack TokenGroup without updateAuthoritygroup', () => { - checkPackUnpack({ - mint: new PublicKey('44444444444444444444444444444444444444444444'), - size: BigInt(10), - maxSize: BigInt(20), - }); - }); - }); - - describe('Token Group Member', () => { - function checkPackUnpack(tokenGroupMember: TokenGroupMember) { - const packed = packTokenGroupMember(tokenGroupMember); - const unpacked = unpackTokenGroupMember(packed); - expect(unpacked).to.deep.equal(tokenGroupMember); - } - it('Can pack and unpack TokenGroupMembergroup', () => { - checkPackUnpack({ - mint: new PublicKey('55555555555555555555555555555555555555555555'), - group: new PublicKey('66666666666666666666666666666666666666666666'), - memberNumber: BigInt(8), - }); - }); - }); -}); diff --git a/token-group/js/tsconfig.all.json b/token-group/js/tsconfig.all.json deleted file mode 100644 index 985513259e2..00000000000 --- a/token-group/js/tsconfig.all.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "./tsconfig.root.json", - "references": [ - { - "path": "./tsconfig.cjs.json" - }, - { - "path": "./tsconfig.esm.json" - } - ] -} diff --git a/token-group/js/tsconfig.base.json b/token-group/js/tsconfig.base.json deleted file mode 100644 index 90620c4e485..00000000000 --- a/token-group/js/tsconfig.base.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "include": [], - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Node", - "esModuleInterop": true, - "isolatedModules": true, - "noEmitOnError": true, - "resolveJsonModule": true, - "strict": true, - "stripInternal": true - } -} diff --git a/token-group/js/tsconfig.cjs.json b/token-group/js/tsconfig.cjs.json deleted file mode 100644 index 2db9b71569e..00000000000 --- a/token-group/js/tsconfig.cjs.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "include": ["src"], - "compilerOptions": { - "outDir": "lib/cjs", - "target": "ES2016", - "module": "CommonJS", - "sourceMap": true - } -} diff --git a/token-group/js/tsconfig.esm.json b/token-group/js/tsconfig.esm.json deleted file mode 100644 index 25e7e25e751..00000000000 --- a/token-group/js/tsconfig.esm.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "include": ["src"], - "compilerOptions": { - "outDir": "lib/esm", - "declarationDir": "lib/types", - "target": "ES2020", - "module": "ES2020", - "sourceMap": true, - "declaration": true, - "declarationMap": true - } -} diff --git a/token-group/js/tsconfig.json b/token-group/js/tsconfig.json deleted file mode 100644 index 2f9b239bfca..00000000000 --- a/token-group/js/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.all.json", - "include": ["src", "test"], - "compilerOptions": { - "noEmit": true, - "skipLibCheck": true - } -} diff --git a/token-group/js/tsconfig.root.json b/token-group/js/tsconfig.root.json deleted file mode 100644 index fadf294ab43..00000000000 --- a/token-group/js/tsconfig.root.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "composite": true - } -} diff --git a/token-group/js/typedoc.json b/token-group/js/typedoc.json deleted file mode 100644 index c39fc53aee1..00000000000 --- a/token-group/js/typedoc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entryPoints": ["src/index.ts"], - "out": "docs", - "readme": "README.md" -} diff --git a/token-lending/cli/Cargo.toml b/token-lending/cli/Cargo.toml index aa9827294be..9181e0c0285 100644 --- a/token-lending/cli/Cargo.toml +++ b/token-lending/cli/Cargo.toml @@ -17,7 +17,7 @@ solana-logger = "2.1.0" solana-sdk = "2.1.0" solana-program = "2.1.0" spl-token-lending = { version = "0.2", path="../program", features = [ "no-entrypoint" ] } -spl-token = { version = "7.0", path="../../token/program", features = [ "no-entrypoint" ] } +spl-token = { version = "7.0", features = [ "no-entrypoint" ] } [[bin]] name = "spl-token-lending" diff --git a/token-lending/flash_loan_receiver/Cargo.toml b/token-lending/flash_loan_receiver/Cargo.toml index 918387aa9b3..00ab194ba13 100644 --- a/token-lending/flash_loan_receiver/Cargo.toml +++ b/token-lending/flash_loan_receiver/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] arrayref = "0.3.9" solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features=["no-entrypoint"] } +spl-token = { version = "7.0", features=["no-entrypoint"] } [lib] crate-type = ["cdylib", "lib"] diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml index bff11a1ed35..07007776104 100644 --- a/token-lending/program/Cargo.toml +++ b/token-lending/program/Cargo.toml @@ -17,7 +17,7 @@ bytemuck = "1.21.0" num-derive = "0.4" num-traits = "0.2" solana-program = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = [ "no-entrypoint" ] } +spl-token = { version = "7.0", features = [ "no-entrypoint" ] } thiserror = "2.0" uint = "0.10" diff --git a/token-metadata/README.md b/token-metadata/README.md new file mode 100644 index 00000000000..dc5a2bf8f62 --- /dev/null +++ b/token-metadata/README.md @@ -0,0 +1,2 @@ +NOTE: The token-metadat interface, program, and clients are now maintained at +[solana-program/token-metadata](https://github.com/solana-program/token-metadata). diff --git a/token-metadata/example/Cargo.toml b/token-metadata/example/Cargo.toml deleted file mode 100644 index b6a4ef30d6a..00000000000 --- a/token-metadata/example/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "spl-token-metadata-example" -version = "0.3.0" -description = "Solana Program Library Token Metadata Example Program" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -solana-program = "2.1.0" -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = ["no-entrypoint"] } -spl-token-metadata-interface = { version = "0.6.0", path = "../interface" } -spl-type-length-value = { version = "0.7.0", path = "../../libraries/type-length-value" } -spl-pod = { version = "0.5.0", path = "../../libraries/pod" } - -[dev-dependencies] -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" -spl-token-client = { version = "0.13.0", path = "../../token/client" } -test-case = "3.3" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token-metadata/example/src/entrypoint.rs b/token-metadata/example/src/entrypoint.rs deleted file mode 100644 index accd30f2c17..00000000000 --- a/token-metadata/example/src/entrypoint.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Program entrypoint - -use { - crate::processor, - solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError, - pubkey::Pubkey, - }, - spl_token_metadata_interface::error::TokenMetadataError, -}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - if let Err(error) = processor::process(program_id, accounts, instruction_data) { - // catch the error so we can print it - error.print::(); - return Err(error); - } - Ok(()) -} diff --git a/token-metadata/example/src/lib.rs b/token-metadata/example/src/lib.rs deleted file mode 100644 index db86409dad6..00000000000 --- a/token-metadata/example/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Crate defining an example program for storing SPL token metadata - -#![allow(clippy::arithmetic_side_effects)] -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -pub mod processor; - -#[cfg(not(feature = "no-entrypoint"))] -mod entrypoint; diff --git a/token-metadata/example/src/processor.rs b/token-metadata/example/src/processor.rs deleted file mode 100644 index fe686e5e0da..00000000000 --- a/token-metadata/example/src/processor.rs +++ /dev/null @@ -1,222 +0,0 @@ -//! Program state processor - -use { - solana_program::{ - account_info::{next_account_info, AccountInfo}, - borsh1::get_instance_packed_len, - entrypoint::ProgramResult, - msg, - program::set_return_data, - program_error::ProgramError, - program_option::COption, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_2022::{extension::StateWithExtensions, state::Mint}, - spl_token_metadata_interface::{ - error::TokenMetadataError, - instruction::{ - Emit, Initialize, RemoveKey, TokenMetadataInstruction, UpdateAuthority, UpdateField, - }, - state::TokenMetadata, - }, - spl_type_length_value::state::{ - realloc_and_pack_first_variable_len, TlvState, TlvStateBorrowed, TlvStateMut, - }, -}; - -fn check_update_authority( - update_authority_info: &AccountInfo, - expected_update_authority: &OptionalNonZeroPubkey, -) -> Result<(), ProgramError> { - if !update_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - let update_authority = Option::::from(*expected_update_authority) - .ok_or(TokenMetadataError::ImmutableMetadata)?; - if update_authority != *update_authority_info.key { - return Err(TokenMetadataError::IncorrectUpdateAuthority.into()); - } - Ok(()) -} - -/// Processes a [Initialize](enum.TokenMetadataInstruction.html) instruction. -pub fn process_initialize( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: Initialize, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let metadata_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let mint_authority_info = next_account_info(account_info_iter)?; - - // scope the mint authority check, in case the mint is in the same account! - { - // IMPORTANT: this example metadata program is designed to work with any - // program that implements the SPL token interface, so there is no - // ownership check on the mint account. - let mint_data = mint_info.try_borrow_data()?; - let mint = StateWithExtensions::::unpack(&mint_data)?; - - if !mint_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - if mint.base.mint_authority.as_ref() != COption::Some(mint_authority_info.key) { - return Err(TokenMetadataError::IncorrectMintAuthority.into()); - } - } - - // get the required size, assumes that there's enough space for the entry - let update_authority = OptionalNonZeroPubkey::try_from(Some(*update_authority_info.key))?; - let token_metadata = TokenMetadata { - name: data.name, - symbol: data.symbol, - uri: data.uri, - update_authority, - mint: *mint_info.key, - ..Default::default() - }; - let instance_size = get_instance_packed_len(&token_metadata)?; - - // allocate a TLV entry for the space and write it in - let mut buffer = metadata_info.try_borrow_mut_data()?; - let mut state = TlvStateMut::unpack(&mut buffer)?; - state.alloc::(instance_size, false)?; - state.pack_first_variable_len_value(&token_metadata)?; - - Ok(()) -} - -/// Processes an [UpdateField](enum.TokenMetadataInstruction.html) instruction. -pub fn process_update_field( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: UpdateField, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let metadata_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - // deserialize the metadata, but scope the data borrow since we'll probably - // realloc the account - let mut token_metadata = { - let buffer = metadata_info.try_borrow_data()?; - let state = TlvStateBorrowed::unpack(&buffer)?; - state.get_first_variable_len_value::()? - }; - - check_update_authority(update_authority_info, &token_metadata.update_authority)?; - - // Update the field - token_metadata.update(data.field, data.value); - - // Update / realloc the account - realloc_and_pack_first_variable_len(metadata_info, &token_metadata)?; - - Ok(()) -} - -/// Processes a [RemoveKey](enum.TokenMetadataInstruction.html) instruction. -pub fn process_remove_key( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: RemoveKey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let metadata_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - // deserialize the metadata, but scope the data borrow since we'll probably - // realloc the account - let mut token_metadata = { - let buffer = metadata_info.try_borrow_data()?; - let state = TlvStateBorrowed::unpack(&buffer)?; - state.get_first_variable_len_value::()? - }; - - check_update_authority(update_authority_info, &token_metadata.update_authority)?; - if !token_metadata.remove_key(&data.key) && !data.idempotent { - return Err(TokenMetadataError::KeyNotFound.into()); - } - realloc_and_pack_first_variable_len(metadata_info, &token_metadata)?; - - Ok(()) -} - -/// Processes a [UpdateAuthority](enum.TokenMetadataInstruction.html) -/// instruction. -pub fn process_update_authority( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: UpdateAuthority, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let metadata_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - // deserialize the metadata, but scope the data borrow since we'll probably - // realloc the account - let mut token_metadata = { - let buffer = metadata_info.try_borrow_data()?; - let state = TlvStateBorrowed::unpack(&buffer)?; - state.get_first_variable_len_value::()? - }; - - check_update_authority(update_authority_info, &token_metadata.update_authority)?; - token_metadata.update_authority = data.new_authority; - // Update the account, no realloc needed! - realloc_and_pack_first_variable_len(metadata_info, &token_metadata)?; - - Ok(()) -} - -/// Processes an [Emit](enum.TokenMetadataInstruction.html) instruction. -pub fn process_emit(program_id: &Pubkey, accounts: &[AccountInfo], data: Emit) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let metadata_info = next_account_info(account_info_iter)?; - - if metadata_info.owner != program_id { - return Err(ProgramError::IllegalOwner); - } - - let buffer = metadata_info.try_borrow_data()?; - let state = TlvStateBorrowed::unpack(&buffer)?; - let metadata_bytes = state.get_first_bytes::()?; - - if let Some(range) = TokenMetadata::get_slice(metadata_bytes, data.start, data.end) { - set_return_data(range); - } - - Ok(()) -} - -/// Processes an [Instruction](enum.Instruction.html). -pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { - let instruction = TokenMetadataInstruction::unpack(input)?; - - match instruction { - TokenMetadataInstruction::Initialize(data) => { - msg!("Instruction: Initialize"); - process_initialize(program_id, accounts, data) - } - TokenMetadataInstruction::UpdateField(data) => { - msg!("Instruction: UpdateField"); - process_update_field(program_id, accounts, data) - } - TokenMetadataInstruction::RemoveKey(data) => { - msg!("Instruction: RemoveKey"); - process_remove_key(program_id, accounts, data) - } - TokenMetadataInstruction::UpdateAuthority(data) => { - msg!("Instruction: UpdateAuthority"); - process_update_authority(program_id, accounts, data) - } - TokenMetadataInstruction::Emit(data) => { - msg!("Instruction: Emit"); - process_emit(program_id, accounts, data) - } - } -} diff --git a/token-metadata/example/tests/emit.rs b/token-metadata/example/tests/emit.rs deleted file mode 100644 index bfac603451b..00000000000 --- a/token-metadata/example/tests/emit.rs +++ /dev/null @@ -1,105 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{setup, setup_metadata, setup_mint}, - solana_program_test::tokio, - solana_sdk::{ - borsh1::try_from_slice_unchecked, program::MAX_RETURN_DATA, pubkey::Pubkey, - signature::Signer, signer::keypair::Keypair, transaction::Transaction, - }, - spl_token_metadata_interface::{borsh, instruction::emit, state::TokenMetadata}, - test_case::test_case, -}; - -#[test_case(Some(40), Some(40) ; "zero bytes")] -#[test_case(Some(40), Some(41) ; "one byte")] -#[test_case(Some(1_000_000), Some(1_000_001) ; "too far")] -#[test_case(Some(50), Some(49) ; "wrong way")] -#[test_case(Some(50), None ; "truncate start")] -#[test_case(None, Some(50) ; "truncate end")] -#[test_case(None, None ; "full data")] -#[tokio::test] -async fn success(start: Option, end: Option) { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let mut context = context.lock().await; - - let update_authority = Pubkey::new_unique(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - - setup_metadata( - &mut context, - &program_id, - token.get_address(), - &token_metadata, - &metadata_keypair, - &mint_authority, - ) - .await; - - let transaction = Transaction::new_signed_with_payer( - &[emit(&program_id, &metadata_pubkey, start, end)], - Some(&payer.pubkey()), - &[payer.as_ref()], - context.last_blockhash, - ); - let simulation = context - .banks_client - .simulate_transaction(transaction) - .await - .unwrap(); - - let metadata_buffer = borsh::to_vec(&token_metadata).unwrap(); - if let Some(check_buffer) = TokenMetadata::get_slice(&metadata_buffer, start, end) { - if !check_buffer.is_empty() { - // pad the data if necessary - let mut return_data = vec![0; MAX_RETURN_DATA]; - let simulation_return_data = - simulation.simulation_details.unwrap().return_data.unwrap(); - assert_eq!(simulation_return_data.program_id, program_id); - return_data[..simulation_return_data.data.len()] - .copy_from_slice(&simulation_return_data.data); - - assert_eq!(*check_buffer, return_data[..check_buffer.len()]); - // we're sure that we're getting the full data, so also compare the deserialized - // type - if start.is_none() && end.is_none() { - let emitted_token_metadata = - try_from_slice_unchecked::(&return_data).unwrap(); - assert_eq!(token_metadata, emitted_token_metadata); - } - } else { - assert!(simulation.simulation_details.unwrap().return_data.is_none()); - } - } else { - assert!(simulation.simulation_details.unwrap().return_data.is_none()); - } -} diff --git a/token-metadata/example/tests/initialize.rs b/token-metadata/example/tests/initialize.rs deleted file mode 100644 index 2dc28ef9b27..00000000000 --- a/token-metadata/example/tests/initialize.rs +++ /dev/null @@ -1,271 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{setup, setup_metadata, setup_mint}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - system_instruction, - transaction::{Transaction, TransactionError}, - }, - spl_token_metadata_interface::{ - error::TokenMetadataError, instruction::initialize, state::TokenMetadata, - }, - spl_type_length_value::{ - error::TlvError, - state::{TlvState, TlvStateBorrowed}, - }, -}; - -#[tokio::test] -async fn success_initialize() { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let mut context = context.lock().await; - - let update_authority = Pubkey::new_unique(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - - setup_metadata( - &mut context, - &program_id, - token.get_address(), - &token_metadata, - &metadata_keypair, - &mint_authority, - ) - .await; - - // check that the data is correct - let fetched_metadata_account = context - .banks_client - .get_account(metadata_pubkey) - .await - .unwrap() - .unwrap(); - let fetched_metadata_state = TlvStateBorrowed::unpack(&fetched_metadata_account.data).unwrap(); - let fetched_metadata = fetched_metadata_state - .get_first_variable_len_value::() - .unwrap(); - assert_eq!(fetched_metadata, token_metadata); - - // fail doing it again, and reverse some params to ensure a new tx - { - let transaction = Transaction::new_signed_with_payer( - &[initialize( - &program_id, - &metadata_pubkey, - &update_authority, - token.get_address(), - &mint_authority_pubkey, - token_metadata.symbol.clone(), // intentionally reversed! - token_metadata.name.clone(), - token_metadata.uri.clone(), - )], - Some(&payer.pubkey()), - &[&payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TlvError::TypeAlreadyExists as u32) - ) - ); - } -} - -#[tokio::test] -async fn fail_without_authority_signature() { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let context = context.lock().await; - - let update_authority = Pubkey::new_unique(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - let rent = context.banks_client.get_rent().await.unwrap(); - let space = token_metadata.tlv_size_of().unwrap(); - let rent_lamports = rent.minimum_balance(space); - let mut initialize_ix = initialize( - &program_id, - &metadata_pubkey, - &update_authority, - token.get_address(), - &mint_authority_pubkey, - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - ); - initialize_ix.accounts[3].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &metadata_pubkey, - rent_lamports, - space.try_into().unwrap(), - &program_id, - ), - initialize_ix, - ], - Some(&payer.pubkey()), - &[&payer, &metadata_keypair], - context.last_blockhash, - ); - - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature,) - ); -} - -#[tokio::test] -async fn fail_incorrect_authority() { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let context = context.lock().await; - - let update_authority = Pubkey::new_unique(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - let rent = context.banks_client.get_rent().await.unwrap(); - let space = token_metadata.tlv_size_of().unwrap(); - let rent_lamports = rent.minimum_balance(space); - let mut initialize_ix = initialize( - &program_id, - &metadata_pubkey, - &update_authority, - token.get_address(), - &metadata_pubkey, - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - ); - initialize_ix.accounts[3].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &metadata_pubkey, - rent_lamports, - space.try_into().unwrap(), - &program_id, - ), - initialize_ix, - ], - Some(&payer.pubkey()), - &[&payer, &metadata_keypair], - context.last_blockhash, - ); - - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenMetadataError::IncorrectMintAuthority as u32) - ) - ); -} diff --git a/token-metadata/example/tests/program_test.rs b/token-metadata/example/tests/program_test.rs deleted file mode 100644 index 19e91eea18d..00000000000 --- a/token-metadata/example/tests/program_test.rs +++ /dev/null @@ -1,167 +0,0 @@ -#![cfg(feature = "test-sbf")] - -use { - solana_program_test::{processor, tokio::sync::Mutex, ProgramTest, ProgramTestContext}, - solana_sdk::{ - pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, system_instruction, - transaction::Transaction, - }, - spl_token_client::{ - client::{ - ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient, - SendTransaction, SimulateTransaction, - }, - token::Token, - }, - spl_token_metadata_interface::{ - instruction::{initialize, update_field}, - state::{Field, TokenMetadata}, - }, - std::sync::Arc, -}; - -fn keypair_clone(kp: &Keypair) -> Keypair { - Keypair::from_bytes(&kp.to_bytes()).expect("failed to copy keypair") -} - -pub async fn setup( - program_id: &Pubkey, -) -> ( - Arc>, - Arc>, - Arc, -) { - let mut program_test = ProgramTest::new( - "spl_token_metadata_example", - *program_id, - processor!(spl_token_metadata_example::processor::process), - ); - - program_test.prefer_bpf(false); // simplicity in the build - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(spl_token_2022::processor::Processor::process), - ); - - let context = program_test.start_with_context().await; - let payer = Arc::new(keypair_clone(&context.payer)); - let context = Arc::new(Mutex::new(context)); - - let client: Arc> = - Arc::new(ProgramBanksClient::new_from_context( - Arc::clone(&context), - ProgramBanksClientProcessTransaction, - )); - (context, client, payer) -} - -pub async fn setup_mint( - program_id: &Pubkey, - mint_authority: &Pubkey, - decimals: u8, - payer: Arc, - client: Arc>, -) -> Token { - let mint_account = Keypair::new(); - let token = Token::new( - client, - program_id, - &mint_account.pubkey(), - Some(decimals), - payer, - ); - token - .create_mint(mint_authority, None, vec![], &[&mint_account]) - .await - .unwrap(); - token -} - -pub async fn setup_metadata( - context: &mut ProgramTestContext, - metadata_program_id: &Pubkey, - mint: &Pubkey, - token_metadata: &TokenMetadata, - metadata_keypair: &Keypair, - mint_authority: &Keypair, -) { - let rent = context.banks_client.get_rent().await.unwrap(); - let space = token_metadata.tlv_size_of().unwrap(); - let rent_lamports = rent.minimum_balance(space); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &metadata_keypair.pubkey(), - rent_lamports, - space.try_into().unwrap(), - metadata_program_id, - ), - initialize( - metadata_program_id, - &metadata_keypair.pubkey(), - &Option::::from(token_metadata.update_authority).unwrap(), - mint, - &mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, metadata_keypair, mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} - -#[allow(dead_code)] -pub async fn setup_update_field( - context: &mut ProgramTestContext, - metadata_program_id: &Pubkey, - token_metadata: &mut TokenMetadata, - metadata: &Pubkey, - update_authority: &Keypair, - field: Field, - value: String, -) { - let rent = context.banks_client.get_rent().await.unwrap(); - let old_space = token_metadata.tlv_size_of().unwrap(); - let old_rent_lamports = rent.minimum_balance(old_space); - - token_metadata.update(field.clone(), value.clone()); - - let new_space = token_metadata.tlv_size_of().unwrap(); - let new_rent_lamports = rent.minimum_balance(new_space); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - metadata, - new_rent_lamports.saturating_sub(old_rent_lamports), - ), - update_field( - metadata_program_id, - metadata, - &update_authority.pubkey(), - field, - value, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, update_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} diff --git a/token-metadata/example/tests/remove_key.rs b/token-metadata/example/tests/remove_key.rs deleted file mode 100644 index 505113fedd3..00000000000 --- a/token-metadata/example/tests/remove_key.rs +++ /dev/null @@ -1,271 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{setup, setup_metadata, setup_mint, setup_update_field}, - solana_program_test::{tokio, ProgramTestBanksClientExt}, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - transaction::{Transaction, TransactionError}, - }, - spl_token_metadata_interface::{ - error::TokenMetadataError, - instruction::remove_key, - state::{Field, TokenMetadata}, - }, - spl_type_length_value::state::{TlvState, TlvStateBorrowed}, -}; - -#[tokio::test] -async fn success_remove() { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let mut context = context.lock().await; - - let update_authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let mut token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority.pubkey()).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - - setup_metadata( - &mut context, - &program_id, - token.get_address(), - &token_metadata, - &metadata_keypair, - &mint_authority, - ) - .await; - - let key = "key".to_string(); - let value = "value".to_string(); - let field = Field::Key(key.clone()); - setup_update_field( - &mut context, - &program_id, - &mut token_metadata, - &metadata_pubkey, - &update_authority, - field, - value, - ) - .await; - - let transaction = Transaction::new_signed_with_payer( - &[remove_key( - &program_id, - &metadata_pubkey, - &update_authority.pubkey(), - key.clone(), - false, // idempotent - )], - Some(&payer.pubkey()), - &[&payer, &update_authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // check that the data is correct - token_metadata.remove_key(&key); - let fetched_metadata_account = context - .banks_client - .get_account(metadata_pubkey) - .await - .unwrap() - .unwrap(); - assert_eq!( - fetched_metadata_account.data.len(), - token_metadata.tlv_size_of().unwrap() - ); - let fetched_metadata_state = TlvStateBorrowed::unpack(&fetched_metadata_account.data).unwrap(); - let fetched_metadata = fetched_metadata_state - .get_first_variable_len_value::() - .unwrap(); - assert_eq!(fetched_metadata, token_metadata); - - // refresh blockhash before trying again - let last_blockhash = context.last_blockhash; - let last_blockhash = context - .banks_client - .get_new_latest_blockhash(&last_blockhash) - .await - .unwrap(); - - // fail doing it again without idempotent flag - let transaction = Transaction::new_signed_with_payer( - &[remove_key( - &program_id, - &metadata_pubkey, - &update_authority.pubkey(), - key.clone(), - false, // idempotent - )], - Some(&payer.pubkey()), - &[&payer, &update_authority], - last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::KeyNotFound as u32) - ) - ); - - // succeed with idempotent flag - let transaction = Transaction::new_signed_with_payer( - &[remove_key( - &program_id, - &metadata_pubkey, - &update_authority.pubkey(), - key, - true, // idempotent - )], - Some(&payer.pubkey()), - &[&payer, &update_authority], - last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} - -#[tokio::test] -async fn fail_authority_checks() { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let mut context = context.lock().await; - - let update_authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority.pubkey()).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - - setup_metadata( - &mut context, - &program_id, - token.get_address(), - &token_metadata, - &metadata_keypair, - &mint_authority, - ) - .await; - - // no signature - let mut instruction = remove_key( - &program_id, - &metadata_pubkey, - &update_authority.pubkey(), - "new_name".to_string(), - true, // idempotent - ); - instruction.accounts[1].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &[payer.as_ref()], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature,) - ); - - // wrong authority - let transaction = Transaction::new_signed_with_payer( - &[remove_key( - &program_id, - &metadata_pubkey, - &payer.pubkey(), - "new_name".to_string(), - true, // idempotent - )], - Some(&payer.pubkey()), - &[payer.as_ref()], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::IncorrectUpdateAuthority as u32), - ) - ); -} diff --git a/token-metadata/example/tests/update_authority.rs b/token-metadata/example/tests/update_authority.rs deleted file mode 100644 index cb2251b8d30..00000000000 --- a/token-metadata/example/tests/update_authority.rs +++ /dev/null @@ -1,264 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{setup, setup_metadata, setup_mint}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - transaction::{Transaction, TransactionError}, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_metadata_interface::{ - error::TokenMetadataError, instruction::update_authority, state::TokenMetadata, - }, - spl_type_length_value::state::{TlvState, TlvStateBorrowed}, -}; - -#[tokio::test] -async fn success_update() { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let mut context = context.lock().await; - - let authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let mut token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(authority.pubkey()).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - - setup_metadata( - &mut context, - &program_id, - token.get_address(), - &token_metadata, - &metadata_keypair, - &mint_authority, - ) - .await; - - let new_update_authority = Keypair::new(); - let new_update_authority_pubkey = - OptionalNonZeroPubkey::try_from(Some(new_update_authority.pubkey())).unwrap(); - token_metadata.update_authority = new_update_authority_pubkey; - - let transaction = Transaction::new_signed_with_payer( - &[update_authority( - &program_id, - &metadata_pubkey, - &authority.pubkey(), - new_update_authority_pubkey, - )], - Some(&payer.pubkey()), - &[&payer, &authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // check that the data is correct - let fetched_metadata_account = context - .banks_client - .get_account(metadata_pubkey) - .await - .unwrap() - .unwrap(); - assert_eq!( - fetched_metadata_account.data.len(), - token_metadata.tlv_size_of().unwrap() - ); - let fetched_metadata_state = TlvStateBorrowed::unpack(&fetched_metadata_account.data).unwrap(); - let fetched_metadata = fetched_metadata_state - .get_first_variable_len_value::() - .unwrap(); - assert_eq!(fetched_metadata, token_metadata); - - // unset - token_metadata.update_authority = None.try_into().unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[update_authority( - &program_id, - &metadata_pubkey, - &new_update_authority.pubkey(), - None.try_into().unwrap(), - )], - Some(&payer.pubkey()), - &[&payer, &new_update_authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let fetched_metadata_account = context - .banks_client - .get_account(metadata_pubkey) - .await - .unwrap() - .unwrap(); - assert_eq!( - fetched_metadata_account.data.len(), - token_metadata.tlv_size_of().unwrap() - ); - let fetched_metadata_state = TlvStateBorrowed::unpack(&fetched_metadata_account.data).unwrap(); - let fetched_metadata = fetched_metadata_state - .get_first_variable_len_value::() - .unwrap(); - assert_eq!(fetched_metadata, token_metadata); - - // fail to update - let transaction = Transaction::new_signed_with_payer( - &[update_authority( - &program_id, - &metadata_pubkey, - &new_update_authority.pubkey(), - Some(new_update_authority.pubkey()).try_into().unwrap(), - )], - Some(&payer.pubkey()), - &[&payer, &new_update_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::ImmutableMetadata as u32) - ) - ); -} - -#[tokio::test] -async fn fail_authority_checks() { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let mut context = context.lock().await; - - let authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(authority.pubkey()).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - - setup_metadata( - &mut context, - &program_id, - token.get_address(), - &token_metadata, - &metadata_keypair, - &mint_authority, - ) - .await; - - // no signature - let mut instruction = update_authority( - &program_id, - &metadata_pubkey, - &authority.pubkey(), - None.try_into().unwrap(), - ); - instruction.accounts[1].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &[payer.as_ref()], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature,) - ); - - // wrong authority - let transaction = Transaction::new_signed_with_payer( - &[update_authority( - &program_id, - &metadata_pubkey, - &payer.pubkey(), - None.try_into().unwrap(), - )], - Some(&payer.pubkey()), - &[payer.as_ref()], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::IncorrectUpdateAuthority as u32), - ) - ); -} diff --git a/token-metadata/example/tests/update_field.rs b/token-metadata/example/tests/update_field.rs deleted file mode 100644 index d44356df91b..00000000000 --- a/token-metadata/example/tests/update_field.rs +++ /dev/null @@ -1,251 +0,0 @@ -#![cfg(feature = "test-sbf")] -#![allow(clippy::items_after_test_module)] - -mod program_test; -use { - program_test::{setup, setup_metadata, setup_mint}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - system_instruction, - transaction::{Transaction, TransactionError}, - }, - spl_token_metadata_interface::{ - error::TokenMetadataError, - instruction::update_field, - state::{Field, TokenMetadata}, - }, - spl_type_length_value::state::{TlvState, TlvStateBorrowed}, - test_case::test_case, -}; - -#[test_case(Field::Name, "This is my larger name".to_string() ; "larger name")] -#[test_case(Field::Name, "Smaller".to_string() ; "smaller name")] -#[test_case(Field::Key("my new field".to_string()), "Some data for the new field!".to_string() ; "new field")] -#[tokio::test] -async fn success_update(field: Field, value: String) { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let mut context = context.lock().await; - - let update_authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let mut token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority.pubkey()).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - - setup_metadata( - &mut context, - &program_id, - token.get_address(), - &token_metadata, - &metadata_keypair, - &mint_authority, - ) - .await; - - let rent = context.banks_client.get_rent().await.unwrap(); - let old_space = token_metadata.tlv_size_of().unwrap(); - let old_rent_lamports = rent.minimum_balance(old_space); - - token_metadata.update(field.clone(), value.clone()); - - let new_space = token_metadata.tlv_size_of().unwrap(); - - if new_space > old_space { - // fails without more lamports - let transaction = Transaction::new_signed_with_payer( - &[update_field( - &program_id, - &metadata_pubkey, - &update_authority.pubkey(), - field.clone(), - value.clone(), - )], - Some(&payer.pubkey()), - &[&payer, &update_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InsufficientFundsForRent { account_index: 2 } - ); - } - - // transfer required lamports - let new_rent_lamports = rent.minimum_balance(new_space); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &payer.pubkey(), - &metadata_pubkey, - new_rent_lamports.saturating_sub(old_rent_lamports), - ), - update_field( - &program_id, - &metadata_pubkey, - &update_authority.pubkey(), - field.clone(), - value.clone(), - ), - ], - Some(&payer.pubkey()), - &[&payer, &update_authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // check that the data is correct - let fetched_metadata_account = context - .banks_client - .get_account(metadata_pubkey) - .await - .unwrap() - .unwrap(); - assert_eq!( - fetched_metadata_account.data.len(), - token_metadata.tlv_size_of().unwrap() - ); - let fetched_metadata_state = TlvStateBorrowed::unpack(&fetched_metadata_account.data).unwrap(); - let fetched_metadata = fetched_metadata_state - .get_first_variable_len_value::() - .unwrap(); - assert_eq!(fetched_metadata, token_metadata); -} - -#[tokio::test] -async fn fail_authority_checks() { - let program_id = Pubkey::new_unique(); - let (context, client, payer) = setup(&program_id).await; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token_program_id = spl_token_2022::id(); - let decimals = 2; - let token = setup_mint( - &token_program_id, - &mint_authority_pubkey, - decimals, - payer.clone(), - client.clone(), - ) - .await; - let mut context = context.lock().await; - - let update_authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority.pubkey()).try_into().unwrap(), - mint: *token.get_address(), - ..Default::default() - }; - - let metadata_keypair = Keypair::new(); - let metadata_pubkey = metadata_keypair.pubkey(); - - setup_metadata( - &mut context, - &program_id, - token.get_address(), - &token_metadata, - &metadata_keypair, - &mint_authority, - ) - .await; - - // no signature - let mut instruction = update_field( - &program_id, - &metadata_pubkey, - &update_authority.pubkey(), - Field::Name, - "new_name".to_string(), - ); - instruction.accounts[1].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&payer.pubkey()), - &[payer.as_ref()], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature,) - ); - - // wrong authority - let transaction = Transaction::new_signed_with_payer( - &[update_field( - &program_id, - &metadata_pubkey, - &payer.pubkey(), - Field::Name, - "new_name".to_string(), - )], - Some(&payer.pubkey()), - &[payer.as_ref()], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::IncorrectUpdateAuthority as u32), - ) - ); -} diff --git a/token-metadata/interface/Cargo.toml b/token-metadata/interface/Cargo.toml deleted file mode 100644 index c904c15a9b5..00000000000 --- a/token-metadata/interface/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "spl-token-metadata-interface" -version = "0.6.0" -description = "Solana Program Library Token Metadata Interface" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -serde-traits = ["dep:serde", "spl-pod/serde-traits"] - -[dependencies] -borsh = "1.5.3" -num-derive = "0.4" -num-traits = "0.2" -serde = { version = "1.0.217", optional = true } -solana-borsh = "2.1.0" -solana-decode-error = "2.1.0" -solana-instruction = "2.1.0" -solana-msg = "2.1.0" -solana-program-error = "2.1.0" -spl-discriminator = { version = "0.4.0", path = "../../libraries/discriminator" } -solana-pubkey = "2.1.0" -spl-type-length-value = { version = "0.7.0", path = "../../libraries/type-length-value" } -spl-pod = { version = "0.5.0", path = "../../libraries/pod", features = [ - "borsh", -] } -thiserror = "2.0" - -[dev-dependencies] -serde_json = "1.0.135" -solana-sha256-hasher = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token-metadata/interface/README.md b/token-metadata/interface/README.md deleted file mode 100644 index 3ee6d402999..00000000000 --- a/token-metadata/interface/README.md +++ /dev/null @@ -1,107 +0,0 @@ -## Token-Metadata Interface - -An interface describing the instructions required for a program to implement -to be considered a "token-metadata" program for SPL token mints. The interface -can be implemented by any program. - -With a common interface, any wallet, dapp, or on-chain program can read the metadata, -and any tool that creates or modifies metadata will just work with any program -that implements the interface. - -There is also a `TokenMetadata` struct that may optionally be implemented, but -is not required because of the `Emit` instruction, which indexers and other off-chain -users can call to get metadata. - -### Example program - -Coming soon! - -### Motivation - -Token creators on Solana need all sorts of functionality for their token-metadata, -and the Metaplex Token-Metadata program has been the one place for all metadata -needs, leading to a feature-rich program that still might not serve all needs. - -At its base, token-metadata is a set of data fields associated to a particular token -mint, so we propose an interface that serves the simplest base case with some -compatibility with existing solutions. - -With this proposal implemented, fungible and non-fungible token creators will -have two options: - -* implement the interface in their own program, so they can eventually extend it -with new functionality or even other interfaces -* use a reference program that implements the simplest case - -### Required Instructions - -All of the following instructions are listed in greater detail in the source code. -Once the interface is decided, the information in the source code will be copied -here. - -#### Initialize - -Initializes the token-metadata TLV entry in an account with an update authority, -name, symbol, and URI. - -Must provide an SPL token mint and be signed by the mint authority. - -#### Update Field - -Updates a field in a token-metadata account. This may be an existing or totally -new field. - -Must be signed by the update authority. - -#### Remove Key - -Unsets a key-value pair, clearing an existing entry. - -Must be signed by the update authority. - -#### Update Authority - -Sets or unsets the token-metadata update authority, which signs any future updates -to the metadata. - -Must be signed by the update authority. - -#### Emit - -Emits token-metadata in the expected `TokenMetadata` state format. Although -implementing a struct that uses the exact state is optional, this instruction is -required. - -### (Optional) State - -A program that implements the interface may write the following data fields -into a type-length-value entry into an account: - -```rust -type Pubkey = [u8; 32]; -type OptionalNonZeroPubkey = Pubkey; // if all zeroes, interpreted as `None` - -pub struct TokenMetadata { - /// The authority that can sign to update the metadata - pub update_authority: OptionalNonZeroPubkey, - /// The associated mint, used to counter spoofing to be sure that metadata - /// belongs to a particular mint - pub mint: Pubkey, - /// The longer name of the token - pub name: String, - /// The shortened symbol for the token - pub symbol: String, - /// The URI pointing to richer metadata - pub uri: String, - /// Any additional metadata about the token as key-value pairs. The program - /// must avoid storing the same key twice. - pub additional_metadata: Vec<(String, String)>, -} -``` - -By storing the metadata in a TLV structure, a developer who implements this -interface in their program can freely add any other data fields in a different -TLV entry. - -You can find more information about TLV / type-length-value structures at the -[spl-type-length-value repo](https://github.com/solana-labs/solana-program-library/tree/master/libraries/type-length-value). diff --git a/token-metadata/interface/src/error.rs b/token-metadata/interface/src/error.rs deleted file mode 100644 index 0ec2fa3cb74..00000000000 --- a/token-metadata/interface/src/error.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Interface error types - -use { - solana_decode_error::DecodeError, - solana_msg::msg, - solana_program_error::{PrintProgramError, ProgramError}, -}; - -/// Errors that may be returned by the interface. -#[repr(u32)] -#[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] -pub enum TokenMetadataError { - /// Incorrect account provided - #[error("Incorrect account provided")] - IncorrectAccount = 901_952_957, - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, - /// Incorrect metadata update authority has signed the instruction - #[error("Incorrect metadata update authority has signed the instruction")] - IncorrectUpdateAuthority, - /// Token metadata has no update authority - #[error("Token metadata has no update authority")] - ImmutableMetadata, - /// Key not found in metadata account - #[error("Key not found in metadata account")] - KeyNotFound, -} - -impl From for ProgramError { - fn from(e: TokenMetadataError) -> Self { - ProgramError::Custom(e as u32) - } -} - -impl DecodeError for TokenMetadataError { - fn type_of() -> &'static str { - "TokenMetadataError" - } -} - -impl PrintProgramError for TokenMetadataError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - TokenMetadataError::IncorrectAccount => { - msg!("Incorrect account provided") - } - TokenMetadataError::MintHasNoMintAuthority => { - msg!("Mint has no mint authority") - } - TokenMetadataError::IncorrectMintAuthority => { - msg!("Incorrect mint authority has signed the instruction",) - } - TokenMetadataError::IncorrectUpdateAuthority => { - msg!("Incorrect metadata update authority has signed the instruction",) - } - TokenMetadataError::ImmutableMetadata => { - msg!("Token metadata has no update authority") - } - TokenMetadataError::KeyNotFound => { - msg!("Key not found in metadata account") - } - } - } -} diff --git a/token-metadata/interface/src/instruction.rs b/token-metadata/interface/src/instruction.rs deleted file mode 100644 index 5fb27c62fdc..00000000000 --- a/token-metadata/interface/src/instruction.rs +++ /dev/null @@ -1,504 +0,0 @@ -//! Instruction types - -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::state::Field, - borsh::{BorshDeserialize, BorshSerialize}, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - spl_discriminator::{discriminator::ArrayDiscriminator, SplDiscriminate}, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -/// Initialization instruction data -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)] -#[discriminator_hash_input("spl_token_metadata_interface:initialize_account")] -pub struct Initialize { - /// Longer name of the token - pub name: String, - /// Shortened symbol of the token - pub symbol: String, - /// URI pointing to more metadata (image, video, etc.) - pub uri: String, -} - -/// Update field instruction data -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)] -#[discriminator_hash_input("spl_token_metadata_interface:updating_field")] -pub struct UpdateField { - /// Field to update in the metadata - pub field: Field, - /// Value to write for the field - pub value: String, -} - -/// Remove key instruction data -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)] -#[discriminator_hash_input("spl_token_metadata_interface:remove_key_ix")] -pub struct RemoveKey { - /// If the idempotent flag is set to true, then the instruction will not - /// error if the key does not exist - pub idempotent: bool, - /// Key to remove in the additional metadata portion - pub key: String, -} - -/// Update authority instruction data -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[discriminator_hash_input("spl_token_metadata_interface:update_the_authority")] -pub struct UpdateAuthority { - /// New authority for the token metadata, or unset if `None` - pub new_authority: OptionalNonZeroPubkey, -} - -/// Instruction data for Emit -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, SplDiscriminate)] -#[discriminator_hash_input("spl_token_metadata_interface:emitter")] -pub struct Emit { - /// Start of range of data to emit - pub start: Option, - /// End of range of data to emit - pub end: Option, -} - -/// All instructions that must be implemented in the token-metadata interface -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Debug, PartialEq)] -pub enum TokenMetadataInstruction { - /// Initializes a TLV entry with the basic token-metadata fields. - /// - /// Assumes that the provided mint is an SPL token mint, that the metadata - /// account is allocated and assigned to the program, and that the metadata - /// account has enough lamports to cover the rent-exempt reserve. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Metadata - /// 1. `[]` Update authority - /// 2. `[]` Mint - /// 3. `[s]` Mint authority - /// - /// Data: `Initialize` data, name / symbol / uri strings - Initialize(Initialize), - - /// Updates a field in a token-metadata account. - /// - /// The field can be one of the required fields (name, symbol, URI), or a - /// totally new field denoted by a "key" string. - /// - /// By the end of the instruction, the metadata account must be properly - /// resized based on the new size of the TLV entry. - /// * If the new size is larger, the program must first reallocate to - /// avoid overwriting other TLV entries. - /// * If the new size is smaller, the program must reallocate at the end - /// so that it's possible to iterate over TLV entries - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Metadata account - /// 1. `[s]` Update authority - /// - /// Data: `UpdateField` data, specifying the new field and value. If the - /// field does not exist on the account, it will be created. If the - /// field does exist, it will be overwritten. - UpdateField(UpdateField), - - /// Removes a key-value pair in a token-metadata account. - /// - /// This only applies to additional fields, and not the base name / symbol / - /// URI fields. - /// - /// By the end of the instruction, the metadata account must be properly - /// resized at the end based on the new size of the TLV entry. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Metadata account - /// 1. `[s]` Update authority - /// - /// Data: the string key to remove. If the idempotent flag is set to false, - /// returns an error if the key is not present - RemoveKey(RemoveKey), - - /// Updates the token-metadata authority - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Metadata account - /// 1. `[s]` Current update authority - /// - /// Data: the new authority. Can be unset using a `None` value - UpdateAuthority(UpdateAuthority), - - /// Emits the token-metadata as return data - /// - /// The format of the data emitted follows exactly the `TokenMetadata` - /// struct, but it's possible that the account data is stored in another - /// format by the program. - /// - /// With this instruction, a program that implements the token-metadata - /// interface can return `TokenMetadata` without adhering to the specific - /// byte layout of the `TokenMetadata` struct in any accounts. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[]` Metadata account - Emit(Emit), -} -impl TokenMetadataInstruction { - /// Unpacks a byte buffer into a - /// [TokenMetadataInstruction](enum.TokenMetadataInstruction.html). - pub fn unpack(input: &[u8]) -> Result { - if input.len() < ArrayDiscriminator::LENGTH { - return Err(ProgramError::InvalidInstructionData); - } - let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH); - Ok(match discriminator { - Initialize::SPL_DISCRIMINATOR_SLICE => { - let data = Initialize::try_from_slice(rest)?; - Self::Initialize(data) - } - UpdateField::SPL_DISCRIMINATOR_SLICE => { - let data = UpdateField::try_from_slice(rest)?; - Self::UpdateField(data) - } - RemoveKey::SPL_DISCRIMINATOR_SLICE => { - let data = RemoveKey::try_from_slice(rest)?; - Self::RemoveKey(data) - } - UpdateAuthority::SPL_DISCRIMINATOR_SLICE => { - let data = UpdateAuthority::try_from_slice(rest)?; - Self::UpdateAuthority(data) - } - Emit::SPL_DISCRIMINATOR_SLICE => { - let data = Emit::try_from_slice(rest)?; - Self::Emit(data) - } - _ => return Err(ProgramError::InvalidInstructionData), - }) - } - - /// Packs a [TokenInstruction](enum.TokenInstruction.html) into a byte - /// buffer. - pub fn pack(&self) -> Vec { - let mut buf = vec![]; - match self { - Self::Initialize(data) => { - buf.extend_from_slice(Initialize::SPL_DISCRIMINATOR_SLICE); - buf.append(&mut borsh::to_vec(data).unwrap()); - } - Self::UpdateField(data) => { - buf.extend_from_slice(UpdateField::SPL_DISCRIMINATOR_SLICE); - buf.append(&mut borsh::to_vec(data).unwrap()); - } - Self::RemoveKey(data) => { - buf.extend_from_slice(RemoveKey::SPL_DISCRIMINATOR_SLICE); - buf.append(&mut borsh::to_vec(data).unwrap()); - } - Self::UpdateAuthority(data) => { - buf.extend_from_slice(UpdateAuthority::SPL_DISCRIMINATOR_SLICE); - buf.append(&mut borsh::to_vec(data).unwrap()); - } - Self::Emit(data) => { - buf.extend_from_slice(Emit::SPL_DISCRIMINATOR_SLICE); - buf.append(&mut borsh::to_vec(data).unwrap()); - } - }; - buf - } -} - -/// Creates an `Initialize` instruction -#[allow(clippy::too_many_arguments)] -pub fn initialize( - program_id: &Pubkey, - metadata: &Pubkey, - update_authority: &Pubkey, - mint: &Pubkey, - mint_authority: &Pubkey, - name: String, - symbol: String, - uri: String, -) -> Instruction { - let data = TokenMetadataInstruction::Initialize(Initialize { name, symbol, uri }); - Instruction { - program_id: *program_id, - accounts: vec![ - AccountMeta::new(*metadata, false), - AccountMeta::new_readonly(*update_authority, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new_readonly(*mint_authority, true), - ], - data: data.pack(), - } -} - -/// Creates an `UpdateField` instruction -pub fn update_field( - program_id: &Pubkey, - metadata: &Pubkey, - update_authority: &Pubkey, - field: Field, - value: String, -) -> Instruction { - let data = TokenMetadataInstruction::UpdateField(UpdateField { field, value }); - Instruction { - program_id: *program_id, - accounts: vec![ - AccountMeta::new(*metadata, false), - AccountMeta::new_readonly(*update_authority, true), - ], - data: data.pack(), - } -} - -/// Creates a `RemoveKey` instruction -pub fn remove_key( - program_id: &Pubkey, - metadata: &Pubkey, - update_authority: &Pubkey, - key: String, - idempotent: bool, -) -> Instruction { - let data = TokenMetadataInstruction::RemoveKey(RemoveKey { key, idempotent }); - Instruction { - program_id: *program_id, - accounts: vec![ - AccountMeta::new(*metadata, false), - AccountMeta::new_readonly(*update_authority, true), - ], - data: data.pack(), - } -} - -/// Creates an `UpdateAuthority` instruction -pub fn update_authority( - program_id: &Pubkey, - metadata: &Pubkey, - current_authority: &Pubkey, - new_authority: OptionalNonZeroPubkey, -) -> Instruction { - let data = TokenMetadataInstruction::UpdateAuthority(UpdateAuthority { new_authority }); - Instruction { - program_id: *program_id, - accounts: vec![ - AccountMeta::new(*metadata, false), - AccountMeta::new_readonly(*current_authority, true), - ], - data: data.pack(), - } -} - -/// Creates an `Emit` instruction -pub fn emit( - program_id: &Pubkey, - metadata: &Pubkey, - start: Option, - end: Option, -) -> Instruction { - let data = TokenMetadataInstruction::Emit(Emit { start, end }); - Instruction { - program_id: *program_id, - accounts: vec![AccountMeta::new_readonly(*metadata, false)], - data: data.pack(), - } -} - -#[cfg(test)] -mod test { - #[cfg(feature = "serde-traits")] - use std::str::FromStr; - use {super::*, crate::NAMESPACE, solana_sha256_hasher::hashv}; - - fn check_pack_unpack( - instruction: TokenMetadataInstruction, - discriminator: &[u8], - data: T, - ) { - let mut expect = vec![]; - expect.extend_from_slice(discriminator.as_ref()); - expect.append(&mut borsh::to_vec(&data).unwrap()); - let packed = instruction.pack(); - assert_eq!(packed, expect); - let unpacked = TokenMetadataInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, instruction); - } - - #[test] - fn initialize_pack() { - let name = "My test token"; - let symbol = "TEST"; - let uri = "http://test.test"; - let data = Initialize { - name: name.to_string(), - symbol: symbol.to_string(), - uri: uri.to_string(), - }; - let check = TokenMetadataInstruction::Initialize(data.clone()); - let preimage = hashv(&[format!("{NAMESPACE}:initialize_account").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - check_pack_unpack(check, discriminator, data); - } - - #[test] - fn update_field_pack() { - let field = "MyTestField"; - let value = "http://test.uri"; - let data = UpdateField { - field: Field::Key(field.to_string()), - value: value.to_string(), - }; - let check = TokenMetadataInstruction::UpdateField(data.clone()); - let preimage = hashv(&[format!("{NAMESPACE}:updating_field").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - check_pack_unpack(check, discriminator, data); - } - - #[test] - fn remove_key_pack() { - let data = RemoveKey { - key: "MyTestField".to_string(), - idempotent: true, - }; - let check = TokenMetadataInstruction::RemoveKey(data.clone()); - let preimage = hashv(&[format!("{NAMESPACE}:remove_key_ix").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - check_pack_unpack(check, discriminator, data); - } - - #[test] - fn update_authority_pack() { - let data = UpdateAuthority { - new_authority: OptionalNonZeroPubkey::default(), - }; - let check = TokenMetadataInstruction::UpdateAuthority(data.clone()); - let preimage = hashv(&[format!("{NAMESPACE}:update_the_authority").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - check_pack_unpack(check, discriminator, data); - } - - #[test] - fn emit_pack() { - let data = Emit { - start: None, - end: Some(10), - }; - let check = TokenMetadataInstruction::Emit(data.clone()); - let preimage = hashv(&[format!("{NAMESPACE}:emitter").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - check_pack_unpack(check, discriminator, data); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn initialize_serde() { - let data = Initialize { - name: "Token Name".to_string(), - symbol: "TST".to_string(), - uri: "uri.test".to_string(), - }; - let ix = TokenMetadataInstruction::Initialize(data); - let serialized = serde_json::to_string(&ix).unwrap(); - let serialized_expected = - "{\"initialize\":{\"name\":\"Token Name\",\"symbol\":\"TST\",\"uri\":\"uri.test\"}}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(ix, deserialized); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn update_field_serde() { - let data = UpdateField { - field: Field::Key("MyField".to_string()), - value: "my field value".to_string(), - }; - let ix = TokenMetadataInstruction::UpdateField(data); - let serialized = serde_json::to_string(&ix).unwrap(); - let serialized_expected = - "{\"updateField\":{\"field\":{\"key\":\"MyField\"},\"value\":\"my field value\"}}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(ix, deserialized); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn remove_key_serde() { - let data = RemoveKey { - key: "MyTestField".to_string(), - idempotent: true, - }; - let ix = TokenMetadataInstruction::RemoveKey(data); - let serialized = serde_json::to_string(&ix).unwrap(); - let serialized_expected = "{\"removeKey\":{\"idempotent\":true,\"key\":\"MyTestField\"}}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(ix, deserialized); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn update_authority_serde() { - let update_authority_option: Option = - Some(Pubkey::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap()); - let update_authority: OptionalNonZeroPubkey = update_authority_option.try_into().unwrap(); - let data = UpdateAuthority { - new_authority: update_authority, - }; - let ix = TokenMetadataInstruction::UpdateAuthority(data); - let serialized = serde_json::to_string(&ix).unwrap(); - let serialized_expected = "{\"updateAuthority\":{\"newAuthority\":\"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM\"}}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(ix, deserialized); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn update_authority_serde_with_none() { - let data = UpdateAuthority { - new_authority: OptionalNonZeroPubkey::default(), - }; - let ix = TokenMetadataInstruction::UpdateAuthority(data); - let serialized = serde_json::to_string(&ix).unwrap(); - let serialized_expected = "{\"updateAuthority\":{\"newAuthority\":null}}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(ix, deserialized); - } - - #[cfg(feature = "serde-traits")] - #[test] - fn emit_serde() { - let data = Emit { - start: None, - end: Some(10), - }; - let ix = TokenMetadataInstruction::Emit(data); - let serialized = serde_json::to_string(&ix).unwrap(); - let serialized_expected = "{\"emit\":{\"start\":null,\"end\":10}}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = serde_json::from_str::(&serialized).unwrap(); - assert_eq!(ix, deserialized); - } -} diff --git a/token-metadata/interface/src/lib.rs b/token-metadata/interface/src/lib.rs deleted file mode 100644 index 26d4e2175b6..00000000000 --- a/token-metadata/interface/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Crate defining an interface for token-metadata - -#![allow(clippy::arithmetic_side_effects)] -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -pub mod error; -pub mod instruction; -pub mod state; - -// Export current sdk types for downstream users building with a different sdk -// version Export borsh for downstream users -pub use { - borsh, solana_borsh, solana_decode_error, solana_instruction, solana_msg, solana_program_error, -}; - -/// Namespace for all programs implementing token-metadata -pub const NAMESPACE: &str = "spl_token_metadata_interface"; diff --git a/token-metadata/interface/src/state.rs b/token-metadata/interface/src/state.rs deleted file mode 100644 index e1404d28f4d..00000000000 --- a/token-metadata/interface/src/state.rs +++ /dev/null @@ -1,219 +0,0 @@ -//! Token-metadata interface state types - -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, - solana_borsh::v1::{get_instance_packed_len, try_from_slice_unchecked}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_type_length_value::{ - state::{TlvState, TlvStateBorrowed}, - variable_len_pack::VariableLenPack, - }, -}; - -/// Data struct for all token-metadata, stored in a TLV entry -/// -/// The type and length parts must be handled by the TLV library, and not stored -/// as part of this struct. -#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] -pub struct TokenMetadata { - /// The authority that can sign to update the metadata - pub update_authority: OptionalNonZeroPubkey, - /// The associated mint, used to counter spoofing to be sure that metadata - /// belongs to a particular mint - pub mint: Pubkey, - /// The longer name of the token - pub name: String, - /// The shortened symbol for the token - pub symbol: String, - /// The URI pointing to richer metadata - pub uri: String, - /// Any additional metadata about the token as key-value pairs. The program - /// must avoid storing the same key twice. - pub additional_metadata: Vec<(String, String)>, -} -impl SplDiscriminate for TokenMetadata { - /// Please use this discriminator in your program when matching - const SPL_DISCRIMINATOR: ArrayDiscriminator = - ArrayDiscriminator::new([112, 132, 90, 90, 11, 88, 157, 87]); -} -impl TokenMetadata { - /// Gives the total size of this struct as a TLV entry in an account - pub fn tlv_size_of(&self) -> Result { - TlvStateBorrowed::get_base_len() - .checked_add(get_instance_packed_len(self)?) - .ok_or(ProgramError::InvalidAccountData) - } - - /// Updates a field in the metadata struct - pub fn update(&mut self, field: Field, value: String) { - match field { - Field::Name => self.name = value, - Field::Symbol => self.symbol = value, - Field::Uri => self.uri = value, - Field::Key(key) => self.set_key_value(key, value), - } - } - - /// Sets a key-value pair in the additional metadata - /// - /// If the key is already present, overwrites the existing entry. Otherwise, - /// adds it to the end. - pub fn set_key_value(&mut self, new_key: String, new_value: String) { - for (key, value) in self.additional_metadata.iter_mut() { - if *key == new_key { - value.replace_range(.., &new_value); - return; - } - } - self.additional_metadata.push((new_key, new_value)); - } - - /// Removes the key-value pair given by the provided key. Returns true if - /// the key was found. - pub fn remove_key(&mut self, key: &str) -> bool { - let mut found_key = false; - self.additional_metadata.retain(|x| { - let should_retain = x.0 != key; - if !should_retain { - found_key = true; - } - should_retain - }); - found_key - } - - /// Get the slice corresponding to the given start and end range - pub fn get_slice(data: &[u8], start: Option, end: Option) -> Option<&[u8]> { - let start = start.unwrap_or(0) as usize; - let end = end.map(|x| x as usize).unwrap_or(data.len()); - data.get(start..end) - } -} -impl VariableLenPack for TokenMetadata { - fn pack_into_slice(&self, dst: &mut [u8]) -> Result<(), ProgramError> { - borsh::to_writer(&mut dst[..], self).map_err(Into::into) - } - fn unpack_from_slice(src: &[u8]) -> Result { - try_from_slice_unchecked(src).map_err(Into::into) - } - fn get_packed_len(&self) -> Result { - get_instance_packed_len(self).map_err(Into::into) - } -} - -/// Fields in the metadata account, used for updating -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)] -pub enum Field { - /// The name field, corresponding to `TokenMetadata.name` - Name, - /// The symbol field, corresponding to `TokenMetadata.symbol` - Symbol, - /// The uri field, corresponding to `TokenMetadata.uri` - Uri, - /// A user field, whose key is given by the associated string - Key(String), -} - -#[cfg(test)] -mod tests { - use {super::*, crate::NAMESPACE, solana_sha256_hasher::hashv}; - - #[test] - fn discriminator() { - let preimage = hashv(&[format!("{NAMESPACE}:token_metadata").as_bytes()]); - let discriminator = - ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); - assert_eq!(TokenMetadata::SPL_DISCRIMINATOR, discriminator); - } - - #[test] - fn update() { - let name = "name".to_string(); - let symbol = "symbol".to_string(); - let uri = "uri".to_string(); - let mut token_metadata = TokenMetadata { - name, - symbol, - uri, - ..Default::default() - }; - - // updating base fields - let new_name = "new_name".to_string(); - token_metadata.update(Field::Name, new_name.clone()); - assert_eq!(token_metadata.name, new_name); - - let new_symbol = "new_symbol".to_string(); - token_metadata.update(Field::Symbol, new_symbol.clone()); - assert_eq!(token_metadata.symbol, new_symbol); - - let new_uri = "new_uri".to_string(); - token_metadata.update(Field::Uri, new_uri.clone()); - assert_eq!(token_metadata.uri, new_uri); - - // add new key-value pairs - let key1 = "key1".to_string(); - let value1 = "value1".to_string(); - token_metadata.update(Field::Key(key1.clone()), value1.clone()); - assert_eq!(token_metadata.additional_metadata.len(), 1); - assert_eq!( - token_metadata.additional_metadata[0], - (key1.clone(), value1.clone()) - ); - - let key2 = "key2".to_string(); - let value2 = "value2".to_string(); - token_metadata.update(Field::Key(key2.clone()), value2.clone()); - assert_eq!(token_metadata.additional_metadata.len(), 2); - assert_eq!( - token_metadata.additional_metadata[0], - (key1.clone(), value1) - ); - assert_eq!( - token_metadata.additional_metadata[1], - (key2.clone(), value2.clone()) - ); - - // update first key, see that order is preserved - let new_value1 = "new_value1".to_string(); - token_metadata.update(Field::Key(key1.clone()), new_value1.clone()); - assert_eq!(token_metadata.additional_metadata.len(), 2); - assert_eq!(token_metadata.additional_metadata[0], (key1, new_value1)); - assert_eq!(token_metadata.additional_metadata[1], (key2, value2)); - } - - #[test] - fn remove_key() { - let name = "name".to_string(); - let symbol = "symbol".to_string(); - let uri = "uri".to_string(); - let mut token_metadata = TokenMetadata { - name, - symbol, - uri, - ..Default::default() - }; - - // add new key-value pair - let key = "key".to_string(); - let value = "value".to_string(); - token_metadata.update(Field::Key(key.clone()), value.clone()); - assert_eq!(token_metadata.additional_metadata.len(), 1); - assert_eq!(token_metadata.additional_metadata[0], (key.clone(), value)); - - // remove it - assert!(token_metadata.remove_key(&key)); - assert_eq!(token_metadata.additional_metadata.len(), 0); - - // remove it again, returns false - assert!(!token_metadata.remove_key(&key)); - assert_eq!(token_metadata.additional_metadata.len(), 0); - } -} diff --git a/token-metadata/js/.eslintignore b/token-metadata/js/.eslintignore deleted file mode 100644 index 6da325effab..00000000000 --- a/token-metadata/js/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -docs -lib -test-ledger - -package-lock.json diff --git a/token-metadata/js/.eslintrc b/token-metadata/js/.eslintrc deleted file mode 100644 index 5aef10a4729..00000000000 --- a/token-metadata/js/.eslintrc +++ /dev/null @@ -1,34 +0,0 @@ -{ - "root": true, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - "plugin:require-extensions/recommended" - ], - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "prettier", - "require-extensions" - ], - "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/consistent-type-imports": "error" - }, - "overrides": [ - { - "files": [ - "examples/**/*", - "test/**/*" - ], - "rules": { - "require-extensions/require-extensions": "off", - "require-extensions/require-index": "off" - } - } - ] -} diff --git a/token-metadata/js/.gitignore b/token-metadata/js/.gitignore deleted file mode 100644 index 21f33db819c..00000000000 --- a/token-metadata/js/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -.idea -.vscode -.DS_Store - -node_modules - -pnpm-lock.yaml -yarn.lock - -docs -lib -test-ledger -*.tsbuildinfo diff --git a/token-metadata/js/.mocharc.json b/token-metadata/js/.mocharc.json deleted file mode 100644 index 451c14c3016..00000000000 --- a/token-metadata/js/.mocharc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extension": ["ts"], - "node-option": ["experimental-specifier-resolution=node", "loader=ts-node/esm"], - "timeout": 5000 -} diff --git a/token-metadata/js/.nojekyll b/token-metadata/js/.nojekyll deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/token-metadata/js/LICENSE b/token-metadata/js/LICENSE deleted file mode 100644 index d6456956733..00000000000 --- a/token-metadata/js/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/token-metadata/js/README.md b/token-metadata/js/README.md deleted file mode 100644 index 805d80184cd..00000000000 --- a/token-metadata/js/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# `@solana/spl-token-metadata` - -A TypeScript interface describing the instructions required for a program to implement to be considered a "token-metadata" program for SPL token mints. The interface can be implemented by any program. - -## Links - -- [TypeScript Docs](https://solana-labs.github.io/solana-program-library/token-metadata/js/) -- [FAQs (Frequently Asked Questions)](#faqs) -- [Install](#install) -- [Build from Source](#build-from-source) - -## FAQs - -### How can I get support? - -Please ask questions in the Solana Stack Exchange: https://solana.stackexchange.com/ - -If you've found a bug or you'd like to request a feature, please -[open an issue](https://github.com/solana-labs/solana-program-library/issues/new). - -## Install - -```shell -npm install --save @solana/spl-token-metadata @solana/web3.js@1 -``` -_OR_ -```shell -yarn add @solana/spl-token-metadata @solana/web3.js@1 -``` - -## Build from Source - -0. Prerequisites - -* Node 16+ -* NPM 8+ - -1. Clone the project: -```shell -git clone https://github.com/solana-labs/solana-program-library.git -``` - -2. Navigate to the library: -```shell -cd solana-program-library/token-metadata/js -``` - -3. Install the dependencies: -```shell -npm install -``` - -4. Build the library: -```shell -npm run build -``` - -5. Build the on-chain programs: -```shell -npm run test:build-programs -``` diff --git a/token-metadata/js/package.json b/token-metadata/js/package.json deleted file mode 100644 index da9766e1951..00000000000 --- a/token-metadata/js/package.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "@solana/spl-token-metadata", - "description": "SPL Token Metadata Interface JS API", - "version": "0.1.6", - "author": "Solana Labs Maintainers ", - "repository": "https://github.com/solana-labs/solana-program-library", - "license": "Apache-2.0", - "type": "module", - "sideEffects": false, - "engines": { - "node": ">=16" - }, - "files": [ - "lib", - "src", - "LICENSE", - "README.md" - ], - "publishConfig": { - "access": "public" - }, - "main": "./lib/cjs/index.js", - "module": "./lib/esm/index.js", - "types": "./lib/types/index.d.ts", - "exports": { - "types": "./lib/types/index.d.ts", - "require": "./lib/cjs/index.js", - "import": "./lib/esm/index.js" - }, - "scripts": { - "build": "tsc --build --verbose tsconfig.all.json", - "clean": "shx rm -rf lib **/*.tsbuildinfo || true", - "deploy": "npm run deploy:docs", - "deploy:docs": "npm run docs && gh-pages --dest token-metadata/js --dist docs --dotfiles", - "docs": "shx rm -rf docs && typedoc && shx cp .nojekyll docs/", - "lint": "eslint --max-warnings 0 .", - "lint:fix": "eslint --fix .", - "nuke": "shx rm -rf node_modules package-lock.json || true", - "postbuild": "shx echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", - "reinstall": "npm run nuke && npm install", - "release": "npm run clean && npm run build", - "test": "mocha test", - "watch": "tsc --build --verbose --watch tsconfig.all.json" - }, - "peerDependencies": { - "@solana/web3.js": "^1.95.5" - }, - "dependencies": { - "@solana/codecs": "2.0.0" - }, - "devDependencies": { - "@solana/spl-type-length-value": "0.2.0", - "@solana/web3.js": "^1.95.5", - "@types/chai": "^5.0.1", - "@types/mocha": "^10.0.10", - "@types/node": "^22.10.5", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "@typescript-eslint/parser": "^8.4.0", - "chai": "^5.1.2", - "eslint": "^8.57.0", - "eslint-plugin-require-extensions": "^0.1.1", - "gh-pages": "^6.3.0", - "mocha": "^11.0.1", - "shx": "^0.3.4", - "ts-node": "^10.9.2", - "tslib": "^2.8.1", - "typedoc": "^0.27.6", - "typescript": "^5.7.2" - } -} diff --git a/token-metadata/js/src/errors.ts b/token-metadata/js/src/errors.ts deleted file mode 100644 index b86785b0bb6..00000000000 --- a/token-metadata/js/src/errors.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Errors match those in rust https://github.com/solana-labs/solana-program-library/blob/master/token-metadata/interface/src/error.rs -// Code follows: https://github.com/solana-labs/solana-program-library/blob/master/token/js/src/errors.tshttps://github.com/solana-labs/solana-program-library/blob/master/token/js/src/errors.ts - -/** Base class for errors */ -export class TokenMetadataError extends Error { - constructor(message?: string) { - super(message); - } -} - -/** Thrown if incorrect account provided */ -export class IncorrectAccountError extends TokenMetadataError { - name = 'IncorrectAccountError'; -} - -/** Thrown if Mint has no mint authority */ -export class MintHasNoMintAuthorityError extends TokenMetadataError { - name = 'MintHasNoMintAuthorityError'; -} - -/** Thrown if Incorrect mint authority has signed the instruction */ -export class IncorrectMintAuthorityError extends TokenMetadataError { - name = 'IncorrectMintAuthorityError'; -} - -/** Thrown if Incorrect mint authority has signed the instruction */ -export class IncorrectUpdateAuthorityError extends TokenMetadataError { - name = 'IncorrectUpdateAuthorityError'; -} - -/** Thrown if Token metadata has no update authority */ -export class ImmutableMetadataError extends TokenMetadataError { - name = 'ImmutableMetadataError'; -} - -/** Thrown if Key not found in metadata account */ -export class KeyNotFoundError extends TokenMetadataError { - name = 'KeyNotFoundError'; -} diff --git a/token-metadata/js/src/field.ts b/token-metadata/js/src/field.ts deleted file mode 100644 index 58db19433ef..00000000000 --- a/token-metadata/js/src/field.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Codec } from '@solana/codecs'; -import { - addCodecSizePrefix, - getU32Codec, - getUtf8Codec, - getStructCodec, - getTupleCodec, - getUnitCodec, -} from '@solana/codecs'; - -export enum Field { - Name, - Symbol, - Uri, -} - -type FieldLayout = { __kind: 'Name' } | { __kind: 'Symbol' } | { __kind: 'Uri' } | { __kind: 'Key'; value: [string] }; - -export const getFieldCodec = () => - [ - ['Name', getUnitCodec()], - ['Symbol', getUnitCodec()], - ['Uri', getUnitCodec()], - ['Key', getStructCodec([['value', getTupleCodec([addCodecSizePrefix(getUtf8Codec(), getU32Codec())])]])], - ] as const; - -export function getFieldConfig(field: Field | string): FieldLayout { - if (field === Field.Name || field === 'Name' || field === 'name') { - return { __kind: 'Name' }; - } else if (field === Field.Symbol || field === 'Symbol' || field === 'symbol') { - return { __kind: 'Symbol' }; - } else if (field === Field.Uri || field === 'Uri' || field === 'uri') { - return { __kind: 'Uri' }; - } else { - return { __kind: 'Key', value: [field] }; - } -} diff --git a/token-metadata/js/src/index.ts b/token-metadata/js/src/index.ts deleted file mode 100644 index faffcde7683..00000000000 --- a/token-metadata/js/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './errors.js'; -export * from './field.js'; -export * from './instruction.js'; -export * from './state.js'; diff --git a/token-metadata/js/src/instruction.ts b/token-metadata/js/src/instruction.ts deleted file mode 100644 index 44fc4a3d82c..00000000000 --- a/token-metadata/js/src/instruction.ts +++ /dev/null @@ -1,201 +0,0 @@ -import type { Encoder } from '@solana/codecs'; -import { - addEncoderSizePrefix, - fixEncoderSize, - getBooleanEncoder, - getBytesEncoder, - getDataEnumCodec, - getOptionEncoder, - getUtf8Encoder, - getStructEncoder, - getTupleEncoder, - getU32Encoder, - getU64Encoder, - transformEncoder, -} from '@solana/codecs'; -import type { VariableSizeEncoder } from '@solana/codecs'; -import type { PublicKey } from '@solana/web3.js'; -import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; - -import type { Field } from './field.js'; -import { getFieldCodec, getFieldConfig } from './field.js'; - -function getInstructionEncoder(discriminator: Uint8Array, dataEncoder: Encoder): Encoder { - return transformEncoder(getTupleEncoder([getBytesEncoder(), dataEncoder]), (data: T): [Uint8Array, T] => [ - discriminator, - data, - ]); -} - -function getPublicKeyEncoder(): Encoder { - return transformEncoder(fixEncoderSize(getBytesEncoder(), 32), (publicKey: PublicKey) => publicKey.toBytes()); -} - -function getStringEncoder(): VariableSizeEncoder { - return addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder()); -} - -/** - * Initializes a TLV entry with the basic token-metadata fields. - * - * Assumes that the provided mint is an SPL token mint, that the metadata - * account is allocated and assigned to the program, and that the metadata - * account has enough lamports to cover the rent-exempt reserve. - */ -export interface InitializeInstructionArgs { - programId: PublicKey; - metadata: PublicKey; - updateAuthority: PublicKey; - mint: PublicKey; - mintAuthority: PublicKey; - name: string; - symbol: string; - uri: string; -} - -export function createInitializeInstruction(args: InitializeInstructionArgs): TransactionInstruction { - const { programId, metadata, updateAuthority, mint, mintAuthority, name, symbol, uri } = args; - return new TransactionInstruction({ - programId, - keys: [ - { isSigner: false, isWritable: true, pubkey: metadata }, - { isSigner: false, isWritable: false, pubkey: updateAuthority }, - { isSigner: false, isWritable: false, pubkey: mint }, - { isSigner: true, isWritable: false, pubkey: mintAuthority }, - ], - data: Buffer.from( - getInstructionEncoder( - new Uint8Array([ - /* await splDiscriminate('spl_token_metadata_interface:initialize_account') */ - 210, 225, 30, 162, 88, 184, 77, 141, - ]), - getStructEncoder([ - ['name', getStringEncoder()], - ['symbol', getStringEncoder()], - ['uri', getStringEncoder()], - ]), - ).encode({ name, symbol, uri }), - ), - }); -} - -/** - * If the field does not exist on the account, it will be created. - * If the field does exist, it will be overwritten. - */ -export interface UpdateFieldInstruction { - programId: PublicKey; - metadata: PublicKey; - updateAuthority: PublicKey; - field: Field | string; - value: string; -} - -export function createUpdateFieldInstruction(args: UpdateFieldInstruction): TransactionInstruction { - const { programId, metadata, updateAuthority, field, value } = args; - return new TransactionInstruction({ - programId, - keys: [ - { isSigner: false, isWritable: true, pubkey: metadata }, - { isSigner: true, isWritable: false, pubkey: updateAuthority }, - ], - data: Buffer.from( - getInstructionEncoder( - new Uint8Array([ - /* await splDiscriminate('spl_token_metadata_interface:updating_field') */ - 221, 233, 49, 45, 181, 202, 220, 200, - ]), - getStructEncoder([ - ['field', getDataEnumCodec(getFieldCodec())], - ['value', getStringEncoder()], - ]), - ).encode({ field: getFieldConfig(field), value }), - ), - }); -} - -export interface RemoveKeyInstructionArgs { - programId: PublicKey; - metadata: PublicKey; - updateAuthority: PublicKey; - key: string; - idempotent: boolean; -} - -export function createRemoveKeyInstruction(args: RemoveKeyInstructionArgs) { - const { programId, metadata, updateAuthority, key, idempotent } = args; - return new TransactionInstruction({ - programId, - keys: [ - { isSigner: false, isWritable: true, pubkey: metadata }, - { isSigner: true, isWritable: false, pubkey: updateAuthority }, - ], - data: Buffer.from( - getInstructionEncoder( - new Uint8Array([ - /* await splDiscriminate('spl_token_metadata_interface:remove_key_ix') */ - 234, 18, 32, 56, 89, 141, 37, 181, - ]), - getStructEncoder([ - ['idempotent', getBooleanEncoder()], - ['key', getStringEncoder()], - ]), - ).encode({ idempotent, key }), - ), - }); -} - -export interface UpdateAuthorityInstructionArgs { - programId: PublicKey; - metadata: PublicKey; - oldAuthority: PublicKey; - newAuthority: PublicKey | null; -} - -export function createUpdateAuthorityInstruction(args: UpdateAuthorityInstructionArgs): TransactionInstruction { - const { programId, metadata, oldAuthority, newAuthority } = args; - - return new TransactionInstruction({ - programId, - keys: [ - { isSigner: false, isWritable: true, pubkey: metadata }, - { isSigner: true, isWritable: false, pubkey: oldAuthority }, - ], - data: Buffer.from( - getInstructionEncoder( - new Uint8Array([ - /* await splDiscriminate('spl_token_metadata_interface:update_the_authority') */ - 215, 228, 166, 228, 84, 100, 86, 123, - ]), - getStructEncoder([['newAuthority', getPublicKeyEncoder()]]), - ).encode({ newAuthority: newAuthority ?? SystemProgram.programId }), - ), - }); -} - -export interface EmitInstructionArgs { - programId: PublicKey; - metadata: PublicKey; - start?: bigint; - end?: bigint; -} - -export function createEmitInstruction(args: EmitInstructionArgs): TransactionInstruction { - const { programId, metadata, start, end } = args; - return new TransactionInstruction({ - programId, - keys: [{ isSigner: false, isWritable: false, pubkey: metadata }], - data: Buffer.from( - getInstructionEncoder( - new Uint8Array([ - /* await splDiscriminate('spl_token_metadata_interface:emitter') */ - 250, 166, 180, 250, 13, 12, 184, 70, - ]), - getStructEncoder([ - ['start', getOptionEncoder(getU64Encoder())], - ['end', getOptionEncoder(getU64Encoder())], - ]), - ).encode({ start: start ?? null, end: end ?? null }), - ), - }); -} diff --git a/token-metadata/js/src/state.ts b/token-metadata/js/src/state.ts deleted file mode 100644 index 6f689ebd893..00000000000 --- a/token-metadata/js/src/state.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import { - addCodecSizePrefix, - fixCodecSize, - getArrayCodec, - getBytesCodec, - getUtf8Codec, - getU32Codec, - getStructCodec, - getTupleCodec, -} from '@solana/codecs'; -import type { ReadonlyUint8Array, VariableSizeCodec } from '@solana/codecs'; - -export const TOKEN_METADATA_DISCRIMINATOR = Buffer.from([112, 132, 90, 90, 11, 88, 157, 87]); - -function getStringCodec(): VariableSizeCodec { - return addCodecSizePrefix(getUtf8Codec(), getU32Codec()); -} - -const tokenMetadataCodec = getStructCodec([ - ['updateAuthority', fixCodecSize(getBytesCodec(), 32)], - ['mint', fixCodecSize(getBytesCodec(), 32)], - ['name', getStringCodec()], - ['symbol', getStringCodec()], - ['uri', getStringCodec()], - ['additionalMetadata', getArrayCodec(getTupleCodec([getStringCodec(), getStringCodec()]))], -]); - -export interface TokenMetadata { - // The authority that can sign to update the metadata - updateAuthority?: PublicKey; - // The associated mint, used to counter spoofing to be sure that metadata belongs to a particular mint - mint: PublicKey; - // The longer name of the token - name: string; - // The shortened symbol for the token - symbol: string; - // The URI pointing to richer metadata - uri: string; - // Any additional metadata about the token as key-value pairs - additionalMetadata: (readonly [string, string])[]; -} - -// Checks if all elements in the array are 0 -function isNonePubkey(buffer: ReadonlyUint8Array): boolean { - for (let i = 0; i < buffer.length; i++) { - if (buffer[i] !== 0) { - return false; - } - } - return true; -} - -// Pack TokenMetadata into byte slab -export function pack(meta: TokenMetadata): ReadonlyUint8Array { - // If no updateAuthority given, set it to the None/Zero PublicKey for encoding - const updateAuthority = meta.updateAuthority ?? PublicKey.default; - return tokenMetadataCodec.encode({ - ...meta, - updateAuthority: updateAuthority.toBuffer(), - mint: meta.mint.toBuffer(), - }); -} - -// unpack byte slab into TokenMetadata -export function unpack(buffer: Buffer | Uint8Array | ReadonlyUint8Array): TokenMetadata { - const data = tokenMetadataCodec.decode(buffer); - - return isNonePubkey(data.updateAuthority) - ? { - mint: new PublicKey(data.mint), - name: data.name, - symbol: data.symbol, - uri: data.uri, - additionalMetadata: data.additionalMetadata, - } - : { - updateAuthority: new PublicKey(data.updateAuthority), - mint: new PublicKey(data.mint), - name: data.name, - symbol: data.symbol, - uri: data.uri, - additionalMetadata: data.additionalMetadata, - }; -} diff --git a/token-metadata/js/test/instruction.test.ts b/token-metadata/js/test/instruction.test.ts deleted file mode 100644 index 2641238ccd2..00000000000 --- a/token-metadata/js/test/instruction.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { expect } from 'chai'; - -import { - createEmitInstruction, - createInitializeInstruction, - createRemoveKeyInstruction, - createUpdateAuthorityInstruction, - createUpdateFieldInstruction, - getFieldCodec, - getFieldConfig, -} from '../src'; -import { - addDecoderSizePrefix, - fixDecoderSize, - getBooleanDecoder, - getBytesDecoder, - getDataEnumCodec, - getOptionDecoder, - getUtf8Decoder, - getU32Decoder, - getU64Decoder, - getStructDecoder, - some, -} from '@solana/codecs'; -import { splDiscriminate } from '@solana/spl-type-length-value'; -import type { Decoder, Option, VariableSizeDecoder } from '@solana/codecs'; -import { PublicKey, type TransactionInstruction } from '@solana/web3.js'; - -function checkPackUnpack( - instruction: TransactionInstruction, - discriminator: Uint8Array, - decoder: Decoder, - values: T, -) { - expect(instruction.data.subarray(0, 8)).to.deep.equal(discriminator); - const unpacked = decoder.decode(instruction.data.subarray(8)); - expect(unpacked).to.deep.equal(values); -} - -function getStringDecoder(): VariableSizeDecoder { - return addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder()); -} - -describe('Token Metadata Instructions', () => { - const programId = new PublicKey('22222222222222222222222222222222222222222222'); - const metadata = new PublicKey('33333333333333333333333333333333333333333333'); - const updateAuthority = new PublicKey('44444444444444444444444444444444444444444444'); - const mint = new PublicKey('55555555555555555555555555555555555555555555'); - const mintAuthority = new PublicKey('66666666666666666666666666666666666666666666'); - - it('Can create Initialize Instruction', async () => { - const name = 'My test token'; - const symbol = 'TEST'; - const uri = 'http://test.test'; - checkPackUnpack( - createInitializeInstruction({ - programId, - metadata, - updateAuthority, - mint, - mintAuthority, - name, - symbol, - uri, - }), - await splDiscriminate('spl_token_metadata_interface:initialize_account'), - getStructDecoder([ - ['name', getStringDecoder()], - ['symbol', getStringDecoder()], - ['uri', getStringDecoder()], - ]), - { name, symbol, uri }, - ); - }); - - it('Can create Update Field Instruction', async () => { - const field = 'MyTestField'; - const value = 'http://test.uri'; - checkPackUnpack( - createUpdateFieldInstruction({ - programId, - metadata, - updateAuthority, - field, - value, - }), - await splDiscriminate('spl_token_metadata_interface:updating_field'), - getStructDecoder([ - ['key', getDataEnumCodec(getFieldCodec())], - ['value', getStringDecoder()], - ]), - { key: getFieldConfig(field), value }, - ); - }); - - it('Can create Update Field Instruction with Field Enum', async () => { - const field = 'Name'; - const value = 'http://test.uri'; - checkPackUnpack( - createUpdateFieldInstruction({ - programId, - metadata, - updateAuthority, - field, - value, - }), - await splDiscriminate('spl_token_metadata_interface:updating_field'), - getStructDecoder([ - ['key', getDataEnumCodec(getFieldCodec())], - ['value', getStringDecoder()], - ]), - { key: getFieldConfig(field), value }, - ); - }); - - it('Can create Remove Key Instruction', async () => { - checkPackUnpack( - createRemoveKeyInstruction({ - programId, - metadata, - updateAuthority: updateAuthority, - key: 'MyTestField', - idempotent: true, - }), - await splDiscriminate('spl_token_metadata_interface:remove_key_ix'), - getStructDecoder([ - ['idempotent', getBooleanDecoder()], - ['key', getStringDecoder()], - ]), - { idempotent: true, key: 'MyTestField' }, - ); - }); - - it('Can create Update Authority Instruction', async () => { - const newAuthority = PublicKey.default; - checkPackUnpack( - createUpdateAuthorityInstruction({ - programId, - metadata, - oldAuthority: updateAuthority, - newAuthority, - }), - await splDiscriminate('spl_token_metadata_interface:update_the_authority'), - getStructDecoder([['newAuthority', fixDecoderSize(getBytesDecoder(), 32)]]), - { newAuthority: Uint8Array.from(newAuthority.toBuffer()) }, - ); - }); - - it('Can create Emit Instruction', async () => { - const start: Option = some(0n); - const end: Option = some(10n); - checkPackUnpack( - createEmitInstruction({ - programId, - metadata, - start: 0n, - end: 10n, - }), - await splDiscriminate('spl_token_metadata_interface:emitter'), - getStructDecoder([ - ['start', getOptionDecoder(getU64Decoder())], - ['end', getOptionDecoder(getU64Decoder())], - ]), - { start, end }, - ); - }); -}); diff --git a/token-metadata/js/test/state.test.ts b/token-metadata/js/test/state.test.ts deleted file mode 100644 index f55dd04a175..00000000000 --- a/token-metadata/js/test/state.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import { expect } from 'chai'; - -import type { TokenMetadata } from '../src/state'; -import { unpack, pack } from '../src'; - -function checkPackUnpack(tokenMetadata: TokenMetadata) { - const packed = pack(tokenMetadata); - const unpacked = unpack(packed); - expect(unpacked).to.deep.equal(tokenMetadata); -} - -describe('Token Metadata State', () => { - it('Can pack and unpack base token metadata', () => { - checkPackUnpack({ - mint: PublicKey.default, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }); - }); - - it('Can pack and unpack with updateAuthority', () => { - checkPackUnpack({ - updateAuthority: new PublicKey('44444444444444444444444444444444444444444444'), - mint: new PublicKey('55555555555555555555555555555555555555555555'), - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }); - }); - - it('Can pack and unpack with additional metadata', () => { - checkPackUnpack({ - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - }); - }); - - it('Can pack and unpack with updateAuthority and additional metadata', () => { - checkPackUnpack({ - updateAuthority: new PublicKey('44444444444444444444444444444444444444444444'), - mint: new PublicKey('55555555555555555555555555555555555555555555'), - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - }); - }); -}); diff --git a/token-metadata/js/tsconfig.all.json b/token-metadata/js/tsconfig.all.json deleted file mode 100644 index 985513259e2..00000000000 --- a/token-metadata/js/tsconfig.all.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "./tsconfig.root.json", - "references": [ - { - "path": "./tsconfig.cjs.json" - }, - { - "path": "./tsconfig.esm.json" - } - ] -} diff --git a/token-metadata/js/tsconfig.base.json b/token-metadata/js/tsconfig.base.json deleted file mode 100644 index 90620c4e485..00000000000 --- a/token-metadata/js/tsconfig.base.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "include": [], - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Node", - "esModuleInterop": true, - "isolatedModules": true, - "noEmitOnError": true, - "resolveJsonModule": true, - "strict": true, - "stripInternal": true - } -} diff --git a/token-metadata/js/tsconfig.cjs.json b/token-metadata/js/tsconfig.cjs.json deleted file mode 100644 index 2db9b71569e..00000000000 --- a/token-metadata/js/tsconfig.cjs.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "include": ["src"], - "compilerOptions": { - "outDir": "lib/cjs", - "target": "ES2016", - "module": "CommonJS", - "sourceMap": true - } -} diff --git a/token-metadata/js/tsconfig.esm.json b/token-metadata/js/tsconfig.esm.json deleted file mode 100644 index 25e7e25e751..00000000000 --- a/token-metadata/js/tsconfig.esm.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "include": ["src"], - "compilerOptions": { - "outDir": "lib/esm", - "declarationDir": "lib/types", - "target": "ES2020", - "module": "ES2020", - "sourceMap": true, - "declaration": true, - "declarationMap": true - } -} diff --git a/token-metadata/js/tsconfig.json b/token-metadata/js/tsconfig.json deleted file mode 100644 index 2f9b239bfca..00000000000 --- a/token-metadata/js/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.all.json", - "include": ["src", "test"], - "compilerOptions": { - "noEmit": true, - "skipLibCheck": true - } -} diff --git a/token-metadata/js/tsconfig.root.json b/token-metadata/js/tsconfig.root.json deleted file mode 100644 index fadf294ab43..00000000000 --- a/token-metadata/js/tsconfig.root.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "composite": true - } -} diff --git a/token-metadata/js/typedoc.json b/token-metadata/js/typedoc.json deleted file mode 100644 index c39fc53aee1..00000000000 --- a/token-metadata/js/typedoc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entryPoints": ["src/index.ts"], - "out": "docs", - "readme": "README.md" -} diff --git a/token-swap/program/Cargo.toml b/token-swap/program/Cargo.toml index b56bbe2ae29..022e9390403 100644 --- a/token-swap/program/Cargo.toml +++ b/token-swap/program/Cargo.toml @@ -19,8 +19,8 @@ num-derive = "0.4" num-traits = "0.2" solana-program = "2.1.0" spl-math = { version = "0.3", path = "../../libraries/math" } -spl-token = { version = "7.0", path = "../../token/program", features = [ "no-entrypoint" ] } -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = [ "no-entrypoint" ] } +spl-token = { version = "7.0", features = [ "no-entrypoint" ] } +spl-token-2022 = { version = "6.0.0", features = [ "no-entrypoint" ] } thiserror = "2.0" arbitrary = { version = "1.4", features = ["derive"], optional = true } roots = { version = "0.0.8", optional = true } diff --git a/token-swap/program/fuzz/Cargo.toml b/token-swap/program/fuzz/Cargo.toml index 46d6bd52a18..77d85c045b5 100644 --- a/token-swap/program/fuzz/Cargo.toml +++ b/token-swap/program/fuzz/Cargo.toml @@ -13,7 +13,7 @@ honggfuzz = { version = "0.5.56" } arbitrary = { version = "1.4", features = ["derive"] } solana-program = "2.1.0" spl-math = { version = "0.3", path = "../../../libraries/math" } -spl-token = { version = "7.0", path = "../../../token/program", features = [ "no-entrypoint" ] } +spl-token = { version = "7.0", features = [ "no-entrypoint" ] } spl-token-swap = { path = "..", features = ["fuzz", "no-entrypoint"] } [[bin]] diff --git a/token-upgrade/cli/Cargo.toml b/token-upgrade/cli/Cargo.toml index 8143bbfcdcc..f542f67007b 100644 --- a/token-upgrade/cli/Cargo.toml +++ b/token-upgrade/cli/Cargo.toml @@ -19,10 +19,10 @@ solana-client = "2.1.0" solana-logger = "2.1.0" solana-remote-wallet = "2.1.0" solana-sdk = "2.1.0" -spl-associated-token-account-client = { version = "2.0.0", path = "../../associated-token-account/client" } -spl-token = { version = "7.0", path = "../../token/program", features = ["no-entrypoint"] } -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = ["no-entrypoint"] } -spl-token-client = { version = "0.13.0", path = "../../token/client" } +spl-associated-token-account-client = { version = "2.0.0" } +spl-token = { version = "7.0", features = ["no-entrypoint"] } +spl-token-2022 = { version = "6.0.0", features = ["no-entrypoint"] } +spl-token-client = { version = "0.13.0" } spl-token-upgrade = { version = "0.1", path = "../program", features = ["no-entrypoint"] } tokio = { version = "1", features = ["full"] } diff --git a/token-upgrade/program/Cargo.toml b/token-upgrade/program/Cargo.toml index 939175faa1f..296cdb2bd18 100644 --- a/token-upgrade/program/Cargo.toml +++ b/token-upgrade/program/Cargo.toml @@ -16,14 +16,14 @@ num-derive = "0.4" num-traits = "0.2" num_enum = "0.7.3" solana-program = "2.1.0" -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = ["no-entrypoint"] } +spl-token-2022 = { version = "6.0.0", features = ["no-entrypoint"] } thiserror = "2.0" [dev-dependencies] solana-program-test = "2.1.0" solana-sdk = "2.1.0" -spl-token = { version = "7.0", path = "../../token/program", features = ["no-entrypoint"] } -spl-token-client = { version = "0.13.0", path = "../../token/client" } +spl-token = { version = "7.0", features = ["no-entrypoint"] } +spl-token-client = { version = "0.13.0" } test-case = "3.3" [lib] diff --git a/token-wrap/program/Cargo.toml b/token-wrap/program/Cargo.toml index 30fa2944786..9fd8596ca3f 100644 --- a/token-wrap/program/Cargo.toml +++ b/token-wrap/program/Cargo.toml @@ -15,9 +15,9 @@ test-sbf = [] bytemuck = { version = "1.21.0", features = ["derive"] } num_enum = "0.7" solana-program = "2.1.0" -spl-associated-token-account = { version = "6.0.0", path = "../../associated-token-account/program", features = ["no-entrypoint"] } -spl-token = { version = "7.0", path = "../../token/program", features = ["no-entrypoint"] } -spl-token-2022 = { version = "6.0.0", path = "../../token/program-2022", features = ["no-entrypoint"] } +spl-associated-token-account = { version = "6.0.0", features = ["no-entrypoint"] } +spl-token = { version = "7.0", features = ["no-entrypoint"] } +spl-token-2022 = { version = "6.0.0", features = ["no-entrypoint"] } thiserror = "2.0" [lib] diff --git a/token/README.md b/token/README.md new file mode 100644 index 00000000000..e2e43253970 --- /dev/null +++ b/token/README.md @@ -0,0 +1,9 @@ +NOTE: The token program is now maintained at +[solana-program/token](https://github.com/solana-program/token). + +The token-2022 program, confidential-transfers, clients, CLI, and JS library are +maintained at +[solana-program/token-2022](https://github.com/solana-program/token-2022). + +The transfer-hook interface, example program, and clients are maintained at +[solana-program/transfer-hook](https://github.com/solana-program/transfer-hook). diff --git a/token/cli/Cargo.toml b/token/cli/Cargo.toml deleted file mode 100644 index 1620c5eaa9c..00000000000 --- a/token/cli/Cargo.toml +++ /dev/null @@ -1,61 +0,0 @@ -[package] -authors = ["Solana Labs Maintainers "] -description = "SPL-Token Command-line Utility" -edition = "2021" -homepage = "https://spl.solana.com/token" -license = "Apache-2.0" -name = "spl-token-cli" -repository = "https://github.com/solana-labs/solana-program-library" -version = "5.0.0" - -[build-dependencies] -walkdir = "2" - -[dependencies] -base64 = "0.22.1" -clap = "3.2.23" -console = "0.15.10" -futures = "0.3" -serde = "1.0.217" -serde_derive = "1.0.103" -serde_json = "1.0.135" -solana-account-decoder = "2.1.0" -solana-clap-v3-utils = "2.1.0" -solana-cli-config = "2.1.0" -solana-cli-output = "2.1.0" -solana-client = "2.1.0" -solana-logger = "2.1.0" -solana-remote-wallet = "2.1.0" -solana-sdk = "2.1.0" -solana-transaction-status = "2.1.0" -spl-associated-token-account-client = { version = "2.0.0", path = "../../associated-token-account/client" } -spl-token = { version = "7.0", path = "../program", features = [ - "no-entrypoint", -] } -spl-token-2022 = { version = "6.0.0", path = "../program-2022", features = [ - "no-entrypoint", -] } -spl-token-client = { version = "0.13.0", path = "../client" } -spl-token-confidential-transfer-proof-generation = { version = "0.2.0", path = "../confidential-transfer/proof-generation" } -spl-token-metadata-interface = { version = "0.6.0", path = "../../token-metadata/interface" } -spl-token-group-interface = { version = "0.5.0", path = "../../token-group/interface" } -spl-memo = { version = "6.0", features = ["no-entrypoint"] } -strum = "0.26" -strum_macros = "0.26" -tokio = "1.42" - -[dev-dependencies] -solana-test-validator = "2.1.0" -assert_cmd = "2.0.16" -libtest-mimic = "0.8" -serial_test = "3.2.0" -tempfile = "3.14.0" - -[[bin]] -name = "spl-token" -path = "src/main.rs" - -[[test]] -name = "command" -path = "tests/command.rs" -harness = false diff --git a/token/cli/README.md b/token/cli/README.md deleted file mode 100644 index 6c051b4645a..00000000000 --- a/token/cli/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# SPL Token program command-line utility - -A basic command-line for creating and using SPL Tokens. See for more details - -## Build - -To build the CLI locally, simply run: - -```sh -cargo build -``` - -## Testing - -The tests require locally built programs for Token, Token-2022, and Associated -Token Account. To build these, you can run: - -```sh -BUILD_DEPENDENT_PROGRAMS=1 cargo build -``` - -This method uses the local `build.rs` file, which can be error-prone, so alternatively, -you can build the programs by running the following commands from this directory: - -```sh -cargo build-sbf --manifest-path ../program/Cargo.toml -cargo build-sbf --manifest-path ../program-2022/Cargo.toml -cargo build-sbf --manifest-path ../../associated-token-account/program/Cargo.toml -``` - -After that, you can run the tests as any other Rust project: - -```sh -cargo test -``` - -To run it locally you can do it like this: - -```sh -cargo build --manifest-path token/cli/Cargo.toml -target/debug/spl-token -``` diff --git a/token/cli/build.rs b/token/cli/build.rs deleted file mode 100644 index c07683ecff8..00000000000 --- a/token/cli/build.rs +++ /dev/null @@ -1,79 +0,0 @@ -extern crate walkdir; - -use { - std::{env, path::Path, process::Command}, - walkdir::WalkDir, -}; - -fn rerun_if_changed(directory: &Path) { - let src = directory.join("src"); - let files_in_src: Vec<_> = WalkDir::new(src) - .into_iter() - .map(|entry| entry.unwrap()) - .filter(|entry| { - if !entry.file_type().is_file() { - return false; - } - true - }) - .map(|f| f.path().to_str().unwrap().to_owned()) - .collect(); - - for file in files_in_src { - if !Path::new(&file).is_file() { - panic!("{} is not a file", file); - } - println!("cargo:rerun-if-changed={}", file); - } - let toml = directory.join("Cargo.toml").to_str().unwrap().to_owned(); - println!("cargo:rerun-if-changed={}", toml); -} - -fn build_bpf(program_directory: &Path) { - let toml_file = program_directory.join("Cargo.toml"); - let toml_file = format!("{}", toml_file.display()); - let args = vec!["build-sbf", "--manifest-path", &toml_file]; - let output = Command::new("cargo") - .args(&args) - .output() - .expect("Error running cargo build-sbf"); - if let Ok(output_str) = std::str::from_utf8(&output.stdout) { - let subs = output_str.split('\n'); - for sub in subs { - println!("cargo:warning=(not a warning) {}", sub); - } - } -} - -fn main() { - let is_debug = env::var("DEBUG").map(|v| v == "true").unwrap_or(false); - let build_dependent_programs = env::var("BUILD_DEPENDENT_PROGRAMS") - .map(|v| v != "false" && v != "0") - .unwrap_or(false); - if is_debug && build_dependent_programs { - let cwd = env::current_dir().expect("Unable to get current working directory"); - let spl_token_2022_dir = cwd - .parent() - .expect("Unable to get parent directory of current working dir") - .join("program-2022"); - rerun_if_changed(&spl_token_2022_dir); - let spl_token_dir = cwd - .parent() - .expect("Unable to get parent directory of current working dir") - .join("program"); - rerun_if_changed(&spl_token_dir); - let spl_associated_token_account_dir = cwd - .parent() - .expect("Unable to get parent directory of current working dir") - .parent() - .expect("Unable to get parent directory of current working dir") - .join("associated-token-account") - .join("program"); - rerun_if_changed(&spl_associated_token_account_dir); - - build_bpf(&spl_token_dir); - build_bpf(&spl_token_2022_dir); - build_bpf(&spl_associated_token_account_dir); - } - println!("cargo:rerun-if-changed=build.rs"); -} diff --git a/token/cli/examples/confidential-transfer.sh b/token/cli/examples/confidential-transfer.sh deleted file mode 100755 index 00536e4a470..00000000000 --- a/token/cli/examples/confidential-transfer.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash - -# Set whichever network you would like to test with -# solana config set -ul - -program_id="TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" - -echo "Setup keypairs" -solana-keygen new -o confidential-mint.json --no-bip39-passphrase -solana-keygen new -o confidential-source.json --no-bip39-passphrase -solana-keygen new -o confidential-destination.json --no-bip39-passphrase -mint_pubkey=$(solana-keygen pubkey "confidential-mint.json") -source_pubkey=$(solana-keygen pubkey "confidential-source.json") -destination_pubkey=$(solana-keygen pubkey "confidential-destination.json") - -set -ex -echo "Initializing mint" -spl-token --program-id "$program_id" create-token confidential-mint.json --enable-confidential-transfers auto -echo "Displaying" -spl-token display "$mint_pubkey" -read -n 1 -p "..." - -echo "Setting up transfer accounts" -spl-token create-account "$mint_pubkey" confidential-source.json -spl-token configure-confidential-transfer-account --address "$source_pubkey" -spl-token create-account "$mint_pubkey" confidential-destination.json -spl-token configure-confidential-transfer-account --address "$destination_pubkey" -spl-token mint "$mint_pubkey" 100 confidential-source.json - -echo "Displaying" -spl-token display "$source_pubkey" -read -n 1 -p "..." - -echo "Depositing into confidential" -spl-token deposit-confidential-tokens "$mint_pubkey" 100 --address "$source_pubkey" -echo "Displaying" -spl-token display "$source_pubkey" -read -n 1 -p "..." - -echo "Applying pending balances" -spl-token apply-pending-balance --address "$source_pubkey" -echo "Displaying" -spl-token display "$source_pubkey" -read -n 1 -p "..." - -echo "Transferring 10" -spl-token transfer "$mint_pubkey" 10 "$destination_pubkey" --from "$source_pubkey" --confidential -echo "Displaying source" -spl-token display "$source_pubkey" -echo "Displaying destination" -spl-token display "$destination_pubkey" -read -n 1 -p "..." - -echo "Applying balance on destination" -spl-token apply-pending-balance --address "$destination_pubkey" -echo "Displaying destination" -spl-token display "$destination_pubkey" -read -n 1 -p "..." - -echo "Transferring 0" -spl-token transfer "$mint_pubkey" 0 "$destination_pubkey" --from "$source_pubkey" --confidential -echo "Displaying destination" -spl-token display "$destination_pubkey" -read -n 1 -p "..." - -echo "Transferring 0 again" -spl-token transfer "$mint_pubkey" 0 "$destination_pubkey" --from "$source_pubkey" --confidential -echo "Displaying destination" -spl-token display "$destination_pubkey" -read -n 1 -p "..." - -echo "Withdrawing 10 from destination" -spl-token apply-pending-balance --address "$destination_pubkey" -spl-token withdraw-confidential-tokens "$mint_pubkey" 10 --address "$destination_pubkey" -echo "Displaying destination" -spl-token display "$destination_pubkey" -read -n 1 -p "..." diff --git a/token/cli/src/bench.rs b/token/cli/src/bench.rs deleted file mode 100644 index 209591cefdd..00000000000 --- a/token/cli/src/bench.rs +++ /dev/null @@ -1,381 +0,0 @@ -/// The `bench` subcommand -use { - crate::{clap_app::Error, command::CommandResult, config::Config}, - clap::ArgMatches, - solana_clap_v3_utils::input_parsers::{pubkey_of_signer, Amount}, - solana_client::{ - nonblocking::rpc_client::RpcClient, rpc_client::RpcClient as BlockingRpcClient, - tpu_client::TpuClient, tpu_client::TpuClientConfig, - }, - solana_remote_wallet::remote_wallet::RemoteWalletManager, - solana_sdk::{ - message::Message, native_token::lamports_to_sol, native_token::Sol, program_pack::Pack, - pubkey::Pubkey, signature::Signer, system_instruction, - }, - spl_associated_token_account_client::address::get_associated_token_address_with_program_id, - spl_token_2022::{ - extension::StateWithExtensions, - instruction, - state::{Account, Mint}, - }, - std::{rc::Rc, sync::Arc, time::Instant}, -}; - -pub(crate) async fn bench_process_command( - matches: &ArgMatches, - config: &Config<'_>, - mut signers: Vec>, - wallet_manager: &mut Option>, -) -> CommandResult { - assert!(!config.sign_only); - - match matches.subcommand() { - Some(("create-accounts", arg_matches)) => { - let token = pubkey_of_signer(arg_matches, "token", wallet_manager) - .unwrap() - .unwrap(); - let n = *arg_matches.get_one::("n").unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", wallet_manager); - signers.push(owner_signer); - - command_create_accounts(config, signers, &token, n, &owner).await?; - } - Some(("close-accounts", arg_matches)) => { - let token = pubkey_of_signer(arg_matches, "token", wallet_manager) - .unwrap() - .unwrap(); - let n = *arg_matches.get_one::("n").unwrap(); - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", wallet_manager); - signers.push(owner_signer); - - command_close_accounts(config, signers, &token, n, &owner).await?; - } - Some(("deposit-into", arg_matches)) => { - let token = pubkey_of_signer(arg_matches, "token", wallet_manager) - .unwrap() - .unwrap(); - let n = *arg_matches.get_one::("n").unwrap(); - let ui_amount = *arg_matches.get_one::("amount").unwrap(); - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", wallet_manager); - signers.push(owner_signer); - let from = pubkey_of_signer(arg_matches, "from", wallet_manager).unwrap(); - command_deposit_into_or_withdraw_from( - config, signers, &token, n, &owner, ui_amount, from, true, - ) - .await?; - } - Some(("withdraw-from", arg_matches)) => { - let token = pubkey_of_signer(arg_matches, "token", wallet_manager) - .unwrap() - .unwrap(); - let n = *arg_matches.get_one::("n").unwrap(); - let ui_amount = *arg_matches.get_one::("amount").unwrap(); - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", wallet_manager); - signers.push(owner_signer); - let to = pubkey_of_signer(arg_matches, "to", wallet_manager).unwrap(); - command_deposit_into_or_withdraw_from( - config, signers, &token, n, &owner, ui_amount, to, false, - ) - .await?; - } - _ => unreachable!(), - } - - Ok("".to_string()) -} - -fn get_token_address_with_seed( - program_id: &Pubkey, - token: &Pubkey, - owner: &Pubkey, - i: usize, -) -> (Pubkey, String) { - let seed = format!("{}{}", i, token)[..31].to_string(); - ( - Pubkey::create_with_seed(owner, &seed, program_id).unwrap(), - seed, - ) -} - -fn get_token_addresses_with_seed( - program_id: &Pubkey, - token: &Pubkey, - owner: &Pubkey, - n: usize, -) -> Vec<(Pubkey, String)> { - (0..n) - .map(|i| get_token_address_with_seed(program_id, token, owner, i)) - .collect() -} - -async fn get_valid_mint_program_id( - rpc_client: &RpcClient, - token: &Pubkey, -) -> Result { - let mint_account = rpc_client - .get_account(token) - .await - .map_err(|err| format!("Token mint {} does not exist: {}", token, err))?; - - StateWithExtensions::::unpack(&mint_account.data) - .map_err(|err| format!("Invalid token mint {}: {}", token, err))?; - Ok(mint_account.owner) -} - -async fn command_create_accounts( - config: &Config<'_>, - signers: Vec>, - token: &Pubkey, - n: usize, - owner: &Pubkey, -) -> Result<(), Error> { - let rpc_client = &config.rpc_client; - - println!("Scanning accounts..."); - let program_id = get_valid_mint_program_id(rpc_client, token).await?; - - let minimum_balance_for_rent_exemption = rpc_client - .get_minimum_balance_for_rent_exemption(Account::get_packed_len()) - .await?; - - let mut lamports_required: u64 = 0; - - let token_addresses_with_seed = get_token_addresses_with_seed(&program_id, token, owner, n); - let mut messages = vec![]; - for address_chunk in token_addresses_with_seed.chunks(100) { - let accounts_chunk = rpc_client - .get_multiple_accounts(&address_chunk.iter().map(|x| x.0).collect::>()) - .await?; - - for (account, (address, seed)) in accounts_chunk.iter().zip(address_chunk) { - if account.is_none() { - lamports_required = - lamports_required.saturating_add(minimum_balance_for_rent_exemption); - messages.push(Message::new( - &[ - system_instruction::create_account_with_seed( - &config.fee_payer()?.pubkey(), - address, - owner, - seed, - minimum_balance_for_rent_exemption, - Account::get_packed_len() as u64, - &program_id, - ), - instruction::initialize_account(&program_id, address, token, owner)?, - ], - Some(&config.fee_payer()?.pubkey()), - )); - } - } - } - - send_messages(config, &messages, lamports_required, signers).await -} - -async fn command_close_accounts( - config: &Config<'_>, - signers: Vec>, - token: &Pubkey, - n: usize, - owner: &Pubkey, -) -> Result<(), Error> { - let rpc_client = &config.rpc_client; - - println!("Scanning accounts..."); - let program_id = get_valid_mint_program_id(rpc_client, token).await?; - - let token_addresses_with_seed = get_token_addresses_with_seed(&program_id, token, owner, n); - let mut messages = vec![]; - for address_chunk in token_addresses_with_seed.chunks(100) { - let accounts_chunk = rpc_client - .get_multiple_accounts(&address_chunk.iter().map(|x| x.0).collect::>()) - .await?; - - for (account, (address, _seed)) in accounts_chunk.iter().zip(address_chunk) { - if let Some(account) = account { - match StateWithExtensions::::unpack(&account.data) { - Ok(token_account) => { - if token_account.base.amount != 0 { - eprintln!( - "Token account {} holds a balance; unable to close it", - address, - ); - } else { - messages.push(Message::new( - &[instruction::close_account( - &program_id, - address, - owner, - owner, - &[], - )?], - Some(&config.fee_payer()?.pubkey()), - )); - } - } - Err(err) => { - eprintln!("Invalid token account {}: {}", address, err) - } - } - } - } - } - - send_messages(config, &messages, 0, signers).await -} - -#[allow(clippy::too_many_arguments)] -async fn command_deposit_into_or_withdraw_from( - config: &Config<'_>, - signers: Vec>, - token: &Pubkey, - n: usize, - owner: &Pubkey, - ui_amount: Amount, - from_or_to: Option, - deposit_into: bool, -) -> Result<(), Error> { - let rpc_client = &config.rpc_client; - - println!("Scanning accounts..."); - let program_id = get_valid_mint_program_id(rpc_client, token).await?; - - let mint_info = config.get_mint_info(token, None).await?; - let from_or_to = from_or_to - .unwrap_or_else(|| get_associated_token_address_with_program_id(owner, token, &program_id)); - config.check_account(&from_or_to, Some(*token)).await?; - let amount = match ui_amount { - Amount::Raw(ui_amount) => ui_amount, - Amount::Decimal(ui_amount) => spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals), - Amount::All => { - return Err( - "Use of ALL keyword currently not supported for the bench command" - .to_string() - .into(), - ); - } - }; - - let token_addresses_with_seed = get_token_addresses_with_seed(&program_id, token, owner, n); - let mut messages = vec![]; - for address_chunk in token_addresses_with_seed.chunks(100) { - let accounts_chunk = rpc_client - .get_multiple_accounts(&address_chunk.iter().map(|x| x.0).collect::>()) - .await?; - - for (account, (address, _seed)) in accounts_chunk.iter().zip(address_chunk) { - if account.is_some() { - messages.push(Message::new( - &[instruction::transfer_checked( - &program_id, - if deposit_into { &from_or_to } else { address }, - token, - if deposit_into { address } else { &from_or_to }, - owner, - &[], - amount, - mint_info.decimals, - )?], - Some(&config.fee_payer()?.pubkey()), - )); - } else { - eprintln!("Token account does not exist: {}", address) - } - } - } - - send_messages(config, &messages, 0, signers).await -} - -async fn send_messages( - config: &Config<'_>, - messages: &[Message], - mut lamports_required: u64, - signers: Vec>, -) -> Result<(), Error> { - if messages.is_empty() { - println!("Nothing to do"); - return Ok(()); - } - - let blockhash = config.rpc_client.get_latest_blockhash().await?; - let mut message = messages[0].clone(); - message.recent_blockhash = blockhash; - lamports_required = lamports_required.saturating_add( - config - .rpc_client - .get_fee_for_message(&message) - .await? - .saturating_mul(messages.len() as u64), - ); - - println!( - "Sending {:?} messages for ~{}", - messages.len(), - Sol(lamports_required) - ); - - check_fee_payer_balance(config, lamports_required).await?; - - // TODO use async tpu client once it's available in 1.11 - let start = Instant::now(); - let rpc_client = BlockingRpcClient::new(config.rpc_client.url()); - let tpu_client = TpuClient::new( - Arc::new(rpc_client), - &config.websocket_url, - TpuClientConfig::default(), - )?; - let transaction_errors = - tpu_client.send_and_confirm_messages_with_spinner(messages, &signers)?; - for (i, transaction_error) in transaction_errors.into_iter().enumerate() { - if let Some(transaction_error) = transaction_error { - println!("Message {} failed with {:?}", i, transaction_error); - } - } - let elapsed = Instant::now().duration_since(start); - let tps = messages.len() as f64 / elapsed.as_secs_f64(); - println!( - "Average TPS: {:.2}\nElapsed time: {} seconds", - tps, - elapsed.as_secs_f64(), - ); - - let stats = config.rpc_client.get_transport_stats(); - println!("Total RPC requests: {}", stats.request_count); - println!( - "Total RPC time: {:.2} seconds", - stats.elapsed_time.as_secs_f64() - ); - if stats.rate_limited_time != std::time::Duration::default() { - println!( - "Total idle time due to RPC rate limiting: {:.2} seconds", - stats.rate_limited_time.as_secs_f64() - ); - } - - Ok(()) -} - -async fn check_fee_payer_balance(config: &Config<'_>, required_balance: u64) -> Result<(), Error> { - let balance = config - .rpc_client - .get_balance(&config.fee_payer()?.pubkey()) - .await?; - if balance < required_balance { - Err(format!( - "Fee payer, {}, has insufficient balance: {} required, {} available", - config.fee_payer()?.pubkey(), - lamports_to_sol(required_balance), - lamports_to_sol(balance) - ) - .into()) - } else { - Ok(()) - } -} diff --git a/token/cli/src/clap_app.rs b/token/cli/src/clap_app.rs deleted file mode 100644 index 814c1c18c67..00000000000 --- a/token/cli/src/clap_app.rs +++ /dev/null @@ -1,2691 +0,0 @@ -#![allow(deprecated)] - -use { - clap::{ - crate_description, crate_name, crate_version, App, AppSettings, Arg, ArgGroup, SubCommand, - }, - solana_clap_v3_utils::{ - fee_payer::fee_payer_arg, - input_parsers::Amount, - input_validators::{is_pubkey, is_url_or_moniker, is_valid_pubkey, is_valid_signer}, - memo::memo_arg, - nonce::*, - offline::{self, *}, - ArgConstant, - }, - solana_sdk::{instruction::AccountMeta, pubkey::Pubkey}, - spl_token_2022::instruction::{AuthorityType, MAX_SIGNERS, MIN_SIGNERS}, - std::{fmt, str::FromStr}, - strum::IntoEnumIterator, - strum_macros::{AsRefStr, EnumIter, EnumString, IntoStaticStr}, -}; - -pub type Error = Box; - -pub const OWNER_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { - name: "owner", - long: "owner", - help: "Address of the primary authority controlling a mint or account. Defaults to the client keypair address.", -}; - -pub const OWNER_KEYPAIR_ARG: ArgConstant<'static> = ArgConstant { - name: "owner", - long: "owner", - help: "Keypair of the primary authority controlling a mint or account. Defaults to the client keypair.", -}; - -pub const MINT_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { - name: "mint_address", - long: "mint-address", - help: "Address of mint that token account is associated with. Required by --sign-only", -}; - -pub const MINT_DECIMALS_ARG: ArgConstant<'static> = ArgConstant { - name: "mint_decimals", - long: "mint-decimals", - help: "Decimals of mint that token account is associated with. Required by --sign-only", -}; - -pub const DELEGATE_ADDRESS_ARG: ArgConstant<'static> = ArgConstant { - name: "delegate_address", - long: "delegate-address", - help: "Address of delegate currently assigned to token account. Required by --sign-only", -}; - -pub const TRANSFER_LAMPORTS_ARG: ArgConstant<'static> = ArgConstant { - name: "transfer_lamports", - long: "transfer-lamports", - help: "Additional lamports to transfer to make account rent-exempt after reallocation. Required by --sign-only", -}; - -pub const MULTISIG_SIGNER_ARG: ArgConstant<'static> = ArgConstant { - name: "multisig_signer", - long: "multisig-signer", - help: "Member signer of a multisig account", -}; - -pub const COMPUTE_UNIT_PRICE_ARG: ArgConstant<'static> = ArgConstant { - name: "compute_unit_price", - long: "--with-compute-unit-price", - help: "Set compute unit price for transaction, in increments of 0.000001 lamports per compute unit.", -}; - -pub const COMPUTE_UNIT_LIMIT_ARG: ArgConstant<'static> = ArgConstant { - name: "compute_unit_limit", - long: "--with-compute-unit-limit", - help: "Set compute unit limit for transaction, in compute units.", -}; - -// The `signer_arg` in clap-v3-utils` specifies the argument as a -// `PubkeySignature` type, but supporting `PubkeySignature` in the token-cli -// requires a significant re-structuring of the code. Therefore, hard-code the -// `signer_arg` and `OfflineArgs` from clap-utils` here and remove -// it in a subsequent PR. -fn signer_arg<'a>() -> Arg<'a> { - Arg::new(SIGNER_ARG.name) - .long(SIGNER_ARG.long) - .takes_value(true) - .value_name("PUBKEY=SIGNATURE") - .requires(BLOCKHASH_ARG.name) - .action(clap::ArgAction::Append) - .multiple_values(false) - .help(SIGNER_ARG.help) -} - -pub trait OfflineArgs { - fn offline_args(self) -> Self; - fn offline_args_config(self, config: &dyn ArgsConfig) -> Self; -} - -impl OfflineArgs for clap::Command<'_> { - fn offline_args_config(self, config: &dyn ArgsConfig) -> Self { - self.arg(config.blockhash_arg(blockhash_arg())) - .arg(config.sign_only_arg(sign_only_arg())) - .arg(config.signer_arg(signer_arg())) - .arg(config.dump_transaction_message_arg(dump_transaction_message())) - } - fn offline_args(self) -> Self { - struct NullArgsConfig {} - impl ArgsConfig for NullArgsConfig {} - self.offline_args_config(&NullArgsConfig {}) - } -} - -pub static VALID_TOKEN_PROGRAM_IDS: [Pubkey; 2] = [spl_token_2022::ID, spl_token::ID]; - -#[derive(AsRefStr, Debug, Clone, Copy, PartialEq, EnumString, IntoStaticStr)] -#[strum(serialize_all = "kebab-case")] -pub enum CommandName { - CreateToken, - Close, - CloseMint, - Bench, - CreateAccount, - CreateMultisig, - Authorize, - SetInterestRate, - Transfer, - Burn, - Mint, - Freeze, - Thaw, - Wrap, - Unwrap, - Approve, - Revoke, - Balance, - Supply, - Accounts, - Address, - AccountInfo, - MultisigInfo, - Display, - Gc, - SyncNative, - EnableRequiredTransferMemos, - DisableRequiredTransferMemos, - EnableCpiGuard, - DisableCpiGuard, - UpdateDefaultAccountState, - UpdateMetadataAddress, - WithdrawWithheldTokens, - SetTransferFee, - WithdrawExcessLamports, - SetTransferHook, - InitializeMetadata, - UpdateMetadata, - InitializeGroup, - UpdateGroupMaxSize, - InitializeMember, - UpdateConfidentialTransferSettings, - ConfigureConfidentialTransferAccount, - EnableConfidentialCredits, - DisableConfidentialCredits, - EnableNonConfidentialCredits, - DisableNonConfidentialCredits, - DepositConfidentialTokens, - WithdrawConfidentialTokens, - ApplyPendingBalance, - UpdateGroupAddress, - UpdateMemberAddress, -} -impl fmt::Display for CommandName { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} -#[derive(Debug, Clone, Copy, PartialEq, EnumString, IntoStaticStr)] -#[strum(serialize_all = "kebab-case")] -pub enum AccountMetaRole { - Readonly, - Writable, - ReadonlySigner, - WritableSigner, -} -impl fmt::Display for AccountMetaRole { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) - } -} -pub fn parse_transfer_hook_account(string: T) -> Result -where - T: AsRef + fmt::Display, -{ - match string.as_ref().split(':').collect::>().as_slice() { - [address, role] => { - let address = Pubkey::from_str(address).map_err(|e| format!("{e}"))?; - let meta = match AccountMetaRole::from_str(role).map_err(|e| format!("{e}"))? { - AccountMetaRole::Readonly => AccountMeta::new_readonly(address, false), - AccountMetaRole::Writable => AccountMeta::new(address, false), - AccountMetaRole::ReadonlySigner => AccountMeta::new_readonly(address, true), - AccountMetaRole::WritableSigner => AccountMeta::new(address, true), - }; - Ok(meta) - } - _ => Err("Transfer hook account must be present as
:".to_string()), - } -} -fn validate_transfer_hook_account(string: T) -> Result<(), String> -where - T: AsRef + fmt::Display, -{ - match string.as_ref().split(':').collect::>().as_slice() { - [address, role] => { - is_valid_pubkey(address)?; - AccountMetaRole::from_str(role) - .map(|_| ()) - .map_err(|e| format!("{e}")) - } - _ => Err("Transfer hook account must be present as
:".to_string()), - } -} -#[derive(Debug, Clone, PartialEq, EnumIter, EnumString, IntoStaticStr)] -#[strum(serialize_all = "kebab-case")] -pub enum CliAuthorityType { - Mint, - Freeze, - Owner, - Close, - CloseMint, - TransferFeeConfig, - WithheldWithdraw, - InterestRate, - PermanentDelegate, - ConfidentialTransferMint, - TransferHookProgramId, - ConfidentialTransferFee, - MetadataPointer, - Metadata, - GroupPointer, - GroupMemberPointer, - Group, -} -impl TryFrom for AuthorityType { - type Error = Error; - fn try_from(authority_type: CliAuthorityType) -> Result { - match authority_type { - CliAuthorityType::Mint => Ok(AuthorityType::MintTokens), - CliAuthorityType::Freeze => Ok(AuthorityType::FreezeAccount), - CliAuthorityType::Owner => Ok(AuthorityType::AccountOwner), - CliAuthorityType::Close => Ok(AuthorityType::CloseAccount), - CliAuthorityType::CloseMint => Ok(AuthorityType::CloseMint), - CliAuthorityType::TransferFeeConfig => Ok(AuthorityType::TransferFeeConfig), - CliAuthorityType::WithheldWithdraw => Ok(AuthorityType::WithheldWithdraw), - CliAuthorityType::InterestRate => Ok(AuthorityType::InterestRate), - CliAuthorityType::PermanentDelegate => Ok(AuthorityType::PermanentDelegate), - CliAuthorityType::ConfidentialTransferMint => { - Ok(AuthorityType::ConfidentialTransferMint) - } - CliAuthorityType::TransferHookProgramId => Ok(AuthorityType::TransferHookProgramId), - CliAuthorityType::ConfidentialTransferFee => { - Ok(AuthorityType::ConfidentialTransferFeeConfig) - } - CliAuthorityType::MetadataPointer => Ok(AuthorityType::MetadataPointer), - CliAuthorityType::Metadata => { - Err("Metadata authority does not map to a token authority type".into()) - } - CliAuthorityType::GroupPointer => Ok(AuthorityType::GroupPointer), - CliAuthorityType::GroupMemberPointer => Ok(AuthorityType::GroupMemberPointer), - CliAuthorityType::Group => { - Err("Group update authority does not map to a token authority type".into()) - } - } - } -} - -pub fn owner_address_arg<'a>() -> Arg<'a> { - Arg::with_name(OWNER_ADDRESS_ARG.name) - .long(OWNER_ADDRESS_ARG.long) - .takes_value(true) - .value_name("OWNER_ADDRESS") - .validator(|s| is_valid_pubkey(s)) - .help(OWNER_ADDRESS_ARG.help) -} - -pub fn owner_keypair_arg_with_value_name<'a>(value_name: &'static str) -> Arg<'a> { - Arg::with_name(OWNER_KEYPAIR_ARG.name) - .long(OWNER_KEYPAIR_ARG.long) - .takes_value(true) - .value_name(value_name) - .validator(|s| is_valid_signer(s)) - .help(OWNER_KEYPAIR_ARG.help) -} - -pub fn owner_keypair_arg<'a>() -> Arg<'a> { - owner_keypair_arg_with_value_name("OWNER_KEYPAIR") -} - -pub fn mint_address_arg<'a>() -> Arg<'a> { - Arg::with_name(MINT_ADDRESS_ARG.name) - .long(MINT_ADDRESS_ARG.long) - .takes_value(true) - .value_name("MINT_ADDRESS") - .validator(|s| is_valid_pubkey(s)) - .help(MINT_ADDRESS_ARG.help) -} - -pub fn mint_decimals_arg<'a>() -> Arg<'a> { - Arg::with_name(MINT_DECIMALS_ARG.name) - .long(MINT_DECIMALS_ARG.long) - .takes_value(true) - .value_name("MINT_DECIMALS") - .value_parser(clap::value_parser!(u8)) - .help(MINT_DECIMALS_ARG.help) -} - -pub trait MintArgs { - fn mint_args(self) -> Self; -} - -impl MintArgs for App<'_> { - fn mint_args(self) -> Self { - self.arg(mint_address_arg().requires(MINT_DECIMALS_ARG.name)) - .arg(mint_decimals_arg().requires(MINT_ADDRESS_ARG.name)) - } -} - -pub fn delegate_address_arg<'a>() -> Arg<'a> { - Arg::with_name(DELEGATE_ADDRESS_ARG.name) - .long(DELEGATE_ADDRESS_ARG.long) - .takes_value(true) - .value_name("DELEGATE_ADDRESS") - .validator(|s| is_valid_pubkey(s)) - .help(DELEGATE_ADDRESS_ARG.help) -} - -pub fn transfer_lamports_arg<'a>() -> Arg<'a> { - Arg::with_name(TRANSFER_LAMPORTS_ARG.name) - .long(TRANSFER_LAMPORTS_ARG.long) - .takes_value(true) - .value_name("LAMPORTS") - .value_parser(clap::value_parser!(u64)) - .help(TRANSFER_LAMPORTS_ARG.help) -} - -pub fn multisig_signer_arg<'a>() -> Arg<'a> { - Arg::with_name(MULTISIG_SIGNER_ARG.name) - .long(MULTISIG_SIGNER_ARG.long) - .validator(|s| is_valid_signer(s)) - .value_name("MULTISIG_SIGNER") - .takes_value(true) - .multiple(true) - .min_values(0_usize) - .max_values(MAX_SIGNERS) - .help(MULTISIG_SIGNER_ARG.help) -} - -fn is_multisig_minimum_signers(string: &str) -> Result<(), String> { - let v = u8::from_str(string).map_err(|e| e.to_string())? as usize; - if v < MIN_SIGNERS { - Err(format!("must be at least {}", MIN_SIGNERS)) - } else if v > MAX_SIGNERS { - Err(format!("must be at most {}", MAX_SIGNERS)) - } else { - Ok(()) - } -} - -fn is_valid_token_program_id(string: T) -> Result<(), String> -where - T: AsRef + fmt::Display, -{ - match is_pubkey(string.as_ref()) { - Ok(()) => { - let program_id = string.as_ref().parse::().unwrap(); - if VALID_TOKEN_PROGRAM_IDS.contains(&program_id) { - Ok(()) - } else { - Err(format!("Unrecognized token program id: {}", program_id)) - } - } - Err(e) => Err(e), - } -} - -struct SignOnlyNeedsFullMintSpec {} -impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[MINT_ADDRESS_ARG.name, MINT_DECIMALS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[MINT_ADDRESS_ARG.name, MINT_DECIMALS_ARG.name]) - } -} - -struct SignOnlyNeedsMintDecimals {} -impl offline::ArgsConfig for SignOnlyNeedsMintDecimals { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[MINT_DECIMALS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[MINT_DECIMALS_ARG.name]) - } -} - -struct SignOnlyNeedsMintAddress {} -impl offline::ArgsConfig for SignOnlyNeedsMintAddress { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[MINT_ADDRESS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[MINT_ADDRESS_ARG.name]) - } -} - -struct SignOnlyNeedsDelegateAddress {} -impl offline::ArgsConfig for SignOnlyNeedsDelegateAddress { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[DELEGATE_ADDRESS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[DELEGATE_ADDRESS_ARG.name]) - } -} - -struct SignOnlyNeedsTransferLamports {} -impl offline::ArgsConfig for SignOnlyNeedsTransferLamports { - fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[TRANSFER_LAMPORTS_ARG.name]) - } - fn signer_arg<'a, 'b>(&self, arg: Arg<'a>) -> Arg<'a> { - arg.requires_all(&[TRANSFER_LAMPORTS_ARG.name]) - } -} - -pub fn minimum_signers_help_string() -> String { - format!( - "The minimum number of signers required to allow the operation. [{} <= M <= N]", - MIN_SIGNERS - ) -} - -pub fn multisig_member_help_string() -> String { - format!( - "The public keys for each of the N signing members of this account. [{} <= N <= {}]", - MIN_SIGNERS, MAX_SIGNERS - ) -} - -pub(crate) trait BenchSubCommand { - fn bench_subcommand(self) -> Self; -} - -impl BenchSubCommand for App<'_> { - fn bench_subcommand(self) -> Self { - self.subcommand( - SubCommand::with_name("bench") - .about("Token benchmarking facilities") - .setting(AppSettings::InferSubcommands) - .setting(AppSettings::SubcommandRequiredElseHelp) - .subcommand( - SubCommand::with_name("create-accounts") - .about("Create multiple token accounts for benchmarking") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the accounts will hold"), - ) - .arg( - Arg::with_name("n") - .value_parser(clap::value_parser!(usize)) - .value_name("N") - .takes_value(true) - .index(2) - .required(true) - .help("The number of accounts to create"), - ) - .arg(owner_address_arg()), - ) - .subcommand( - SubCommand::with_name("close-accounts") - .about("Close multiple token accounts used for benchmarking") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the accounts held"), - ) - .arg( - Arg::with_name("n") - .value_parser(clap::value_parser!(usize)) - .value_name("N") - .takes_value(true) - .index(2) - .required(true) - .help("The number of accounts to close"), - ) - .arg(owner_address_arg()), - ) - .subcommand( - SubCommand::with_name("deposit-into") - .about("Deposit tokens into multiple accounts") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the accounts will hold"), - ) - .arg( - Arg::with_name("n") - .value_parser(clap::value_parser!(usize)) - .value_name("N") - .takes_value(true) - .index(2) - .required(true) - .help("The number of accounts to deposit into"), - ) - .arg( - Arg::with_name("amount") - .value_parser(Amount::parse) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(3) - .required(true) - .help("Amount to deposit into each account, in tokens"), - ) - .arg( - Arg::with_name("from") - .long("from") - .validator(|s| is_valid_pubkey(s)) - .value_name("SOURCE_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The source token account address [default: associated token account for --owner]") - ) - .arg(owner_address_arg()), - ) - .subcommand( - SubCommand::with_name("withdraw-from") - .about("Withdraw tokens from multiple accounts") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the accounts hold"), - ) - .arg( - Arg::with_name("n") - .value_parser(clap::value_parser!(usize)) - .value_name("N") - .takes_value(true) - .index(2) - .required(true) - .help("The number of accounts to withdraw from"), - ) - .arg( - Arg::with_name("amount") - .value_parser(Amount::parse) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(3) - .required(true) - .help("Amount to withdraw from each account, in tokens"), - ) - .arg( - Arg::with_name("to") - .long("to") - .validator(|s| is_valid_pubkey(s)) - .value_name("RECIPIENT_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The recipient token account address [default: associated token account for --owner]") - ) - .arg(owner_address_arg()), - ), - ) - } -} - -pub fn app<'a>( - default_decimals: &'a str, - minimum_signers_help: &'a str, - multisig_member_help: &'a str, -) -> App<'a> { - App::new(crate_name!()) - .about(crate_description!()) - .version(crate_version!()) - .setting(AppSettings::SubcommandRequiredElseHelp) - .arg( - Arg::with_name("config_file") - .short('C') - .long("config") - .value_name("PATH") - .takes_value(true) - .global(true) - .help("Configuration file to use"), - ) - .arg( - Arg::with_name("verbose") - .short('v') - .long("verbose") - .takes_value(false) - .global(true) - .help("Show additional information"), - ) - .arg( - Arg::with_name("output_format") - .long("output") - .value_name("FORMAT") - .global(true) - .takes_value(true) - .possible_values(["json", "json-compact"]) - .help("Return information in specified output format"), - ) - .arg( - Arg::with_name("program_2022") - .long("program-2022") - .takes_value(false) - .global(true) - .conflicts_with("program_id") - .help("Use token extension program token 2022 with program id: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"), - ) - .arg( - Arg::with_name("program_id") - .short('p') - .long("program-id") - .value_name("ADDRESS") - .takes_value(true) - .global(true) - .conflicts_with("program_2022") - .validator(|s| is_valid_token_program_id(s)) - .help("SPL Token program id"), - ) - .arg( - Arg::with_name("json_rpc_url") - .short('u') - .long("url") - .value_name("URL_OR_MONIKER") - .takes_value(true) - .global(true) - .validator(|s| is_url_or_moniker(s)) - .help( - "URL for Solana's JSON RPC or moniker (or their first letter): \ - [mainnet-beta, testnet, devnet, localhost] \ - Default from the configuration file." - ), - ) - .arg(fee_payer_arg().global(true)) - .arg( - Arg::with_name("use_unchecked_instruction") - .long("use-unchecked-instruction") - .takes_value(false) - .global(true) - .hidden(true) - .help("Use unchecked instruction if appropriate. Supports transfer, burn, mint, and approve."), - ) - .arg( - Arg::with_name(COMPUTE_UNIT_LIMIT_ARG.name) - .long(COMPUTE_UNIT_LIMIT_ARG.long) - .takes_value(true) - .global(true) - .value_name("COMPUTE-UNIT-LIMIT") - .value_parser(clap::value_parser!(u32)) - .help(COMPUTE_UNIT_LIMIT_ARG.help) - ) - .arg( - Arg::with_name(COMPUTE_UNIT_PRICE_ARG.name) - .long(COMPUTE_UNIT_PRICE_ARG.long) - .takes_value(true) - .global(true) - .value_name("COMPUTE-UNIT-PRICE") - .value_parser(clap::value_parser!(u64)) - .help(COMPUTE_UNIT_PRICE_ARG.help) - ) - .bench_subcommand() - .subcommand(SubCommand::with_name(CommandName::CreateToken.into()).about("Create a new token") - .arg( - Arg::with_name("token_keypair") - .value_name("TOKEN_KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .index(1) - .help( - "Specify the token keypair. \ - This may be a keypair file or the ASK keyword. \ - [default: randomly generated keypair]" - ), - ) - .arg( - Arg::with_name("mint_authority") - .long("mint-authority") - .alias("owner") - .value_name("ADDRESS") - .validator(|s| is_valid_pubkey(s)) - .takes_value(true) - .help( - "Specify the mint authority address. \ - Defaults to the client keypair address." - ), - ) - .arg( - Arg::with_name("decimals") - .long("decimals") - .value_parser(clap::value_parser!(u8)) - .value_name("DECIMALS") - .takes_value(true) - .default_value(default_decimals) - .help("Number of base 10 digits to the right of the decimal place"), - ) - .arg( - Arg::with_name("enable_freeze") - .long("enable-freeze") - .takes_value(false) - .help( - "Enable the mint authority to freeze token accounts for this mint" - ), - ) - .arg( - Arg::with_name("enable_close") - .long("enable-close") - .takes_value(false) - .help( - "Enable the mint authority to close this mint" - ), - ) - .arg( - Arg::with_name("interest_rate") - .long("interest-rate") - .value_name("RATE_BPS") - .takes_value(true) - .help( - "Specify the interest rate in basis points. \ - Rate authority defaults to the mint authority." - ), - ) - .arg( - Arg::with_name("metadata_address") - .long("metadata-address") - .value_name("ADDRESS") - .validator(|s| is_valid_pubkey(s)) - .takes_value(true) - .conflicts_with("enable_metadata") - .help( - "Specify address that stores token metadata." - ), - ) - .arg( - Arg::with_name("group_address") - .long("group-address") - .value_name("ADDRESS") - .validator(|s| is_valid_pubkey(s)) - .takes_value(true) - .conflicts_with("enable_group") - .help( - "Specify address that stores token group configurations." - ), - ) - .arg( - Arg::with_name("member_address") - .long("member-address") - .value_name("ADDRESS") - .validator(|s| is_valid_pubkey(s)) - .takes_value(true) - .conflicts_with("enable_member") - .help( - "Specify address that stores token member configurations." - ), - ) - .arg( - Arg::with_name("enable_non_transferable") - .long("enable-non-transferable") - .alias("enable-nontransferable") - .takes_value(false) - .help( - "Permanently force tokens to be non-transferable. They may still be burned." - ), - ) - .arg( - Arg::with_name("default_account_state") - .long("default-account-state") - .requires("enable_freeze") - .takes_value(true) - .possible_values(["initialized", "frozen"]) - .help("Specify that accounts have a default state. \ - Note: specifying \"initialized\" adds an extension, which gives \ - the option of specifying default frozen accounts in the future. \ - This behavior is not the same as the default, which makes it \ - impossible to specify a default account state in the future."), - ) - .arg( - Arg::with_name("transfer_fee") - .long("transfer-fee") - .value_names(&["FEE_IN_BASIS_POINTS", "MAXIMUM_FEE"]) - .takes_value(true) - .number_of_values(2) - .hidden(true) - .conflicts_with("transfer_fee_basis_points") - .conflicts_with("transfer_fee_maximum_fee") - .help( - "Add a transfer fee to the mint. \ - The mint authority can set the fee and withdraw collected fees.", - ), - ) - .arg( - Arg::with_name("transfer_fee_basis_points") - .long("transfer-fee-basis-points") - .value_names(&["FEE_IN_BASIS_POINTS"]) - .takes_value(true) - .number_of_values(1) - .conflicts_with("transfer_fee") - .requires("transfer_fee_maximum_fee") - .value_parser(clap::value_parser!(u16)) - .help( - "Add transfer fee to the mint. \ - The mint authority can set the fee.", - ), - ) - .arg( - Arg::with_name("transfer_fee_maximum_fee") - .long("transfer-fee-maximum-fee") - .value_names(&["MAXIMUM_FEE"]) - .takes_value(true) - .number_of_values(1) - .conflicts_with("transfer_fee") - .requires("transfer_fee_basis_points") - .value_parser(Amount::parse) - .help( - "Add a UI amount maximum transfer fee to the mint. \ - The mint authority can set and collect fees" - ) - ) - .arg( - Arg::with_name("enable_permanent_delegate") - .long("enable-permanent-delegate") - .takes_value(false) - .help( - "Enable the mint authority to be permanent delegate for this mint" - ), - ) - .arg( - Arg::with_name("enable_confidential_transfers") - .long("enable-confidential-transfers") - .value_names(&["APPROVE-POLICY"]) - .takes_value(true) - .possible_values(["auto", "manual"]) - .help( - "Enable accounts to make confidential transfers. If \"auto\" \ - is selected, then accounts are automatically approved to make \ - confidential transfers. If \"manual\" is selected, then the \ - confidential transfer mint authority must approve each account \ - before it can make confidential transfers." - ) - ) - .arg( - Arg::with_name("transfer_hook") - .long("transfer-hook") - .value_name("TRANSFER_HOOK_PROGRAM_ID") - .validator(|s| is_valid_pubkey(s)) - .takes_value(true) - .help("Enable the mint authority to set the transfer hook program for this mint"), - ) - .arg( - Arg::with_name("enable_metadata") - .long("enable-metadata") - .conflicts_with("metadata_address") - .takes_value(false) - .help("Enables metadata in the mint. The mint authority must initialize the metadata."), - ) - .arg( - Arg::with_name("enable_group") - .long("enable-group") - .conflicts_with("group_address") - .takes_value(false) - .help("Enables group configurations in the mint. The mint authority must initialize the group."), - ) - .arg( - Arg::with_name("enable_member") - .long("enable-member") - .conflicts_with("member_address") - .takes_value(false) - .help("Enables group member configurations in the mint. The mint authority must initialize the member."), - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - .arg(memo_arg()) - ) - .subcommand( - SubCommand::with_name(CommandName::SetInterestRate.into()) - .about("Set the interest rate for an interest-bearing token") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .help("The interest-bearing token address"), - ) - .arg( - Arg::with_name("rate") - .value_name("RATE") - .takes_value(true) - .required(true) - .help("The new interest rate in basis points"), - ) - .arg( - Arg::with_name("rate_authority") - .long("rate-authority") - .validator(|s| is_valid_signer(s)) - .value_name("SIGNER") - .takes_value(true) - .help( - "Specify the rate authority keypair. \ - Defaults to the client keypair address." - ) - ) - ) - .subcommand( - SubCommand::with_name(CommandName::SetTransferHook.into()) - .about("Set the transfer hook program id for a token") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .index(1) - .help("The token address with an existing transfer hook"), - ) - .arg( - Arg::with_name("new_program_id") - .validator(|s| is_valid_pubkey(s)) - .value_name("NEW_PROGRAM_ID") - .takes_value(true) - .required_unless("disable") - .index(2) - .help("The new transfer hook program id to set for this mint"), - ) - .arg( - Arg::with_name("disable") - .long("disable") - .takes_value(false) - .conflicts_with("new_program_id") - .help("Disable transfer hook functionality by setting the program id to None.") - ) - .arg( - Arg::with_name("authority") - .long("authority") - .alias("owner") - .validator(|s| is_valid_signer(s)) - .value_name("SIGNER") - .takes_value(true) - .help("Specify the authority keypair. Defaults to the client keypair address.") - ) - ) - .subcommand( - SubCommand::with_name(CommandName::InitializeMetadata.into()) - .about("Initialize metadata extension on a token mint") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .index(1) - .help("The token address with no metadata present"), - ) - .arg( - Arg::with_name("name") - .value_name("TOKEN_NAME") - .takes_value(true) - .required(true) - .index(2) - .help("The name of the token to set in metadata"), - ) - .arg( - Arg::with_name("symbol") - .value_name("TOKEN_SYMBOL") - .takes_value(true) - .required(true) - .index(3) - .help("The symbol of the token to set in metadata"), - ) - .arg( - Arg::with_name("uri") - .value_name("TOKEN_URI") - .takes_value(true) - .required(true) - .index(4) - .help("The URI of the token to set in metadata"), - ) - .arg( - Arg::with_name("mint_authority") - .long("mint-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the mint authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg( - Arg::with_name("update_authority") - .long("update-authority") - .value_name("ADDRESS") - .validator(|s| is_valid_pubkey(s)) - .takes_value(true) - .help( - "Specify the update authority address. \ - Defaults to the client keypair address." - ), - ) - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateMetadata.into()) - .about("Update metadata on a token mint that has the extension") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .index(1) - .help("The token address with no metadata present"), - ) - .arg( - Arg::with_name("field") - .value_name("FIELD_NAME") - .takes_value(true) - .required(true) - .index(2) - .help("The name of the field to update. Can be a base field (\"name\", \"symbol\", or \"uri\") or any new field to add."), - ) - .arg( - Arg::with_name("value") - .value_name("VALUE_STRING") - .takes_value(true) - .index(3) - .required_unless("remove") - .help("The value for the field"), - ) - .arg( - Arg::with_name("remove") - .long("remove") - .takes_value(false) - .conflicts_with("value") - .help("Remove the key and value for the given field. Does not work with base fields: \"name\", \"symbol\", or \"uri\".") - ) - .arg( - Arg::with_name("authority") - .long("authority") - .validator(|s| is_valid_signer(s)) - .value_name("SIGNER") - .takes_value(true) - .help("Specify the metadata update authority keypair. Defaults to the client keypair.") - ) - .nonce_args(true) - .arg(transfer_lamports_arg()) - .offline_args_config(&SignOnlyNeedsTransferLamports{}), - ) - .subcommand( - SubCommand::with_name(CommandName::InitializeGroup.into()) - .about("Initialize group extension on a token mint") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .index(1) - .help("The token address of the group account."), - ) - .arg( - Arg::with_name("max_size") - .value_parser(clap::value_parser!(u64)) - .value_name("MAX_SIZE") - .takes_value(true) - .required(true) - .index(2) - .help("The number of members in the group."), - ) - .arg( - Arg::with_name("mint_authority") - .long("mint-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the mint authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg( - Arg::with_name("update_authority") - .long("update-authority") - .value_name("ADDRESS") - .validator(|s| is_valid_pubkey(s)) - .takes_value(true) - .help( - "Specify the update authority address. \ - Defaults to the client keypair address." - ), - ) - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateGroupMaxSize.into()) - .about("Updates the maximum number of members for a group.") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .index(1) - .help("The token address of the group account."), - ) - .arg( - Arg::with_name("new_max_size") - .value_parser(clap::value_parser!(u64)) - .value_name("NEW_MAX_SIZE") - .takes_value(true) - .required(true) - .index(2) - .help("The number of members in the group."), - ) - .arg( - Arg::with_name("update_authority") - .long("update-authority") - .value_name("SIGNER") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the update authority address. \ - Defaults to the client keypair address." - ), - ) - ) - .subcommand( - SubCommand::with_name(CommandName::InitializeMember.into()) - .about("Initialize group member extension on a token mint") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .index(1) - .help("The token address of the member account."), - ) - .arg( - Arg::with_name("group_token") - .validator(|s| is_valid_pubkey(s)) - .value_name("GROUP_TOKEN_ADDRESS") - .takes_value(true) - .required(true) - .index(2) - .help("The token address of the group account that the token will join."), - ) - .arg( - Arg::with_name("mint_authority") - .long("mint-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the mint authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg( - Arg::with_name("group_update_authority") - .long("group-update-authority") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the update authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair address." - ), - ) - ) - .subcommand( - SubCommand::with_name(CommandName::CreateAccount.into()) - .about("Create a new token account") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token that the account will hold"), - ) - .arg( - Arg::with_name("account_keypair") - .value_name("ACCOUNT_KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .index(2) - .help( - "Specify the account keypair. \ - This may be a keypair file or the ASK keyword. \ - [default: associated token account for --owner]" - ), - ) - .arg( - Arg::with_name("immutable") - .long("immutable") - .takes_value(false) - .help( - "Lock the owner of this token account from ever being changed" - ), - ) - .arg(owner_address_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::CreateMultisig.into()) - .about("Create a new account describing an M:N multisignature") - .arg( - Arg::with_name("minimum_signers") - .value_name("MINIMUM_SIGNERS") - .validator(is_multisig_minimum_signers) - .takes_value(true) - .index(1) - .required(true) - .help(minimum_signers_help), - ) - .arg( - Arg::with_name("multisig_member") - .value_name("MULTISIG_MEMBER_PUBKEY") - .validator(|s| is_valid_pubkey(s)) - .takes_value(true) - .index(2) - .required(true) - .min_values(MIN_SIGNERS) - .max_values(MAX_SIGNERS) - .help(multisig_member_help), - ) - .arg( - Arg::with_name("address_keypair") - .long("address-keypair") - .value_name("ADDRESS_KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the address keypair. \ - This may be a keypair file or the ASK keyword. \ - [default: randomly generated keypair]" - ), - ) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::Authorize.into()) - .about("Authorize a new signing keypair to a token or token account") - .arg( - Arg::with_name("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint or account"), - ) - .arg( - Arg::with_name("authority_type") - .value_name("AUTHORITY_TYPE") - .takes_value(true) - .possible_values(CliAuthorityType::iter().map(Into::<&str>::into).collect::>()) - .index(2) - .required(true) - .help("The new authority type. \ - Token mints support `mint`, `freeze`, and mint extension authorities; \ - Token accounts support `owner`, `close`, and account extension \ - authorities."), - ) - .arg( - Arg::with_name("new_authority") - .validator(|s| is_valid_pubkey(s)) - .value_name("AUTHORITY_ADDRESS") - .takes_value(true) - .index(3) - .required_unless("disable") - .help("The address of the new authority"), - ) - .arg( - Arg::with_name("authority") - .long("authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the current authority keypair. \ - Defaults to the client keypair." - ), - ) - .arg( - Arg::with_name("disable") - .long("disable") - .takes_value(false) - .conflicts_with("new_authority") - .help("Disable mint, freeze, or close functionality by setting authority to None.") - ) - .arg( - Arg::with_name("force") - .long("force") - .hidden(true) - .help("Force re-authorize the wallet's associate token account. Don't use this flag"), - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::Transfer.into()) - .about("Transfer tokens between accounts") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("Token to transfer"), - ) - .arg( - Arg::with_name("amount") - .value_parser(Amount::parse) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to send, in tokens; accepts keyword ALL"), - ) - .arg( - Arg::with_name("recipient") - .validator(|s| is_valid_pubkey(s)) - .value_name("RECIPIENT_WALLET_ADDRESS or RECIPIENT_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(3) - .required(true) - .help("If a token account address is provided, use it as the recipient. \ - Otherwise assume the recipient address is a user wallet and transfer to \ - the associated token account") - ) - .arg( - Arg::with_name("from") - .validator(|s| is_valid_pubkey(s)) - .value_name("SENDER_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .long("from") - .help("Specify the sending token account \ - [default: owner's associated token account]") - ) - .arg(owner_keypair_arg_with_value_name("SENDER_TOKEN_OWNER_KEYPAIR") - .help( - "Specify the owner of the sending token account. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg( - Arg::with_name("allow_unfunded_recipient") - .long("allow-unfunded-recipient") - .takes_value(false) - .help("Complete the transfer even if the recipient address is not funded") - ) - .arg( - Arg::with_name("allow_empty_recipient") - .long("allow-empty-recipient") - .takes_value(false) - .hidden(true) // Deprecated, use --allow-unfunded-recipient instead - ) - .arg( - Arg::with_name("fund_recipient") - .long("fund-recipient") - .takes_value(false) - .conflicts_with("confidential") - .help("Create the associated token account for the recipient if doesn't already exist") - ) - .arg( - Arg::with_name("no_wait") - .long("no-wait") - .takes_value(false) - .help("Return signature immediately after submitting the transaction, instead of waiting for confirmations"), - ) - .arg( - Arg::with_name("allow_non_system_account_recipient") - .long("allow-non-system-account-recipient") - .takes_value(false) - .help("Send tokens to the recipient even if the recipient is not a wallet owned by System Program."), - ) - .arg( - Arg::with_name("no_recipient_is_ata_owner") - .long("no-recipient-is-ata-owner") - .takes_value(false) - .requires("sign_only") - .help("In sign-only mode, specifies that the recipient is the owner of the associated token account rather than an actual token account"), - ) - .arg( - Arg::with_name("recipient_is_ata_owner") - .long("recipient-is-ata-owner") - .takes_value(false) - .hidden(true) - .conflicts_with("no_recipient_is_ata_owner") - .requires("sign_only") - .help("recipient-is-ata-owner is now the default behavior. The option has been deprecated and will be removed in a future release."), - ) - .arg( - Arg::with_name("expected_fee") - .long("expected-fee") - .value_parser(Amount::parse) - .value_name("EXPECTED_FEE") - .takes_value(true) - .help("Expected fee amount collected during the transfer"), - ) - .arg( - Arg::with_name("transfer_hook_account") - .long("transfer-hook-account") - .validator(|s| validate_transfer_hook_account(s)) - .value_name("PUBKEY:ROLE") - .takes_value(true) - .multiple(true) - .min_values(0_usize) - .help("Additional pubkey(s) required for a transfer hook and their \ - role, in the format \":\". The role must be \ - \"readonly\", \"writable\". \"readonly-signer\", or \"writable-signer\".\ - Used for offline transaction creation and signing.") - ) - .arg( - Arg::with_name("confidential") - .long("confidential") - .takes_value(false) - .conflicts_with("fund_recipient") - .help("Send tokens confidentially. Both sender and recipient accounts must \ - be pre-configured for confidential transfers.") - ) - .arg(multisig_signer_arg()) - .arg(mint_decimals_arg()) - .nonce_args(true) - .arg(memo_arg()) - .offline_args_config(&SignOnlyNeedsMintDecimals{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Burn.into()) - .about("Burn tokens from an account") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token account address to burn from"), - ) - .arg( - Arg::with_name("amount") - .value_parser(Amount::parse) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to burn, in tokens; accepts keyword ALL"), - ) - .arg(owner_keypair_arg_with_value_name("TOKEN_OWNER_KEYPAIR") - .help( - "Specify the burnt token owner account. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(multisig_signer_arg()) - .mint_args() - .nonce_args(true) - .arg(memo_arg()) - .offline_args_config(&SignOnlyNeedsFullMintSpec{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Mint.into()) - .about("Mint new tokens") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token to mint"), - ) - .arg( - Arg::with_name("amount") - .value_parser(Amount::parse) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to mint, in tokens"), - ) - .arg( - Arg::with_name("recipient") - .validator(|s| is_valid_pubkey(s)) - .value_name("RECIPIENT_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("recipient_owner") - .index(3) - .help("The token account address of recipient \ - [default: associated token account for --mint-authority]"), - ) - .arg( - Arg::with_name("recipient_owner") - .long("recipient-owner") - .validator(|s| is_valid_pubkey(s)) - .value_name("RECIPIENT_WALLET_ADDRESS") - .takes_value(true) - .conflicts_with("recipient") - .help("The owner of the recipient associated token account"), - ) - .arg( - Arg::with_name("mint_authority") - .long("mint-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the mint authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(mint_decimals_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .arg(memo_arg()) - .offline_args_config(&SignOnlyNeedsMintDecimals{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Freeze.into()) - .about("Freeze a token account") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to freeze"), - ) - .arg( - Arg::with_name("freeze_authority") - .long("freeze-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the freeze authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(mint_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args_config(&SignOnlyNeedsMintAddress{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Thaw.into()) - .about("Thaw a token account") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to thaw"), - ) - .arg( - Arg::with_name("freeze_authority") - .long("freeze-authority") - .alias("owner") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the freeze authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(mint_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args_config(&SignOnlyNeedsMintAddress{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Wrap.into()) - .about("Wrap native SOL in a SOL token account") - .arg( - Arg::with_name("amount") - .value_parser(Amount::parse) - .value_name("AMOUNT") - .takes_value(true) - .index(1) - .required(true) - .help("Amount of SOL to wrap"), - ) - .arg( - Arg::with_name("wallet_keypair") - .alias("owner") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .index(2) - .help( - "Specify the keypair for the wallet which will have its native SOL wrapped. \ - This wallet will be assigned as the owner of the wrapped SOL token account. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg( - Arg::with_name("create_aux_account") - .takes_value(false) - .long("create-aux-account") - .help("Wrap SOL in an auxiliary account instead of associated token account"), - ) - .arg( - Arg::with_name("immutable") - .long("immutable") - .takes_value(false) - .help( - "Lock the owner of this token account from ever being changed" - ), - ) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::Unwrap.into()) - .about("Unwrap a SOL token account") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .help("The address of the auxiliary token account to unwrap \ - [default: associated token account for --owner]"), - ) - .arg( - Arg::with_name("wallet_keypair") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .index(2) - .help( - "Specify the keypair for the wallet which owns the wrapped SOL. \ - This wallet will receive the unwrapped SOL. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::Approve.into()) - .about("Approve a delegate for a token account") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to delegate"), - ) - .arg( - Arg::with_name("amount") - .value_parser(Amount::parse) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to approve, in tokens"), - ) - .arg( - Arg::with_name("delegate") - .validator(|s| is_valid_pubkey(s)) - .value_name("DELEGATE_TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(3) - .required(true) - .help("The token account address of delegate"), - ) - .arg( - owner_keypair_arg() - ) - .arg(multisig_signer_arg()) - .mint_args() - .nonce_args(true) - .offline_args_config(&SignOnlyNeedsFullMintSpec{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Revoke.into()) - .about("Revoke a delegate's authority") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account"), - ) - .arg(owner_keypair_arg() - ) - .arg(delegate_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args_config(&SignOnlyNeedsDelegateAddress{}), - ) - .subcommand( - SubCommand::with_name(CommandName::Close.into()) - .about("Close a token account") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("Token of the associated account to close. \ - To close a specific account, use the `--address` parameter instead"), - ) - .arg( - Arg::with_name("recipient") - .long("recipient") - .validator(|s| is_valid_pubkey(s)) - .value_name("REFUND_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the account to receive remaining SOL [default: --owner]"), - ) - .arg( - Arg::with_name("close_authority") - .long("close-authority") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the token's close authority if it has one, \ - otherwise specify the token's owner keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("Specify the token account to close \ - [default: owner's associated token account]"), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::CloseMint.into()) - .about("Close a token mint") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("Token to close"), - ) - .arg( - Arg::with_name("recipient") - .long("recipient") - .validator(|s| is_valid_pubkey(s)) - .value_name("REFUND_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the account to receive remaining SOL [default: --owner]"), - ) - .arg( - Arg::with_name("close_authority") - .long("close-authority") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the token's close authority. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::Balance.into()) - .about("Get token account balance") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("Token of associated account. To query a specific account, use the `--address` parameter instead"), - ) - .arg(owner_address_arg().conflicts_with("address")) - .arg( - Arg::with_name("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .long("address") - .conflicts_with("token") - .help("Specify the token account to query \ - [default: owner's associated token account]"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::Supply.into()) - .about("Get token supply") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token address"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::Accounts.into()) - .about("List all token accounts by owner") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .help("Limit results to the given token. [Default: list accounts for all tokens]"), - ) - .arg( - Arg::with_name("delegated") - .long("delegated") - .takes_value(false) - .conflicts_with("externally_closeable") - .help( - "Limit results to accounts with transfer delegations" - ), - ) - .arg( - Arg::with_name("externally_closeable") - .long("externally-closeable") - .takes_value(false) - .conflicts_with("delegated") - .help( - "Limit results to accounts with external close authorities" - ), - ) - .arg( - Arg::with_name("addresses_only") - .long("addresses-only") - .takes_value(false) - .conflicts_with("verbose") - .conflicts_with("output_format") - .help( - "Print token account addresses only" - ), - ) - .arg(owner_address_arg()) - ) - .subcommand( - SubCommand::with_name(CommandName::Address.into()) - .about("Get wallet address") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .long("token") - .requires("verbose") - .help("Return the associated token address for the given token. \ - [Default: return the client keypair address]") - ) - .arg( - owner_address_arg() - .requires("token") - .help("Return the associated token address for the given owner. \ - [Default: return the associated token address for the client keypair]"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::AccountInfo.into()) - .about("Query details of an SPL Token account by address (DEPRECATED: use `spl-token display`)") - .setting(AppSettings::Hidden) - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .conflicts_with("address") - .required_unless("address") - .help("Token of associated account. \ - To query a specific account, use the `--address` parameter instead"), - ) - .arg( - Arg::with_name(OWNER_ADDRESS_ARG.name) - .takes_value(true) - .value_name("OWNER_ADDRESS") - .validator(|s| is_valid_signer(s)) - .help(OWNER_ADDRESS_ARG.help) - .index(2) - .conflicts_with("address") - .help("Owner of the associated account for the specified token. \ - To query a specific account, use the `--address` parameter instead. \ - Defaults to the client keypair."), - ) - .arg( - Arg::with_name("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .long("address") - .conflicts_with("token") - .help("Specify the token account to query"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::MultisigInfo.into()) - .about("Query details of an SPL Token multisig account by address (DEPRECATED: use `spl-token display`)") - .setting(AppSettings::Hidden) - .arg( - Arg::with_name("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("MULTISIG_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the SPL Token multisig account to query"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::Display.into()) - .about("Query details of an SPL Token mint, account, or multisig by address") - .arg( - Arg::with_name("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the SPL Token mint, account, or multisig to query"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::Gc.into()) - .about("Cleanup unnecessary token accounts") - .arg(owner_keypair_arg()) - .arg( - Arg::with_name("close_empty_associated_accounts") - .long("close-empty-associated-accounts") - .takes_value(false) - .help("close all empty associated token accounts (to get SOL back)") - ) - ) - .subcommand( - SubCommand::with_name(CommandName::SyncNative.into()) - .about("Sync a native SOL token account to its underlying lamports") - .arg( - owner_address_arg() - .index(1) - .conflicts_with("address") - .help("Owner of the associated account for the native token. \ - To query a specific account, use the `--address` parameter instead. \ - Defaults to the client keypair."), - ) - .arg( - Arg::with_name("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .long("address") - .conflicts_with("owner") - .help("Specify the specific token account address to sync"), - ), - ) - .subcommand( - SubCommand::with_name(CommandName::EnableRequiredTransferMemos.into()) - .about("Enable required transfer memos for token account") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to require transfer memos for") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DisableRequiredTransferMemos.into()) - .about("Disable required transfer memos for token account") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to stop requiring transfer memos for"), - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::EnableCpiGuard.into()) - .about("Enable CPI Guard for token account") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to enable CPI Guard for") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DisableCpiGuard.into()) - .about("Disable CPI Guard for token account") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to disable CPI Guard for"), - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateDefaultAccountState.into()) - .about("Updates default account state for the mint. Requires the default account state extension.") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint to update default account state"), - ) - .arg( - Arg::with_name("state") - .value_name("STATE") - .takes_value(true) - .possible_values(["initialized", "frozen"]) - .index(2) - .required(true) - .help("The new default account state."), - ) - .arg( - Arg::with_name("freeze_authority") - .long("freeze-authority") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the token's freeze authority. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateMetadataAddress.into()) - .about("Updates metadata pointer address for the mint. Requires the metadata pointer extension.") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint to update the metadata pointer address"), - ) - .arg( - Arg::with_name("metadata_address") - .index(2) - .validator(|s| is_valid_pubkey(s)) - .value_name("METADATA_ADDRESS") - .takes_value(true) - .required_unless("disable") - .help("Specify address that stores token's metadata-pointer"), - ) - .arg( - Arg::with_name("disable") - .long("disable") - .takes_value(false) - .conflicts_with("metadata_address") - .help("Unset metadata pointer address.") - ) - .arg( - Arg::with_name("authority") - .long("authority") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the token's metadata-pointer authority. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateGroupAddress.into()) - .about("Updates group pointer address for the mint. Requires the group pointer extension.") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint to update the group pointer address"), - ) - .arg( - Arg::with_name("group_address") - .index(2) - .validator(|s| is_valid_pubkey(s)) - .value_name("GROUP_ADDRESS") - .takes_value(true) - .required_unless("disable") - .help("Specify address that stores token's group-pointer"), - ) - .arg( - Arg::with_name("disable") - .long("disable") - .takes_value(false) - .conflicts_with("group_address") - .help("Unset group pointer address.") - ) - .arg( - Arg::with_name("authority") - .long("authority") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the token's group-pointer authority. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateMemberAddress.into()) - .about("Updates group member pointer address for the mint. Requires the group member pointer extension.") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint to update the group member pointer address"), - ) - .arg( - Arg::with_name("member_address") - .index(2) - .validator(|s| is_valid_pubkey(s)) - .value_name("MEMBER_ADDRESS") - .takes_value(true) - .required_unless("disable") - .help("Specify address that stores token's group-member-pointer"), - ) - .arg( - Arg::with_name("disable") - .long("disable") - .takes_value(false) - .conflicts_with("member_address") - .help("Unset group member pointer address.") - ) - .arg( - Arg::with_name("authority") - .long("authority") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the token's group-member-pointer authority. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair.", - ), - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::WithdrawWithheldTokens.into()) - .about("Withdraw withheld transfer fee tokens from mint and / or account(s)") - .arg( - Arg::with_name("account") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token account to receive withdrawn tokens"), - ) - .arg( - Arg::with_name("source") - .validator(|s| is_valid_pubkey(s)) - .value_name("ACCOUNT_ADDRESS") - .takes_value(true) - .multiple(true) - .min_values(0_usize) - .index(2) - .help("The token accounts to withdraw from") - ) - .arg( - Arg::with_name("include_mint") - .long("include-mint") - .takes_value(false) - .help("Also withdraw withheld tokens from the mint"), - ) - .arg( - Arg::with_name("withdraw_withheld_authority") - .long("withdraw-withheld-authority") - .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) - .takes_value(true) - .help( - "Specify the withdraw withheld authority keypair. \ - This may be a keypair file or the ASK keyword. \ - Defaults to the client keypair." - ), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - .group( - ArgGroup::with_name("source_or_mint") - .arg("source") - .arg("include_mint") - .multiple(true) - .required(true) - ) - ) - .subcommand( - SubCommand::with_name(CommandName::SetTransferFee.into()) - .about("Set the transfer fee for a token with a configured transfer fee") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .required(true) - .help("The interest-bearing token address"), - ) - .arg( - Arg::with_name("transfer_fee_basis_points") - .value_name("FEE_IN_BASIS_POINTS") - .takes_value(true) - .required(true) - .help("The new transfer fee in basis points"), - ) - .arg( - Arg::with_name("maximum_fee") - .value_name("MAXIMUM_FEE") - .value_parser(Amount::parse) - .takes_value(true) - .required(true) - .help("The new maximum transfer fee in UI amount"), - ) - .arg( - Arg::with_name("transfer_fee_authority") - .long("transfer-fee-authority") - .validator(|s| is_valid_signer(s)) - .value_name("SIGNER") - .takes_value(true) - .help( - "Specify the rate authority keypair. \ - Defaults to the client keypair address." - ) - ) - .arg(mint_decimals_arg()) - .offline_args_config(&SignOnlyNeedsMintDecimals{}) - ) - .subcommand( - SubCommand::with_name(CommandName::WithdrawExcessLamports.into()) - .about("Withdraw lamports from a Token Program owned account") - .arg( - Arg::with_name("from") - .validator(|s| is_valid_pubkey(s)) - .value_name("SOURCE_ACCOUNT_ADDRESS") - .takes_value(true) - .required(true) - .help("Specify the address of the account to recover lamports from"), - ) - .arg( - Arg::with_name("recipient") - .validator(|s| is_valid_pubkey(s)) - .value_name("REFUND_ACCOUNT_ADDRESS") - .takes_value(true) - .required(true) - .help("Specify the address of the account to send lamports to"), - ) - .arg(owner_address_arg()) - .arg(multisig_signer_arg()) - ) - .subcommand( - SubCommand::with_name(CommandName::UpdateConfidentialTransferSettings.into()) - .about("Update confidential transfer configuration for a token") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The address of the token mint to update confidential transfer configuration for") - ) - .arg( - Arg::with_name("approve_policy") - .long("approve-policy") - .value_name("APPROVE_POLICY") - .takes_value(true) - .possible_values(["auto", "manual"]) - .help( - "Policy for enabling accounts to make confidential transfers. If \"auto\" \ - is selected, then accounts are automatically approved to make \ - confidential transfers. If \"manual\" is selected, then the \ - confidential transfer mint authority must approve each account \ - before it can make confidential transfers." - ) - ) - .arg( - Arg::with_name("auditor_pubkey") - .long("auditor-pubkey") - .value_name("AUDITOR_PUBKEY") - .takes_value(true) - .help( - "The auditor encryption public key. The corresponding private key for \ - this auditor public key can be used to decrypt all confidential \ - transfers involving tokens from this mint. Currently, the auditor \ - public key can only be specified as a direct *base64* encoding of \ - an ElGamal public key. More methods of specifying the auditor public \ - key will be supported in a future version. To disable auditability \ - feature for the token, use \"none\"." - ) - ) - .group( - ArgGroup::with_name("update_fields").args(&["approve_policy", "auditor_pubkey"]) - .required(true) - .multiple(true) - ) - .arg( - Arg::with_name("confidential_transfer_authority") - .long("confidential-transfer-authority") - .validator(|s| is_valid_signer(s)) - .value_name("SIGNER") - .takes_value(true) - .help( - "Specify the confidential transfer authority keypair. \ - Defaults to the client keypair address." - ) - ) - .nonce_args(true) - .offline_args(), - ) - .subcommand( - SubCommand::with_name(CommandName::ConfigureConfidentialTransferAccount.into()) - .about("Configure confidential transfers for token account") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to configure confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg( - Arg::with_name("maximum_pending_balance_credit_counter") - .long("maximum-pending-balance-credit-counter") - .value_name("MAXIMUM-CREDIT-COUNTER") - .takes_value(true) - .help( - "The maximum pending balance credit counter. \ - This parameter limits the number of confidential transfers that a token account \ - can receive to facilitate decryption of the encrypted balance. \ - Defaults to 65536 (2^16)" - ) - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::EnableConfidentialCredits.into()) - .about("Enable confidential transfers for token account. To enable confidential transfers \ - for the first time, use `configure-confidential-transfer-account` instead.") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to enable confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DisableConfidentialCredits.into()) - .about("Disable confidential transfers for token account") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to disable confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::EnableNonConfidentialCredits.into()) - .about("Enable non-confidential transfers for token account.") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to enable non-confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DisableNonConfidentialCredits.into()) - .about("Disable non-confidential transfers for token account") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .conflicts_with("token") - .help("The address of the token account to disable non-confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::DepositConfidentialTokens.into()) - .about("Deposit amounts for confidential transfers") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("amount") - .value_parser(Amount::parse) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to deposit; accepts keyword ALL"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the token account to configure confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .arg(mint_decimals_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::WithdrawConfidentialTokens.into()) - .about("Withdraw amounts for confidential transfers") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required(true) - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("amount") - .value_parser(Amount::parse) - .value_name("TOKEN_AMOUNT") - .takes_value(true) - .index(2) - .required(true) - .help("Amount to deposit; accepts keyword ALL"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the token account to configure confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .arg(mint_decimals_arg()) - .nonce_args(true) - ) - .subcommand( - SubCommand::with_name(CommandName::ApplyPendingBalance.into()) - .about("Collect confidential tokens from pending to available balance") - .arg( - Arg::with_name("token") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(1) - .required_unless("address") - .help("The token address with confidential transfers enabled"), - ) - .arg( - Arg::with_name("address") - .long("address") - .validator(|s| is_valid_pubkey(s)) - .value_name("TOKEN_ACCOUNT_ADDRESS") - .takes_value(true) - .help("The address of the token account to configure confidential transfers for \ - [default: owner's associated token account]") - ) - .arg( - owner_address_arg() - ) - .arg(multisig_signer_arg()) - .nonce_args(true) - ) -} diff --git a/token/cli/src/command.rs b/token/cli/src/command.rs deleted file mode 100644 index 8a7cf30b5dd..00000000000 --- a/token/cli/src/command.rs +++ /dev/null @@ -1,4734 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -use { - crate::{ - bench::*, - clap_app::*, - config::{Config, MintInfo}, - encryption_keypair::*, - output::*, - sort::{sort_and_parse_token_accounts, AccountFilter}, - }, - clap::{value_t, value_t_or_exit, ArgMatches}, - futures::try_join, - serde::Serialize, - solana_account_decoder::{ - parse_account_data::SplTokenAdditionalData, - parse_token::{get_token_account_mint, parse_token_v2, TokenAccountType, UiAccountState}, - UiAccountData, - }, - solana_clap_v3_utils::{ - input_parsers::{pubkey_of_signer, pubkeys_of_multiple_signers, Amount}, - keypair::signer_from_path, - }, - solana_cli_output::{ - return_signers_data, CliSignOnlyData, CliSignature, OutputFormat, QuietDisplay, - ReturnSignersConfig, VerboseDisplay, - }, - solana_client::rpc_request::TokenAccountsFilter, - solana_remote_wallet::remote_wallet::RemoteWalletManager, - solana_sdk::{ - instruction::AccountMeta, - native_token::*, - program_option::COption, - pubkey::Pubkey, - signature::{Keypair, Signer}, - system_program, - }, - spl_associated_token_account_client::address::get_associated_token_address_with_program_id, - spl_token_2022::{ - extension::{ - confidential_transfer::{ - account_info::{ - ApplyPendingBalanceAccountInfo, TransferAccountInfo, WithdrawAccountInfo, - }, - ConfidentialTransferAccount, ConfidentialTransferMint, - }, - confidential_transfer_fee::ConfidentialTransferFeeConfig, - cpi_guard::CpiGuard, - default_account_state::DefaultAccountState, - group_member_pointer::GroupMemberPointer, - group_pointer::GroupPointer, - interest_bearing_mint::InterestBearingConfig, - memo_transfer::MemoTransfer, - metadata_pointer::MetadataPointer, - mint_close_authority::MintCloseAuthority, - permanent_delegate::PermanentDelegate, - transfer_fee::{TransferFeeAmount, TransferFeeConfig}, - transfer_hook::TransferHook, - BaseStateWithExtensions, ExtensionType, StateWithExtensionsOwned, - }, - solana_zk_sdk::encryption::{ - auth_encryption::AeKey, - elgamal::{self, ElGamalKeypair}, - pod::elgamal::PodElGamalPubkey, - }, - state::{Account, AccountState, Mint}, - }, - spl_token_client::{ - client::{ProgramRpcClientSendTransaction, RpcClientResponse}, - token::{ - ComputeUnitLimit, ExtensionInitializationParams, ProofAccount, - ProofAccountWithCiphertext, Token, - }, - }, - spl_token_confidential_transfer_proof_generation::{ - transfer::TransferProofData, withdraw::WithdrawProofData, - }, - spl_token_group_interface::state::TokenGroup, - spl_token_metadata_interface::state::{Field, TokenMetadata}, - std::{collections::HashMap, fmt::Display, process::exit, rc::Rc, str::FromStr, sync::Arc}, -}; - -fn print_error_and_exit(e: E) -> T { - eprintln!("error: {}", e); - exit(1) -} - -fn amount_to_raw_amount(amount: Amount, decimals: u8, all_amount: Option, name: &str) -> u64 { - match amount { - Amount::Raw(ui_amount) => ui_amount, - Amount::Decimal(ui_amount) => spl_token::ui_amount_to_amount(ui_amount, decimals), - Amount::All => { - if let Some(raw_amount) = all_amount { - raw_amount - } else { - eprintln!("ALL keyword is not allowed for {}", name); - exit(1) - } - } - } -} - -type BulkSigners = Vec>; -pub type CommandResult = Result; - -fn push_signer_with_dedup(signer: Arc, bulk_signers: &mut BulkSigners) { - if !bulk_signers.contains(&signer) { - bulk_signers.push(signer); - } -} - -fn new_throwaway_signer() -> (Arc, Pubkey) { - let keypair = Keypair::new(); - let pubkey = keypair.pubkey(); - (Arc::new(keypair) as Arc, pubkey) -} - -fn get_signer( - matches: &ArgMatches, - keypair_name: &str, - wallet_manager: &mut Option>, -) -> Option<(Arc, Pubkey)> { - matches.value_of(keypair_name).map(|path| { - let signer = signer_from_path(matches, path, keypair_name, wallet_manager) - .unwrap_or_else(print_error_and_exit); - let signer_pubkey = signer.pubkey(); - (Arc::from(signer), signer_pubkey) - }) -} - -async fn check_wallet_balance( - config: &Config<'_>, - wallet: &Pubkey, - required_balance: u64, -) -> Result<(), Error> { - let balance = config.rpc_client.get_balance(wallet).await?; - if balance < required_balance { - Err(format!( - "Wallet {}, has insufficient balance: {} required, {} available", - wallet, - lamports_to_sol(required_balance), - lamports_to_sol(balance) - ) - .into()) - } else { - Ok(()) - } -} - -fn base_token_client( - config: &Config<'_>, - token_pubkey: &Pubkey, - decimals: Option, -) -> Result, Error> { - Ok(Token::new( - config.program_client.clone(), - &config.program_id, - token_pubkey, - decimals, - config.fee_payer()?.clone(), - )) -} - -fn config_token_client( - token: Token, - config: &Config<'_>, -) -> Result, Error> { - let token = token.with_compute_unit_limit(config.compute_unit_limit.clone()); - - let token = if let Some(compute_unit_price) = config.compute_unit_price { - token.with_compute_unit_price(compute_unit_price) - } else { - token - }; - - if let (Some(nonce_account), Some(nonce_authority), Some(nonce_blockhash)) = ( - config.nonce_account, - &config.nonce_authority, - config.nonce_blockhash, - ) { - Ok(token.with_nonce( - &nonce_account, - Arc::clone(nonce_authority), - &nonce_blockhash, - )) - } else { - Ok(token) - } -} - -fn token_client_from_config( - config: &Config<'_>, - token_pubkey: &Pubkey, - decimals: Option, -) -> Result, Error> { - let token = base_token_client(config, token_pubkey, decimals)?; - config_token_client(token, config) -} - -fn native_token_client_from_config( - config: &Config<'_>, -) -> Result, Error> { - let token = Token::new_native( - config.program_client.clone(), - &config.program_id, - config.fee_payer()?.clone(), - ); - - let token = token.with_compute_unit_limit(config.compute_unit_limit.clone()); - - let token = if let Some(compute_unit_price) = config.compute_unit_price { - token.with_compute_unit_price(compute_unit_price) - } else { - token - }; - - if let (Some(nonce_account), Some(nonce_authority), Some(nonce_blockhash)) = ( - config.nonce_account, - &config.nonce_authority, - config.nonce_blockhash, - ) { - Ok(token.with_nonce( - &nonce_account, - Arc::clone(nonce_authority), - &nonce_blockhash, - )) - } else { - Ok(token) - } -} - -#[derive(strum_macros::Display, Debug)] -#[strum(serialize_all = "kebab-case")] -enum Pointer { - Metadata, - Group, - GroupMember, -} - -#[allow(clippy::too_many_arguments)] -async fn command_create_token( - config: &Config<'_>, - decimals: u8, - token_pubkey: Pubkey, - authority: Pubkey, - enable_freeze: bool, - enable_close: bool, - enable_non_transferable: bool, - enable_permanent_delegate: bool, - memo: Option, - metadata_address: Option, - group_address: Option, - member_address: Option, - rate_bps: Option, - default_account_state: Option, - transfer_fee: Option<(u16, u64)>, - confidential_transfer_auto_approve: Option, - transfer_hook_program_id: Option, - enable_metadata: bool, - enable_group: bool, - enable_member: bool, - bulk_signers: Vec>, -) -> CommandResult { - println_display( - config, - format!( - "Creating token {} under program {}", - token_pubkey, config.program_id - ), - ); - - let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; - - let freeze_authority = if enable_freeze { Some(authority) } else { None }; - - let mut extensions = vec![]; - - if enable_close { - extensions.push(ExtensionInitializationParams::MintCloseAuthority { - close_authority: Some(authority), - }); - } - - if enable_permanent_delegate { - extensions.push(ExtensionInitializationParams::PermanentDelegate { - delegate: authority, - }); - } - - if let Some(rate_bps) = rate_bps { - extensions.push(ExtensionInitializationParams::InterestBearingConfig { - rate_authority: Some(authority), - rate: rate_bps, - }) - } - - if enable_non_transferable { - extensions.push(ExtensionInitializationParams::NonTransferable); - } - - if let Some(state) = default_account_state { - assert!( - enable_freeze, - "Token requires a freeze authority to default to frozen accounts" - ); - extensions.push(ExtensionInitializationParams::DefaultAccountState { state }) - } - - if let Some((transfer_fee_basis_points, maximum_fee)) = transfer_fee { - extensions.push(ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(authority), - withdraw_withheld_authority: Some(authority), - transfer_fee_basis_points, - maximum_fee, - }); - } - - if let Some(auto_approve) = confidential_transfer_auto_approve { - extensions.push(ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority), - auto_approve_new_accounts: auto_approve, - auditor_elgamal_pubkey: None, - }); - if transfer_fee.is_some() { - // Deriving ElGamal key from default signer. Custom ElGamal keys - // will be supported in the future once upgrading to clap-v3. - // - // NOTE: Seed bytes are hardcoded to be empty bytes for now. They - // will be updated once custom ElGamal keys are supported. - let elgamal_keypair = - ElGamalKeypair::new_from_signer(config.default_signer()?.as_ref(), b"").unwrap(); - extensions.push( - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(authority), - withdraw_withheld_authority_elgamal_pubkey: (*elgamal_keypair.pubkey()).into(), - }, - ); - } - } - - if let Some(program_id) = transfer_hook_program_id { - extensions.push(ExtensionInitializationParams::TransferHook { - authority: Some(authority), - program_id: Some(program_id), - }); - } - - if let Some(text) = memo { - token.with_memo(text, vec![config.default_signer()?.pubkey()]); - } - - // CLI checks that only one is set - if metadata_address.is_some() || enable_metadata { - let metadata_address = if enable_metadata { - Some(token_pubkey) - } else { - metadata_address - }; - extensions.push(ExtensionInitializationParams::MetadataPointer { - authority: Some(authority), - metadata_address, - }); - } - - if group_address.is_some() || enable_group { - let group_address = if enable_group { - Some(token_pubkey) - } else { - group_address - }; - extensions.push(ExtensionInitializationParams::GroupPointer { - authority: Some(authority), - group_address, - }); - } - - if member_address.is_some() || enable_member { - let member_address = if enable_member { - Some(token_pubkey) - } else { - member_address - }; - extensions.push(ExtensionInitializationParams::GroupMemberPointer { - authority: Some(authority), - member_address, - }); - } - - let res = token - .create_mint( - &authority, - freeze_authority.as_ref(), - extensions, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - - if enable_metadata { - println_display( - config, - format!( - "To initialize metadata inside the mint, please run \ - `spl-token initialize-metadata {token_pubkey} `, \ - and sign with the mint authority.", - ), - ); - } - - if enable_group { - println_display( - config, - format!( - "To initialize group configurations inside the mint, please run `spl-token initialize-group {token_pubkey} `, and sign with the mint authority.", - ), - ); - } - - if enable_member { - println_display( - config, - format!( - "To initialize group member configurations inside the mint, please run `spl-token initialize-member {token_pubkey}`, and sign with the mint authority and the group's update authority.", - ), - ); - } - - Ok(match tx_return { - TransactionReturnData::CliSignature(cli_signature) => format_output( - CliCreateToken { - address: token_pubkey.to_string(), - decimals, - transaction_data: cli_signature, - }, - &CommandName::CreateToken, - config, - ), - TransactionReturnData::CliSignOnlyData(cli_sign_only_data) => { - format_output(cli_sign_only_data, &CommandName::CreateToken, config) - } - }) -} - -async fn command_set_interest_rate( - config: &Config<'_>, - token_pubkey: Pubkey, - rate_authority: Pubkey, - rate_bps: i16, - bulk_signers: Vec>, -) -> CommandResult { - let mut token = token_client_from_config(config, &token_pubkey, None)?; - // Because set_interest_rate depends on the time, it can cost more between - // simulation and execution. To help that, just set a static compute limit - // if none has been set - if !matches!(config.compute_unit_limit, ComputeUnitLimit::Static(_)) { - token = token.with_compute_unit_limit(ComputeUnitLimit::Static(2_500)); - } - - if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - if let Ok(interest_rate_config) = mint_state.get_extension::() { - let mint_rate_authority_pubkey = - Option::::from(interest_rate_config.rate_authority); - - if mint_rate_authority_pubkey != Some(rate_authority) { - return Err(format!( - "Mint {} has interest rate authority {}, but {} was provided", - token_pubkey, - mint_rate_authority_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - rate_authority - ) - .into()); - } - } else { - return Err(format!("Mint {} is not interest-bearing", token_pubkey).into()); - } - } - - println_display( - config, - format!( - "Setting Interest Rate for {} to {} bps", - token_pubkey, rate_bps - ), - ); - - let res = token - .update_interest_rate(&rate_authority, rate_bps, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_set_transfer_hook_program( - config: &Config<'_>, - token_pubkey: Pubkey, - authority: Pubkey, - new_program_id: Option, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - - if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - if let Ok(extension) = mint_state.get_extension::() { - let authority_pubkey = Option::::from(extension.authority); - - if authority_pubkey != Some(authority) { - return Err(format!( - "Mint {} has transfer hook authority {}, but {} was provided", - token_pubkey, - authority_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - authority - ) - .into()); - } - } else { - return Err( - format!("Mint {} does not have permissioned-transfers", token_pubkey).into(), - ); - } - } - - println_display( - config, - format!( - "Setting Transfer Hook Program id for {} to {}", - token_pubkey, - new_program_id - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()) - ), - ); - - let res = token - .update_transfer_hook_program_id(&authority, new_program_id, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_initialize_metadata( - config: &Config<'_>, - token_pubkey: Pubkey, - update_authority: Pubkey, - mint_authority: Pubkey, - name: String, - symbol: String, - uri: String, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - - let res = token - .token_metadata_initialize_with_rent_transfer( - &config.fee_payer()?.pubkey(), - &update_authority, - &mint_authority, - name, - symbol, - uri, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_update_metadata( - config: &Config<'_>, - token_pubkey: Pubkey, - authority: Pubkey, - field: Field, - value: Option, - transfer_lamports: Option, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - - let res = if let Some(value) = value { - token - .token_metadata_update_field_with_rent_transfer( - &config.fee_payer()?.pubkey(), - &authority, - field, - value, - transfer_lamports, - &bulk_signers, - ) - .await? - } else if let Field::Key(key) = field { - token - .token_metadata_remove_key( - &authority, - key, - true, // idempotent - &bulk_signers, - ) - .await? - } else { - return Err(format!( - "Attempting to remove field {field:?}, which cannot be removed. \ - Please re-run the command with a value of \"\" rather than the `--remove` flag." - ) - .into()); - }; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_initialize_group( - config: &Config<'_>, - token_pubkey: Pubkey, - mint_authority: Pubkey, - update_authority: Pubkey, - max_size: u64, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - - let res = token - .token_group_initialize_with_rent_transfer( - &config.fee_payer()?.pubkey(), - &mint_authority, - &update_authority, - max_size, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_update_group_max_size( - config: &Config<'_>, - token_pubkey: Pubkey, - update_authority: Pubkey, - new_max_size: u64, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - - let res = token - .token_group_update_max_size(&update_authority, new_max_size, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_initialize_member( - config: &Config<'_>, - member_token_pubkey: Pubkey, - mint_authority: Pubkey, - group_token_pubkey: Pubkey, - group_update_authority: Pubkey, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &member_token_pubkey, None)?; - - let res = token - .token_group_initialize_member_with_rent_transfer( - &config.fee_payer()?.pubkey(), - &mint_authority, - &group_token_pubkey, - &group_update_authority, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_set_transfer_fee( - config: &Config<'_>, - token_pubkey: Pubkey, - transfer_fee_authority: Pubkey, - transfer_fee_basis_points: u16, - maximum_fee: Amount, - mint_decimals: Option, - bulk_signers: Vec>, -) -> CommandResult { - let decimals = if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - if mint_decimals.is_some() && mint_decimals != Some(mint_state.base.decimals) { - return Err(format!( - "Decimals {} was provided, but actual value is {}", - mint_decimals.unwrap(), - mint_state.base.decimals - ) - .into()); - } - - if let Ok(transfer_fee_config) = mint_state.get_extension::() { - let mint_fee_authority_pubkey = - Option::::from(transfer_fee_config.transfer_fee_config_authority); - - if mint_fee_authority_pubkey != Some(transfer_fee_authority) { - return Err(format!( - "Mint {} has transfer fee authority {}, but {} was provided", - token_pubkey, - mint_fee_authority_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - transfer_fee_authority - ) - .into()); - } - } else { - return Err(format!("Mint {} does not have a transfer fee", token_pubkey).into()); - } - mint_state.base.decimals - } else { - mint_decimals.unwrap() - }; - - let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; - let maximum_fee = amount_to_raw_amount(maximum_fee, decimals, None, "MAXIMUM_FEE"); - - println_display( - config, - format!( - "Setting transfer fee for {} to {} bps, {} maximum", - token_pubkey, - transfer_fee_basis_points, - spl_token::amount_to_ui_amount(maximum_fee, decimals) - ), - ); - - let res = token - .set_transfer_fee( - &transfer_fee_authority, - transfer_fee_basis_points, - maximum_fee, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_create_account( - config: &Config<'_>, - token_pubkey: Pubkey, - owner: Pubkey, - maybe_account: Option, - immutable_owner: bool, - bulk_signers: Vec>, -) -> CommandResult { - let token = token_client_from_config(config, &token_pubkey, None)?; - let mut extensions = vec![]; - - let (account, is_associated) = if let Some(account) = maybe_account { - ( - account, - token.get_associated_token_address(&owner) == account, - ) - } else { - (token.get_associated_token_address(&owner), true) - }; - - println_display(config, format!("Creating account {}", account)); - - if !config.sign_only { - if let Some(account_data) = config.program_client.get_account(account).await? { - if account_data.owner != system_program::id() || !is_associated { - return Err(format!("Error: Account already exists: {}", account).into()); - } - } - } - - if immutable_owner { - if config.program_id == spl_token::id() { - return Err(format!( - "Specified --immutable, but token program {} does not support the extension", - config.program_id - ) - .into()); - } else if is_associated { - println_display( - config, - "Note: --immutable specified, but Token-2022 ATAs are always immutable, ignoring" - .to_string(), - ); - } else { - extensions.push(ExtensionType::ImmutableOwner); - } - } - - let res = if is_associated { - token.create_associated_token_account(&owner).await - } else { - let signer = bulk_signers - .iter() - .find(|signer| signer.pubkey() == account) - .unwrap_or_else(|| panic!("No signer provided for account {}", account)); - - token - .create_auxiliary_token_account_with_extension_space(&**signer, &owner, extensions) - .await - }?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_create_multisig( - config: &Config<'_>, - multisig: Arc, - minimum_signers: u8, - multisig_members: Vec, -) -> CommandResult { - println_display( - config, - format!( - "Creating {}/{} multisig {} under program {}", - minimum_signers, - multisig_members.len(), - multisig.pubkey(), - config.program_id, - ), - ); - - // default is safe here because create_multisig doesn't use it - let token = token_client_from_config(config, &Pubkey::default(), None)?; - - let res = token - .create_multisig( - &*multisig, - &multisig_members.iter().collect::>(), - minimum_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_authorize( - config: &Config<'_>, - account: Pubkey, - authority_type: CliAuthorityType, - authority: Pubkey, - new_authority: Option, - force_authorize: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - let auth_str: &'static str = (&authority_type).into(); - - let (mint_pubkey, previous_authority) = if !config.sign_only { - let target_account = config.get_account_checked(&account).await?; - - let (mint_pubkey, previous_authority) = if let Ok(mint) = - StateWithExtensionsOwned::::unpack(target_account.data.clone()) - { - let previous_authority = match authority_type { - CliAuthorityType::Owner | CliAuthorityType::Close => Err(format!( - "Authority type `{}` not supported for SPL Token mints", - auth_str - )), - CliAuthorityType::Mint => Ok(Option::::from(mint.base.mint_authority)), - CliAuthorityType::Freeze => Ok(Option::::from(mint.base.freeze_authority)), - CliAuthorityType::CloseMint => { - if let Ok(mint_close_authority) = mint.get_extension::() { - Ok(Option::::from(mint_close_authority.close_authority)) - } else { - Err(format!( - "Mint `{}` does not support close authority", - account - )) - } - } - CliAuthorityType::TransferFeeConfig => { - if let Ok(transfer_fee_config) = mint.get_extension::() { - Ok(Option::::from( - transfer_fee_config.transfer_fee_config_authority, - )) - } else { - Err(format!("Mint `{}` does not support transfer fees", account)) - } - } - CliAuthorityType::WithheldWithdraw => { - if let Ok(transfer_fee_config) = mint.get_extension::() { - Ok(Option::::from( - transfer_fee_config.withdraw_withheld_authority, - )) - } else { - Err(format!("Mint `{}` does not support transfer fees", account)) - } - } - CliAuthorityType::InterestRate => { - if let Ok(interest_rate_config) = mint.get_extension::() - { - Ok(Option::::from(interest_rate_config.rate_authority)) - } else { - Err(format!("Mint `{}` is not interest-bearing", account)) - } - } - CliAuthorityType::PermanentDelegate => { - if let Ok(permanent_delegate) = mint.get_extension::() { - Ok(Option::::from(permanent_delegate.delegate)) - } else { - Err(format!( - "Mint `{}` does not support permanent delegate", - account - )) - } - } - CliAuthorityType::ConfidentialTransferMint => { - if let Ok(confidential_transfer_mint) = - mint.get_extension::() - { - Ok(Option::::from(confidential_transfer_mint.authority)) - } else { - Err(format!( - "Mint `{}` does not support confidential transfers", - account - )) - } - } - CliAuthorityType::TransferHookProgramId => { - if let Ok(extension) = mint.get_extension::() { - Ok(Option::::from(extension.authority)) - } else { - Err(format!( - "Mint `{}` does not support a transfer hook program", - account - )) - } - } - CliAuthorityType::ConfidentialTransferFee => { - if let Ok(confidential_transfer_fee_config) = - mint.get_extension::() - { - Ok(Option::::from( - confidential_transfer_fee_config.authority, - )) - } else { - Err(format!( - "Mint `{}` does not support confidential transfer fees", - account - )) - } - } - CliAuthorityType::MetadataPointer => { - if let Ok(extension) = mint.get_extension::() { - Ok(Option::::from(extension.authority)) - } else { - Err(format!( - "Mint `{}` does not support a metadata pointer", - account - )) - } - } - CliAuthorityType::Metadata => { - if let Ok(extension) = mint.get_variable_len_extension::() { - Ok(Option::::from(extension.update_authority)) - } else { - Err(format!("Mint `{account}` does not support metadata")) - } - } - CliAuthorityType::GroupPointer => { - if let Ok(extension) = mint.get_extension::() { - Ok(Option::::from(extension.authority)) - } else { - Err(format!( - "Mint `{}` does not support a group pointer", - account - )) - } - } - CliAuthorityType::GroupMemberPointer => { - if let Ok(extension) = mint.get_extension::() { - Ok(Option::::from(extension.authority)) - } else { - Err(format!( - "Mint `{}` does not support a group member pointer", - account - )) - } - } - CliAuthorityType::Group => { - if let Ok(extension) = mint.get_extension::() { - Ok(Option::::from(extension.update_authority)) - } else { - Err(format!("Mint `{}` does not support token groups", account)) - } - } - }?; - - Ok((account, previous_authority)) - } else if let Ok(token_account) = - StateWithExtensionsOwned::::unpack(target_account.data) - { - let check_associated_token_account = || -> Result<(), Error> { - let maybe_associated_token_account = get_associated_token_address_with_program_id( - &token_account.base.owner, - &token_account.base.mint, - &config.program_id, - ); - if account == maybe_associated_token_account - && !force_authorize - && Some(authority) != new_authority - { - Err(format!( - "Error: attempting to change the `{}` of an associated token account", - auth_str - ) - .into()) - } else { - Ok(()) - } - }; - - let previous_authority = match authority_type { - CliAuthorityType::Mint - | CliAuthorityType::Freeze - | CliAuthorityType::CloseMint - | CliAuthorityType::TransferFeeConfig - | CliAuthorityType::WithheldWithdraw - | CliAuthorityType::InterestRate - | CliAuthorityType::PermanentDelegate - | CliAuthorityType::ConfidentialTransferMint - | CliAuthorityType::TransferHookProgramId - | CliAuthorityType::ConfidentialTransferFee - | CliAuthorityType::MetadataPointer - | CliAuthorityType::Metadata - | CliAuthorityType::GroupPointer - | CliAuthorityType::Group - | CliAuthorityType::GroupMemberPointer => Err(format!( - "Authority type `{auth_str}` not supported for SPL Token accounts", - )), - CliAuthorityType::Owner => { - check_associated_token_account()?; - Ok(Some(token_account.base.owner)) - } - CliAuthorityType::Close => { - check_associated_token_account()?; - Ok(Some( - token_account - .base - .close_authority - .unwrap_or(token_account.base.owner), - )) - } - }?; - - Ok((token_account.base.mint, previous_authority)) - } else { - Err("Unsupported account data format".to_string()) - }?; - - (mint_pubkey, previous_authority) - } else { - // default is safe here because authorize doesn't use it - (Pubkey::default(), None) - }; - - let token = token_client_from_config(config, &mint_pubkey, None)?; - - println_display( - config, - format!( - "Updating {}\n Current {}: {}\n New {}: {}", - account, - auth_str, - previous_authority - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| if config.sign_only { - "unknown".to_string() - } else { - "disabled".to_string() - }), - auth_str, - new_authority - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()) - ), - ); - - let res = match authority_type { - CliAuthorityType::Metadata => { - token - .token_metadata_update_authority(&authority, new_authority, &bulk_signers) - .await? - } - CliAuthorityType::Group => { - token - .token_group_update_authority(&authority, new_authority, &bulk_signers) - .await? - } - _ => { - token - .set_authority( - &account, - &authority, - new_authority.as_ref(), - authority_type.try_into()?, - &bulk_signers, - ) - .await? - } - }; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_transfer( - config: &Config<'_>, - token_pubkey: Pubkey, - ui_amount: Amount, - recipient: Pubkey, - sender: Option, - sender_owner: Pubkey, - allow_unfunded_recipient: bool, - fund_recipient: bool, - mint_decimals: Option, - no_recipient_is_ata_owner: bool, - use_unchecked_instruction: bool, - ui_fee: Option, - memo: Option, - bulk_signers: BulkSigners, - no_wait: bool, - allow_non_system_account_recipient: bool, - transfer_hook_accounts: Option>, - confidential_transfer_args: Option<&ConfidentialTransferArgs>, -) -> CommandResult { - let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?; - - // if the user got the decimals wrong, they may well have calculated the - // transfer amount wrong we only check in online mode, because in offline, - // mint_info.decimals is always 9 - if !config.sign_only && mint_decimals.is_some() && mint_decimals != Some(mint_info.decimals) { - return Err(format!( - "Decimals {} was provided, but actual value is {}", - mint_decimals.unwrap(), - mint_info.decimals - ) - .into()); - } - - // decimals determines whether transfer_checked is used or not - // in online mode, mint_decimals may be None but mint_info.decimals is always - // correct in offline mode, mint_info.decimals may be wrong, but - // mint_decimals is always provided and in online mode, when mint_decimals - // is provided, it is verified correct hence the fallthrough logic here - let decimals = if use_unchecked_instruction { - None - } else if mint_decimals.is_some() { - mint_decimals - } else { - Some(mint_info.decimals) - }; - - let token = if let Some(transfer_hook_accounts) = transfer_hook_accounts { - token_client_from_config(config, &token_pubkey, decimals)? - .with_transfer_hook_accounts(transfer_hook_accounts) - } else if config.sign_only { - // we need to pass in empty transfer hook accounts on sign-only, - // otherwise the token client will try to fetch the mint account and fail - token_client_from_config(config, &token_pubkey, decimals)? - .with_transfer_hook_accounts(vec![]) - } else { - token_client_from_config(config, &token_pubkey, decimals)? - }; - - // pubkey of the actual account we are sending from - let sender = if let Some(sender) = sender { - sender - } else { - token.get_associated_token_address(&sender_owner) - }; - - // the sender balance - let sender_balance = if config.sign_only { - None - } else { - Some(token.get_account_info(&sender).await?.base.amount) - }; - - // the amount the user wants to transfer, as a u64 - let transfer_balance = match ui_amount { - Amount::Raw(ui_amount) => ui_amount, - Amount::Decimal(ui_amount) => spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals), - Amount::All => { - if config.sign_only { - return Err("Use of ALL keyword to burn tokens requires online signing" - .to_string() - .into()); - } - sender_balance.unwrap() - } - }; - - println_display( - config, - format!( - "{}Transfer {} tokens\n Sender: {}\n Recipient: {}", - if confidential_transfer_args.is_some() { - "Confidential " - } else { - "" - }, - spl_token::amount_to_ui_amount(transfer_balance, mint_info.decimals), - sender, - recipient - ), - ); - - if let Some(sender_balance) = sender_balance { - if transfer_balance > sender_balance && confidential_transfer_args.is_none() { - return Err(format!( - "Error: Sender has insufficient funds, current balance is {}", - spl_token_2022::amount_to_ui_amount_string_trimmed( - sender_balance, - mint_info.decimals - ) - ) - .into()); - } - } - - let maybe_fee = - ui_fee.map(|v| amount_to_raw_amount(v, mint_info.decimals, None, "EXPECTED_FEE")); - - // determine whether recipient is a token account or an expected owner of one - let recipient_is_token_account = if !config.sign_only { - // in online mode we can fetch it and see - let maybe_recipient_account_data = config.program_client.get_account(recipient).await?; - - // if the account exists, and: - // * its a token for this program, we are happy - // * its a system account, we are happy - // * its a non-account for this program, we error helpfully - // * its a token account for a different program, we error helpfully - // * otherwise its probably a program account owner of an ata, in which case we - // gate transfer with a flag - if let Some(recipient_account_data) = maybe_recipient_account_data { - let recipient_account_owner = recipient_account_data.owner; - let maybe_account_state = - StateWithExtensionsOwned::::unpack(recipient_account_data.data); - - if recipient_account_owner == config.program_id && maybe_account_state.is_ok() { - if let Ok(memo_transfer) = maybe_account_state?.get_extension::() { - if memo_transfer.require_incoming_transfer_memos.into() && memo.is_none() { - return Err( - "Error: Recipient expects a transfer memo, but none was provided. \ - Provide a memo using `--with-memo`." - .into(), - ); - } - } - - true - } else if recipient_account_owner == system_program::id() { - false - } else if recipient_account_owner == config.program_id { - return Err( - "Error: Recipient is owned by this token program, but is not a token account." - .into(), - ); - } else if VALID_TOKEN_PROGRAM_IDS.contains(&recipient_account_owner) { - return Err(format!( - "Error: Recipient is owned by {}, but the token mint is owned by {}.", - recipient_account_owner, config.program_id - ) - .into()); - } else if allow_non_system_account_recipient { - false - } else { - return Err("Error: The recipient address is not owned by the System Program. \ - Add `--allow-non-system-account-recipient` to complete the transfer.".into()); - } - } - // if it doesn't exist, it definitely isn't a token account! - // we gate transfer with a different flag - else if maybe_recipient_account_data.is_none() && allow_unfunded_recipient { - false - } else { - return Err("Error: The recipient address is not funded. \ - Add `--allow-unfunded-recipient` to complete the transfer." - .into()); - } - } else { - // in offline mode we gotta trust them - no_recipient_is_ata_owner - }; - - // now if its a token account, life is ez - let (recipient_token_account, fundable_owner) = if recipient_is_token_account { - (recipient, None) - } - // but if not, we need to determine if we can or should create an ata for recipient - else { - // first, get the ata address - let recipient_token_account = token.get_associated_token_address(&recipient); - - println_display( - config, - format!( - " Recipient associated token account: {}", - recipient_token_account - ), - ); - - // if we can fetch it to determine if it exists, do so - let needs_funding = if !config.sign_only { - if let Some(recipient_token_account_data) = config - .program_client - .get_account(recipient_token_account) - .await? - { - let recipient_token_account_owner = recipient_token_account_data.owner; - - if let Ok(account_state) = - StateWithExtensionsOwned::::unpack(recipient_token_account_data.data) - { - if let Ok(memo_transfer) = account_state.get_extension::() { - if memo_transfer.require_incoming_transfer_memos.into() && memo.is_none() { - return Err( - "Error: Recipient expects a transfer memo, but none was provided. \ - Provide a memo using `--with-memo`." - .into(), - ); - } - } - } - - if recipient_token_account_owner == system_program::id() { - true - } else if recipient_token_account_owner == config.program_id { - false - } else { - return Err( - format!("Error: Unsupported recipient address: {}", recipient).into(), - ); - } - } else { - true - } - } - // otherwise trust the cli flag - else { - fund_recipient - }; - - // and now we determine if we will actually fund it, based on its need and our - // willingness - let fundable_owner = if needs_funding { - if confidential_transfer_args.is_some() { - return Err( - "Error: Recipient's associated token account does not exist. \ - Accounts cannot be funded for confidential transfers." - .into(), - ); - } else if fund_recipient { - println_display( - config, - format!(" Funding recipient: {}", recipient_token_account,), - ); - - Some(recipient) - } else { - return Err( - "Error: Recipient's associated token account does not exist. \ - Add `--fund-recipient` to fund their account" - .into(), - ); - } - } else { - None - }; - - (recipient_token_account, fundable_owner) - }; - - // set up memo if provided... - if let Some(text) = memo { - token.with_memo(text, vec![config.default_signer()?.pubkey()]); - } - - // fetch confidential transfer info for recipient and auditor - let (recipient_elgamal_pubkey, auditor_elgamal_pubkey) = if let Some(args) = - confidential_transfer_args - { - if !config.sign_only { - // we can use the mint data from the start of the function, but will require - // non-trivial amount of refactoring the code due to ownership; for now, we - // fetch the mint a second time. This can potentially be optimized - // in the future. - let confidential_transfer_mint = config.get_account_checked(&token_pubkey).await?; - let mint_state = - StateWithExtensionsOwned::::unpack(confidential_transfer_mint.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - let auditor_elgamal_pubkey = if let Ok(confidential_transfer_mint) = - mint_state.get_extension::() - { - let expected_auditor_elgamal_pubkey = Option::::from( - confidential_transfer_mint.auditor_elgamal_pubkey, - ); - - // if auditor ElGamal pubkey is provided, check consistency with the one in the - // mint if auditor ElGamal pubkey is not provided, then use the - // expected one from the mint, which could also be `None` if - // auditing is disabled - if args.auditor_elgamal_pubkey.is_some() - && expected_auditor_elgamal_pubkey != args.auditor_elgamal_pubkey - { - return Err(format!( - "Mint {} has confidential transfer auditor {}, but {} was provided", - token_pubkey, - expected_auditor_elgamal_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - args.auditor_elgamal_pubkey.unwrap(), - ) - .into()); - } - - expected_auditor_elgamal_pubkey - } else { - return Err(format!( - "Mint {} does not support confidential transfers", - token_pubkey - ) - .into()); - }; - - let recipient_account = config.get_account_checked(&recipient_token_account).await?; - let recipient_elgamal_pubkey = - StateWithExtensionsOwned::::unpack(recipient_account.data)? - .get_extension::()? - .elgamal_pubkey; - - (Some(recipient_elgamal_pubkey), auditor_elgamal_pubkey) - } else { - let recipient_elgamal_pubkey = args - .recipient_elgamal_pubkey - .expect("Recipient ElGamal pubkey must be provided"); - let auditor_elgamal_pubkey = args - .auditor_elgamal_pubkey - .expect("Auditor ElGamal pubkey must be provided"); - - (Some(recipient_elgamal_pubkey), Some(auditor_elgamal_pubkey)) - } - } else { - (None, None) - }; - - // ...and, finally, the transfer - let res = match (fundable_owner, maybe_fee, confidential_transfer_args) { - (Some(recipient_owner), None, None) => { - token - .create_recipient_associated_account_and_transfer( - &sender, - &recipient_token_account, - &recipient_owner, - &sender_owner, - transfer_balance, - maybe_fee, - &bulk_signers, - ) - .await? - } - (Some(_), _, _) => { - panic!("Recipient account cannot be created for transfer with fees or confidential transfers"); - } - (None, Some(fee), None) => { - token - .transfer_with_fee( - &sender, - &recipient_token_account, - &sender_owner, - transfer_balance, - fee, - &bulk_signers, - ) - .await? - } - (None, None, Some(args)) => { - // deserialize `pod` ElGamal pubkeys - let recipient_elgamal_pubkey: elgamal::ElGamalPubkey = recipient_elgamal_pubkey - .unwrap() - .try_into() - .expect("Invalid recipient ElGamal pubkey"); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.map(|pubkey| { - let auditor_elgamal_pubkey: elgamal::ElGamalPubkey = - pubkey.try_into().expect("Invalid auditor ElGamal pubkey"); - auditor_elgamal_pubkey - }); - - let context_state_authority = config.fee_payer()?; - let context_state_authority_pubkey = context_state_authority.pubkey(); - let equality_proof_context_state_account = Keypair::new(); - let equality_proof_pubkey = equality_proof_context_state_account.pubkey(); - let ciphertext_validity_proof_context_state_account = Keypair::new(); - let ciphertext_validity_proof_pubkey = - ciphertext_validity_proof_context_state_account.pubkey(); - let range_proof_context_state_account = Keypair::new(); - let range_proof_pubkey = range_proof_context_state_account.pubkey(); - - let state = token.get_account_info(&sender).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let transfer_account_info = TransferAccountInfo::new(extension); - - let TransferProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - } = transfer_account_info - .generate_split_transfer_proof_data( - transfer_balance, - &args.sender_elgamal_keypair, - &args.sender_aes_key, - &recipient_elgamal_pubkey, - auditor_elgamal_pubkey.as_ref(), - ) - .unwrap(); - - let transfer_amount_auditor_ciphertext_lo = - ciphertext_validity_proof_data_with_ciphertext.ciphertext_lo; - let transfer_amount_auditor_ciphertext_hi = - ciphertext_validity_proof_data_with_ciphertext.ciphertext_hi; - - // setup proofs - let create_range_proof_context_signer = &[&range_proof_context_state_account]; - let create_equality_proof_context_signer = &[&equality_proof_context_state_account]; - let create_ciphertext_validity_proof_context_signer = - &[&ciphertext_validity_proof_context_state_account]; - - let _ = try_join!( - token.confidential_transfer_create_context_state_account( - &range_proof_pubkey, - &context_state_authority_pubkey, - &range_proof_data, - true, - create_range_proof_context_signer - ), - token.confidential_transfer_create_context_state_account( - &equality_proof_pubkey, - &context_state_authority_pubkey, - &equality_proof_data, - false, - create_equality_proof_context_signer - ), - token.confidential_transfer_create_context_state_account( - &ciphertext_validity_proof_pubkey, - &context_state_authority_pubkey, - &ciphertext_validity_proof_data_with_ciphertext.proof_data, - false, - create_ciphertext_validity_proof_context_signer - ) - )?; - - // do the transfer - let equality_proof_context_proof_account = - ProofAccount::ContextAccount(equality_proof_pubkey); - let ciphertext_validity_proof_context_proof_account = - ProofAccount::ContextAccount(ciphertext_validity_proof_pubkey); - let range_proof_context_proof_account = - ProofAccount::ContextAccount(range_proof_pubkey); - - let ciphertext_validity_proof_account_with_ciphertext = ProofAccountWithCiphertext { - proof_account: ciphertext_validity_proof_context_proof_account, - ciphertext_lo: transfer_amount_auditor_ciphertext_lo, - ciphertext_hi: transfer_amount_auditor_ciphertext_hi, - }; - - let transfer_result = token - .confidential_transfer_transfer( - &sender, - &recipient_token_account, - &sender_owner, - Some(&equality_proof_context_proof_account), - Some(&ciphertext_validity_proof_account_with_ciphertext), - Some(&range_proof_context_proof_account), - transfer_balance, - Some(transfer_account_info), - &args.sender_elgamal_keypair, - &args.sender_aes_key, - &recipient_elgamal_pubkey, - auditor_elgamal_pubkey.as_ref(), - &bulk_signers, - ) - .await?; - - // close context state accounts - let close_context_state_signer = &[&context_state_authority]; - let _ = try_join!( - token.confidential_transfer_close_context_state_account( - &equality_proof_pubkey, - &sender, - &context_state_authority_pubkey, - close_context_state_signer - ), - token.confidential_transfer_close_context_state_account( - &ciphertext_validity_proof_pubkey, - &sender, - &context_state_authority_pubkey, - close_context_state_signer - ), - token.confidential_transfer_close_context_state_account( - &range_proof_pubkey, - &sender, - &context_state_authority_pubkey, - close_context_state_signer - ), - )?; - - transfer_result - } - (None, Some(_), Some(_)) => { - panic!("Confidential transfer with fee is not yet supported."); - } - (None, None, None) => { - token - .transfer( - &sender, - &recipient_token_account, - &sender_owner, - transfer_balance, - &bulk_signers, - ) - .await? - } - }; - - let tx_return = finish_tx(config, &res, no_wait).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_burn( - config: &Config<'_>, - account: Pubkey, - owner: Pubkey, - ui_amount: Amount, - mint_address: Option, - mint_decimals: Option, - use_unchecked_instruction: bool, - memo: Option, - bulk_signers: BulkSigners, -) -> CommandResult { - let mint_address = config.check_account(&account, mint_address).await?; - let mint_info = config.get_mint_info(&mint_address, mint_decimals).await?; - let decimals = if use_unchecked_instruction { - None - } else { - Some(mint_info.decimals) - }; - - let token = token_client_from_config(config, &mint_info.address, decimals)?; - - let amount = match ui_amount { - Amount::Raw(ui_amount) => ui_amount, - Amount::Decimal(ui_amount) => spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals), - Amount::All => { - if config.sign_only { - return Err("Use of ALL keyword to burn tokens requires online signing" - .to_string() - .into()); - } - token.get_account_info(&account).await?.base.amount - } - }; - - println_display( - config, - format!( - "Burn {} tokens\n Source: {}", - spl_token::amount_to_ui_amount(amount, mint_info.decimals), - account - ), - ); - - if let Some(text) = memo { - token.with_memo(text, vec![config.default_signer()?.pubkey()]); - } - - let res = token.burn(&account, &owner, amount, &bulk_signers).await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_mint( - config: &Config<'_>, - token: Pubkey, - ui_amount: Amount, - recipient: Pubkey, - mint_info: MintInfo, - mint_authority: Pubkey, - use_unchecked_instruction: bool, - memo: Option, - bulk_signers: BulkSigners, -) -> CommandResult { - let amount = amount_to_raw_amount(ui_amount, mint_info.decimals, None, "TOKEN_AMOUNT"); - - println_display( - config, - format!( - "Minting {} tokens\n Token: {}\n Recipient: {}", - spl_token::amount_to_ui_amount(amount, mint_info.decimals), - token, - recipient - ), - ); - - let decimals = if use_unchecked_instruction { - None - } else { - Some(mint_info.decimals) - }; - - let token = token_client_from_config(config, &mint_info.address, decimals)?; - if let Some(text) = memo { - token.with_memo(text, vec![config.default_signer()?.pubkey()]); - } - - let res = token - .mint_to(&recipient, &mint_authority, amount, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_freeze( - config: &Config<'_>, - account: Pubkey, - mint_address: Option, - freeze_authority: Pubkey, - bulk_signers: BulkSigners, -) -> CommandResult { - let mint_address = config.check_account(&account, mint_address).await?; - let mint_info = config.get_mint_info(&mint_address, None).await?; - - println_display( - config, - format!( - "Freezing account: {}\n Token: {}", - account, mint_info.address - ), - ); - - // we dont use the decimals from mint_info because its not need and in sign-only - // its wrong - let token = token_client_from_config(config, &mint_info.address, None)?; - let res = token - .freeze(&account, &freeze_authority, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_thaw( - config: &Config<'_>, - account: Pubkey, - mint_address: Option, - freeze_authority: Pubkey, - bulk_signers: BulkSigners, -) -> CommandResult { - let mint_address = config.check_account(&account, mint_address).await?; - let mint_info = config.get_mint_info(&mint_address, None).await?; - - println_display( - config, - format!( - "Thawing account: {}\n Token: {}", - account, mint_info.address - ), - ); - - // we dont use the decimals from mint_info because its not need and in sign-only - // its wrong - let token = token_client_from_config(config, &mint_info.address, None)?; - let res = token - .thaw(&account, &freeze_authority, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_wrap( - config: &Config<'_>, - amount: Amount, - wallet_address: Pubkey, - wrapped_sol_account: Option, - immutable_owner: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - let lamports = match amount { - Amount::Raw(amount) => amount, - Amount::Decimal(amount) => sol_to_lamports(amount), - Amount::All => { - return Err("ALL keyword not supported for SOL amount".into()); - } - }; - let token = native_token_client_from_config(config)?; - - let account = - wrapped_sol_account.unwrap_or_else(|| token.get_associated_token_address(&wallet_address)); - - println_display( - config, - format!( - "Wrapping {} SOL into {}", - lamports_to_sol(lamports), - account - ), - ); - - if !config.sign_only { - if let Some(account_data) = config.program_client.get_account(account).await? { - if account_data.owner != system_program::id() { - return Err(format!("Error: Account already exists: {}", account).into()); - } - } - - check_wallet_balance(config, &wallet_address, lamports).await?; - } - - let res = if immutable_owner { - if config.program_id == spl_token::id() { - return Err(format!( - "Specified --immutable, but token program {} does not support the extension", - config.program_id - ) - .into()); - } - - token - .wrap(&account, &wallet_address, lamports, &bulk_signers) - .await? - } else { - // this case is hit for a token22 ata, which is always immutable. but it does - // the right thing anyway - token - .wrap_with_mutable_ownership(&account, &wallet_address, lamports, &bulk_signers) - .await? - }; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_unwrap( - config: &Config<'_>, - wallet_address: Pubkey, - maybe_account: Option, - bulk_signers: BulkSigners, -) -> CommandResult { - let use_associated_account = maybe_account.is_none(); - let token = native_token_client_from_config(config)?; - - let account = - maybe_account.unwrap_or_else(|| token.get_associated_token_address(&wallet_address)); - - println_display(config, format!("Unwrapping {}", account)); - - if !config.sign_only { - let account_data = config.get_account_checked(&account).await?; - - if !use_associated_account { - let account_state = StateWithExtensionsOwned::::unpack(account_data.data)?; - - if account_state.base.mint != *token.get_address() { - return Err(format!("{} is not a native token account", account).into()); - } - } - - if account_data.lamports == 0 { - if use_associated_account { - return Err("No wrapped SOL in associated account; did you mean to specify an auxiliary address?".to_string().into()); - } else { - return Err(format!("No wrapped SOL in {}", account).into()); - } - } - - println_display( - config, - format!(" Amount: {} SOL", lamports_to_sol(account_data.lamports)), - ); - } - - println_display(config, format!(" Recipient: {}", &wallet_address)); - - let res = token - .close_account(&account, &wallet_address, &wallet_address, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_approve( - config: &Config<'_>, - account: Pubkey, - owner: Pubkey, - ui_amount: Amount, - delegate: Pubkey, - mint_address: Option, - mint_decimals: Option, - use_unchecked_instruction: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - let mint_address = config.check_account(&account, mint_address).await?; - let mint_info = config.get_mint_info(&mint_address, mint_decimals).await?; - let amount = amount_to_raw_amount(ui_amount, mint_info.decimals, None, "TOKEN_AMOUNT"); - let decimals = if use_unchecked_instruction { - None - } else { - Some(mint_info.decimals) - }; - - println_display( - config, - format!( - "Approve {} tokens\n Account: {}\n Delegate: {}", - spl_token::amount_to_ui_amount(amount, mint_info.decimals), - account, - delegate - ), - ); - - let token = token_client_from_config(config, &mint_info.address, decimals)?; - let res = token - .approve(&account, &delegate, &owner, amount, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_revoke( - config: &Config<'_>, - account: Pubkey, - owner: Pubkey, - delegate: Option, - bulk_signers: BulkSigners, -) -> CommandResult { - let (mint_pubkey, delegate) = if !config.sign_only { - let source_account = config.get_account_checked(&account).await?; - let source_state = StateWithExtensionsOwned::::unpack(source_account.data) - .map_err(|_| format!("Could not deserialize token account {}", account))?; - - let delegate = if let COption::Some(delegate) = source_state.base.delegate { - Some(delegate) - } else { - None - }; - - (source_state.base.mint, delegate) - } else { - // default is safe here because revoke doesn't use it - (Pubkey::default(), delegate) - }; - - if let Some(delegate) = delegate { - println_display( - config, - format!( - "Revoking approval\n Account: {}\n Delegate: {}", - account, delegate - ), - ); - } else { - return Err(format!("No delegate on account {}", account).into()); - } - - let token = token_client_from_config(config, &mint_pubkey, None)?; - let res = token.revoke(&account, &owner, &bulk_signers).await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_close( - config: &Config<'_>, - account: Pubkey, - close_authority: Pubkey, - recipient: Pubkey, - bulk_signers: BulkSigners, -) -> CommandResult { - let mut results = vec![]; - let token = if !config.sign_only { - let source_account = config.get_account_checked(&account).await?; - - let source_state = StateWithExtensionsOwned::::unpack(source_account.data) - .map_err(|_| format!("Could not deserialize token account {}", account))?; - let source_amount = source_state.base.amount; - - if !source_state.base.is_native() && source_amount > 0 { - return Err(format!( - "Account {} still has {} tokens; empty the account in order to close it.", - account, source_amount, - ) - .into()); - } - - let token = token_client_from_config(config, &source_state.base.mint, None)?; - if let Ok(extension) = source_state.get_extension::() { - if u64::from(extension.withheld_amount) != 0 { - let res = token.harvest_withheld_tokens_to_mint(&[&account]).await?; - let tx_return = finish_tx(config, &res, false).await?; - results.push(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }); - } - } - - token - } else { - // default is safe here because close doesn't use it - token_client_from_config(config, &Pubkey::default(), None)? - }; - - let res = token - .close_account(&account, &recipient, &close_authority, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - results.push(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }); - Ok(results.join("")) -} - -async fn command_close_mint( - config: &Config<'_>, - token_pubkey: Pubkey, - close_authority: Pubkey, - recipient: Pubkey, - bulk_signers: BulkSigners, -) -> CommandResult { - if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - let mint_supply = mint_state.base.supply; - - if mint_supply > 0 { - return Err(format!( - "Mint {} still has {} outstanding tokens; these must be burned before closing the mint.", - token_pubkey, mint_supply, - ) - .into()); - } - - if let Ok(mint_close_authority) = mint_state.get_extension::() { - let mint_close_authority_pubkey = - Option::::from(mint_close_authority.close_authority); - - if mint_close_authority_pubkey != Some(close_authority) { - return Err(format!( - "Mint {} has close authority {}, but {} was provided", - token_pubkey, - mint_close_authority_pubkey - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - close_authority - ) - .into()); - } - } else { - return Err(format!("Mint {} does not support close authority", token_pubkey).into()); - } - } - - let token = token_client_from_config(config, &token_pubkey, None)?; - let res = token - .close_account(&token_pubkey, &recipient, &close_authority, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_balance(config: &Config<'_>, address: Pubkey) -> CommandResult { - let balance = config - .rpc_client - .get_token_account_balance(&address) - .await - .map_err(|_| format!("Could not find token account {}", address))?; - let cli_token_amount = CliTokenAmount { amount: balance }; - Ok(config.output_format.formatted_string(&cli_token_amount)) -} - -async fn command_supply(config: &Config<'_>, token: Pubkey) -> CommandResult { - let supply = config.rpc_client.get_token_supply(&token).await?; - let cli_token_amount = CliTokenAmount { amount: supply }; - Ok(config.output_format.formatted_string(&cli_token_amount)) -} - -async fn command_accounts( - config: &Config<'_>, - maybe_token: Option, - owner: Pubkey, - account_filter: AccountFilter, - print_addresses_only: bool, -) -> CommandResult { - let filters = if let Some(token_pubkey) = maybe_token { - let _ = config.get_mint_info(&token_pubkey, None).await?; - vec![TokenAccountsFilter::Mint(token_pubkey)] - } else if config.restrict_to_program_id { - vec![TokenAccountsFilter::ProgramId(config.program_id)] - } else { - vec![ - TokenAccountsFilter::ProgramId(spl_token::id()), - TokenAccountsFilter::ProgramId(spl_token_2022::id()), - ] - }; - - let mut accounts = vec![]; - for filter in filters { - accounts.push( - config - .rpc_client - .get_token_accounts_by_owner(&owner, filter) - .await?, - ); - } - let accounts = accounts.into_iter().flatten().collect(); - - let cli_token_accounts = - sort_and_parse_token_accounts(&owner, accounts, maybe_token.is_some(), account_filter)?; - - if print_addresses_only { - Ok(cli_token_accounts - .accounts - .into_iter() - .flatten() - .map(|a| a.address) - .collect::>() - .join("\n")) - } else { - Ok(config.output_format.formatted_string(&cli_token_accounts)) - } -} - -async fn command_address( - config: &Config<'_>, - token: Option, - owner: Pubkey, -) -> CommandResult { - let mut cli_address = CliWalletAddress { - wallet_address: owner.to_string(), - ..CliWalletAddress::default() - }; - if let Some(token) = token { - config.get_mint_info(&token, None).await?; - let associated_token_address = - get_associated_token_address_with_program_id(&owner, &token, &config.program_id); - cli_address.associated_token_address = Some(associated_token_address.to_string()); - } - Ok(config.output_format.formatted_string(&cli_address)) -} - -async fn command_display(config: &Config<'_>, address: Pubkey) -> CommandResult { - let account_data = config.get_account_checked(&address).await?; - - let (additional_data, has_permanent_delegate) = - if let Some(mint_address) = get_token_account_mint(&account_data.data) { - let mint_account = config.get_account_checked(&mint_address).await?; - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", mint_address))?; - - let has_permanent_delegate = - if let Ok(permanent_delegate) = mint_state.get_extension::() { - Option::::from(permanent_delegate.delegate).is_some() - } else { - false - }; - let additional_data = SplTokenAdditionalData::with_decimals(mint_state.base.decimals); - - (Some(additional_data), has_permanent_delegate) - } else { - (None, false) - }; - - let token_data = parse_token_v2(&account_data.data, additional_data.as_ref()); - - match token_data { - Ok(TokenAccountType::Account(account)) => { - let mint_address = Pubkey::from_str(&account.mint)?; - let owner = Pubkey::from_str(&account.owner)?; - let associated_address = get_associated_token_address_with_program_id( - &owner, - &mint_address, - &config.program_id, - ); - - let cli_output = CliTokenAccount { - address: address.to_string(), - program_id: config.program_id.to_string(), - is_associated: associated_address == address, - account, - has_permanent_delegate, - }; - - Ok(config.output_format.formatted_string(&cli_output)) - } - Ok(TokenAccountType::Mint(mint)) => { - let epoch_info = config.rpc_client.get_epoch_info().await?; - let cli_output = CliMint { - address: address.to_string(), - epoch: epoch_info.epoch, - program_id: config.program_id.to_string(), - mint, - }; - - Ok(config.output_format.formatted_string(&cli_output)) - } - Ok(TokenAccountType::Multisig(multisig)) => { - let cli_output = CliMultisig { - address: address.to_string(), - program_id: config.program_id.to_string(), - multisig, - }; - - Ok(config.output_format.formatted_string(&cli_output)) - } - Err(e) => Err(e.into()), - } -} - -async fn command_gc( - config: &Config<'_>, - owner: Pubkey, - close_empty_associated_accounts: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - println_display( - config, - format!( - "Fetching token accounts associated with program {}", - config.program_id - ), - ); - let accounts = config - .rpc_client - .get_token_accounts_by_owner(&owner, TokenAccountsFilter::ProgramId(config.program_id)) - .await?; - if accounts.is_empty() { - println_display(config, "Nothing to do".to_string()); - return Ok("".to_string()); - } - - let mut accounts_by_token = HashMap::new(); - - for keyed_account in accounts { - if let UiAccountData::Json(parsed_account) = keyed_account.account.data { - if let Ok(TokenAccountType::Account(ui_token_account)) = - serde_json::from_value(parsed_account.parsed) - { - let frozen = ui_token_account.state == UiAccountState::Frozen; - let decimals = ui_token_account.token_amount.decimals; - - let token = ui_token_account - .mint - .parse::() - .unwrap_or_else(|err| panic!("Invalid mint: {}", err)); - let token_account = keyed_account - .pubkey - .parse::() - .unwrap_or_else(|err| panic!("Invalid token account: {}", err)); - let token_amount = ui_token_account - .token_amount - .amount - .parse::() - .unwrap_or_else(|err| panic!("Invalid token amount: {}", err)); - - let close_authority = ui_token_account.close_authority.map_or(owner, |s| { - s.parse::() - .unwrap_or_else(|err| panic!("Invalid close authority: {}", err)) - }); - - let entry = accounts_by_token - .entry((token, decimals)) - .or_insert_with(HashMap::new); - entry.insert(token_account, (token_amount, frozen, close_authority)); - } - } - } - - let mut results = vec![]; - for ((token_pubkey, decimals), accounts) in accounts_by_token.into_iter() { - println_display(config, format!("Processing token: {}", token_pubkey)); - - let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; - let total_balance: u64 = accounts.values().map(|account| account.0).sum(); - - let associated_token_account = token.get_associated_token_address(&owner); - if !accounts.contains_key(&associated_token_account) && total_balance > 0 { - token.create_associated_token_account(&owner).await?; - } - - for (address, (amount, frozen, close_authority)) in accounts { - let is_associated = address == associated_token_account; - - // only close the associated account if --close-empty-associated-accounts is - // provided - if is_associated && !close_empty_associated_accounts { - continue; - } - - // never close the associated account if *any* account carries a balance - if is_associated && total_balance > 0 { - continue; - } - - // dont attempt to close frozen accounts - if frozen { - continue; - } - - if is_associated { - println!("Closing associated account {}", address); - } - - // this logic is quite fiendish, but its more readable this way than if/else - let maybe_res = match (close_authority == owner, is_associated, amount == 0) { - // owner authority, associated or auxiliary, empty -> close - (true, _, true) => Some( - token - .close_account(&address, &owner, &owner, &bulk_signers) - .await, - ), - // owner authority, auxiliary, nonempty -> empty and close - (true, false, false) => Some( - token - .empty_and_close_account( - &address, - &owner, - &associated_token_account, - &owner, - &bulk_signers, - ) - .await, - ), - // separate authority, auxiliary, nonempty -> transfer - (false, false, false) => Some( - token - .transfer( - &address, - &associated_token_account, - &owner, - amount, - &bulk_signers, - ) - .await, - ), - // separate authority, associated or auxiliary, empty -> print warning - (false, _, true) => { - println_display( - config, - format!( - "Note: skipping {} due to separate close authority {}; \ - revoke authority and rerun gc, or rerun gc with --owner", - address, close_authority - ), - ); - None - } - // anything else, including a nonempty associated account -> unreachable - (_, _, _) => unreachable!(), - }; - - if let Some(res) = maybe_res { - let tx_return = finish_tx(config, &res?, false).await?; - - results.push(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }); - }; - } - } - - Ok(results.join("")) -} - -async fn command_sync_native(config: &Config<'_>, native_account_address: Pubkey) -> CommandResult { - let token = native_token_client_from_config(config)?; - - if !config.sign_only { - let account_data = config.get_account_checked(&native_account_address).await?; - let account_state = StateWithExtensionsOwned::::unpack(account_data.data)?; - - if account_state.base.mint != *token.get_address() { - return Err(format!("{} is not a native token account", native_account_address).into()); - } - } - - let res = token.sync_native(&native_account_address).await?; - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_withdraw_excess_lamports( - config: &Config<'_>, - source_account: Pubkey, - destination_account: Pubkey, - authority: Pubkey, - bulk_signers: Vec>, -) -> CommandResult { - // default is safe here because withdraw_excess_lamports doesn't use it - let token = token_client_from_config(config, &Pubkey::default(), None)?; - println_display( - config, - format!( - "Withdrawing excess lamports\n Sender: {}\n Destination: {}", - source_account, destination_account - ), - ); - - let res = token - .withdraw_excess_lamports( - &source_account, - &destination_account, - &authority, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -// both enables and disables required transfer memos, via enable_memos bool -async fn command_required_transfer_memos( - config: &Config<'_>, - token_account_address: Pubkey, - owner: Pubkey, - bulk_signers: BulkSigners, - enable_memos: bool, -) -> CommandResult { - if config.sign_only { - panic!("Config can not be sign-only for enabling/disabling required transfer memos."); - } - - let account = config.get_account_checked(&token_account_address).await?; - let current_account_len = account.data.len(); - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - // Reallocation (if needed) - let mut existing_extensions: Vec = state_with_extension.get_extension_types()?; - if existing_extensions.contains(&ExtensionType::MemoTransfer) { - let extension_state = state_with_extension - .get_extension::()? - .require_incoming_transfer_memos - .into(); - - if extension_state == enable_memos { - return Ok(format!( - "Required transfer memos were already {}", - if extension_state { - "enabled" - } else { - "disabled" - } - )); - } - } else { - existing_extensions.push(ExtensionType::MemoTransfer); - let needed_account_len = - ExtensionType::try_calculate_account_len::(&existing_extensions)?; - if needed_account_len > current_account_len { - token - .reallocate( - &token_account_address, - &owner, - &[ExtensionType::MemoTransfer], - &bulk_signers, - ) - .await?; - } - } - - let res = if enable_memos { - token - .enable_required_transfer_memos(&token_account_address, &owner, &bulk_signers) - .await - } else { - token - .disable_required_transfer_memos(&token_account_address, &owner, &bulk_signers) - .await - }?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -// both enables and disables cpi guard, via enable_guard bool -async fn command_cpi_guard( - config: &Config<'_>, - token_account_address: Pubkey, - owner: Pubkey, - bulk_signers: BulkSigners, - enable_guard: bool, -) -> CommandResult { - if config.sign_only { - panic!("Config can not be sign-only for enabling/disabling required transfer memos."); - } - - let account = config.get_account_checked(&token_account_address).await?; - let current_account_len = account.data.len(); - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - // reallocation (if needed) - let mut existing_extensions: Vec = state_with_extension.get_extension_types()?; - if existing_extensions.contains(&ExtensionType::CpiGuard) { - let extension_state = state_with_extension - .get_extension::()? - .lock_cpi - .into(); - - if extension_state == enable_guard { - return Ok(format!( - "CPI Guard was already {}", - if extension_state { - "enabled" - } else { - "disabled" - } - )); - } - } else { - existing_extensions.push(ExtensionType::CpiGuard); - let required_account_len = - ExtensionType::try_calculate_account_len::(&existing_extensions)?; - if required_account_len > current_account_len { - token - .reallocate( - &token_account_address, - &owner, - &[ExtensionType::CpiGuard], - &bulk_signers, - ) - .await?; - } - } - - let res = if enable_guard { - token - .enable_cpi_guard(&token_account_address, &owner, &bulk_signers) - .await - } else { - token - .disable_cpi_guard(&token_account_address, &owner, &bulk_signers) - .await - }?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_update_pointer_address( - config: &Config<'_>, - token_pubkey: Pubkey, - authority: Pubkey, - new_address: Option, - bulk_signers: BulkSigners, - pointer: Pointer, -) -> CommandResult { - if config.sign_only { - panic!( - "Config can not be sign-only for updating {} pointer address.", - pointer - ); - } - - let token = token_client_from_config(config, &token_pubkey, None)?; - let res = match pointer { - Pointer::Metadata => { - token - .update_metadata_address(&authority, new_address, &bulk_signers) - .await - } - Pointer::Group => { - token - .update_group_address(&authority, new_address, &bulk_signers) - .await - } - Pointer::GroupMember => { - token - .update_group_member_address(&authority, new_address, &bulk_signers) - .await - } - }?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_update_default_account_state( - config: &Config<'_>, - token_pubkey: Pubkey, - freeze_authority: Pubkey, - new_default_state: AccountState, - bulk_signers: BulkSigners, -) -> CommandResult { - if !config.sign_only { - let mint_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = StateWithExtensionsOwned::::unpack(mint_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - match mint_state.base.freeze_authority { - COption::None => { - return Err(format!("Mint {} has no freeze authority.", token_pubkey).into()) - } - COption::Some(mint_freeze_authority) => { - if mint_freeze_authority != freeze_authority { - return Err(format!( - "Mint {} has a freeze authority {}, {} provided", - token_pubkey, mint_freeze_authority, freeze_authority - ) - .into()); - } - } - } - - if let Ok(default_account_state) = mint_state.get_extension::() { - if default_account_state.state == u8::from(new_default_state) { - let state_string = match new_default_state { - AccountState::Frozen => "frozen", - AccountState::Initialized => "initialized", - _ => unreachable!(), - }; - return Err(format!( - "Mint {} already has default account state {}", - token_pubkey, state_string - ) - .into()); - } - } else { - return Err(format!( - "Mint {} does not support default account states", - token_pubkey - ) - .into()); - } - } - - let token = token_client_from_config(config, &token_pubkey, None)?; - let res = token - .set_default_account_state(&freeze_authority, &new_default_state, &bulk_signers) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_withdraw_withheld_tokens( - config: &Config<'_>, - destination_token_account: Pubkey, - source_token_accounts: Vec, - authority: Pubkey, - include_mint: bool, - bulk_signers: BulkSigners, -) -> CommandResult { - if config.sign_only { - panic!("Config can not be sign-only for withdrawing withheld tokens."); - } - let destination_account = config - .get_account_checked(&destination_token_account) - .await?; - let destination_state = StateWithExtensionsOwned::::unpack(destination_account.data) - .map_err(|_| { - format!( - "Could not deserialize token account {}", - destination_token_account - ) - })?; - let token_pubkey = destination_state.base.mint; - destination_state - .get_extension::() - .map_err(|_| format!("Token mint {} has no transfer fee configured", token_pubkey))?; - - let token = token_client_from_config(config, &token_pubkey, None)?; - let mut results = vec![]; - if include_mint { - let res = token - .withdraw_withheld_tokens_from_mint( - &destination_token_account, - &authority, - &bulk_signers, - ) - .await; - let tx_return = finish_tx(config, &res?, false).await?; - results.push(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }); - } - - let source_refs = source_token_accounts.iter().collect::>(); - // this can be tweaked better, but keep it simple for now - const MAX_WITHDRAWAL_ACCOUNTS: usize = 25; - for sources in source_refs.chunks(MAX_WITHDRAWAL_ACCOUNTS) { - let res = token - .withdraw_withheld_tokens_from_accounts( - &destination_token_account, - &authority, - sources, - &bulk_signers, - ) - .await; - let tx_return = finish_tx(config, &res?, false).await?; - results.push(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }); - } - - Ok(results.join("")) -} - -async fn command_update_confidential_transfer_settings( - config: &Config<'_>, - token_pubkey: Pubkey, - authority: Pubkey, - auto_approve: Option, - auditor_pubkey: Option, - bulk_signers: Vec>, -) -> CommandResult { - let (new_auto_approve, new_auditor_pubkey) = if !config.sign_only { - let confidential_transfer_account = config.get_account_checked(&token_pubkey).await?; - - let mint_state = - StateWithExtensionsOwned::::unpack(confidential_transfer_account.data) - .map_err(|_| format!("Could not deserialize token mint {}", token_pubkey))?; - - if let Ok(confidential_transfer_mint) = - mint_state.get_extension::() - { - let expected_authority = Option::::from(confidential_transfer_mint.authority); - - if expected_authority != Some(authority) { - return Err(format!( - "Mint {} has confidential transfer authority {}, but {} was provided", - token_pubkey, - expected_authority - .map(|pubkey| pubkey.to_string()) - .unwrap_or_else(|| "disabled".to_string()), - authority - ) - .into()); - } - - let new_auto_approve = if let Some(auto_approve) = auto_approve { - auto_approve - } else { - bool::from(confidential_transfer_mint.auto_approve_new_accounts) - }; - - let new_auditor_pubkey = if let Some(auditor_pubkey) = auditor_pubkey { - auditor_pubkey.into() - } else { - Option::::from(confidential_transfer_mint.auditor_elgamal_pubkey) - }; - - (new_auto_approve, new_auditor_pubkey) - } else { - return Err(format!( - "Mint {} does not support confidential transfers", - token_pubkey - ) - .into()); - } - } else { - let new_auto_approve = auto_approve.expect("The approve policy must be provided"); - let new_auditor_pubkey = auditor_pubkey - .expect("The auditor encryption pubkey must be provided") - .into(); - - (new_auto_approve, new_auditor_pubkey) - }; - - println_display( - config, - format!( - "Updating confidential transfer settings for {}:", - token_pubkey, - ), - ); - - if auto_approve.is_some() { - println_display( - config, - format!( - " approve policy set to {}", - if new_auto_approve { "auto" } else { "manual" } - ), - ); - } - - if auditor_pubkey.is_some() { - if let Some(new_auditor_pubkey) = new_auditor_pubkey { - println_display( - config, - format!(" auditor encryption pubkey set to {}", new_auditor_pubkey,), - ); - } else { - println_display(config, " auditability disabled".to_string()) - } - } - - let token = token_client_from_config(config, &token_pubkey, None)?; - let res = token - .confidential_transfer_update_mint( - &authority, - new_auto_approve, - new_auditor_pubkey, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_configure_confidential_transfer_account( - config: &Config<'_>, - maybe_token: Option, - owner: Pubkey, - maybe_account: Option, - maximum_credit_counter: Option, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - bulk_signers: BulkSigners, -) -> CommandResult { - if config.sign_only { - panic!("Sign-only is not yet supported."); - } - - let token_account_address = if let Some(account) = maybe_account { - account - } else { - let token_pubkey = - maybe_token.expect("Either a valid token or account address must be provided"); - let token = token_client_from_config(config, &token_pubkey, None)?; - token.get_associated_token_address(&owner) - }; - - let account = config.get_account_checked(&token_account_address).await?; - let current_account_len = account.data.len(); - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - // Reallocation (if needed) - let mut existing_extensions: Vec = state_with_extension.get_extension_types()?; - if !existing_extensions.contains(&ExtensionType::ConfidentialTransferAccount) { - let mut extra_extensions = vec![ExtensionType::ConfidentialTransferAccount]; - if existing_extensions.contains(&ExtensionType::TransferFeeAmount) { - extra_extensions.push(ExtensionType::ConfidentialTransferFeeAmount); - } - existing_extensions.extend_from_slice(&extra_extensions); - let needed_account_len = - ExtensionType::try_calculate_account_len::(&existing_extensions)?; - if needed_account_len > current_account_len { - token - .reallocate( - &token_account_address, - &owner, - &extra_extensions, - &bulk_signers, - ) - .await?; - } - } - - let res = token - .confidential_transfer_configure_token_account( - &token_account_address, - &owner, - None, - maximum_credit_counter, - elgamal_keypair, - aes_key, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -async fn command_enable_disable_confidential_transfers( - config: &Config<'_>, - maybe_token: Option, - owner: Pubkey, - maybe_account: Option, - bulk_signers: BulkSigners, - allow_confidential_credits: Option, - allow_non_confidential_credits: Option, -) -> CommandResult { - if config.sign_only { - panic!("Sign-only is not yet supported."); - } - - let token_account_address = if let Some(account) = maybe_account { - account - } else { - let token_pubkey = - maybe_token.expect("Either a valid token or account address must be provided"); - let token = token_client_from_config(config, &token_pubkey, None)?; - token.get_associated_token_address(&owner) - }; - - let account = config.get_account_checked(&token_account_address).await?; - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - let existing_extensions: Vec = state_with_extension.get_extension_types()?; - if !existing_extensions.contains(&ExtensionType::ConfidentialTransferAccount) { - panic!( - "Confidential transfer is not yet configured for this account. \ - Use `configure-confidential-transfer-account` command instead." - ); - } - - let res = if let Some(allow_confidential_credits) = allow_confidential_credits { - let extension_state = state_with_extension - .get_extension::()? - .allow_confidential_credits - .into(); - - if extension_state == allow_confidential_credits { - return Ok(format!( - "Confidential transfers are already {}", - if extension_state { - "enabled" - } else { - "disabled" - } - )); - } - - if allow_confidential_credits { - token - .confidential_transfer_enable_confidential_credits( - &token_account_address, - &owner, - &bulk_signers, - ) - .await - } else { - token - .confidential_transfer_disable_confidential_credits( - &token_account_address, - &owner, - &bulk_signers, - ) - .await - } - } else { - let allow_non_confidential_credits = - allow_non_confidential_credits.expect("Nothing to be done"); - let extension_state = state_with_extension - .get_extension::()? - .allow_non_confidential_credits - .into(); - - if extension_state == allow_non_confidential_credits { - return Ok(format!( - "Non-confidential transfers are already {}", - if extension_state { - "enabled" - } else { - "disabled" - } - )); - } - - if allow_non_confidential_credits { - token - .confidential_transfer_enable_non_confidential_credits( - &token_account_address, - &owner, - &bulk_signers, - ) - .await - } else { - token - .confidential_transfer_disable_non_confidential_credits( - &token_account_address, - &owner, - &bulk_signers, - ) - .await - } - }?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} -#[derive(PartialEq, Eq)] -enum ConfidentialInstructionType { - Deposit, - Withdraw, -} - -#[allow(clippy::too_many_arguments)] -async fn command_deposit_withdraw_confidential_tokens( - config: &Config<'_>, - token_pubkey: Pubkey, - owner: Pubkey, - maybe_account: Option, - bulk_signers: BulkSigners, - ui_amount: Amount, - mint_decimals: Option, - instruction_type: ConfidentialInstructionType, - elgamal_keypair: Option<&ElGamalKeypair>, - aes_key: Option<&AeKey>, -) -> CommandResult { - if config.sign_only { - panic!("Sign-only is not yet supported."); - } - - // check if mint decimals provided is consistent - let mint_info = config.get_mint_info(&token_pubkey, mint_decimals).await?; - - if !config.sign_only && mint_decimals.is_some() && mint_decimals != Some(mint_info.decimals) { - return Err(format!( - "Decimals {} was provided, but actual value is {}", - mint_decimals.unwrap(), - mint_info.decimals - ) - .into()); - } - - let decimals = if let Some(decimals) = mint_decimals { - decimals - } else { - mint_info.decimals - }; - - // derive ATA if account address not provided - let token_account_address = if let Some(account) = maybe_account { - account - } else { - let token = token_client_from_config(config, &token_pubkey, Some(decimals))?; - token.get_associated_token_address(&owner) - }; - - let account = config.get_account_checked(&token_account_address).await?; - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - // the amount the user wants to deposit or withdraw, as a u64 - let amount = match ui_amount { - Amount::Raw(ui_amount) => ui_amount, - Amount::Decimal(ui_amount) => spl_token::ui_amount_to_amount(ui_amount, mint_info.decimals), - Amount::All => { - if config.sign_only { - return Err("Use of ALL keyword to burn tokens requires online signing" - .to_string() - .into()); - } - if instruction_type == ConfidentialInstructionType::Withdraw { - return Err("ALL keyword is not currently supported for withdraw" - .to_string() - .into()); - } - state_with_extension.base.amount - } - }; - - match instruction_type { - ConfidentialInstructionType::Deposit => { - println_display( - config, - format!( - "Depositing {} confidential tokens", - spl_token::amount_to_ui_amount(amount, mint_info.decimals), - ), - ); - let current_balance = state_with_extension.base.amount; - if amount > current_balance { - return Err(format!( - "Error: Insufficient funds, current balance is {}", - spl_token_2022::amount_to_ui_amount_string_trimmed( - current_balance, - mint_info.decimals - ) - ) - .into()); - } - } - ConfidentialInstructionType::Withdraw => { - println_display( - config, - format!( - "Withdrawing {} confidential tokens", - spl_token::amount_to_ui_amount(amount, mint_info.decimals) - ), - ); - } - } - - let res = match instruction_type { - ConfidentialInstructionType::Deposit => { - token - .confidential_transfer_deposit( - &token_account_address, - &owner, - amount, - decimals, - &bulk_signers, - ) - .await? - } - ConfidentialInstructionType::Withdraw => { - let elgamal_keypair = elgamal_keypair.expect("ElGamal keypair must be provided"); - let aes_key = aes_key.expect("AES key must be provided"); - - let extension_state = - state_with_extension.get_extension::()?; - let withdraw_account_info = WithdrawAccountInfo::new(extension_state); - - let context_state_authority = config.fee_payer()?; - let equality_proof_context_state_keypair = Keypair::new(); - let equality_proof_context_state_pubkey = equality_proof_context_state_keypair.pubkey(); - let range_proof_context_state_keypair = Keypair::new(); - let range_proof_context_state_pubkey = range_proof_context_state_keypair.pubkey(); - - let WithdrawProofData { - equality_proof_data, - range_proof_data, - } = withdraw_account_info.generate_proof_data(amount, elgamal_keypair, aes_key)?; - - // set up context state accounts - let context_state_authority_pubkey = context_state_authority.pubkey(); - let create_equality_proof_signer = &[&equality_proof_context_state_keypair]; - let create_range_proof_signer = &[&range_proof_context_state_keypair]; - - let _ = try_join!( - token.confidential_transfer_create_context_state_account( - &equality_proof_context_state_pubkey, - &context_state_authority_pubkey, - &equality_proof_data, - false, - create_equality_proof_signer - ), - token.confidential_transfer_create_context_state_account( - &range_proof_context_state_pubkey, - &context_state_authority_pubkey, - &range_proof_data, - true, - create_range_proof_signer, - ) - )?; - - // do the withdrawal - let withdraw_result = token - .confidential_transfer_withdraw( - &token_account_address, - &owner, - Some(&ProofAccount::ContextAccount( - equality_proof_context_state_pubkey, - )), - Some(&ProofAccount::ContextAccount( - range_proof_context_state_pubkey, - )), - amount, - decimals, - Some(withdraw_account_info), - elgamal_keypair, - aes_key, - &bulk_signers, - ) - .await?; - - // close context state account - let close_context_state_signer = &[&context_state_authority]; - let _ = try_join!( - token.confidential_transfer_close_context_state_account( - &equality_proof_context_state_pubkey, - &token_account_address, - &context_state_authority_pubkey, - close_context_state_signer - ), - token.confidential_transfer_close_context_state_account( - &range_proof_context_state_pubkey, - &token_account_address, - &context_state_authority_pubkey, - close_context_state_signer - ) - )?; - - withdraw_result - } - }; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -#[allow(clippy::too_many_arguments)] -async fn command_apply_pending_balance( - config: &Config<'_>, - maybe_token: Option, - owner: Pubkey, - maybe_account: Option, - bulk_signers: BulkSigners, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, -) -> CommandResult { - if config.sign_only { - panic!("Sign-only is not yet supported."); - } - - // derive ATA if account address not provided - let token_account_address = if let Some(account) = maybe_account { - account - } else { - let token_pubkey = - maybe_token.expect("Either a valid token or account address must be provided"); - let token = token_client_from_config(config, &token_pubkey, None)?; - token.get_associated_token_address(&owner) - }; - - let account = config.get_account_checked(&token_account_address).await?; - - let state_with_extension = StateWithExtensionsOwned::::unpack(account.data)?; - let token = token_client_from_config(config, &state_with_extension.base.mint, None)?; - - let extension_state = state_with_extension.get_extension::()?; - let account_info = ApplyPendingBalanceAccountInfo::new(extension_state); - - let res = token - .confidential_transfer_apply_pending_balance( - &token_account_address, - &owner, - Some(account_info), - elgamal_keypair.secret(), - aes_key, - &bulk_signers, - ) - .await?; - - let tx_return = finish_tx(config, &res, false).await?; - Ok(match tx_return { - TransactionReturnData::CliSignature(signature) => { - config.output_format.formatted_string(&signature) - } - TransactionReturnData::CliSignOnlyData(sign_only_data) => { - config.output_format.formatted_string(&sign_only_data) - } - }) -} - -struct ConfidentialTransferArgs { - sender_elgamal_keypair: ElGamalKeypair, - sender_aes_key: AeKey, - recipient_elgamal_pubkey: Option, - auditor_elgamal_pubkey: Option, -} - -pub async fn process_command<'a>( - sub_command: &CommandName, - sub_matches: &ArgMatches, - config: &Config<'a>, - mut wallet_manager: Option>, - mut bulk_signers: Vec>, -) -> CommandResult { - match (sub_command, sub_matches) { - (CommandName::Bench, arg_matches) => { - bench_process_command( - arg_matches, - config, - std::mem::take(&mut bulk_signers), - &mut wallet_manager, - ) - .await - } - (CommandName::CreateToken, arg_matches) => { - let decimals = *arg_matches.get_one::("decimals").unwrap(); - let mint_authority = - config.pubkey_or_default(arg_matches, "mint_authority", &mut wallet_manager)?; - let memo = value_t!(arg_matches, "memo", String).ok(); - let rate_bps = value_t!(arg_matches, "interest_rate", i16).ok(); - let metadata_address = value_t!(arg_matches, "metadata_address", Pubkey).ok(); - let group_address = value_t!(arg_matches, "group_address", Pubkey).ok(); - let member_address = value_t!(arg_matches, "member_address", Pubkey).ok(); - - let transfer_fee = arg_matches.values_of("transfer_fee").map(|mut v| { - println_display(config,"transfer-fee has been deprecated and will be removed in a future release. Please specify --transfer-fee-basis-points and --transfer-fee-maximum-fee with a UI amount".to_string()); - ( - v.next() - .unwrap() - .parse::() - .unwrap_or_else(print_error_and_exit), - v.next() - .unwrap() - .parse::() - .unwrap_or_else(print_error_and_exit), - ) - }); - - let transfer_fee_basis_point = arg_matches.get_one::("transfer_fee_basis_points"); - let transfer_fee_maximum_fee = arg_matches - .get_one::("transfer_fee_maximum_fee") - .map(|v| amount_to_raw_amount(*v, decimals, None, "MAXIMUM_FEE")); - let transfer_fee = transfer_fee_basis_point - .map(|v| (*v, transfer_fee_maximum_fee.unwrap())) - .or(transfer_fee); - - let (token_signer, token) = - get_signer(arg_matches, "token_keypair", &mut wallet_manager) - .unwrap_or_else(new_throwaway_signer); - push_signer_with_dedup(token_signer, &mut bulk_signers); - let default_account_state = - arg_matches - .value_of("default_account_state") - .map(|s| match s { - "initialized" => AccountState::Initialized, - "frozen" => AccountState::Frozen, - _ => unreachable!(), - }); - let transfer_hook_program_id = - pubkey_of_signer(arg_matches, "transfer_hook", &mut wallet_manager).unwrap(); - - let confidential_transfer_auto_approve = arg_matches - .value_of("enable_confidential_transfers") - .map(|b| b == "auto"); - - command_create_token( - config, - decimals, - token, - mint_authority, - arg_matches.is_present("enable_freeze"), - arg_matches.is_present("enable_close"), - arg_matches.is_present("enable_non_transferable"), - arg_matches.is_present("enable_permanent_delegate"), - memo, - metadata_address, - group_address, - member_address, - rate_bps, - default_account_state, - transfer_fee, - confidential_transfer_auto_approve, - transfer_hook_program_id, - arg_matches.is_present("enable_metadata"), - arg_matches.is_present("enable_group"), - arg_matches.is_present("enable_member"), - bulk_signers, - ) - .await - } - (CommandName::SetInterestRate, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let rate_bps = value_t_or_exit!(arg_matches, "rate", i16); - let (rate_authority_signer, rate_authority_pubkey) = - config.signer_or_default(arg_matches, "rate_authority", &mut wallet_manager); - let bulk_signers = vec![rate_authority_signer]; - - command_set_interest_rate( - config, - token_pubkey, - rate_authority_pubkey, - rate_bps, - bulk_signers, - ) - .await - } - (CommandName::SetTransferHook, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let new_program_id = - pubkey_of_signer(arg_matches, "new_program_id", &mut wallet_manager).unwrap(); - let (authority_signer, authority_pubkey) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - let bulk_signers = vec![authority_signer]; - - command_set_transfer_hook_program( - config, - token_pubkey, - authority_pubkey, - new_program_id, - bulk_signers, - ) - .await - } - (CommandName::InitializeMetadata, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let name = arg_matches.value_of("name").unwrap().to_string(); - let symbol = arg_matches.value_of("symbol").unwrap().to_string(); - let uri = arg_matches.value_of("uri").unwrap().to_string(); - let (mint_authority_signer, mint_authority) = - config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager); - let bulk_signers = vec![mint_authority_signer]; - let update_authority = - config.pubkey_or_default(arg_matches, "update_authority", &mut wallet_manager)?; - - command_initialize_metadata( - config, - token_pubkey, - update_authority, - mint_authority, - name, - symbol, - uri, - bulk_signers, - ) - .await - } - (CommandName::UpdateMetadata, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let (authority_signer, authority) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - let field = arg_matches.value_of("field").unwrap(); - let field = match field.to_lowercase().as_str() { - "name" => Field::Name, - "symbol" => Field::Symbol, - "uri" => Field::Uri, - _ => Field::Key(field.to_string()), - }; - let value = arg_matches.value_of("value").map(|v| v.to_string()); - let transfer_lamports = arg_matches - .get_one::(TRANSFER_LAMPORTS_ARG.name) - .copied(); - let bulk_signers = vec![authority_signer]; - - command_update_metadata( - config, - token_pubkey, - authority, - field, - value, - transfer_lamports, - bulk_signers, - ) - .await - } - (CommandName::InitializeGroup, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let max_size = *arg_matches.get_one::("max_size").unwrap(); - let (mint_authority_signer, mint_authority) = - config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager); - let update_authority = - config.pubkey_or_default(arg_matches, "update_authority", &mut wallet_manager)?; - let bulk_signers = vec![mint_authority_signer]; - - command_initialize_group( - config, - token_pubkey, - mint_authority, - update_authority, - max_size, - bulk_signers, - ) - .await - } - (CommandName::UpdateGroupMaxSize, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let new_max_size = *arg_matches.get_one::("new_max_size").unwrap(); - let (update_authority_signer, update_authority) = - config.signer_or_default(arg_matches, "update_authority", &mut wallet_manager); - let bulk_signers = vec![update_authority_signer]; - - command_update_group_max_size( - config, - token_pubkey, - update_authority, - new_max_size, - bulk_signers, - ) - .await - } - (CommandName::InitializeMember, arg_matches) => { - let member_token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let group_token_pubkey = - pubkey_of_signer(arg_matches, "group_token", &mut wallet_manager) - .unwrap() - .unwrap(); - let (mint_authority_signer, mint_authority) = - config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager); - let (group_update_authority_signer, group_update_authority) = config.signer_or_default( - arg_matches, - "group_update_authority", - &mut wallet_manager, - ); - let mut bulk_signers = vec![mint_authority_signer]; - push_signer_with_dedup(group_update_authority_signer, &mut bulk_signers); - - command_initialize_member( - config, - member_token_pubkey, - mint_authority, - group_token_pubkey, - group_update_authority, - bulk_signers, - ) - .await - } - (CommandName::CreateAccount, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - - // No need to add a signer when creating an associated token account - let account = get_signer(arg_matches, "account_keypair", &mut wallet_manager).map( - |(signer, account)| { - push_signer_with_dedup(signer, &mut bulk_signers); - account - }, - ); - - let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager)?; - command_create_account( - config, - token, - owner, - account, - arg_matches.is_present("immutable"), - bulk_signers, - ) - .await - } - (CommandName::CreateMultisig, arg_matches) => { - let minimum_signers = arg_matches - .get_one("minimum_signers") - .map(|v: &String| v.parse::().unwrap()) - .unwrap(); - let multisig_members = - pubkeys_of_multiple_signers(arg_matches, "multisig_member", &mut wallet_manager) - .unwrap_or_else(print_error_and_exit) - .unwrap(); - if minimum_signers as usize > multisig_members.len() { - eprintln!( - "error: MINIMUM_SIGNERS cannot be greater than the number \ - of MULTISIG_MEMBERs passed" - ); - exit(1); - } - - let (signer, _) = get_signer(arg_matches, "address_keypair", &mut wallet_manager) - .unwrap_or_else(new_throwaway_signer); - - command_create_multisig(config, signer, minimum_signers, multisig_members).await - } - (CommandName::Authorize, arg_matches) => { - let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) - .unwrap() - .unwrap(); - let authority_type = arg_matches.value_of("authority_type").unwrap(); - let authority_type = CliAuthorityType::from_str(authority_type)?; - - let (authority_signer, authority) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(authority_signer, &mut bulk_signers); - } - - let new_authority = - pubkey_of_signer(arg_matches, "new_authority", &mut wallet_manager).unwrap(); - let force_authorize = arg_matches.is_present("force"); - command_authorize( - config, - address, - authority_type, - authority, - new_authority, - force_authorize, - bulk_signers, - ) - .await - } - (CommandName::Transfer, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let amount = *arg_matches.get_one::("amount").unwrap(); - let recipient = pubkey_of_signer(arg_matches, "recipient", &mut wallet_manager) - .unwrap() - .unwrap(); - let sender = pubkey_of_signer(arg_matches, "from", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - - let confidential_transfer_args = if arg_matches.is_present("confidential") { - // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be - // supported in the future once upgrading to clap-v3. - // - // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be - // updated once custom ElGamal and AES keys are supported. - let sender_elgamal_keypair = - ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); - let sender_aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); - - // Sign-only mode is not yet supported for confidential transfers, so set - // recipient and auditor ElGamal public to `None` by default. - Some(ConfidentialTransferArgs { - sender_elgamal_keypair, - sender_aes_key, - recipient_elgamal_pubkey: None, - auditor_elgamal_pubkey: None, - }) - } else { - None - }; - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let mint_decimals = arg_matches.get_one::(MINT_DECIMALS_ARG.name).copied(); - let fund_recipient = arg_matches.is_present("fund_recipient"); - let allow_unfunded_recipient = arg_matches.is_present("allow_empty_recipient") - || arg_matches.is_present("allow_unfunded_recipient"); - - let recipient_is_ata_owner = arg_matches.is_present("recipient_is_ata_owner"); - let no_recipient_is_ata_owner = - arg_matches.is_present("no_recipient_is_ata_owner") || !recipient_is_ata_owner; - if recipient_is_ata_owner { - println_display(config, "recipient-is-ata-owner is now the default behavior. The option has been deprecated and will be removed in a future release.".to_string()); - } - let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); - let expected_fee = arg_matches.get_one::("expected_fee").copied(); - let memo = value_t!(arg_matches, "memo", String).ok(); - let transfer_hook_accounts = arg_matches.values_of("transfer_hook_account").map(|v| { - v.into_iter() - .map(|s| parse_transfer_hook_account(s).unwrap()) - .collect::>() - }); - - command_transfer( - config, - token, - amount, - recipient, - sender, - owner, - allow_unfunded_recipient, - fund_recipient, - mint_decimals, - no_recipient_is_ata_owner, - use_unchecked_instruction, - expected_fee, - memo, - bulk_signers, - arg_matches.is_present("no_wait"), - arg_matches.is_present("allow_non_system_account_recipient"), - transfer_hook_accounts, - confidential_transfer_args.as_ref(), - ) - .await - } - (CommandName::Burn, arg_matches) => { - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let amount = *arg_matches.get_one::("amount").unwrap(); - let mint_address = - pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); - let mint_decimals = arg_matches - .get_one(MINT_DECIMALS_ARG.name) - .map(|v: &String| v.parse::().unwrap()); - let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); - let memo = value_t!(arg_matches, "memo", String).ok(); - command_burn( - config, - account, - owner, - amount, - mint_address, - mint_decimals, - use_unchecked_instruction, - memo, - bulk_signers, - ) - .await - } - (CommandName::Mint, arg_matches) => { - let (mint_authority_signer, mint_authority) = - config.signer_or_default(arg_matches, "mint_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(mint_authority_signer, &mut bulk_signers); - } - - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let amount = *arg_matches.get_one::("amount").unwrap(); - let mint_decimals = arg_matches.get_one::(MINT_DECIMALS_ARG.name).copied(); - let mint_info = config.get_mint_info(&token, mint_decimals).await?; - let recipient = if let Some(address) = - pubkey_of_signer(arg_matches, "recipient", &mut wallet_manager).unwrap() - { - address - } else if let Some(address) = - pubkey_of_signer(arg_matches, "recipient_owner", &mut wallet_manager).unwrap() - { - get_associated_token_address_with_program_id(&address, &token, &config.program_id) - } else { - let owner = config.default_signer()?.pubkey(); - config.associated_token_address_for_token_and_program( - &mint_info.address, - &owner, - &mint_info.program_id, - )? - }; - config.check_account(&recipient, Some(token)).await?; - let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); - let memo = value_t!(arg_matches, "memo", String).ok(); - command_mint( - config, - token, - amount, - recipient, - mint_info, - mint_authority, - use_unchecked_instruction, - memo, - bulk_signers, - ) - .await - } - (CommandName::Freeze, arg_matches) => { - let (freeze_authority_signer, freeze_authority) = - config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(freeze_authority_signer, &mut bulk_signers); - } - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let mint_address = - pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); - command_freeze( - config, - account, - mint_address, - freeze_authority, - bulk_signers, - ) - .await - } - (CommandName::Thaw, arg_matches) => { - let (freeze_authority_signer, freeze_authority) = - config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(freeze_authority_signer, &mut bulk_signers); - } - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let mint_address = - pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); - command_thaw( - config, - account, - mint_address, - freeze_authority, - bulk_signers, - ) - .await - } - (CommandName::Wrap, arg_matches) => { - let amount = *arg_matches.get_one::("amount").unwrap(); - let account = if arg_matches.is_present("create_aux_account") { - let (signer, account) = new_throwaway_signer(); - bulk_signers.push(signer); - Some(account) - } else { - // No need to add a signer when creating an associated token account - None - }; - - let (wallet_signer, wallet_address) = - config.signer_or_default(arg_matches, "wallet_keypair", &mut wallet_manager); - push_signer_with_dedup(wallet_signer, &mut bulk_signers); - - command_wrap( - config, - amount, - wallet_address, - account, - arg_matches.is_present("immutable"), - bulk_signers, - ) - .await - } - (CommandName::Unwrap, arg_matches) => { - let (wallet_signer, wallet_address) = - config.signer_or_default(arg_matches, "wallet_keypair", &mut wallet_manager); - push_signer_with_dedup(wallet_signer, &mut bulk_signers); - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager).unwrap(); - command_unwrap(config, wallet_address, account, bulk_signers).await - } - (CommandName::Approve, arg_matches) => { - let (owner_signer, owner_address) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let amount = *arg_matches.get_one::("amount").unwrap(); - let delegate = pubkey_of_signer(arg_matches, "delegate", &mut wallet_manager) - .unwrap() - .unwrap(); - let mint_address = - pubkey_of_signer(arg_matches, MINT_ADDRESS_ARG.name, &mut wallet_manager).unwrap(); - let mint_decimals = arg_matches - .get_one(MINT_DECIMALS_ARG.name) - .map(|v: &String| v.parse::().unwrap()); - let use_unchecked_instruction = arg_matches.is_present("use_unchecked_instruction"); - command_approve( - config, - account, - owner_address, - amount, - delegate, - mint_address, - mint_decimals, - use_unchecked_instruction, - bulk_signers, - ) - .await - } - (CommandName::Revoke, arg_matches) => { - let (owner_signer, owner_address) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let delegate_address = - pubkey_of_signer(arg_matches, DELEGATE_ADDRESS_ARG.name, &mut wallet_manager) - .unwrap(); - command_revoke( - config, - account, - owner_address, - delegate_address, - bulk_signers, - ) - .await - } - (CommandName::Close, arg_matches) => { - let (close_authority_signer, close_authority) = - config.signer_or_default(arg_matches, "close_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(close_authority_signer, &mut bulk_signers); - } - - let address = config - .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) - .await?; - let recipient = - config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager)?; - command_close(config, address, close_authority, recipient, bulk_signers).await - } - (CommandName::CloseMint, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let (close_authority_signer, close_authority) = - config.signer_or_default(arg_matches, "close_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(close_authority_signer, &mut bulk_signers); - } - let recipient = - config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager)?; - - command_close_mint(config, token, close_authority, recipient, bulk_signers).await - } - (CommandName::Balance, arg_matches) => { - let address = config - .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) - .await?; - command_balance(config, address).await - } - (CommandName::Supply, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - command_supply(config, token).await - } - (CommandName::Accounts, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager)?; - let filter = if arg_matches.is_present("delegated") { - AccountFilter::Delegated - } else if arg_matches.is_present("externally_closeable") { - AccountFilter::ExternallyCloseable - } else { - AccountFilter::All - }; - - command_accounts( - config, - token, - owner, - filter, - arg_matches.is_present("addresses_only"), - ) - .await - } - (CommandName::Address, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - let owner = config.pubkey_or_default(arg_matches, "owner", &mut wallet_manager)?; - command_address(config, token, owner).await - } - (CommandName::AccountInfo, arg_matches) => { - let address = config - .associated_token_address_or_override(arg_matches, "address", &mut wallet_manager) - .await?; - command_display(config, address).await - } - (CommandName::MultisigInfo, arg_matches) => { - let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) - .unwrap() - .unwrap(); - command_display(config, address).await - } - (CommandName::Display, arg_matches) => { - let address = pubkey_of_signer(arg_matches, "address", &mut wallet_manager) - .unwrap() - .unwrap(); - command_display(config, address).await - } - (CommandName::Gc, arg_matches) => { - match config.output_format { - OutputFormat::Json | OutputFormat::JsonCompact => { - eprintln!( - "`spl-token gc` does not support the `--output` parameter at this time" - ); - exit(1); - } - _ => {} - } - - let close_empty_associated_accounts = - arg_matches.is_present("close_empty_associated_accounts"); - - let (owner_signer, owner_address) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - command_gc( - config, - owner_address, - close_empty_associated_accounts, - bulk_signers, - ) - .await - } - (CommandName::SyncNative, arg_matches) => { - let native_mint = *native_token_client_from_config(config)?.get_address(); - let address = config - .associated_token_address_for_token_or_override( - arg_matches, - "address", - &mut wallet_manager, - Some(native_mint), - ) - .await; - command_sync_native(config, address?).await - } - (CommandName::EnableRequiredTransferMemos, arg_matches) => { - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - // Since account is required argument it will always be present - let token_account = - config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; - command_required_transfer_memos(config, token_account, owner, bulk_signers, true).await - } - (CommandName::DisableRequiredTransferMemos, arg_matches) => { - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - // Since account is required argument it will always be present - let token_account = - config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; - command_required_transfer_memos(config, token_account, owner, bulk_signers, false).await - } - (CommandName::EnableCpiGuard, arg_matches) => { - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - // Since account is required argument it will always be present - let token_account = - config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; - command_cpi_guard(config, token_account, owner, bulk_signers, true).await - } - (CommandName::DisableCpiGuard, arg_matches) => { - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - // Since account is required argument it will always be present - let token_account = - config.pubkey_or_default(arg_matches, "account", &mut wallet_manager)?; - command_cpi_guard(config, token_account, owner, bulk_signers, false).await - } - (CommandName::UpdateDefaultAccountState, arg_matches) => { - // Since account is required argument it will always be present - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let (freeze_authority_signer, freeze_authority) = - config.signer_or_default(arg_matches, "freeze_authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(freeze_authority_signer, &mut bulk_signers); - } - let new_default_state = arg_matches.value_of("state").unwrap(); - let new_default_state = match new_default_state { - "initialized" => AccountState::Initialized, - "frozen" => AccountState::Frozen, - _ => unreachable!(), - }; - command_update_default_account_state( - config, - token, - freeze_authority, - new_default_state, - bulk_signers, - ) - .await - } - (CommandName::UpdateMetadataAddress, arg_matches) => { - // Since account is required argument it will always be present - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - - let (authority_signer, authority) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(authority_signer, &mut bulk_signers); - } - let metadata_address = value_t!(arg_matches, "metadata_address", Pubkey).ok(); - - command_update_pointer_address( - config, - token, - authority, - metadata_address, - bulk_signers, - Pointer::Metadata, - ) - .await - } - (CommandName::UpdateGroupAddress, arg_matches) => { - // Since account is required argument it will always be present - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - - let (authority_signer, authority) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(authority_signer, &mut bulk_signers); - } - let group_address = value_t!(arg_matches, "group_address", Pubkey).ok(); - - command_update_pointer_address( - config, - token, - authority, - group_address, - bulk_signers, - Pointer::Group, - ) - .await - } - (CommandName::UpdateMemberAddress, arg_matches) => { - // Since account is required argument it will always be present - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - - let (authority_signer, authority) = - config.signer_or_default(arg_matches, "authority", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(authority_signer, &mut bulk_signers); - } - let member_address = value_t!(arg_matches, "member_address", Pubkey).ok(); - - command_update_pointer_address( - config, - token, - authority, - member_address, - bulk_signers, - Pointer::GroupMember, - ) - .await - } - (CommandName::WithdrawWithheldTokens, arg_matches) => { - let (authority_signer, authority) = config.signer_or_default( - arg_matches, - "withdraw_withheld_authority", - &mut wallet_manager, - ); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(authority_signer, &mut bulk_signers); - } - // Since destination is required it will always be present - let destination_token_account = - pubkey_of_signer(arg_matches, "account", &mut wallet_manager) - .unwrap() - .unwrap(); - let include_mint = arg_matches.is_present("include_mint"); - let source_accounts = arg_matches - .values_of("source") - .unwrap_or_default() - .map(|s| Pubkey::from_str(s).unwrap_or_else(print_error_and_exit)) - .collect::>(); - command_withdraw_withheld_tokens( - config, - destination_token_account, - source_accounts, - authority, - include_mint, - bulk_signers, - ) - .await - } - (CommandName::SetTransferFee, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let transfer_fee_basis_points = - value_t_or_exit!(arg_matches, "transfer_fee_basis_points", u16); - let maximum_fee = *arg_matches.get_one::("maximum_fee").unwrap(); - let (transfer_fee_authority_signer, transfer_fee_authority_pubkey) = config - .signer_or_default(arg_matches, "transfer_fee_authority", &mut wallet_manager); - let mint_decimals = arg_matches.get_one::(MINT_DECIMALS_ARG.name).copied(); - let bulk_signers = vec![transfer_fee_authority_signer]; - - command_set_transfer_fee( - config, - token_pubkey, - transfer_fee_authority_pubkey, - transfer_fee_basis_points, - maximum_fee, - mint_decimals, - bulk_signers, - ) - .await - } - (CommandName::WithdrawExcessLamports, arg_matches) => { - let (signer, authority) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(signer, &mut bulk_signers); - } - - let source = config.pubkey_or_default(arg_matches, "from", &mut wallet_manager)?; - let destination = - config.pubkey_or_default(arg_matches, "recipient", &mut wallet_manager)?; - - command_withdraw_excess_lamports(config, source, destination, authority, bulk_signers) - .await - } - (CommandName::UpdateConfidentialTransferSettings, arg_matches) => { - let token_pubkey = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - - let auto_approve = arg_matches.value_of("approve_policy").map(|b| b == "auto"); - - let auditor_encryption_pubkey = if arg_matches.is_present("auditor_pubkey") { - Some(elgamal_pubkey_or_none(arg_matches, "auditor_pubkey")?) - } else { - None - }; - - let (authority_signer, authority_pubkey) = config.signer_or_default( - arg_matches, - "confidential_transfer_authority", - &mut wallet_manager, - ); - let bulk_signers = vec![authority_signer]; - - command_update_confidential_transfer_settings( - config, - token_pubkey, - authority_pubkey, - auto_approve, - auditor_encryption_pubkey, - bulk_signers, - ) - .await - } - (CommandName::ConfigureConfidentialTransferAccount, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - - let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); - - // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be - // supported in the future once upgrading to clap-v3. - // - // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be - // updated once custom ElGamal and AES keys are supported. - let elgamal_keypair = ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); - let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let maximum_credit_counter = - if arg_matches.is_present("maximum_pending_balance_credit_counter") { - let maximum_credit_counter = value_t_or_exit!( - arg_matches.value_of("maximum_pending_balance_credit_counter"), - u64 - ); - Some(maximum_credit_counter) - } else { - None - }; - - command_configure_confidential_transfer_account( - config, - token, - owner, - account, - maximum_credit_counter, - &elgamal_keypair, - &aes_key, - bulk_signers, - ) - .await - } - (c @ CommandName::EnableConfidentialCredits, arg_matches) - | (c @ CommandName::DisableConfidentialCredits, arg_matches) - | (c @ CommandName::EnableNonConfidentialCredits, arg_matches) - | (c @ CommandName::DisableNonConfidentialCredits, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - - let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - let (allow_confidential_credits, allow_non_confidential_credits) = match c { - CommandName::EnableConfidentialCredits => (Some(true), None), - CommandName::DisableConfidentialCredits => (Some(false), None), - CommandName::EnableNonConfidentialCredits => (None, Some(true)), - CommandName::DisableNonConfidentialCredits => (None, Some(false)), - _ => (None, None), - }; - - command_enable_disable_confidential_transfers( - config, - token, - owner, - account, - bulk_signers, - allow_confidential_credits, - allow_non_confidential_credits, - ) - .await - } - (c @ CommandName::DepositConfidentialTokens, arg_matches) - | (c @ CommandName::WithdrawConfidentialTokens, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager) - .unwrap() - .unwrap(); - let amount = *arg_matches.get_one::("amount").unwrap(); - let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - let mint_decimals = arg_matches.get_one::(MINT_DECIMALS_ARG.name).copied(); - - let (instruction_type, elgamal_keypair, aes_key) = match c { - CommandName::DepositConfidentialTokens => { - (ConfidentialInstructionType::Deposit, None, None) - } - CommandName::WithdrawConfidentialTokens => { - // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be - // supported in the future once upgrading to clap-v3. - // - // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be - // updated once custom ElGamal and AES keys are supported. - let elgamal_keypair = - ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); - let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); - - ( - ConfidentialInstructionType::Withdraw, - Some(elgamal_keypair), - Some(aes_key), - ) - } - _ => panic!("Instruction not supported"), - }; - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - command_deposit_withdraw_confidential_tokens( - config, - token, - owner, - account, - bulk_signers, - amount, - mint_decimals, - instruction_type, - elgamal_keypair.as_ref(), - aes_key.as_ref(), - ) - .await - } - (CommandName::ApplyPendingBalance, arg_matches) => { - let token = pubkey_of_signer(arg_matches, "token", &mut wallet_manager).unwrap(); - - let (owner_signer, owner) = - config.signer_or_default(arg_matches, "owner", &mut wallet_manager); - - let account = pubkey_of_signer(arg_matches, "address", &mut wallet_manager).unwrap(); - - // Deriving ElGamal and AES key from signer. Custom ElGamal and AES keys will be - // supported in the future once upgrading to clap-v3. - // - // NOTE:: Seed bytes are hardcoded to be empty bytes for now. They will be - // updated once custom ElGamal and AES keys are supported. - let elgamal_keypair = ElGamalKeypair::new_from_signer(&*owner_signer, b"").unwrap(); - let aes_key = AeKey::new_from_signer(&*owner_signer, b"").unwrap(); - - if config.multisigner_pubkeys.is_empty() { - push_signer_with_dedup(owner_signer, &mut bulk_signers); - } - - command_apply_pending_balance( - config, - token, - owner, - account, - bulk_signers, - &elgamal_keypair, - &aes_key, - ) - .await - } - } -} - -fn format_output(command_output: T, command_name: &CommandName, config: &Config) -> String -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - config.output_format.formatted_string(&CommandOutput { - command_name: command_name.to_string(), - command_output, - }) -} -enum TransactionReturnData { - CliSignature(CliSignature), - CliSignOnlyData(CliSignOnlyData), -} - -async fn finish_tx<'a>( - config: &Config<'a>, - rpc_response: &RpcClientResponse, - no_wait: bool, -) -> Result { - match rpc_response { - RpcClientResponse::Transaction(transaction) => { - Ok(TransactionReturnData::CliSignOnlyData(return_signers_data( - transaction, - &ReturnSignersConfig { - dump_transaction_message: config.dump_transaction_message, - }, - ))) - } - RpcClientResponse::Signature(signature) if no_wait => { - Ok(TransactionReturnData::CliSignature(CliSignature { - signature: signature.to_string(), - })) - } - RpcClientResponse::Signature(signature) => { - let blockhash = config.program_client.get_latest_blockhash().await?; - config - .rpc_client - .confirm_transaction_with_spinner( - signature, - &blockhash, - config.rpc_client.commitment(), - ) - .await?; - - Ok(TransactionReturnData::CliSignature(CliSignature { - signature: signature.to_string(), - })) - } - RpcClientResponse::Simulation(_) => { - // Implement this once the CLI supports dry-running / simulation - unreachable!() - } - } -} diff --git a/token/cli/src/config.rs b/token/cli/src/config.rs deleted file mode 100644 index 74116217121..00000000000 --- a/token/cli/src/config.rs +++ /dev/null @@ -1,619 +0,0 @@ -use { - crate::clap_app::{Error, COMPUTE_UNIT_LIMIT_ARG, COMPUTE_UNIT_PRICE_ARG, MULTISIG_SIGNER_ARG}, - clap::ArgMatches, - solana_clap_v3_utils::{ - input_parsers::pubkey_of_signer, - input_validators::normalize_to_url_if_moniker, - keypair::SignerFromPathConfig, - nonce::{NONCE_ARG, NONCE_AUTHORITY_ARG}, - offline::{BLOCKHASH_ARG, DUMP_TRANSACTION_MESSAGE, SIGNER_ARG, SIGN_ONLY_ARG}, - }, - solana_cli_output::OutputFormat, - solana_client::nonblocking::rpc_client::RpcClient, - solana_remote_wallet::remote_wallet::RemoteWalletManager, - solana_sdk::{ - account::Account as RawAccount, commitment_config::CommitmentConfig, hash::Hash, - pubkey::Pubkey, signature::Signer, signer::null_signer::NullSigner, - }, - spl_associated_token_account_client::address::get_associated_token_address_with_program_id, - spl_token_2022::{ - extension::StateWithExtensionsOwned, - state::{Account, Mint}, - }, - spl_token_client::{ - client::{ - ProgramClient, ProgramOfflineClient, ProgramRpcClient, ProgramRpcClientSendTransaction, - }, - token::ComputeUnitLimit, - }, - std::{process::exit, rc::Rc, str::FromStr, sync::Arc, time::Duration}, -}; - -type SignersOf = Vec<(Arc, Pubkey)>; -fn signers_of( - matches: &ArgMatches, - name: &str, - wallet_manager: &mut Option>, -) -> Result, Box> { - if let Some(values) = matches.values_of(name) { - let mut results = Vec::new(); - for (i, value) in values.enumerate() { - let name = format!("{}-{}", name, i.saturating_add(1)); - let signer = signer_from_path(matches, value, &name, wallet_manager)?; - let signer_pubkey = signer.pubkey(); - results.push((Arc::from(signer), signer_pubkey)); - } - Ok(Some(results)) - } else { - Ok(None) - } -} - -pub(crate) struct MintInfo { - pub program_id: Pubkey, - pub address: Pubkey, - pub decimals: u8, -} - -const DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(30); -const DEFAULT_CONFIRM_TX_TIMEOUT: Duration = Duration::from_secs(5); - -pub struct Config<'a> { - pub default_signer: Option>, - pub rpc_client: Arc, - pub program_client: Arc>, - pub websocket_url: String, - pub output_format: OutputFormat, - pub fee_payer: Option>, - pub nonce_account: Option, - pub nonce_authority: Option>, - pub nonce_blockhash: Option, - pub sign_only: bool, - pub dump_transaction_message: bool, - pub multisigner_pubkeys: Vec<&'a Pubkey>, - pub program_id: Pubkey, - pub restrict_to_program_id: bool, - pub compute_unit_price: Option, - pub compute_unit_limit: ComputeUnitLimit, -} - -impl<'a> Config<'a> { - pub async fn new( - matches: &ArgMatches, - wallet_manager: &mut Option>, - bulk_signers: &mut Vec>, - multisigner_ids: &'a mut Vec, - ) -> Config<'a> { - let cli_config = if let Some(config_file) = matches.value_of("config_file") { - solana_cli_config::Config::load(config_file).unwrap_or_else(|_| { - eprintln!("error: Could not find config file `{}`", config_file); - exit(1); - }) - } else if let Some(config_file) = &*solana_cli_config::CONFIG_FILE { - solana_cli_config::Config::load(config_file).unwrap_or_default() - } else { - solana_cli_config::Config::default() - }; - let json_rpc_url = normalize_to_url_if_moniker( - matches - .value_of("json_rpc_url") - .unwrap_or(&cli_config.json_rpc_url), - ); - let websocket_url = solana_cli_config::Config::compute_websocket_url(&json_rpc_url); - let commitment_config = CommitmentConfig::from_str(&cli_config.commitment) - .unwrap_or_else(|_| CommitmentConfig::confirmed()); - let rpc_client = Arc::new(RpcClient::new_with_timeouts_and_commitment( - json_rpc_url, - DEFAULT_RPC_TIMEOUT, - commitment_config, - DEFAULT_CONFIRM_TX_TIMEOUT, - )); - let sign_only = matches.try_contains_id(SIGN_ONLY_ARG.name).unwrap_or(false); - let program_client: Arc> = if sign_only { - let blockhash = matches - .get_one::(BLOCKHASH_ARG.name) - .copied() - .unwrap_or_default(); - Arc::new(ProgramOfflineClient::new( - blockhash, - ProgramRpcClientSendTransaction, - )) - } else { - Arc::new(ProgramRpcClient::new( - rpc_client.clone(), - ProgramRpcClientSendTransaction, - )) - }; - Self::new_with_clients_and_ws_url( - matches, - wallet_manager, - bulk_signers, - multisigner_ids, - rpc_client, - program_client, - websocket_url, - ) - .await - } - - fn extract_multisig_signers( - matches: &ArgMatches, - wallet_manager: &mut Option>, - bulk_signers: &mut Vec>, - multisigner_ids: &'a mut Vec, - ) -> Vec<&'a Pubkey> { - let multisig_signers = signers_of(matches, MULTISIG_SIGNER_ARG.name, wallet_manager) - .unwrap_or_else(|e| { - eprintln!("error: {}", e); - exit(1); - }); - if let Some(mut multisig_signers) = multisig_signers { - multisig_signers.sort_by(|(_, lp), (_, rp)| lp.cmp(rp)); - let (signers, pubkeys): (Vec<_>, Vec<_>) = multisig_signers.into_iter().unzip(); - bulk_signers.extend(signers); - multisigner_ids.extend(pubkeys); - } - multisigner_ids.iter().collect::>() - } - - pub async fn new_with_clients_and_ws_url( - matches: &ArgMatches, - wallet_manager: &mut Option>, - bulk_signers: &mut Vec>, - multisigner_ids: &'a mut Vec, - rpc_client: Arc, - program_client: Arc>, - websocket_url: String, - ) -> Config<'a> { - let cli_config = if let Some(config_file) = matches.value_of("config_file") { - solana_cli_config::Config::load(config_file).unwrap_or_else(|_| { - eprintln!("error: Could not find config file `{}`", config_file); - exit(1); - }) - } else if let Some(config_file) = &*solana_cli_config::CONFIG_FILE { - solana_cli_config::Config::load(config_file).unwrap_or_default() - } else { - solana_cli_config::Config::default() - }; - let multisigner_pubkeys = - Self::extract_multisig_signers(matches, wallet_manager, bulk_signers, multisigner_ids); - - let config = SignerFromPathConfig { - allow_null_signer: !multisigner_pubkeys.is_empty(), - }; - - let default_keypair = cli_config.keypair_path.clone(); - - let default_signer: Option> = { - if let Some(owner_path) = matches.try_get_one::("owner").ok().flatten() { - signer_from_path_with_config(matches, owner_path, "owner", wallet_manager, &config) - .ok() - } else { - signer_from_path_with_config( - matches, - &default_keypair, - "default", - wallet_manager, - &config, - ) - .map_err(|e| { - if std::fs::metadata(&default_keypair).is_ok() { - eprintln!("error: {}", e); - exit(1); - } else { - e - } - }) - .ok() - } - } - .map(Arc::from); - - let fee_payer: Option> = matches - .value_of("fee_payer") - .map(|path| { - Arc::from( - signer_from_path(matches, path, "fee_payer", wallet_manager).unwrap_or_else( - |e| { - eprintln!("error: {}", e); - exit(1); - }, - ), - ) - }) - .or_else(|| default_signer.clone()); - - let verbose = matches.is_present("verbose"); - let output_format = matches - .value_of("output_format") - .map(|value| match value { - "json" => OutputFormat::Json, - "json-compact" => OutputFormat::JsonCompact, - _ => unreachable!(), - }) - .unwrap_or(if verbose { - OutputFormat::DisplayVerbose - } else { - OutputFormat::Display - }); - - let nonce_account = match pubkey_of_signer(matches, NONCE_ARG.name, wallet_manager) { - Ok(account) => account, - Err(e) => { - if e.is::() { - None - } else { - eprintln!("error: {}", e); - exit(1); - } - } - }; - let nonce_authority = if nonce_account.is_some() { - let (nonce_authority, _) = signer_from_path( - matches, - matches - .value_of(NONCE_AUTHORITY_ARG.name) - .unwrap_or(&cli_config.keypair_path), - NONCE_AUTHORITY_ARG.name, - wallet_manager, - ) - .map(Arc::from) - .map(|s: Arc| { - let p = s.pubkey(); - (s, p) - }) - .unwrap_or_else(|e| { - eprintln!("error: {}", e); - exit(1); - }); - - Some(nonce_authority) - } else { - None - }; - - let sign_only = matches.try_contains_id(SIGN_ONLY_ARG.name).unwrap_or(false); - let dump_transaction_message = matches - .try_contains_id(DUMP_TRANSACTION_MESSAGE.name) - .unwrap_or(false); - - let pubkey_from_matches = |name| { - matches - .try_get_one::(name) - .ok() - .flatten() - .and_then(|pubkey| Pubkey::from_str(pubkey).ok()) - }; - - let default_program_id = spl_token::id(); - let (program_id, restrict_to_program_id) = if matches.is_present("program_2022") { - (spl_token_2022::id(), true) - } else if let Some(program_id) = pubkey_from_matches("program_id") { - (program_id, true) - } else if !sign_only { - if let Some(address) = pubkey_from_matches("token") - .or_else(|| pubkey_from_matches("account")) - .or_else(|| pubkey_from_matches("address")) - { - ( - rpc_client - .get_account(&address) - .await - .map(|account| account.owner) - .unwrap_or(default_program_id), - false, - ) - } else { - (default_program_id, false) - } - } else { - (default_program_id, false) - }; - - if matches.try_contains_id(BLOCKHASH_ARG.name).unwrap_or(false) - && matches - .try_contains_id(COMPUTE_UNIT_PRICE_ARG.name) - .unwrap_or(false) - && !matches - .try_contains_id(COMPUTE_UNIT_LIMIT_ARG.name) - .unwrap_or(false) - { - clap::Error::with_description( - format!( - "Need to set `{}` if `{}` and `--{}` are set", - COMPUTE_UNIT_LIMIT_ARG.long, COMPUTE_UNIT_PRICE_ARG.long, BLOCKHASH_ARG.long, - ), - clap::ErrorKind::MissingRequiredArgument, - ) - .exit(); - } - - let nonce_blockhash = matches - .try_get_one::(BLOCKHASH_ARG.name) - .ok() - .flatten() - .copied(); - - let compute_unit_price = matches.get_one::(COMPUTE_UNIT_PRICE_ARG.name).copied(); - - let compute_unit_limit = matches - .get_one::(COMPUTE_UNIT_LIMIT_ARG.name) - .copied() - .map(ComputeUnitLimit::Static) - .unwrap_or_else(|| { - if nonce_blockhash.is_some() { - ComputeUnitLimit::Default - } else { - ComputeUnitLimit::Simulated - } - }); - - Self { - default_signer, - rpc_client, - program_client, - websocket_url, - output_format, - fee_payer, - nonce_account, - nonce_authority, - nonce_blockhash, - sign_only, - dump_transaction_message, - multisigner_pubkeys, - program_id, - restrict_to_program_id, - compute_unit_price, - compute_unit_limit, - } - } - - // Returns Ok(default signer), or Err if there is no default signer configured - pub(crate) fn default_signer(&self) -> Result, Error> { - if let Some(default_signer) = &self.default_signer { - Ok(default_signer.clone()) - } else { - Err("default signer is required, please specify a valid default signer by identifying a \ - valid configuration file using the --config argument, or by creating a valid config \ - at the default location of ~/.config/solana/cli/config.yml using the solana config \ - command".to_string().into()) - } - } - - // Returns Ok(fee payer), or Err if there is no fee payer configured - pub fn fee_payer(&self) -> Result, Error> { - if let Some(fee_payer) = &self.fee_payer { - Ok(fee_payer.clone()) - } else { - Err("fee payer is required, please specify a valid fee payer using the --fee-payer argument, \ - or by identifying a valid configuration file using the --config argument, or by creating \ - a valid config at the default location of ~/.config/solana/cli/config.yml using the solana \ - config command".to_string().into()) - } - } - - // Check if an explicit token account address was provided, otherwise - // return the associated token address for the default address. - pub(crate) async fn associated_token_address_or_override( - &self, - arg_matches: &ArgMatches, - override_name: &str, - wallet_manager: &mut Option>, - ) -> Result { - let token = pubkey_of_signer(arg_matches, "token", wallet_manager) - .map_err(|e| -> Error { e.to_string().into() })?; - self.associated_token_address_for_token_or_override( - arg_matches, - override_name, - wallet_manager, - token, - ) - .await - } - - // Check if an explicit token account address was provided, otherwise - // return the associated token address for the default address. - pub(crate) async fn associated_token_address_for_token_or_override( - &self, - arg_matches: &ArgMatches, - override_name: &str, - wallet_manager: &mut Option>, - token: Option, - ) -> Result { - if let Some(address) = pubkey_of_signer(arg_matches, override_name, wallet_manager) - .map_err(|e| -> Error { e.to_string().into() })? - { - return Ok(address); - } - - let token = token.unwrap(); - let program_id = self.get_mint_info(&token, None).await?.program_id; - let owner = self.pubkey_or_default(arg_matches, "owner", wallet_manager)?; - self.associated_token_address_for_token_and_program(&token, &owner, &program_id) - } - - pub(crate) fn associated_token_address_for_token_and_program( - &self, - token: &Pubkey, - owner: &Pubkey, - program_id: &Pubkey, - ) -> Result { - Ok(get_associated_token_address_with_program_id( - owner, token, program_id, - )) - } - - // Checks if an explicit address was provided, otherwise return the default - // address if there is one - pub(crate) fn pubkey_or_default( - &self, - arg_matches: &ArgMatches, - address_name: &str, - wallet_manager: &mut Option>, - ) -> Result { - if let Some(address) = pubkey_of_signer(arg_matches, address_name, wallet_manager) - .map_err(|e| -> Error { e.to_string().into() })? - { - return Ok(address); - } - - Ok(self.default_signer()?.pubkey()) - } - - // Checks if an explicit signer was provided, otherwise return the default - // signer. - pub(crate) fn signer_or_default( - &self, - arg_matches: &ArgMatches, - authority_name: &str, - wallet_manager: &mut Option>, - ) -> (Arc, Pubkey) { - // If there are `--multisig-signers` on the command line, allow `NullSigner`s to - // be returned for multisig account addresses - let config = SignerFromPathConfig { - allow_null_signer: !self.multisigner_pubkeys.is_empty(), - }; - let mut load_authority = move || -> Result, Error> { - if authority_name != "owner" { - if let Some(keypair_path) = arg_matches.value_of(authority_name) { - return signer_from_path_with_config( - arg_matches, - keypair_path, - authority_name, - wallet_manager, - &config, - ) - .map(Arc::from) - .map_err(|e| e.to_string().into()); - } - } - - self.default_signer() - }; - - let authority = load_authority().unwrap_or_else(|e| { - eprintln!("error: {}", e); - exit(1); - }); - - let authority_address = authority.pubkey(); - (authority, authority_address) - } - - pub(crate) async fn get_account_checked( - &self, - account_pubkey: &Pubkey, - ) -> Result { - if let Ok(Some(account)) = self.program_client.get_account(*account_pubkey).await { - if self.program_id == account.owner { - Ok(account) - } else { - Err(format!( - "Account {} is owned by {}, not configured program id {}", - account_pubkey, account.owner, self.program_id - ) - .into()) - } - } else { - Err(format!("Account {} not found", account_pubkey).into()) - } - } - - pub(crate) async fn get_mint_info( - &self, - mint: &Pubkey, - mint_decimals: Option, - ) -> Result { - if self.sign_only { - Ok(MintInfo { - program_id: self.program_id, - address: *mint, - decimals: mint_decimals.unwrap_or_default(), - }) - } else { - let account = self.get_account_checked(mint).await?; - let mint_account = StateWithExtensionsOwned::::unpack(account.data) - .map_err(|_| format!("Could not find mint account {}", mint))?; - if let Some(decimals) = mint_decimals { - if decimals != mint_account.base.decimals { - return Err(format!( - "Mint {:?} has decimals {}, not configured decimals {}", - mint, mint_account.base.decimals, decimals - ) - .into()); - } - } - Ok(MintInfo { - program_id: account.owner, - address: *mint, - decimals: mint_account.base.decimals, - }) - } - } - - pub(crate) async fn check_account( - &self, - token_account: &Pubkey, - mint_address: Option, - ) -> Result { - if !self.sign_only { - let account = self.get_account_checked(token_account).await?; - let source_account = StateWithExtensionsOwned::::unpack(account.data) - .map_err(|_| format!("Could not find token account {}", token_account))?; - let source_mint = source_account.base.mint; - if let Some(mint) = mint_address { - if source_mint != mint { - return Err(format!( - "Source {:?} does not contain {:?} tokens", - token_account, mint - ) - .into()); - } - } - Ok(source_mint) - } else { - Ok(mint_address.unwrap_or_default()) - } - } -} - -// In clap v2, `value_of` returns `None` if the argument id is not previously -// specified in `Arg`. In contrast, in clap v3, `value_of` panics in this case. -// Therefore, compared to the same function in solana-clap-utils, -// `signer_from_path` in solana-clap-v3-utils errors early when `path` is a -// valid pubkey, but `SIGNER_ARG.name` is not specified in the args. -// This function behaves exactly as `signer_from_path` from solana-clap-utils by -// catching this special case. -fn signer_from_path( - matches: &ArgMatches, - path: &str, - keypair_name: &str, - wallet_manager: &mut Option>, -) -> Result, Box> { - let config = SignerFromPathConfig::default(); - signer_from_path_with_config(matches, path, keypair_name, wallet_manager, &config) -} - -fn signer_from_path_with_config( - matches: &ArgMatches, - path: &str, - keypair_name: &str, - wallet_manager: &mut Option>, - config: &SignerFromPathConfig, -) -> Result, Box> { - if let Ok(pubkey) = Pubkey::from_str(path) { - if matches.try_contains_id(SIGNER_ARG.name).is_err() - && (config.allow_null_signer || matches.try_contains_id(SIGN_ONLY_ARG.name)?) - { - return Ok(Box::new(NullSigner::new(&pubkey))); - } - } - - solana_clap_v3_utils::keypair::signer_from_path_with_config( - matches, - path, - keypair_name, - wallet_manager, - config, - ) -} diff --git a/token/cli/src/encryption_keypair.rs b/token/cli/src/encryption_keypair.rs deleted file mode 100644 index 4873d117512..00000000000 --- a/token/cli/src/encryption_keypair.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Temporary ElGamal keypair argument parser. -//! -//! NOTE: this module should be removed in the next Solana upgrade. - -use { - base64::{prelude::BASE64_STANDARD, Engine}, - clap::ArgMatches, - spl_token_2022::solana_zk_sdk::encryption::{ - elgamal::{ElGamalKeypair, ElGamalPubkey}, - pod::elgamal::PodElGamalPubkey, - }, -}; - -const ELGAMAL_PUBKEY_MAX_BASE64_LEN: usize = 44; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum ElGamalPubkeyOrNone { - ElGamalPubkey(PodElGamalPubkey), - None, -} - -impl From for Option { - fn from(val: ElGamalPubkeyOrNone) -> Self { - match val { - ElGamalPubkeyOrNone::ElGamalPubkey(pubkey) => Some(pubkey), - ElGamalPubkeyOrNone::None => None, - } - } -} - -pub(crate) fn elgamal_pubkey_or_none( - matches: &ArgMatches, - name: &str, -) -> Result { - let arg_str = matches.value_of(name).unwrap(); - if arg_str == "none" { - return Ok(ElGamalPubkeyOrNone::None); - } - let elgamal_pubkey = elgamal_pubkey_of(matches, name)?; - Ok(ElGamalPubkeyOrNone::ElGamalPubkey(elgamal_pubkey)) -} - -pub(crate) fn elgamal_pubkey_of( - matches: &ArgMatches, - name: &str, -) -> Result { - if let Ok(keypair) = elgamal_keypair_of(matches, name) { - let elgamal_pubkey = (*keypair.pubkey()).into(); - Ok(elgamal_pubkey) - } else { - let arg_str = matches.value_of(name).unwrap(); - if let Some(pubkey) = elgamal_pubkey_from_str(arg_str) { - Ok(pubkey) - } else { - Err("failed to read ElGamal pubkey".to_string()) - } - } -} - -pub(crate) fn elgamal_keypair_of( - matches: &ArgMatches, - name: &str, -) -> Result { - let path = matches.value_of(name).unwrap(); - ElGamalKeypair::read_json_file(path).map_err(|e| e.to_string()) -} - -fn elgamal_pubkey_from_str(s: &str) -> Option { - if s.len() > ELGAMAL_PUBKEY_MAX_BASE64_LEN { - return None; - } - let pubkey_vec = BASE64_STANDARD.decode(s).ok()?; - let elgamal_pubkey = ElGamalPubkey::try_from(pubkey_vec.as_ref()).ok()?; - Some(elgamal_pubkey.into()) -} diff --git a/token/cli/src/lib.rs b/token/cli/src/lib.rs deleted file mode 100644 index b38c26b44fd..00000000000 --- a/token/cli/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod bench; -pub mod clap_app; -pub mod command; -pub mod config; -mod encryption_keypair; -mod output; -mod sort; diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs deleted file mode 100644 index 1f93fad01e5..00000000000 --- a/token/cli/src/main.rs +++ /dev/null @@ -1,39 +0,0 @@ -use { - solana_sdk::signer::Signer, - spl_token_cli::{clap_app::*, command::process_command, config::Config}, - std::{str::FromStr, sync::Arc}, -}; - -#[tokio::main] -async fn main() -> Result<(), Error> { - let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); - let minimum_signers_help = minimum_signers_help_string(); - let multisig_member_help = multisig_member_help_string(); - let app_matches = app( - &default_decimals, - &minimum_signers_help, - &multisig_member_help, - ) - .get_matches(); - - let mut wallet_manager = None; - let mut bulk_signers: Vec> = Vec::new(); - - let (sub_command, matches) = app_matches.subcommand().unwrap(); - let sub_command = CommandName::from_str(sub_command).unwrap(); - - let mut multisigner_ids = Vec::new(); - let config = Config::new( - matches, - &mut wallet_manager, - &mut bulk_signers, - &mut multisigner_ids, - ) - .await; - - solana_logger::setup_with_default("solana=info"); - let result = - process_command(&sub_command, matches, &config, wallet_manager, bulk_signers).await?; - println!("{}", result); - Ok(()) -} diff --git a/token/cli/src/output.rs b/token/cli/src/output.rs deleted file mode 100644 index 335d6197d0a..00000000000 --- a/token/cli/src/output.rs +++ /dev/null @@ -1,1006 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -use { - crate::{config::Config, sort::UnsupportedAccount}, - console::{style, Emoji}, - serde::{Deserialize, Serialize, Serializer}, - solana_account_decoder::{ - parse_token::{UiAccountState, UiMint, UiMultisig, UiTokenAccount, UiTokenAmount}, - parse_token_extension::{ - UiConfidentialTransferAccount, UiConfidentialTransferFeeAmount, - UiConfidentialTransferFeeConfig, UiConfidentialTransferMint, UiCpiGuard, - UiDefaultAccountState, UiExtension, UiGroupMemberPointer, UiGroupPointer, - UiInterestBearingConfig, UiMemoTransfer, UiMetadataPointer, UiMintCloseAuthority, - UiPermanentDelegate, UiTokenGroup, UiTokenGroupMember, UiTokenMetadata, - UiTransferFeeAmount, UiTransferFeeConfig, UiTransferHook, UiTransferHookAccount, - }, - }, - solana_cli_output::{display::writeln_name_value, OutputFormat, QuietDisplay, VerboseDisplay}, - std::fmt::{self, Display}, -}; - -static WARNING: Emoji = Emoji("⚠️", "!"); - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CommandOutput -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - pub(crate) command_name: String, - pub(crate) command_output: T, -} - -impl Display for CommandOutput -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(&self.command_output, f) - } -} - -impl QuietDisplay for CommandOutput -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result { - QuietDisplay::write_str(&self.command_output, w) - } -} - -impl VerboseDisplay for CommandOutput -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result { - writeln_name_value(w, "Command: ", &self.command_name)?; - VerboseDisplay::write_str(&self.command_output, w) - } -} - -pub(crate) fn println_display(config: &Config, message: String) { - match config.output_format { - OutputFormat::Display | OutputFormat::DisplayVerbose => { - println!("{}", message); - } - _ => {} - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliCreateToken -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - pub(crate) address: String, - pub(crate) decimals: u8, - pub(crate) transaction_data: T, -} - -impl Display for CliCreateToken -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f)?; - writeln_name_value(f, "Address: ", &self.address)?; - writeln_name_value(f, "Decimals: ", &format!("{}", self.decimals))?; - Display::fmt(&self.transaction_data, f) - } -} -impl QuietDisplay for CliCreateToken -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result { - writeln!(w)?; - writeln_name_value(w, "Address: ", &self.address)?; - writeln_name_value(w, "Decimals: ", &format!("{}", self.decimals))?; - QuietDisplay::write_str(&self.transaction_data, w) - } -} -impl VerboseDisplay for CliCreateToken -where - T: Serialize + Display + QuietDisplay + VerboseDisplay, -{ - fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result { - writeln!(w)?; - writeln_name_value(w, "Address: ", &self.address)?; - writeln_name_value(w, "Decimals: ", &format!("{}", self.decimals))?; - VerboseDisplay::write_str(&self.transaction_data, w) - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliTokenAmount { - #[serde(flatten)] - pub(crate) amount: UiTokenAmount, -} - -impl QuietDisplay for CliTokenAmount {} -impl VerboseDisplay for CliTokenAmount { - fn write_str(&self, w: &mut dyn fmt::Write) -> fmt::Result { - writeln!(w, "ui amount: {}", self.amount.real_number_string_trimmed())?; - writeln!(w, "decimals: {}", self.amount.decimals)?; - writeln!(w, "amount: {}", self.amount.amount) - } -} - -impl fmt::Display for CliTokenAmount { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "{}", self.amount.real_number_string_trimmed()) - } -} - -#[derive(Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliWalletAddress { - pub(crate) wallet_address: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) associated_token_address: Option, -} - -impl QuietDisplay for CliWalletAddress {} -impl VerboseDisplay for CliWalletAddress {} - -impl fmt::Display for CliWalletAddress { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Wallet address: {}", self.wallet_address)?; - if let Some(associated_token_address) = &self.associated_token_address { - writeln!(f, "Associated token address: {}", associated_token_address)?; - } - Ok(()) - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliMultisig { - pub(crate) address: String, - pub(crate) program_id: String, - #[serde(flatten)] - pub(crate) multisig: UiMultisig, -} - -impl QuietDisplay for CliMultisig {} -impl VerboseDisplay for CliMultisig {} - -impl fmt::Display for CliMultisig { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let m = self.multisig.num_required_signers; - let n = self.multisig.num_valid_signers; - - writeln!(f)?; - writeln!(f, "{}", style("SPL Token Multisig").bold())?; - writeln_name_value(f, " Address:", &self.address)?; - writeln_name_value(f, " Program:", &self.program_id)?; - writeln_name_value(f, " M/N:", &format!("{}/{}", m, n))?; - writeln!(f, " {}", style("Signers:").bold())?; - let width = if n >= 9 { 4 } else { 3 }; - for i in 0..n as usize { - let title = format!(" {1:>0$}:", width, i + 1); - let pubkey = &self.multisig.signers[i]; - writeln_name_value(f, &title, pubkey)?; - } - Ok(()) - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliTokenAccount { - pub(crate) address: String, - pub(crate) program_id: String, - pub(crate) is_associated: bool, - #[serde(flatten)] - pub(crate) account: UiTokenAccount, - #[serde(skip_serializing)] - pub(crate) has_permanent_delegate: bool, -} - -impl QuietDisplay for CliTokenAccount {} -impl VerboseDisplay for CliTokenAccount {} - -impl fmt::Display for CliTokenAccount { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f)?; - writeln!(f, "{}", style("SPL Token Account").bold())?; - if self.is_associated { - writeln_name_value(f, " Address:", &self.address)?; - } else { - writeln_name_value(f, " Address:", &format!("{} (Aux*)", self.address))?; - } - writeln_name_value(f, " Program:", &self.program_id)?; - writeln_name_value( - f, - " Balance:", - &self.account.token_amount.real_number_string_trimmed(), - )?; - writeln_name_value( - f, - " Decimals:", - self.account.token_amount.decimals.to_string().as_ref(), - )?; - let mint = format!( - "{}{}", - self.account.mint, - if self.account.is_native { - " (native)" - } else { - "" - } - ); - writeln_name_value(f, " Mint:", &mint)?; - writeln_name_value(f, " Owner:", &self.account.owner)?; - writeln_name_value(f, " State:", &format!("{:?}", self.account.state))?; - if let Some(delegate) = &self.account.delegate { - writeln!(f, " {}", style("Delegation:").bold())?; - writeln_name_value(f, " Delegate:", delegate)?; - let allowance = self.account.delegated_amount.as_ref().unwrap(); - writeln_name_value(f, " Allowance:", &allowance.real_number_string_trimmed())?; - } else { - writeln_name_value(f, " Delegation:", "")?; - } - writeln_name_value( - f, - " Close authority:", - self.account - .close_authority - .as_ref() - .unwrap_or(&String::new()), - )?; - - if !self.account.extensions.is_empty() { - writeln!(f, "{}", style("Extensions:").bold())?; - for extension in &self.account.extensions { - display_ui_extension(f, 0, extension)?; - } - } - - if !self.is_associated { - writeln!(f)?; - writeln!(f, "* Please run `spl-token gc` to clean up Aux accounts")?; - } - - if self.has_permanent_delegate { - writeln!(f)?; - writeln!( - f, - "* {} ", - style("This token has a permanent delegate!").bold() - )?; - writeln!( - f, - " This means the mint may withdraw {} funds from this account at {} time.", - style("all").bold(), - style("any").bold(), - )?; - writeln!(f, " If this was not adequately disclosed to you, you may be dealing with a malicious mint.")?; - } - - Ok(()) - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliMint { - pub(crate) address: String, - pub(crate) program_id: String, - #[serde(skip_serializing)] - pub(crate) epoch: u64, - #[serde(flatten)] - pub(crate) mint: UiMint, -} - -impl QuietDisplay for CliMint {} -impl VerboseDisplay for CliMint {} - -impl fmt::Display for CliMint { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f)?; - writeln!(f, "{}", style("SPL Token Mint").bold())?; - - writeln_name_value(f, " Address:", &self.address)?; - writeln_name_value(f, " Program:", &self.program_id)?; - writeln_name_value(f, " Supply:", &self.mint.supply)?; - writeln_name_value(f, " Decimals:", &self.mint.decimals.to_string())?; - writeln_name_value( - f, - " Mint authority:", - self.mint.mint_authority.as_ref().unwrap_or(&String::new()), - )?; - writeln_name_value( - f, - " Freeze authority:", - self.mint - .freeze_authority - .as_ref() - .unwrap_or(&String::new()), - )?; - - if !self.mint.extensions.is_empty() { - writeln!(f, "{}", style("Extensions").bold())?; - for extension in &self.mint.extensions { - display_ui_extension(f, self.epoch, extension)?; - } - } - - Ok(()) - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct CliTokenAccounts { - #[serde(serialize_with = "flattened")] - pub(crate) accounts: Vec>, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub(crate) unsupported_accounts: Vec, - #[serde(skip_serializing)] - pub(crate) max_len_balance: usize, - #[serde(skip_serializing)] - pub(crate) aux_len: usize, - #[serde(skip_serializing)] - pub(crate) explicit_token: bool, -} - -impl QuietDisplay for CliTokenAccounts {} -impl VerboseDisplay for CliTokenAccounts { - fn write_str(&self, w: &mut dyn fmt::Write) -> fmt::Result { - let mut gc_alert = false; - - let mut delegate_padding = 9; - let mut close_authority_padding = 15; - for accounts_list in self.accounts.iter() { - for account in accounts_list { - if account.account.delegated_amount.is_some() { - delegate_padding = delegate_padding.max( - account - .account - .delegated_amount - .as_ref() - .unwrap() - .amount - .len(), - ); - } - - if account.account.close_authority.is_some() { - close_authority_padding = 44; - } - } - } - - let header = if self.explicit_token { - format!( - "{:<44} {:<44} {:<5$} {:<6$} {:<7$}", - "Program", - "Account", - "Delegated", - "Close Authority", - "Balance", - delegate_padding, - close_authority_padding, - self.max_len_balance - ) - } else { - format!( - "{:<44} {:<44} {:<44} {:<6$} {:<7$} {:<8$}", - "Program", - "Token", - "Account", - "Delegated", - "Close Authority", - "Balance", - delegate_padding, - close_authority_padding, - self.max_len_balance - ) - }; - writeln!(w, "{}", header)?; - writeln!(w, "{}", "-".repeat(header.len() + self.aux_len))?; - - for accounts_list in self.accounts.iter() { - let mut aux_counter = 1; - for account in accounts_list { - let maybe_aux = if !account.is_associated { - gc_alert = true; - let message = format!(" (Aux-{}*)", aux_counter); - aux_counter += 1; - message - } else { - "".to_string() - }; - - let maybe_frozen = if let UiAccountState::Frozen = account.account.state { - format!(" {} Frozen", WARNING) - } else { - "".to_string() - }; - - let maybe_delegated = account - .account - .delegated_amount - .clone() - .map(|d| d.amount) - .unwrap_or_else(|| "".to_string()); - - let maybe_close_authority = - account.account.close_authority.clone().unwrap_or_default(); - - if self.explicit_token { - writeln!( - w, - "{:<44} {:<44} {:<7$} {:<8$} {:<9$}{:<10$}{}", - account.program_id, - account.address, - maybe_delegated, - maybe_close_authority, - account.account.token_amount.real_number_string_trimmed(), - maybe_aux, - maybe_frozen, - delegate_padding, - close_authority_padding, - self.max_len_balance, - self.aux_len, - )?; - } else { - writeln!( - w, - "{:<44} {:<44} {:<44} {:<8$} {:<9$} {:<10$}{:<11$}{}", - account.program_id, - account.account.mint, - account.address, - maybe_delegated, - maybe_close_authority, - account.account.token_amount.real_number_string_trimmed(), - maybe_aux, - maybe_frozen, - delegate_padding, - close_authority_padding, - self.max_len_balance, - self.aux_len, - )?; - } - } - } - for unsupported_account in &self.unsupported_accounts { - writeln!( - w, - "{:<44} {}", - unsupported_account.address, unsupported_account.err - )?; - } - if gc_alert { - writeln!(w)?; - writeln!(w, "* Please run `spl-token gc` to clean up Aux accounts")?; - } - Ok(()) - } -} - -impl fmt::Display for CliTokenAccounts { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut gc_alert = false; - let header = if self.explicit_token { - format!("{:<1$}", "Balance", self.max_len_balance) - } else { - format!("{:<44} {:<2$}", "Token", "Balance", self.max_len_balance) - }; - writeln!(f, "{}", header)?; - writeln!(f, "{}", "-".repeat(header.len() + self.aux_len))?; - - for accounts_list in self.accounts.iter() { - let mut aux_counter = 1; - for account in accounts_list { - let maybe_aux = if !account.is_associated { - gc_alert = true; - let message = format!(" (Aux-{}*)", aux_counter); - aux_counter += 1; - message - } else { - "".to_string() - }; - let maybe_frozen = if let UiAccountState::Frozen = account.account.state { - format!(" {} Frozen", WARNING) - } else { - "".to_string() - }; - if self.explicit_token { - writeln!( - f, - "{:<3$}{:<4$}{}", - account.account.token_amount.real_number_string_trimmed(), - maybe_aux, - maybe_frozen, - self.max_len_balance, - self.aux_len, - )?; - } else { - writeln!( - f, - "{:<44} {:<4$}{:<5$}{}", - account.account.mint, - account.account.token_amount.real_number_string_trimmed(), - maybe_aux, - maybe_frozen, - self.max_len_balance, - self.aux_len, - )?; - } - } - } - for unsupported_account in &self.unsupported_accounts { - writeln!( - f, - "{:<44} {}", - unsupported_account.address, unsupported_account.err - )?; - } - if gc_alert { - writeln!(f)?; - writeln!(f, "* Please run `spl-token gc` to clean up Aux accounts")?; - } - Ok(()) - } -} - -fn display_ui_extension( - f: &mut fmt::Formatter, - epoch: u64, - ui_extension: &UiExtension, -) -> fmt::Result { - match ui_extension { - UiExtension::TransferFeeConfig(UiTransferFeeConfig { - transfer_fee_config_authority, - withdraw_withheld_authority, - withheld_amount, - older_transfer_fee, - newer_transfer_fee, - }) => { - writeln!(f, " {}", style("Transfer fees:").bold())?; - - if epoch >= newer_transfer_fee.epoch { - writeln!( - f, - " {} {}bps", - style("Current fee:").bold(), - newer_transfer_fee.transfer_fee_basis_points - )?; - writeln_name_value( - f, - " Current maximum:", - &newer_transfer_fee.maximum_fee.to_string(), - )?; - } else { - writeln!( - f, - " {} {}bps", - style("Current fee:").bold(), - older_transfer_fee.transfer_fee_basis_points - )?; - writeln_name_value( - f, - " Current maximum:", - &older_transfer_fee.maximum_fee.to_string(), - )?; - writeln!( - f, - " {} {}bps", - style("Upcoming fee:").bold(), - newer_transfer_fee.transfer_fee_basis_points - )?; - writeln_name_value( - f, - " Upcoming maximum:", - &newer_transfer_fee.maximum_fee.to_string(), - )?; - writeln!( - f, - " {} Epoch {} ({} epochs)", - style("Switchover at:").bold(), - newer_transfer_fee.epoch, - newer_transfer_fee.epoch - epoch - )?; - } - - writeln_name_value( - f, - " Config authority:", - transfer_fee_config_authority - .as_ref() - .unwrap_or(&String::new()), - )?; - writeln_name_value( - f, - " Withdrawal authority:", - withdraw_withheld_authority - .as_ref() - .unwrap_or(&String::new()), - )?; - writeln_name_value(f, " Withheld fees:", &withheld_amount.to_string()) - } - UiExtension::TransferFeeAmount(UiTransferFeeAmount { withheld_amount }) => { - writeln_name_value(f, " Transfer fees withheld:", &withheld_amount.to_string()) - } - UiExtension::MintCloseAuthority(UiMintCloseAuthority { close_authority }) => { - if let Some(close_authority) = close_authority { - writeln_name_value(f, " Close authority:", close_authority) - } else { - Ok(()) - } - } - UiExtension::DefaultAccountState(UiDefaultAccountState { account_state }) => { - writeln_name_value(f, " Default state:", &format!("{:?}", account_state)) - } - UiExtension::ImmutableOwner => writeln!(f, " {}", style("Immutable owner").bold()), - UiExtension::MemoTransfer(UiMemoTransfer { - require_incoming_transfer_memos, - }) => writeln_name_value( - f, - " Transfer memo:", - if *require_incoming_transfer_memos { - "Required" - } else { - "Not required" - }, - ), - UiExtension::NonTransferable | UiExtension::NonTransferableAccount => { - writeln!(f, " {}", style("Non-transferable").bold()) - } - UiExtension::InterestBearingConfig(UiInterestBearingConfig { - rate_authority, - pre_update_average_rate, - current_rate, - .. - }) => { - writeln!(f, " {}", style("Interest-bearing:").bold())?; - writeln!( - f, - " {} {}bps", - style("Current rate:").bold(), - current_rate - )?; - writeln!( - f, - " {} {}bps", - style("Average rate:").bold(), - pre_update_average_rate - )?; - writeln_name_value( - f, - " Rate authority:", - rate_authority.as_ref().unwrap_or(&String::new()), - ) - } - UiExtension::CpiGuard(UiCpiGuard { lock_cpi }) => writeln_name_value( - f, - " CPI Guard:", - if *lock_cpi { "Enabled" } else { "Disabled" }, - ), - UiExtension::PermanentDelegate(UiPermanentDelegate { delegate }) => { - if let Some(delegate) = delegate { - writeln_name_value(f, " Permanent delegate:", delegate) - } else { - Ok(()) - } - } - UiExtension::ConfidentialTransferAccount(UiConfidentialTransferAccount { - approved, - elgamal_pubkey, - pending_balance_lo, - pending_balance_hi, - available_balance, - decryptable_available_balance, - allow_confidential_credits, - allow_non_confidential_credits, - pending_balance_credit_counter, - maximum_pending_balance_credit_counter, - expected_pending_balance_credit_counter, - actual_pending_balance_credit_counter, - }) => { - writeln!(f, " {}", style("Confidential transfer:").bold())?; - writeln_name_value(f, " Approved:", &format!("{approved}"))?; - writeln_name_value(f, " Encryption key:", elgamal_pubkey)?; - writeln_name_value(f, " Pending Balance Low:", pending_balance_lo)?; - writeln_name_value(f, " Pending Balance High:", pending_balance_hi)?; - writeln_name_value(f, " Available Balance:", available_balance)?; - writeln_name_value( - f, - " Decryptable Available Balance:", - decryptable_available_balance, - )?; - writeln_name_value( - f, - " Confidential Credits:", - if *allow_confidential_credits { - "Enabled" - } else { - "Disabled" - }, - )?; - writeln_name_value( - f, - " Non-Confidential Credits:", - if *allow_non_confidential_credits { - "Enabled" - } else { - "Disabled" - }, - )?; - writeln_name_value( - f, - " Pending Balance Credit Counter:", - &format!("{pending_balance_credit_counter}"), - )?; - writeln_name_value( - f, - " Maximum Pending Balance Credit Counter:", - &format!("{maximum_pending_balance_credit_counter}"), - )?; - writeln_name_value( - f, - " Expected Pending Balance Credit Counter:", - &format!("{expected_pending_balance_credit_counter}"), - )?; - writeln_name_value( - f, - " Actual Pending Balance Credit Counter:", - &format!("{actual_pending_balance_credit_counter}"), - ) - } - UiExtension::ConfidentialTransferMint(UiConfidentialTransferMint { - authority, - auto_approve_new_accounts, - auditor_elgamal_pubkey, - }) => { - writeln!(f, " {}", style("Confidential transfer:").bold())?; - writeln!( - f, - " {}: {}", - style("Authority").bold(), - if let Some(authority) = authority.as_ref() { - authority - } else { - "authority disabled" - } - )?; - writeln!( - f, - " {}: {}", - style("Account approve policy").bold(), - if *auto_approve_new_accounts { - "auto" - } else { - "manual" - }, - )?; - writeln!( - f, - " {}: {}", - style("Audit key").bold(), - if let Some(auditor_pubkey) = auditor_elgamal_pubkey.as_ref() { - auditor_pubkey - } else { - "audits are disabled" - } - ) - } - UiExtension::ConfidentialTransferFeeConfig(UiConfidentialTransferFeeConfig { - authority, - withdraw_withheld_authority_elgamal_pubkey, - harvest_to_mint_enabled, - withheld_amount, - }) => { - writeln!(f, " {}", style("Confidential transfer fee:").bold())?; - writeln_name_value( - f, - " Authority:", - if let Some(pubkey) = authority { - pubkey - } else { - "Disabled" - }, - )?; - writeln_name_value( - f, - " Withdraw Withheld Encryption key:", - if let Some(pubkey) = withdraw_withheld_authority_elgamal_pubkey { - pubkey - } else { - "Disabled" - }, - )?; - writeln_name_value( - f, - " Harvest to mint:", - if *harvest_to_mint_enabled { - "Enabled" - } else { - "Disabled" - }, - )?; - writeln_name_value(f, " Withheld Amount:", withheld_amount) - } - UiExtension::ConfidentialTransferFeeAmount(UiConfidentialTransferFeeAmount { - withheld_amount, - }) => writeln_name_value(f, " Confidential Transfer Fee Amount:", withheld_amount), - UiExtension::TransferHook(UiTransferHook { - authority, - program_id, - }) => { - writeln!(f, " {}", style("Transfer Hook:").bold())?; - writeln_name_value( - f, - " Authority:", - if let Some(pubkey) = authority { - pubkey - } else { - "Disabled" - }, - )?; - writeln_name_value( - f, - " Program Id:", - if let Some(pubkey) = program_id { - pubkey - } else { - "Disabled" - }, - ) - } - // don't display the "transferring" flag, since it's just for internal use - UiExtension::TransferHookAccount(UiTransferHookAccount { .. }) => Ok(()), - UiExtension::MetadataPointer(UiMetadataPointer { - authority, - metadata_address, - }) => { - writeln!(f, " {}", style("Metadata Pointer:").bold())?; - writeln_name_value( - f, - " Authority:", - if let Some(pubkey) = authority { - pubkey - } else { - "Disabled" - }, - )?; - writeln_name_value( - f, - " Metadata address:", - if let Some(pubkey) = metadata_address { - pubkey - } else { - "Disabled" - }, - ) - } - UiExtension::TokenMetadata(UiTokenMetadata { - update_authority, - mint, - name, - symbol, - uri, - additional_metadata, - }) => { - writeln!(f, " {}", style("Metadata:").bold())?; - writeln_name_value( - f, - " Update Authority:", - if let Some(pubkey) = update_authority { - pubkey - } else { - "Disabled" - }, - )?; - writeln_name_value(f, " Mint:", mint)?; - writeln_name_value(f, " Name:", name)?; - writeln_name_value(f, " Symbol:", symbol)?; - writeln_name_value(f, " URI:", uri)?; - for (key, value) in additional_metadata { - writeln_name_value(f, &format!(" {key}:"), value)?; - } - Ok(()) - } - UiExtension::GroupPointer(UiGroupPointer { - authority, - group_address, - }) => { - writeln!(f, " {}", style("Group Pointer:").bold())?; - writeln_name_value( - f, - " Authority:", - if let Some(pubkey) = authority { - pubkey - } else { - "Disabled" - }, - )?; - writeln_name_value( - f, - " Group address:", - if let Some(pubkey) = group_address { - pubkey - } else { - "Disabled" - }, - ) - } - UiExtension::GroupMemberPointer(UiGroupMemberPointer { - authority, - member_address, - }) => { - writeln!(f, " {}", style("Group Member Pointer:").bold())?; - writeln_name_value( - f, - " Authority:", - if let Some(pubkey) = authority { - pubkey - } else { - "Disabled" - }, - )?; - writeln_name_value( - f, - " Member address:", - if let Some(pubkey) = member_address { - pubkey - } else { - "Disabled" - }, - ) - } - UiExtension::TokenGroup(UiTokenGroup { - update_authority, - mint, - size, - max_size, - }) => { - writeln!(f, " {}", style("Token Group:").bold())?; - writeln_name_value( - f, - " Update Authority:", - if let Some(pubkey) = update_authority { - pubkey - } else { - "Disabled" - }, - )?; - writeln_name_value(f, " Mint:", mint)?; - writeln_name_value(f, " Size:", &format!("{size}"))?; - writeln_name_value(f, " Max Size:", &format!("{max_size}")) - } - UiExtension::TokenGroupMember(UiTokenGroupMember { - mint, - group, - member_number, - }) => { - writeln!(f, " {}", style("Token Group Member:").bold())?; - writeln_name_value(f, " Mint:", mint)?; - writeln_name_value(f, " Group:", group)?; - writeln_name_value(f, " Member Number:", &format!("{member_number}")) - } - // ExtensionType::Uninitialized is a hack to ensure a mint/account is never the same length - // as a multisig - UiExtension::Uninitialized => Ok(()), - UiExtension::UnparseableExtension => writeln_name_value( - f, - " Unparseable extension:", - "Consider upgrading to a newer version of spl-token", - ), - // remove when upgrading v2.1.1+ and match on ConfidentialMintBurn - #[allow(unreachable_patterns)] - _ => Ok(()), - } -} - -fn flattened( - vec: &[Vec], - serializer: S, -) -> Result { - let flattened: Vec<_> = vec.iter().flatten().collect(); - flattened.serialize(serializer) -} diff --git a/token/cli/src/sort.rs b/token/cli/src/sort.rs deleted file mode 100644 index b82dbe41711..00000000000 --- a/token/cli/src/sort.rs +++ /dev/null @@ -1,124 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -use { - crate::{ - clap_app::Error, - output::{CliTokenAccount, CliTokenAccounts}, - }, - serde::{Deserialize, Serialize}, - solana_account_decoder::{parse_token::TokenAccountType, UiAccountData}, - solana_client::rpc_response::RpcKeyedAccount, - solana_sdk::pubkey::Pubkey, - spl_associated_token_account_client::address::get_associated_token_address_with_program_id, - std::{ - collections::{btree_map::Entry, BTreeMap}, - str::FromStr, - }, -}; - -#[derive(Serialize, Deserialize)] -pub(crate) struct UnsupportedAccount { - pub address: String, - pub err: String, -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub(crate) enum AccountFilter { - Delegated, - ExternallyCloseable, - All, -} - -pub(crate) fn sort_and_parse_token_accounts( - owner: &Pubkey, - accounts: Vec, - explicit_token: bool, - account_filter: AccountFilter, -) -> Result { - let mut cli_accounts: BTreeMap<(Pubkey, Pubkey), Vec> = BTreeMap::new(); - let mut unsupported_accounts = vec![]; - let mut max_len_balance = 0; - let mut aux_count = 0; - - for keyed_account in accounts { - let address_str = keyed_account.pubkey; - let address = Pubkey::from_str(&address_str)?; - let program_id = Pubkey::from_str(&keyed_account.account.owner)?; - - if let UiAccountData::Json(parsed_account) = keyed_account.account.data { - match serde_json::from_value(parsed_account.parsed) { - Ok(TokenAccountType::Account(ui_token_account)) => { - let mint = Pubkey::from_str(&ui_token_account.mint)?; - let btree_key = (program_id, mint); - let is_associated = - get_associated_token_address_with_program_id(owner, &mint, &program_id) - == address; - - match account_filter { - AccountFilter::Delegated if ui_token_account.delegate.is_none() => continue, - AccountFilter::ExternallyCloseable - if ui_token_account.close_authority.is_none() => - { - continue - } - _ => (), - } - - if !is_associated { - aux_count += 1; - } - - max_len_balance = max_len_balance.max( - ui_token_account - .token_amount - .real_number_string_trimmed() - .len(), - ); - - let cli_account = CliTokenAccount { - address: address_str, - program_id: program_id.to_string(), - account: ui_token_account, - is_associated, - has_permanent_delegate: false, - }; - - let entry = cli_accounts.entry(btree_key); - match entry { - Entry::Occupied(_) => { - entry.and_modify(|e| { - if is_associated { - e.insert(0, cli_account) - } else { - e.push(cli_account) - } - }); - } - Entry::Vacant(_) => { - entry.or_insert_with(|| vec![cli_account]); - } - } - } - Ok(_) => unsupported_accounts.push(UnsupportedAccount { - address: address_str, - err: "Not a token account".to_string(), - }), - Err(err) => unsupported_accounts.push(UnsupportedAccount { - address: address_str, - err: format!("Account parse failure: {}", err), - }), - } - } - } - - Ok(CliTokenAccounts { - accounts: cli_accounts.into_values().collect(), - unsupported_accounts, - max_len_balance, - aux_len: if aux_count > 0 { - format!(" (Aux-{}*)", aux_count).chars().count() + 1 - } else { - 0 - }, - explicit_token, - }) -} diff --git a/token/cli/tests/command.rs b/token/cli/tests/command.rs deleted file mode 100644 index 34bd1e7f9f7..00000000000 --- a/token/cli/tests/command.rs +++ /dev/null @@ -1,4330 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -use { - libtest_mimic::{Arguments, Trial}, - solana_cli_output::OutputFormat, - solana_client::{nonblocking::rpc_client::RpcClient, rpc_request::TokenAccountsFilter}, - solana_sdk::{ - bpf_loader_upgradeable, - hash::Hash, - program_option::COption, - program_pack::Pack, - pubkey::Pubkey, - signature::{write_keypair_file, Keypair, Signer}, - system_instruction, system_program, - transaction::Transaction, - }, - solana_test_validator::{TestValidator, TestValidatorGenesis, UpgradeableProgramInfo}, - spl_associated_token_account_client::address::get_associated_token_address_with_program_id, - spl_token_2022::{ - extension::{ - confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, - confidential_transfer_fee::ConfidentialTransferFeeConfig, - cpi_guard::CpiGuard, - default_account_state::DefaultAccountState, - group_member_pointer::GroupMemberPointer, - group_pointer::GroupPointer, - interest_bearing_mint::InterestBearingConfig, - memo_transfer::MemoTransfer, - metadata_pointer::MetadataPointer, - non_transferable::NonTransferable, - transfer_fee::{TransferFeeAmount, TransferFeeConfig}, - transfer_hook::TransferHook, - BaseStateWithExtensions, StateWithExtensionsOwned, - }, - instruction::create_native_mint, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, - state::{Account, AccountState, Mint, Multisig}, - }, - spl_token_cli::{ - clap_app::*, - command::{process_command, CommandResult}, - config::Config, - }, - spl_token_client::{ - client::{ - ProgramClient, ProgramOfflineClient, ProgramRpcClient, ProgramRpcClientSendTransaction, - }, - token::{ComputeUnitLimit, Token}, - }, - spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, - spl_token_metadata_interface::state::TokenMetadata, - std::{ - ffi::{OsStr, OsString}, - path::PathBuf, - str::FromStr, - sync::Arc, - }, - tempfile::NamedTempFile, -}; - -macro_rules! async_trial { - ($test_func:ident, $test_validator:ident, $payer:ident) => {{ - let local_test_validator = $test_validator.clone(); - let local_payer = $payer.clone(); - Trial::test(stringify!($test_func), move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - $test_func(local_test_validator.as_ref(), local_payer.as_ref()).await - }); - Ok(()) - }) - }}; -} - -#[tokio::main] -async fn main() { - let args = Arguments::from_args(); - let (test_validator, payer) = new_validator_for_test().await; - let test_validator = Arc::new(test_validator); - let payer = Arc::new(payer); - - // setup the native mint to be used by other tests - do_create_native_mint(&test_validator.get_async_rpc_client(), payer.as_ref()).await; - - // the GC test requires its own whole environment - let (gc_test_validator, gc_payer) = new_validator_for_test().await; - let gc_test_validator = Arc::new(gc_test_validator); - let gc_payer = Arc::new(gc_payer); - - // maybe come up with a way to do this through a some macro tag on the function? - let tests = vec![ - async_trial!(create_token_default, test_validator, payer), - async_trial!(create_token_2022, test_validator, payer), - async_trial!(create_token_interest_bearing, test_validator, payer), - async_trial!(set_interest_rate, test_validator, payer), - async_trial!(supply, test_validator, payer), - async_trial!(create_account_default, test_validator, payer), - async_trial!(account_info, test_validator, payer), - async_trial!(balance, test_validator, payer), - async_trial!(mint, test_validator, payer), - async_trial!(balance_after_mint, test_validator, payer), - async_trial!(balance_after_mint_with_owner, test_validator, payer), - async_trial!(accounts, test_validator, payer), - async_trial!(accounts_with_owner, test_validator, payer), - async_trial!(wrapped_sol, test_validator, payer), - async_trial!(transfer, test_validator, payer), - async_trial!(transfer_fund_recipient, test_validator, payer), - async_trial!(transfer_non_standard_recipient, test_validator, payer), - async_trial!(allow_non_system_account_recipient, test_validator, payer), - async_trial!(close_account, test_validator, payer), - async_trial!(disable_mint_authority, test_validator, payer), - async_trial!(set_owner, test_validator, payer), - async_trial!(transfer_with_account_delegate, test_validator, payer), - async_trial!(burn, test_validator, payer), - async_trial!(burn_with_account_delegate, test_validator, payer), - async_trial!(burn_with_permanent_delegate, test_validator, payer), - async_trial!(transfer_with_permanent_delegate, test_validator, payer), - async_trial!(close_mint, test_validator, payer), - async_trial!(required_transfer_memos, test_validator, payer), - async_trial!(cpi_guard, test_validator, payer), - async_trial!(immutable_accounts, test_validator, payer), - async_trial!(non_transferable, test_validator, payer), - async_trial!(default_account_state, test_validator, payer), - async_trial!(transfer_fee, test_validator, payer), - async_trial!(transfer_fee_basis_point, test_validator, payer), - async_trial!(confidential_transfer, test_validator, payer), - async_trial!(multisig_transfer, test_validator, payer), - async_trial!(offline_multisig_transfer_with_nonce, test_validator, payer), - async_trial!( - withdraw_excess_lamports_from_multisig, - test_validator, - payer - ), - async_trial!(withdraw_excess_lamports_from_mint, test_validator, payer), - async_trial!(withdraw_excess_lamports_from_account, test_validator, payer), - async_trial!(metadata_pointer, test_validator, payer), - async_trial!(group_pointer, test_validator, payer), - async_trial!(group_member_pointer, test_validator, payer), - async_trial!(transfer_hook, test_validator, payer), - async_trial!(transfer_hook_with_transfer_fee, test_validator, payer), - async_trial!(metadata, test_validator, payer), - async_trial!(group, test_validator, payer), - async_trial!(confidential_transfer_with_fee, test_validator, payer), - async_trial!(compute_budget, test_validator, payer), - // GC messes with every other test, so have it on its own test validator - async_trial!(gc, gc_test_validator, gc_payer), - ]; - - libtest_mimic::run(&args, tests).exit(); -} - -fn clone_keypair(keypair: &Keypair) -> Keypair { - Keypair::from_bytes(&keypair.to_bytes()).unwrap() -} - -const TEST_DECIMALS: u8 = 9; - -async fn new_validator_for_test() -> (TestValidator, Keypair) { - solana_logger::setup(); - let mut test_validator_genesis = TestValidatorGenesis::default(); - test_validator_genesis.add_upgradeable_programs_with_path(&[ - UpgradeableProgramInfo { - program_id: spl_token::id(), - loader: bpf_loader_upgradeable::id(), - program_path: PathBuf::from("../../target/deploy/spl_token.so"), - upgrade_authority: Pubkey::new_unique(), - }, - UpgradeableProgramInfo { - program_id: spl_associated_token_account_client::program::id(), - loader: bpf_loader_upgradeable::id(), - program_path: PathBuf::from("../../target/deploy/spl_associated_token_account.so"), - upgrade_authority: Pubkey::new_unique(), - }, - UpgradeableProgramInfo { - program_id: spl_token_2022::id(), - loader: bpf_loader_upgradeable::id(), - program_path: PathBuf::from("../../target/deploy/spl_token_2022.so"), - upgrade_authority: Pubkey::new_unique(), - }, - ]); - test_validator_genesis.start_async().await -} - -fn test_config_with_default_signer<'a>( - test_validator: &TestValidator, - payer: &Keypair, - program_id: &Pubkey, -) -> Config<'a> { - let websocket_url = test_validator.rpc_pubsub_url(); - let rpc_client = Arc::new(test_validator.get_async_rpc_client()); - let program_client: Arc> = Arc::new( - ProgramRpcClient::new(rpc_client.clone(), ProgramRpcClientSendTransaction), - ); - Config { - rpc_client, - program_client, - websocket_url, - output_format: OutputFormat::JsonCompact, - fee_payer: Some(Arc::new(clone_keypair(payer))), - default_signer: Some(Arc::new(clone_keypair(payer))), - nonce_account: None, - nonce_authority: None, - nonce_blockhash: None, - sign_only: false, - dump_transaction_message: false, - multisigner_pubkeys: vec![], - program_id: *program_id, - restrict_to_program_id: true, - compute_unit_price: None, - compute_unit_limit: ComputeUnitLimit::Simulated, - } -} - -fn test_config_without_default_signer<'a>( - test_validator: &TestValidator, - program_id: &Pubkey, -) -> Config<'a> { - let websocket_url = test_validator.rpc_pubsub_url(); - let rpc_client = Arc::new(test_validator.get_async_rpc_client()); - let program_client: Arc> = Arc::new( - ProgramRpcClient::new(rpc_client.clone(), ProgramRpcClientSendTransaction), - ); - Config { - rpc_client, - program_client, - websocket_url, - output_format: OutputFormat::JsonCompact, - fee_payer: None, - default_signer: None, - nonce_account: None, - nonce_authority: None, - nonce_blockhash: None, - sign_only: false, - dump_transaction_message: false, - multisigner_pubkeys: vec![], - program_id: *program_id, - restrict_to_program_id: true, - compute_unit_price: None, - compute_unit_limit: ComputeUnitLimit::Simulated, - } -} - -async fn create_nonce(config: &Config<'_>, authority: &Keypair) -> Pubkey { - let nonce = Keypair::new(); - - let nonce_rent = config - .rpc_client - .get_minimum_balance_for_rent_exemption(solana_sdk::nonce::State::size()) - .await - .unwrap(); - let instr = system_instruction::create_nonce_account( - &authority.pubkey(), - &nonce.pubkey(), - &authority.pubkey(), // Make the fee payer the nonce account authority - nonce_rent, - ); - - let blockhash = config.rpc_client.get_latest_blockhash().await.unwrap(); - let tx = Transaction::new_signed_with_payer( - &instr, - Some(&authority.pubkey()), - &[&nonce, authority], - blockhash, - ); - - config - .rpc_client - .send_and_confirm_transaction(&tx) - .await - .unwrap(); - nonce.pubkey() -} - -async fn do_create_native_mint(rpc_client: &RpcClient, payer: &Keypair) { - let native_mint = spl_token_2022::native_mint::id(); - if rpc_client.get_account(&native_mint).await.is_err() { - let transaction = Transaction::new_signed_with_payer( - &[create_native_mint(&spl_token_2022::id(), &payer.pubkey()).unwrap()], - Some(&payer.pubkey()), - &[payer], - rpc_client.get_latest_blockhash().await.unwrap(), - ); - rpc_client - .send_and_confirm_transaction(&transaction) - .await - .unwrap(); - } -} - -async fn create_token(config: &Config<'_>, payer: &Keypair) -> Pubkey { - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - process_test_command( - config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - ], - ) - .await - .unwrap(); - token.pubkey() -} - -async fn create_interest_bearing_token( - config: &Config<'_>, - payer: &Keypair, - rate_bps: i16, -) -> Pubkey { - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - process_test_command( - config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--interest-rate", - &rate_bps.to_string(), - ], - ) - .await - .unwrap(); - token.pubkey() -} - -async fn create_auxiliary_account(config: &Config<'_>, payer: &Keypair, mint: Pubkey) -> Pubkey { - let auxiliary = Keypair::new(); - let auxiliary_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&auxiliary, &auxiliary_keypair_file).unwrap(); - process_test_command( - config, - payer, - &[ - "spl-token", - CommandName::CreateAccount.into(), - &mint.to_string(), - auxiliary_keypair_file.path().to_str().unwrap(), - ], - ) - .await - .unwrap(); - auxiliary.pubkey() -} - -async fn create_associated_account( - config: &Config<'_>, - payer: &Keypair, - mint: &Pubkey, - owner: &Pubkey, -) -> Pubkey { - process_test_command( - config, - payer, - &[ - "spl-token", - CommandName::CreateAccount.into(), - &mint.to_string(), - "--owner", - &owner.to_string(), - ], - ) - .await - .unwrap(); - get_associated_token_address_with_program_id(owner, mint, &config.program_id) -} - -async fn mint_tokens( - config: &Config<'_>, - payer: &Keypair, - mint: Pubkey, - ui_amount: f64, - recipient: Pubkey, -) -> CommandResult { - process_test_command( - config, - payer, - &[ - "spl-token", - CommandName::Mint.into(), - &mint.to_string(), - &ui_amount.to_string(), - &recipient.to_string(), - ], - ) - .await -} - -async fn run_transfer_test(config: &Config<'_>, payer: &Keypair) { - let token = create_token(config, payer).await; - let source = create_associated_account(config, payer, &token, &payer.pubkey()).await; - let destination = create_auxiliary_account(config, payer, token).await; - let ui_amount = 100.0; - mint_tokens(config, payer, token, ui_amount, source) - .await - .unwrap(); - let result = process_test_command( - config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - &token.to_string(), - "10", - &destination.to_string(), - ], - ) - .await; - result.unwrap(); - - let account = config.rpc_client.get_account(&source).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); - assert_eq!(token_account.base.amount, amount); - let account = config.rpc_client.get_account(&destination).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let amount = spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS); - assert_eq!(token_account.base.amount, amount); -} - -async fn process_test_command(config: &Config<'_>, payer: &Keypair, args: I) -> CommandResult -where - I: IntoIterator, - T: Into + Clone, -{ - let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); - let minimum_signers_help = minimum_signers_help_string(); - let multisig_member_help = multisig_member_help_string(); - - let app_matches = app( - &default_decimals, - &minimum_signers_help, - &multisig_member_help, - ) - .get_matches_from(args); - let (sub_command, matches) = app_matches.subcommand().unwrap(); - let sub_command = CommandName::from_str(sub_command).unwrap(); - - let wallet_manager = None; - let bulk_signers: Vec> = vec![Arc::new(clone_keypair(payer))]; - process_command(&sub_command, matches, config, wallet_manager, bulk_signers).await -} - -async fn exec_test_cmd>(config: &Config<'_>, args: &[T]) -> CommandResult { - let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); - let minimum_signers_help = minimum_signers_help_string(); - let multisig_member_help = multisig_member_help_string(); - - let app_matches = app( - &default_decimals, - &minimum_signers_help, - &multisig_member_help, - ) - .get_matches_from(args); - let (sub_command, matches) = app_matches.subcommand().unwrap(); - let sub_command = CommandName::from_str(sub_command).unwrap(); - - let mut wallet_manager = None; - let mut bulk_signers: Vec> = Vec::new(); - let mut multisigner_ids = Vec::new(); - - let config = Config::new_with_clients_and_ws_url( - matches, - &mut wallet_manager, - &mut bulk_signers, - &mut multisigner_ids, - config.rpc_client.clone(), - config.program_client.clone(), - config.websocket_url.clone(), - ) - .await; - - process_command(&sub_command, matches, &config, wallet_manager, bulk_signers).await -} - -async fn create_token_default(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let result = process_test_command( - &config, - payer, - &["spl-token", CommandName::CreateToken.into()], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - assert_eq!(account.owner, *program_id); - } -} - -async fn create_token_2022(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - let mut wallet_manager = None; - let mut bulk_signers: Vec> = Vec::new(); - let mut multisigner_ids = Vec::new(); - - let args = &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-2022", - ]; - - let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS); - let minimum_signers_help = minimum_signers_help_string(); - let multisig_member_help = multisig_member_help_string(); - - let app_matches = app( - &default_decimals, - &minimum_signers_help, - &multisig_member_help, - ) - .get_matches_from(args); - let (_, matches) = app_matches.subcommand().unwrap(); - - let config = Config::new_with_clients_and_ws_url( - matches, - &mut wallet_manager, - &mut bulk_signers, - &mut multisigner_ids, - config.rpc_client.clone(), - config.program_client.clone(), - config.websocket_url.clone(), - ) - .await; - - assert_eq!(config.program_id, spl_token_2022::ID); -} - -async fn create_token_interest_bearing(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - let rate_bps: i16 = 100; - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--interest-rate", - &rate_bps.to_string(), - ], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_account - .get_extension::() - .unwrap(); - assert_eq!(account.owner, spl_token_2022::id()); - assert_eq!(i16::from(extension.current_rate), rate_bps); - assert_eq!( - Option::::from(extension.rate_authority), - Some(payer.pubkey()) - ); -} - -async fn set_interest_rate(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - let initial_rate: i16 = 100; - let new_rate: i16 = 300; - let token = create_interest_bearing_token(&config, payer, initial_rate).await; - let account = config.rpc_client.get_account(&token).await.unwrap(); - let mint_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_account - .get_extension::() - .unwrap(); - assert_eq!(account.owner, spl_token_2022::id()); - assert_eq!(i16::from(extension.current_rate), initial_rate); - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::SetInterestRate.into(), - &token.to_string(), - &new_rate.to_string(), - ], - ) - .await; - let _value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let account = config.rpc_client.get_account(&token).await.unwrap(); - let mint_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_account - .get_extension::() - .unwrap(); - assert_eq!(i16::from(extension.current_rate), new_rate); -} - -async fn supply(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let result = process_test_command( - &config, - payer, - &["spl-token", CommandName::Supply.into(), &token.to_string()], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - assert_eq!(value["amount"], "0"); - assert_eq!(value["uiAmountString"], "0"); - } -} - -async fn create_account_default(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateAccount.into(), - &token.to_string(), - ], - ) - .await; - result.unwrap(); - } -} - -async fn account_info(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let _account = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::AccountInfo.into(), - &token.to_string(), - ], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let account = get_associated_token_address_with_program_id( - &payer.pubkey(), - &token, - &config.program_id, - ); - assert_eq!(value["address"], account.to_string()); - assert_eq!(value["mint"], token.to_string()); - assert_eq!(value["isAssociated"], true); - assert_eq!(value["isNative"], false); - assert_eq!(value["owner"], payer.pubkey().to_string()); - assert_eq!(value["state"], "initialized"); - } -} - -async fn balance(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let _account = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let result = process_test_command( - &config, - payer, - &["spl-token", CommandName::Balance.into(), &token.to_string()], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - assert_eq!(value["amount"], "0"); - assert_eq!(value["uiAmountString"], "0"); - } -} - -async fn mint(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let account = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let mut amount = 0; - - // mint via implicit owner - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Mint.into(), - &token.to_string(), - "1", - ], - ) - .await - .unwrap(); - amount += spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); - - let account_data = config.rpc_client.get_account(&account).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); - assert_eq!(token_account.base.amount, amount); - assert_eq!(token_account.base.mint, token); - assert_eq!(token_account.base.owner, payer.pubkey()); - - // mint via explicit recipient - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Mint.into(), - &token.to_string(), - "1", - &account.to_string(), - ], - ) - .await - .unwrap(); - amount += spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); - - let account_data = config.rpc_client.get_account(&account).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); - assert_eq!(token_account.base.amount, amount); - assert_eq!(token_account.base.mint, token); - assert_eq!(token_account.base.owner, payer.pubkey()); - - // mint via explicit owner - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Mint.into(), - &token.to_string(), - "1", - "--recipient-owner", - &payer.pubkey().to_string(), - ], - ) - .await - .unwrap(); - amount += spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); - - let account_data = config.rpc_client.get_account(&account).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); - assert_eq!(token_account.base.amount, amount); - assert_eq!(token_account.base.mint, token); - assert_eq!(token_account.base.owner, payer.pubkey()); - } -} - -async fn balance_after_mint(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let account = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, account) - .await - .unwrap(); - let result = process_test_command( - &config, - payer, - &["spl-token", CommandName::Balance.into(), &token.to_string()], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let amount = spl_token::ui_amount_to_amount(ui_amount, TEST_DECIMALS); - assert_eq!(value["amount"], format!("{}", amount)); - assert_eq!(value["uiAmountString"], format!("{}", ui_amount)); - } -} - -async fn balance_after_mint_with_owner(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let account = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, account) - .await - .unwrap(); - let config = test_config_without_default_signer(test_validator, program_id); - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Balance.into(), - &token.to_string(), - "--owner", - &payer.pubkey().to_string(), - ], - ) - .await; - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let amount = spl_token::ui_amount_to_amount(ui_amount, TEST_DECIMALS); - assert_eq!(value["amount"], format!("{}", amount)); - assert_eq!(value["uiAmountString"], format!("{}", ui_amount)); - } -} - -async fn accounts(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token1 = create_token(&config, payer).await; - let _account1 = create_associated_account(&config, payer, &token1, &payer.pubkey()).await; - let token2 = create_token(&config, payer).await; - let _account2 = create_associated_account(&config, payer, &token2, &payer.pubkey()).await; - let token3 = create_token(&config, payer).await; - let result = - process_test_command(&config, payer, &["spl-token", CommandName::Accounts.into()]) - .await - .unwrap(); - assert!(result.contains(&token1.to_string())); - assert!(result.contains(&token2.to_string())); - assert!(!result.contains(&token3.to_string())); - } -} - -async fn accounts_with_owner(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token1 = create_token(&config, payer).await; - let _account1 = create_associated_account(&config, payer, &token1, &payer.pubkey()).await; - let token2 = create_token(&config, payer).await; - let _account2 = create_associated_account(&config, payer, &token2, &payer.pubkey()).await; - let token3 = create_token(&config, payer).await; - let config = test_config_without_default_signer(test_validator, program_id); - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Accounts.into(), - "--owner", - &payer.pubkey().to_string(), - ], - ) - .await - .unwrap(); - assert!(result.contains(&token1.to_string())); - assert!(result.contains(&token2.to_string())); - assert!(!result.contains(&token3.to_string())); - } -} - -async fn wrapped_sol(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let native_mint = *Token::new_native( - config.program_client.clone(), - program_id, - config.fee_payer().unwrap().clone(), - ) - .get_address(); - let _result = process_test_command( - &config, - payer, - &["spl-token", CommandName::Wrap.into(), "0.5"], - ) - .await - .unwrap(); - let wrapped_address = get_associated_token_address_with_program_id( - &payer.pubkey(), - &native_mint, - &config.program_id, - ); - let account = config - .rpc_client - .get_account(&wrapped_address) - .await - .unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.mint, native_mint); - assert_eq!(token_account.base.owner, payer.pubkey()); - assert!(token_account.base.is_native()); - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Unwrap.into(), - &wrapped_address.to_string(), - ], - ) - .await; - result.unwrap(); - config - .rpc_client - .get_account(&wrapped_address) - .await - .unwrap_err(); - - // now use `close` to close it - let token = create_token(&config, payer).await; - let source = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let _result = process_test_command( - &config, - payer, - &["spl-token", CommandName::Wrap.into(), "10.0"], - ) - .await - .unwrap(); - - let recipient = - get_associated_token_address_with_program_id(&payer.pubkey(), &native_mint, program_id); - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Close.into(), - "--address", - &source.to_string(), - "--recipient", - &recipient.to_string(), - ], - ) - .await; - result.unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&recipient) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "10000000000"); - } -} - -async fn transfer(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - run_transfer_test(&config, payer).await; - } -} - -async fn transfer_fund_recipient(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let source = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let recipient = Keypair::new().pubkey().to_string(); - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, source) - .await - .unwrap(); - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "10", - &recipient, - ], - ) - .await; - result.unwrap(); - - let account = config.rpc_client.get_account(&source).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!( - token_account.base.amount, - spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS) - ); - } -} - -async fn transfer_non_standard_recipient(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - for other_program_id in VALID_TOKEN_PROGRAM_IDS - .iter() - .filter(|id| *id != program_id) - { - let mut config = - test_config_with_default_signer(test_validator, payer, other_program_id); - let wrong_program_token = create_token(&config, payer).await; - let wrong_program_account = - create_associated_account(&config, payer, &wrong_program_token, &payer.pubkey()) - .await; - config.program_id = *program_id; - let config = config; - - let token = create_token(&config, payer).await; - let source = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let recipient = Keypair::new().pubkey(); - let recipient_token_account = get_associated_token_address_with_program_id( - &recipient, - &token, - &config.program_id, - ); - let system_token_account = get_associated_token_address_with_program_id( - &system_program::id(), - &token, - &config.program_id, - ); - let amount = 100; - mint_tokens(&config, payer, token, amount as f64, source) - .await - .unwrap(); - - // transfer fails to unfunded recipient without flag - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - &token.to_string(), - "1", - &recipient.to_string(), - ], - ) - .await - .unwrap_err(); - - // with unfunded flag, transfer goes through - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "1", - &recipient.to_string(), - ], - ) - .await - .unwrap(); - let account = config - .rpc_client - .get_account(&recipient_token_account) - .await - .unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!( - token_account.base.amount, - spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS) - ); - - // transfer fails to non-system recipient without flag - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - &token.to_string(), - "1", - &system_program::id().to_string(), - ], - ) - .await - .unwrap_err(); - - // with non-system flag, transfer goes through - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-non-system-account-recipient", - &token.to_string(), - "1", - &system_program::id().to_string(), - ], - ) - .await - .unwrap(); - let account = config - .rpc_client - .get_account(&system_token_account) - .await - .unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!( - token_account.base.amount, - spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS) - ); - - // transfer to same-program non-account fails - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-non-system-account-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "1", - &token.to_string(), - ], - ) - .await - .unwrap_err(); - - // transfer to other-program account fails - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-non-system-account-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "1", - &wrong_program_account.to_string(), - ], - ) - .await - .unwrap_err(); - } - } -} - -async fn allow_non_system_account_recipient(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token::id()); - - let token = create_token(&config, payer).await; - let source = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let recipient = Keypair::new().pubkey().to_string(); - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, source) - .await - .unwrap(); - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--fund-recipient", - "--allow-non-system-account-recipient", - "--allow-unfunded-recipient", - &token.to_string(), - "10", - &recipient, - ], - ) - .await; - result.unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); -} - -async fn close_account(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - - let native_mint = Token::new_native( - config.program_client.clone(), - program_id, - config.fee_payer().unwrap().clone(), - ); - let recipient_owner = Pubkey::new_unique(); - native_mint - .get_or_create_associated_account_info(&recipient_owner) - .await - .unwrap(); - - let token = create_token(&config, payer).await; - - let system_recipient = Keypair::new().pubkey(); - let wsol_recipient = native_mint.get_associated_token_address(&recipient_owner); - - for recipient in [system_recipient, wsol_recipient] { - let base_balance = config - .rpc_client - .get_account(&recipient) - .await - .map(|account| account.lamports) - .unwrap_or(0); - - let source = create_auxiliary_account(&config, payer, token).await; - let token_rent_amount = config - .rpc_client - .get_account(&source) - .await - .unwrap() - .lamports; - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Close.into(), - "--address", - &source.to_string(), - "--recipient", - &recipient.to_string(), - ], - ) - .await - .unwrap(); - - let recipient_data = config.rpc_client.get_account(&recipient).await.unwrap(); - - assert_eq!(recipient_data.lamports, base_balance + token_rent_amount); - if recipient == wsol_recipient { - let recipient_account = - StateWithExtensionsOwned::::unpack(recipient_data.data).unwrap(); - assert_eq!(recipient_account.base.amount, token_rent_amount); - } - } - } -} - -async fn disable_mint_authority(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &token.to_string(), - "mint", - "--disable", - ], - ) - .await; - result.unwrap(); - - let account = config.rpc_client.get_account(&token).await.unwrap(); - let mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(mint.base.mint_authority, COption::None); - } -} - -async fn gc(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let mut config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let _account = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let _aux1 = create_auxiliary_account(&config, payer, token).await; - let _aux2 = create_auxiliary_account(&config, payer, token).await; - let _aux3 = create_auxiliary_account(&config, payer, token).await; - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Accounts.into(), - &token.to_string(), - ], - ) - .await - .unwrap(); - let value: serde_json::Value = serde_json::from_str(&result).unwrap(); - assert_eq!( - value["accounts"] - .as_array() - .unwrap() - .iter() - .filter(|x| x["mint"] == token.to_string()) - .count(), - 4 - ); - config.output_format = OutputFormat::Display; // fixup eventually? - let _result = process_test_command(&config, payer, &["spl-token", CommandName::Gc.into()]) - .await - .unwrap(); - config.output_format = OutputFormat::JsonCompact; - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Accounts.into(), - &token.to_string(), - ], - ) - .await - .unwrap(); - let value: serde_json::Value = serde_json::from_str(&result).unwrap(); - assert_eq!( - value["accounts"] - .as_array() - .unwrap() - .iter() - .filter(|x| x["mint"] == token.to_string()) - .count(), - 1 - ); - - config.output_format = OutputFormat::Display; - - // test implicit transfer - let token = create_token(&config, payer).await; - let ata = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let aux = create_auxiliary_account(&config, payer, token).await; - mint_tokens(&config, payer, token, 1.0, ata).await.unwrap(); - mint_tokens(&config, payer, token, 1.0, aux).await.unwrap(); - - process_test_command(&config, payer, &["spl-token", CommandName::Gc.into()]) - .await - .unwrap(); - - let ui_ata = config - .rpc_client - .get_token_account(&ata) - .await - .unwrap() - .unwrap(); - - // aux is gone and its tokens are in ata - let amount = spl_token::ui_amount_to_amount(2.0, TEST_DECIMALS); - assert_eq!(ui_ata.token_amount.amount, format!("{amount}")); - config.rpc_client.get_account(&aux).await.unwrap_err(); - - // test ata closure - let token = create_token(&config, payer).await; - let ata = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Gc.into(), - "--close-empty-associated-accounts", - ], - ) - .await - .unwrap(); - - // ata is gone - config.rpc_client.get_account(&ata).await.unwrap_err(); - - // test a tricky corner case of both - let token = create_token(&config, payer).await; - let ata = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let aux = create_auxiliary_account(&config, payer, token).await; - mint_tokens(&config, payer, token, 1.0, aux).await.unwrap(); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Gc.into(), - "--close-empty-associated-accounts", - ], - ) - .await - .unwrap(); - - let ui_ata = config - .rpc_client - .get_token_account(&ata) - .await - .unwrap() - .unwrap(); - - // aux is gone and its tokens are in ata, and ata has not been closed - let amount = spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); - assert_eq!(ui_ata.token_amount.amount, format!("{amount}")); - config.rpc_client.get_account(&aux).await.unwrap_err(); - - // test that balance moves off an uncloseable account - let token = create_token(&config, payer).await; - let ata = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let aux = create_auxiliary_account(&config, payer, token).await; - let close_authority = Keypair::new().pubkey(); - mint_tokens(&config, payer, token, 1.0, aux).await.unwrap(); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &aux.to_string(), - "close", - &close_authority.to_string(), - ], - ) - .await - .unwrap(); - - process_test_command(&config, payer, &["spl-token", CommandName::Gc.into()]) - .await - .unwrap(); - - let ui_ata = config - .rpc_client - .get_token_account(&ata) - .await - .unwrap() - .unwrap(); - - // aux tokens are now in ata - let amount = spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS); - assert_eq!(ui_ata.token_amount.amount, format!("{amount}")); - } -} - -async fn set_owner(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let aux = create_auxiliary_account(&config, payer, token).await; - let aux_string = aux.to_string(); - let _result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &aux_string, - "owner", - &aux_string, - ], - ) - .await - .unwrap(); - let account = config.rpc_client.get_account(&aux).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!(token_account.base.mint, token); - assert_eq!(token_account.base.owner, aux); - } -} - -async fn transfer_with_account_delegate(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - - let token = create_token(&config, payer).await; - let source = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let destination = create_auxiliary_account(&config, payer, token).await; - let delegate = Keypair::new(); - - let delegate_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&delegate, &delegate_keypair_file).unwrap(); - let fee_payer_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(payer, &fee_payer_keypair_file).unwrap(); - - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, source) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - let amount = spl_token::ui_amount_to_amount(100.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); - assert_eq!(ui_account.delegate, None); - assert_eq!(ui_account.delegated_amount, None); - let ui_account = config - .rpc_client - .get_token_account(&destination) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.token_amount.amount, "0"); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Approve.into(), - &source.to_string(), - "10", - &delegate.pubkey().to_string(), - "--owner", - fee_payer_keypair_file.path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.delegate.unwrap(), delegate.pubkey().to_string()); - let amount = spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS); - assert_eq!( - ui_account.delegated_amount.unwrap().amount, - format!("{amount}") - ); - - let result = exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &token.to_string(), - "10", - &destination.to_string(), - "--from", - &source.to_string(), - "--owner", - delegate_keypair_file.path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await; - result.unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); - assert_eq!(ui_account.delegate, None); - assert_eq!(ui_account.delegated_amount, None); - let ui_account = config - .rpc_client - .get_token_account(&destination) - .await - .unwrap() - .unwrap(); - let amount = spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); - } -} - -async fn burn(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let mut config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let source = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, source) - .await - .unwrap(); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Burn.into(), - &source.to_string(), - "10", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&source).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); - assert_eq!(token_account.base.amount, amount); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Burn.into(), - &source.to_string(), - "ALL", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&source).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let amount = spl_token::ui_amount_to_amount(0.0, TEST_DECIMALS); - assert_eq!(token_account.base.amount, amount); - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Burn.into(), - &source.to_string(), - "10", - ], - ) - .await; - assert!(result.is_err()); - - // Use of the ALL keyword not supported with offline signing - config.sign_only = true; - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Burn.into(), - &source.to_string(), - "ALL", - ], - ) - .await; - assert!(result.is_err_and(|err| err.to_string().contains("ALL"))); - } -} - -async fn burn_with_account_delegate(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - - let token = create_token(&config, payer).await; - let source = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let delegate = Keypair::new(); - - let delegate_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&delegate, &delegate_keypair_file).unwrap(); - let fee_payer_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(payer, &fee_payer_keypair_file).unwrap(); - - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, source) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - let amount = spl_token::ui_amount_to_amount(100.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); - assert_eq!(ui_account.delegate, None); - assert_eq!(ui_account.delegated_amount, None); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Approve.into(), - &source.to_string(), - "10", - &delegate.pubkey().to_string(), - "--owner", - fee_payer_keypair_file.path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - assert_eq!(ui_account.delegate.unwrap(), delegate.pubkey().to_string()); - let amount = spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS); - assert_eq!( - ui_account.delegated_amount.unwrap().amount, - format!("{amount}") - ); - - let result = exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Burn.into(), - &source.to_string(), - "10", - "--owner", - delegate_keypair_file.path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await; - result.unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); - assert_eq!(ui_account.delegate, None); - assert_eq!(ui_account.delegated_amount, None); - } -} - -async fn burn_with_permanent_delegate(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - let token = token.pubkey(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--enable-permanent-delegate", - ], - ) - .await - .unwrap(); - - let permanent_delegate_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(payer, &permanent_delegate_keypair_file).unwrap(); - - let unknown_owner = Keypair::new(); - let source = - create_associated_account(&config, &unknown_owner, &token, &unknown_owner.pubkey()).await; - let ui_amount = 100.0; - - mint_tokens(&config, payer, token, ui_amount, source) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - - let amount = spl_token::ui_amount_to_amount(100.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Burn.into(), - &source.to_string(), - "10", - "--owner", - permanent_delegate_keypair_file.path().to_str().unwrap(), - ], - ) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - - let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); -} - -async fn transfer_with_permanent_delegate(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - let token = token.pubkey(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--enable-permanent-delegate", - ], - ) - .await - .unwrap(); - - let unknown_owner = Keypair::new(); - let source = - create_associated_account(&config, &unknown_owner, &token, &unknown_owner.pubkey()).await; - let destination = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - - let permanent_delegate_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(payer, &permanent_delegate_keypair_file).unwrap(); - - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, source) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - - let amount = spl_token::ui_amount_to_amount(100.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); - - let ui_account = config - .rpc_client - .get_token_account(&destination) - .await - .unwrap() - .unwrap(); - - assert_eq!(ui_account.token_amount.amount, "0"); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &token.to_string(), - "50", - &destination.to_string(), - "--from", - &source.to_string(), - "--owner", - permanent_delegate_keypair_file.path().to_str().unwrap(), - ], - ) - .await - .unwrap(); - - let ui_account = config - .rpc_client - .get_token_account(&destination) - .await - .unwrap() - .unwrap(); - - let amount = spl_token::ui_amount_to_amount(50.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); - - let ui_account = config - .rpc_client - .get_token_account(&source) - .await - .unwrap() - .unwrap(); - - let amount = spl_token::ui_amount_to_amount(50.0, TEST_DECIMALS); - assert_eq!(ui_account.token_amount.amount, format!("{amount}")); -} - -async fn close_mint(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - - let token = Keypair::new(); - let token_pubkey = token.pubkey(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--enable-close", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data); - assert!(test_mint.is_ok()); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CloseMint.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await; - assert!(account.is_err()); -} - -async fn required_transfer_memos(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, &program_id); - let token = create_token(&config, payer).await; - let destination_account = create_auxiliary_account(&config, payer, token).await; - let token_account = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - - mint_tokens(&config, payer, token, 100.0, token_account) - .await - .unwrap(); - - // enable works - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::EnableRequiredTransferMemos.into(), - &destination_account.to_string(), - ], - ) - .await - .unwrap(); - - let extensions = StateWithExtensionsOwned::::unpack( - config - .rpc_client - .get_account(&destination_account) - .await - .unwrap() - .data, - ) - .unwrap(); - let memo_transfer = extensions.get_extension::().unwrap(); - let enabled: bool = memo_transfer.require_incoming_transfer_memos.into(); - assert!(enabled); - - // transfer requires a memo - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &token_account.to_string(), - &token.to_string(), - "1", - &destination_account.to_string(), - ], - ) - .await - .unwrap_err(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &token_account.to_string(), - // malicious compliance - "--with-memo", - "memo", - &token.to_string(), - "1", - &destination_account.to_string(), - ], - ) - .await - .unwrap(); - let account_data = config - .rpc_client - .get_account(&destination_account) - .await - .unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account_data.data).unwrap(); - assert_eq!( - account_state.base.amount, - spl_token::ui_amount_to_amount(1.0, TEST_DECIMALS) - ); - - // disable works - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::DisableRequiredTransferMemos.into(), - &destination_account.to_string(), - ], - ) - .await - .unwrap(); - let extensions = StateWithExtensionsOwned::::unpack( - config - .rpc_client - .get_account(&destination_account) - .await - .unwrap() - .data, - ) - .unwrap(); - let memo_transfer = extensions.get_extension::().unwrap(); - let enabled: bool = memo_transfer.require_incoming_transfer_memos.into(); - assert!(!enabled); -} - -async fn cpi_guard(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, &program_id); - let token = create_token(&config, payer).await; - let token_account = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - - // enable works - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::EnableCpiGuard.into(), - &token_account.to_string(), - ], - ) - .await - .unwrap(); - let extensions = StateWithExtensionsOwned::::unpack( - config - .rpc_client - .get_account(&token_account) - .await - .unwrap() - .data, - ) - .unwrap(); - let cpi_guard = extensions.get_extension::().unwrap(); - let enabled: bool = cpi_guard.lock_cpi.into(); - assert!(enabled); - - // disable works - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::DisableCpiGuard.into(), - &token_account.to_string(), - ], - ) - .await - .unwrap(); - let extensions = StateWithExtensionsOwned::::unpack( - config - .rpc_client - .get_account(&token_account) - .await - .unwrap() - .data, - ) - .unwrap(); - let cpi_guard = extensions.get_extension::().unwrap(); - let enabled: bool = cpi_guard.lock_cpi.into(); - assert!(!enabled); -} - -async fn immutable_accounts(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, &program_id); - let token = create_token(&config, payer).await; - let new_owner = Keypair::new().pubkey(); - let native_mint = *Token::new_native( - config.program_client.clone(), - &program_id, - config.fee_payer().unwrap().clone(), - ) - .get_address(); - - // cannot reassign an ata - let account = create_associated_account(&config, payer, &token, &payer.pubkey()).await; - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &account.to_string(), - "owner", - &new_owner.to_string(), - ], - ) - .await; - result.unwrap_err(); - - // immutable works for create-account - let aux_account = Keypair::new(); - let aux_pubkey = aux_account.pubkey(); - let aux_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&aux_account, &aux_keypair_file).unwrap(); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateAccount.into(), - &token.to_string(), - aux_keypair_file.path().to_str().unwrap(), - "--immutable", - ], - ) - .await - .unwrap(); - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &aux_pubkey.to_string(), - "owner", - &new_owner.to_string(), - ], - ) - .await; - result.unwrap_err(); - - // immutable works for wrap - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Wrap.into(), - "--create-aux-account", - "--immutable", - "0.5", - ], - ) - .await - .unwrap(); - - let accounts = config - .rpc_client - .get_token_accounts_by_owner(&payer.pubkey(), TokenAccountsFilter::Mint(native_mint)) - .await - .unwrap(); - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &accounts[0].pubkey, - "owner", - &new_owner.to_string(), - ], - ) - .await; - result.unwrap_err(); -} - -async fn non_transferable(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - let token_pubkey = token.pubkey(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--enable-non-transferable", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert!(test_mint.get_extension::().is_ok()); - - let associated_account = - create_associated_account(&config, payer, &token_pubkey, &payer.pubkey()).await; - let aux_account = create_auxiliary_account(&config, payer, token_pubkey).await; - mint_tokens(&config, payer, token_pubkey, 100.0, associated_account) - .await - .unwrap(); - - // transfer not allowed - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &associated_account.to_string(), - &token_pubkey.to_string(), - "1", - &aux_account.to_string(), - ], - ) - .await - .unwrap_err(); -} - -async fn default_account_state(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, &program_id); - - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - let token_pubkey = token.pubkey(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--enable-freeze", - "--default-account-state", - "frozen", - ], - ) - .await - .unwrap(); - - let mint_account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint = StateWithExtensionsOwned::::unpack(mint_account.data).unwrap(); - let extension = mint.get_extension::().unwrap(); - assert_eq!(extension.state, u8::from(AccountState::Frozen)); - - let frozen_account = - create_associated_account(&config, payer, &token_pubkey, &payer.pubkey()).await; - let token_account = config - .rpc_client - .get_account(&frozen_account) - .await - .unwrap(); - let account = StateWithExtensionsOwned::::unpack(token_account.data).unwrap(); - assert_eq!(account.base.state, AccountState::Frozen); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateDefaultAccountState.into(), - &token_pubkey.to_string(), - "initialized", - ], - ) - .await - .unwrap(); - let unfrozen_account = create_auxiliary_account(&config, payer, token_pubkey).await; - let token_account = config - .rpc_client - .get_account(&unfrozen_account) - .await - .unwrap(); - let account = StateWithExtensionsOwned::::unpack(token_account.data).unwrap(); - assert_eq!(account.base.state, AccountState::Initialized); -} - -async fn transfer_fee(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - - let transfer_fee_basis_points = 100; - let maximum_fee = 10_000_000_000; - - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - let token_pubkey = token.pubkey(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--transfer-fee", - &transfer_fee_basis_points.to_string(), - &maximum_fee.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = test_mint.get_extension::().unwrap(); - assert_eq!( - u16::from(extension.older_transfer_fee.transfer_fee_basis_points), - transfer_fee_basis_points - ); - assert_eq!( - u64::from(extension.older_transfer_fee.maximum_fee), - maximum_fee - ); - assert_eq!( - u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), - transfer_fee_basis_points - ); - assert_eq!( - u64::from(extension.newer_transfer_fee.maximum_fee), - maximum_fee - ); - - let total_amount = 1000.0; - let transfer_amount = 100.0; - let token_account = - create_associated_account(&config, payer, &token_pubkey, &payer.pubkey()).await; - let source_account = create_auxiliary_account(&config, payer, token_pubkey).await; - mint_tokens(&config, payer, token_pubkey, total_amount, source_account) - .await - .unwrap(); - - // withdraw from account directly - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &source_account.to_string(), - &token_pubkey.to_string(), - &transfer_amount.to_string(), - &token_account.to_string(), - "--expected-fee", - "1", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state.get_extension::().unwrap(); - let withheld_amount = - spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS); - assert_eq!(withheld_amount, 1.0); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::WithdrawWithheldTokens.into(), - &token_account.to_string(), - &token_account.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state.get_extension::().unwrap(); - assert_eq!(u64::from(extension.withheld_amount), 0); - - // withdraw from mint after account closure - // gather fees - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - "--from", - &source_account.to_string(), - &token_pubkey.to_string(), - &(total_amount - transfer_amount).to_string(), - &token_account.to_string(), - "--expected-fee", - "9", - ], - ) - .await - .unwrap(); - - // burn tokens - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let burn_amount = spl_token::amount_to_ui_amount(account_state.base.amount, TEST_DECIMALS); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Burn.into(), - &token_account.to_string(), - &burn_amount.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state.get_extension::().unwrap(); - let withheld_amount = - spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS); - assert_eq!(withheld_amount, 9.0); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Close.into(), - "--address", - &token_account.to_string(), - "--recipient", - &payer.pubkey().to_string(), - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - let withheld_amount = - spl_token::amount_to_ui_amount(u64::from(extension.withheld_amount), TEST_DECIMALS); - assert_eq!(withheld_amount, 9.0); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::WithdrawWithheldTokens.into(), - &source_account.to_string(), - "--include-mint", - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - assert_eq!(u64::from(extension.withheld_amount), 0); - - // set the transfer fee - let new_transfer_fee_basis_points = 800; - let new_maximum_fee = 5_000_000.0; - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::SetTransferFee.into(), - &token_pubkey.to_string(), - &new_transfer_fee_basis_points.to_string(), - &new_maximum_fee.to_string(), - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - assert_eq!( - u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), - new_transfer_fee_basis_points - ); - let new_maximum_fee = spl_token::ui_amount_to_amount(new_maximum_fee, TEST_DECIMALS); - assert_eq!( - u64::from(extension.newer_transfer_fee.maximum_fee), - new_maximum_fee - ); - - // disable transfer fee authority - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - "--disable", - &token_pubkey.to_string(), - "transfer-fee-config", - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - Option::::from(extension.transfer_fee_config_authority), - None, - ); - - // disable withdraw withheld authority - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - "--disable", - &token_pubkey.to_string(), - "withheld-withdraw", - ], - ) - .await - .unwrap(); - - let mint = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(mint.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - Option::::from(extension.withdraw_withheld_authority), - None, - ); -} - -async fn transfer_fee_basis_point(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - - let transfer_fee_basis_points = 100; - let maximum_fee = 1.2; - let decimal = 9; - - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - let token_pubkey = token.pubkey(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--transfer-fee-basis-points", - &transfer_fee_basis_points.to_string(), - "--transfer-fee-maximum-fee", - &maximum_fee.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = test_mint.get_extension::().unwrap(); - assert_eq!( - u16::from(extension.older_transfer_fee.transfer_fee_basis_points), - transfer_fee_basis_points - ); - assert_eq!( - u64::from(extension.older_transfer_fee.maximum_fee), - (maximum_fee * i32::pow(10, decimal) as f64) as u64 - ); - assert_eq!( - u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), - transfer_fee_basis_points - ); - assert_eq!( - u64::from(extension.newer_transfer_fee.maximum_fee), - (maximum_fee * i32::pow(10, decimal) as f64) as u64 - ); -} - -async fn confidential_transfer(test_validator: &TestValidator, payer: &Keypair) { - use spl_token_2022::solana_zk_sdk::encryption::elgamal::ElGamalKeypair; - - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - - // create token with confidential transfers enabled - let auto_approve = false; - let confidential_transfer_mint_authority = payer.pubkey(); - - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - let token_pubkey = token.pubkey(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--enable-confidential-transfers", - "manual", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = test_mint - .get_extension::() - .unwrap(); - - assert_eq!( - Option::::from(extension.authority), - Some(confidential_transfer_mint_authority), - ); - assert_eq!( - bool::from(extension.auto_approve_new_accounts), - auto_approve, - ); - assert_eq!( - Option::::from(extension.auditor_elgamal_pubkey), - None, - ); - - // update confidential transfer mint settings - let auditor_keypair = ElGamalKeypair::new_rand(); - let auditor_pubkey: PodElGamalPubkey = (*auditor_keypair.pubkey()).into(); - let new_auto_approve = true; - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateConfidentialTransferSettings.into(), - &token_pubkey.to_string(), - "--auditor-pubkey", - &auditor_pubkey.to_string(), - "--approve-policy", - "auto", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = test_mint - .get_extension::() - .unwrap(); - - assert_eq!( - bool::from(extension.auto_approve_new_accounts), - new_auto_approve, - ); - assert_eq!( - Option::::from(extension.auditor_elgamal_pubkey), - Some(auditor_pubkey), - ); - - // create a confidential transfer account - let token_account = - create_associated_account(&config, payer, &token_pubkey, &payer.pubkey()).await; - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::ConfigureConfidentialTransferAccount.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(bool::from(extension.approved)); - assert!(bool::from(extension.allow_confidential_credits)); - assert!(bool::from(extension.allow_non_confidential_credits)); - - // disable and enable confidential transfers for an account - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::DisableConfidentialCredits.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(!bool::from(extension.allow_confidential_credits)); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::EnableConfidentialCredits.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(bool::from(extension.allow_confidential_credits)); - - // disable and enable non-confidential transfers for an account - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::DisableNonConfidentialCredits.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(!bool::from(extension.allow_non_confidential_credits)); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::EnableNonConfidentialCredits.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_account).await.unwrap(); - let account_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = account_state - .get_extension::() - .unwrap(); - assert!(bool::from(extension.allow_non_confidential_credits)); - - // deposit confidential tokens - let deposit_amount = 100.0; - mint_tokens(&config, payer, token_pubkey, deposit_amount, token_account) - .await - .unwrap(); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::DepositConfidentialTokens.into(), - &token_pubkey.to_string(), - &deposit_amount.to_string(), - ], - ) - .await - .unwrap(); - - // apply pending balance - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::ApplyPendingBalance.into(), - &token_pubkey.to_string(), - ], - ) - .await - .unwrap(); - - // confidential transfer - let destination_account = create_auxiliary_account(&config, payer, token_pubkey).await; - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::ConfigureConfidentialTransferAccount.into(), - "--address", - &destination_account.to_string(), - ], - ) - .await - .unwrap(); // configure destination account for confidential transfers first - - let transfer_amount = 100.0; - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Transfer.into(), - &token_pubkey.to_string(), - &transfer_amount.to_string(), - &destination_account.to_string(), - "--confidential", - ], - ) - .await - .unwrap(); - - // withdraw confidential tokens - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::ApplyPendingBalance.into(), - "--address", - &destination_account.to_string(), - ], - ) - .await - .unwrap(); // apply pending balance first - - let withdraw_amount = 100.0; - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::WithdrawConfidentialTokens.into(), - &token_pubkey.to_string(), - &withdraw_amount.to_string(), - "--address", - &destination_account.to_string(), - ], - ) - .await - .unwrap(); - - // disable confidential transfers for mint - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &token_pubkey.to_string(), - "confidential-transfer-mint", - "--disable", - ], - ) - .await - .unwrap(); -} - -async fn confidential_transfer_with_fee(test_validator: &TestValidator, payer: &Keypair) { - let config = test_config_with_default_signer(test_validator, payer, &spl_token_2022::id()); - - // create token with confidential transfers enabled - let auto_approve = true; - let confidential_transfer_mint_authority = payer.pubkey(); - - let token = Keypair::new(); - let token_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&token, &token_keypair_file).unwrap(); - let token_pubkey = token.pubkey(); - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_keypair_file.path().to_str().unwrap(), - "--enable-confidential-transfers", - "auto", - "--transfer-fee", - "100", - "1000000000", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); - let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = test_mint - .get_extension::() - .unwrap(); - - assert_eq!( - Option::::from(extension.authority), - Some(confidential_transfer_mint_authority), - ); - assert_eq!( - bool::from(extension.auto_approve_new_accounts), - auto_approve, - ); - assert_eq!( - Option::::from(extension.auditor_elgamal_pubkey), - None, - ); - - let extension = test_mint - .get_extension::() - .unwrap(); - assert_eq!( - Option::::from(extension.authority), - Some(confidential_transfer_mint_authority), - ); -} - -async fn multisig_transfer(test_validator: &TestValidator, payer: &Keypair) { - let m = 3; - let n = 5u8; - // need to add "payer" to make the config provide the right signer - let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = - std::iter::once(clone_keypair(payer)) - .chain(std::iter::repeat_with(Keypair::new).take((n - 2) as usize)) - .map(|s| { - let keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&s, &keypair_file).unwrap(); - (s.pubkey(), keypair_file) - }) - .unzip(); - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let config = test_config_with_default_signer(test_validator, payer, program_id); - let token = create_token(&config, payer).await; - let multisig = Arc::new(Keypair::new()); - let multisig_pubkey = multisig.pubkey(); - - // add the multisig as a member to itself, make it self-owned - let multisig_members = std::iter::once(multisig_pubkey) - .chain(multisig_members.iter().cloned()) - .collect::>(); - let multisig_path = NamedTempFile::new().unwrap(); - write_keypair_file(&multisig, &multisig_path).unwrap(); - let multisig_paths = std::iter::once(&multisig_path) - .chain(multisig_paths.iter()) - .collect::>(); - - let multisig_strings = multisig_members - .iter() - .map(|p| p.to_string()) - .collect::>(); - process_test_command( - &config, - payer, - [ - "spl-token", - CommandName::CreateMultisig.into(), - "--address-keypair", - multisig_path.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - &m.to_string(), - ] - .into_iter() - .chain(multisig_strings.iter().map(|p| p.as_str())), - ) - .await - .unwrap(); - - let account = config - .rpc_client - .get_account(&multisig_pubkey) - .await - .unwrap(); - let multisig = Multisig::unpack(&account.data).unwrap(); - assert_eq!(multisig.m, m); - assert_eq!(multisig.n, n); - - let source = create_associated_account(&config, payer, &token, &multisig_pubkey).await; - let destination = create_auxiliary_account(&config, payer, token).await; - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, source) - .await - .unwrap(); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &token.to_string(), - "10", - &destination.to_string(), - "--multisig-signer", - multisig_paths[0].path().to_str().unwrap(), - "--multisig-signer", - multisig_paths[1].path().to_str().unwrap(), - "--multisig-signer", - multisig_paths[2].path().to_str().unwrap(), - "--from", - &source.to_string(), - "--owner", - &multisig_pubkey.to_string(), - "--fee-payer", - multisig_paths[1].path().to_str().unwrap(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&source).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!( - token_account.base.amount, - spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS) - ); - let account = config.rpc_client.get_account(&destination).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - assert_eq!( - token_account.base.amount, - spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS) - ); - } -} - -async fn do_offline_multisig_transfer( - test_validator: &TestValidator, - payer: &Keypair, - compute_unit_price: Option, -) { - let m = 2; - let n = 3u8; - - let fee_payer_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(payer, &fee_payer_keypair_file).unwrap(); - - let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = std::iter::repeat_with(Keypair::new) - .take(n as usize) - .map(|s| { - let keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&s, &keypair_file).unwrap(); - (s.pubkey(), keypair_file) - }) - .unzip(); - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let mut config = test_config_with_default_signer(test_validator, payer, program_id); - config.compute_unit_limit = ComputeUnitLimit::Default; - let token = create_token(&config, payer).await; - let nonce = create_nonce(&config, payer).await; - - let nonce_account = config.rpc_client.get_account(&nonce).await.unwrap(); - let start_hash_index = 4 + 4 + 32; - let blockhash = Hash::new(&nonce_account.data[start_hash_index..start_hash_index + 32]); - - let multisig = Arc::new(Keypair::new()); - let multisig_pubkey = multisig.pubkey(); - let multisig_path = NamedTempFile::new().unwrap(); - write_keypair_file(&multisig, &multisig_path).unwrap(); - - let multisig_strings = multisig_members - .iter() - .map(|p| p.to_string()) - .collect::>(); - process_test_command( - &config, - payer, - [ - "spl-token", - CommandName::CreateMultisig.into(), - "--address-keypair", - multisig_path.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - &m.to_string(), - ] - .into_iter() - .chain(multisig_strings.iter().map(|p| p.as_str())), - ) - .await - .unwrap(); - - let source = create_associated_account(&config, payer, &token, &multisig_pubkey).await; - let destination = create_auxiliary_account(&config, payer, token).await; - let ui_amount = 100.0; - mint_tokens(&config, payer, token, ui_amount, source) - .await - .unwrap(); - - let offline_program_client: Arc> = - Arc::new(ProgramOfflineClient::new( - blockhash, - ProgramRpcClientSendTransaction, - )); - let mut args = vec![ - "spl-token".to_string(), - CommandName::Transfer.as_ref().to_string(), - token.to_string(), - "100".to_string(), - destination.to_string(), - "--blockhash".to_string(), - blockhash.to_string(), - "--nonce".to_string(), - nonce.to_string(), - "--nonce-authority".to_string(), - payer.pubkey().to_string(), - "--sign-only".to_string(), - "--mint-decimals".to_string(), - format!("{}", TEST_DECIMALS), - "--multisig-signer".to_string(), - multisig_paths[1].path().to_str().unwrap().to_string(), - "--multisig-signer".to_string(), - multisig_members[2].to_string(), - "--from".to_string(), - source.to_string(), - "--owner".to_string(), - multisig_pubkey.to_string(), - "--fee-payer".to_string(), - payer.pubkey().to_string(), - "--program-id".to_string(), - program_id.to_string(), - ]; - if let Some(compute_unit_price) = compute_unit_price { - args.push("--with-compute-unit-price".to_string()); - args.push(compute_unit_price.to_string()); - args.push("--with-compute-unit-limit".to_string()); - args.push(10_000.to_string()); - } - config.program_client = offline_program_client; - let result = exec_test_cmd(&config, &args).await.unwrap(); - // the provided signer has a signature, denoted by the pubkey followed - // by "=" and the signature - let member_prefix = format!("{}=", multisig_members[1]); - let signature_position = result.find(&member_prefix).unwrap(); - let end_position = result[signature_position..].find('\n').unwrap(); - let signer = result[signature_position..].get(..end_position).unwrap(); - - // other three expected signers are absent - let absent_signers_position = result.find("Absent Signers").unwrap(); - let absent_signers = result.get(absent_signers_position..).unwrap(); - assert!(absent_signers.contains(&multisig_members[2].to_string())); - assert!(absent_signers.contains(&payer.pubkey().to_string())); - - // and nothing else is marked a signer - assert!(!absent_signers.contains(&multisig_members[0].to_string())); - assert!(!absent_signers.contains(&multisig_pubkey.to_string())); - assert!(!absent_signers.contains(&nonce.to_string())); - assert!(!absent_signers.contains(&source.to_string())); - assert!(!absent_signers.contains(&destination.to_string())); - assert!(!absent_signers.contains(&token.to_string())); - - // now send the transaction - let rpc_program_client: Arc> = Arc::new( - ProgramRpcClient::new(config.rpc_client.clone(), ProgramRpcClientSendTransaction), - ); - config.program_client = rpc_program_client; - let mut args = vec![ - "spl-token".to_string(), - CommandName::Transfer.as_ref().to_string(), - token.to_string(), - "100".to_string(), - destination.to_string(), - "--blockhash".to_string(), - blockhash.to_string(), - "--nonce".to_string(), - nonce.to_string(), - "--nonce-authority".to_string(), - fee_payer_keypair_file.path().to_str().unwrap().to_string(), - "--mint-decimals".to_string(), - format!("{}", TEST_DECIMALS), - "--multisig-signer".to_string(), - multisig_members[1].to_string(), - "--multisig-signer".to_string(), - multisig_paths[2].path().to_str().unwrap().to_string(), - "--from".to_string(), - source.to_string(), - "--owner".to_string(), - multisig_pubkey.to_string(), - "--fee-payer".to_string(), - fee_payer_keypair_file.path().to_str().unwrap().to_string(), - "--program-id".to_string(), - program_id.to_string(), - "--signer".to_string(), - signer.to_string(), - ]; - if let Some(compute_unit_price) = compute_unit_price { - args.push("--with-compute-unit-price".to_string()); - args.push(compute_unit_price.to_string()); - args.push("--with-compute-unit-limit".to_string()); - args.push(10_000.to_string()); - } - exec_test_cmd(&config, &args).await.unwrap(); - - let account = config.rpc_client.get_account(&source).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let amount = spl_token::ui_amount_to_amount(0.0, TEST_DECIMALS); - assert_eq!(token_account.base.amount, amount); - let account = config.rpc_client.get_account(&destination).await.unwrap(); - let token_account = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let amount = spl_token::ui_amount_to_amount(100.0, TEST_DECIMALS); - assert_eq!(token_account.base.amount, amount); - - // get new nonce - let nonce_account = config.rpc_client.get_account(&nonce).await.unwrap(); - let blockhash = Hash::new(&nonce_account.data[start_hash_index..start_hash_index + 32]); - let mut args = vec![ - "spl-token".to_string(), - CommandName::Close.as_ref().to_string(), - "--address".to_string(), - source.to_string(), - "--blockhash".to_string(), - blockhash.to_string(), - "--nonce".to_string(), - nonce.to_string(), - "--nonce-authority".to_string(), - fee_payer_keypair_file.path().to_str().unwrap().to_string(), - "--multisig-signer".to_string(), - multisig_paths[1].path().to_str().unwrap().to_string(), - "--multisig-signer".to_string(), - multisig_paths[2].path().to_str().unwrap().to_string(), - "--owner".to_string(), - multisig_pubkey.to_string(), - "--fee-payer".to_string(), - fee_payer_keypair_file.path().to_str().unwrap().to_string(), - "--program-id".to_string(), - program_id.to_string(), - ]; - if let Some(compute_unit_price) = compute_unit_price { - args.push("--with-compute-unit-price".to_string()); - args.push(compute_unit_price.to_string()); - args.push("--with-compute-unit-limit".to_string()); - args.push(10_000.to_string()); - } - exec_test_cmd(&config, &args).await.unwrap(); - let _ = config.rpc_client.get_account(&source).await.unwrap_err(); - } -} - -async fn offline_multisig_transfer_with_nonce(test_validator: &TestValidator, payer: &Keypair) { - do_offline_multisig_transfer(test_validator, payer, None).await; - do_offline_multisig_transfer(test_validator, payer, Some(10)).await; -} - -async fn withdraw_excess_lamports_from_multisig(test_validator: &TestValidator, payer: &Keypair) { - let m = 3; - let n = 5u8; - // need to add "payer" to make the config provide the right signer - let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = - std::iter::once(clone_keypair(payer)) - .chain(std::iter::repeat_with(Keypair::new).take((n - 2) as usize)) - .map(|s| { - let keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(&s, &keypair_file).unwrap(); - (s.pubkey(), keypair_file) - }) - .unzip(); - - let fee_payer_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(payer, &fee_payer_keypair_file).unwrap(); - - let owner_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(payer, &owner_keypair_file).unwrap(); - - let program_id = &spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, program_id); - - let multisig = Arc::new(Keypair::new()); - let multisig_pubkey = multisig.pubkey(); - - // add the multisig as a member to itself, make it self-owned - let multisig_members = std::iter::once(multisig_pubkey) - .chain(multisig_members.iter().cloned()) - .collect::>(); - let multisig_path = NamedTempFile::new().unwrap(); - write_keypair_file(&multisig, &multisig_path).unwrap(); - let multisig_paths = std::iter::once(&multisig_path) - .chain(multisig_paths.iter()) - .collect::>(); - - let multisig_strings = multisig_members - .iter() - .map(|p| p.to_string()) - .collect::>(); - process_test_command( - &config, - payer, - [ - "spl-token", - CommandName::CreateMultisig.into(), - "--address-keypair", - multisig_path.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - &m.to_string(), - ] - .into_iter() - .chain(multisig_strings.iter().map(|p| p.as_str())), - ) - .await - .unwrap(); - - let account = config - .rpc_client - .get_account(&multisig_pubkey) - .await - .unwrap(); - let multisig = Multisig::unpack(&account.data).unwrap(); - assert_eq!(multisig.m, m); - assert_eq!(multisig.n, n); - - let receiver = Keypair::new(); - let excess_lamports = 4000 * 1_000_000_000; - - config - .rpc_client - .send_and_confirm_transaction(&Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - &multisig_pubkey, - excess_lamports, - )], - Some(&payer.pubkey()), - &[&payer], - config.rpc_client.get_latest_blockhash().await.unwrap(), - )) - .await - .unwrap(); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::WithdrawExcessLamports.into(), - &multisig_pubkey.to_string(), - &receiver.pubkey().to_string(), - "--owner", - &multisig_pubkey.to_string(), - "--multisig-signer", - multisig_paths[0].path().to_str().unwrap(), - "--multisig-signer", - multisig_paths[1].path().to_str().unwrap(), - "--multisig-signer", - multisig_paths[2].path().to_str().unwrap(), - "--fee-payer", - fee_payer_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - assert_eq!( - excess_lamports, - config - .rpc_client - .get_balance(&receiver.pubkey()) - .await - .unwrap() - ); -} - -async fn withdraw_excess_lamports_from_mint(test_validator: &TestValidator, payer: &Keypair) { - let program_id = &spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, program_id); - let owner_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(payer, &owner_keypair_file).unwrap(); - - let receiver = Keypair::new(); - - let token_keypair = Keypair::new(); - let token_path = NamedTempFile::new().unwrap(); - write_keypair_file(&token_keypair, &token_path).unwrap(); - let token_pubkey = token_keypair.pubkey(); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_path.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - let excess_lamports = 4000 * 1_000_000_000; - config - .rpc_client - .send_and_confirm_transaction(&Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - &token_pubkey, - excess_lamports, - )], - Some(&payer.pubkey()), - &[&payer], - config.rpc_client.get_latest_blockhash().await.unwrap(), - )) - .await - .unwrap(); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::WithdrawExcessLamports.into(), - &token_pubkey.to_string(), - &receiver.pubkey().to_string(), - "--owner", - owner_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - assert_eq!( - excess_lamports, - config - .rpc_client - .get_balance(&receiver.pubkey()) - .await - .unwrap() - ); -} - -async fn withdraw_excess_lamports_from_account(test_validator: &TestValidator, payer: &Keypair) { - let program_id = &spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, program_id); - let owner_keypair_file = NamedTempFile::new().unwrap(); - write_keypair_file(payer, &owner_keypair_file).unwrap(); - - let receiver = Keypair::new(); - - let token_keypair = Keypair::new(); - let token_path = NamedTempFile::new().unwrap(); - write_keypair_file(&token_keypair, &token_path).unwrap(); - let token_pubkey = token_keypair.pubkey(); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - token_path.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - let excess_lamports = 4000 * 1_000_000_000; - let token_account = - create_associated_account(&config, payer, &token_pubkey, &payer.pubkey()).await; - - config - .rpc_client - .send_and_confirm_transaction(&Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - &token_account, - excess_lamports, - )], - Some(&payer.pubkey()), - &[&payer], - config.rpc_client.get_latest_blockhash().await.unwrap(), - )) - .await - .unwrap(); - - exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::WithdrawExcessLamports.into(), - &token_account.to_string(), - &receiver.pubkey().to_string(), - "--owner", - owner_keypair_file.path().to_str().unwrap(), - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); - - assert_eq!( - excess_lamports, - config - .rpc_client - .get_balance(&receiver.pubkey()) - .await - .unwrap() - ); -} - -async fn metadata_pointer(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, &program_id); - let metadata_address = Pubkey::new_unique(); - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--metadata-address", - &metadata_address.to_string(), - ], - ) - .await; - - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - extension.metadata_address, - Some(metadata_address).try_into().unwrap() - ); - - let new_metadata_address = Pubkey::new_unique(); - - let _new_result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateMetadataAddress.into(), - &mint.to_string(), - &new_metadata_address.to_string(), - ], - ) - .await; - - let new_account = config.rpc_client.get_account(&mint).await.unwrap(); - let new_mint_state = StateWithExtensionsOwned::::unpack(new_account.data).unwrap(); - - let new_extension = new_mint_state.get_extension::().unwrap(); - - assert_eq!( - new_extension.metadata_address, - Some(new_metadata_address).try_into().unwrap() - ); - - let _result_with_disable = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateMetadataAddress.into(), - &mint.to_string(), - "--disable", - ], - ) - .await; - - let new_account_disable = config.rpc_client.get_account(&mint).await.unwrap(); - let new_mint_state_disable = - StateWithExtensionsOwned::::unpack(new_account_disable.data).unwrap(); - - let new_extension_disable = new_mint_state_disable - .get_extension::() - .unwrap(); - - assert_eq!( - new_extension_disable.metadata_address, - None.try_into().unwrap() - ); -} - -async fn group_pointer(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, &program_id); - let group_address = Pubkey::new_unique(); - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--group-address", - &group_address.to_string(), - ], - ) - .await - .unwrap(); - - let value: serde_json::Value = serde_json::from_str(&result).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - extension.group_address, - Some(group_address).try_into().unwrap() - ); - - let new_group_address = Pubkey::new_unique(); - - let _new_result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateGroupAddress.into(), - &mint.to_string(), - &new_group_address.to_string(), - ], - ) - .await; - - let new_account = config.rpc_client.get_account(&mint).await.unwrap(); - let new_mint_state = StateWithExtensionsOwned::::unpack(new_account.data).unwrap(); - - let new_extension = new_mint_state.get_extension::().unwrap(); - - assert_eq!( - new_extension.group_address, - Some(new_group_address).try_into().unwrap() - ); - - let _result_with_disable = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateGroupAddress.into(), - &mint.to_string(), - "--disable", - ], - ) - .await - .unwrap(); - - let new_account_disable = config.rpc_client.get_account(&mint).await.unwrap(); - let new_mint_state_disable = - StateWithExtensionsOwned::::unpack(new_account_disable.data).unwrap(); - - let new_extension_disable = new_mint_state_disable - .get_extension::() - .unwrap(); - - assert_eq!( - new_extension_disable.group_address, - None.try_into().unwrap() - ); -} - -async fn group_member_pointer(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, &program_id); - let member_address = Pubkey::new_unique(); - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--member-address", - &member_address.to_string(), - ], - ) - .await - .unwrap(); - - let value: serde_json::Value = serde_json::from_str(&result).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - extension.member_address, - Some(member_address).try_into().unwrap() - ); - - let new_member_address = Pubkey::new_unique(); - - let _new_result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateMemberAddress.into(), - &mint.to_string(), - &new_member_address.to_string(), - ], - ) - .await - .unwrap(); - - let new_account = config.rpc_client.get_account(&mint).await.unwrap(); - let new_mint_state = StateWithExtensionsOwned::::unpack(new_account.data).unwrap(); - - let new_extension = new_mint_state - .get_extension::() - .unwrap(); - - assert_eq!( - new_extension.member_address, - Some(new_member_address).try_into().unwrap() - ); - - let _result_with_disable = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateMemberAddress.into(), - &mint.to_string(), - "--disable", - ], - ) - .await; - - let new_account_disable = config.rpc_client.get_account(&mint).await.unwrap(); - let new_mint_state_disable = - StateWithExtensionsOwned::::unpack(new_account_disable.data).unwrap(); - - let new_extension_disable = new_mint_state_disable - .get_extension::() - .unwrap(); - - assert_eq!( - new_extension_disable.member_address, - None.try_into().unwrap() - ); -} - -async fn transfer_hook(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let mut config = test_config_with_default_signer(test_validator, payer, &program_id); - let transfer_hook_program_id = Pubkey::new_unique(); - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--transfer-hook", - &transfer_hook_program_id.to_string(), - ], - ) - .await; - - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - extension.program_id, - Some(transfer_hook_program_id).try_into().unwrap() - ); - - let new_transfer_hook_program_id = Pubkey::new_unique(); - - let _new_result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::SetTransferHook.into(), - &mint.to_string(), - &new_transfer_hook_program_id.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!( - extension.program_id, - Some(new_transfer_hook_program_id).try_into().unwrap() - ); - - // Make sure that parsing transfer hook accounts works - let real_program_client = config.program_client; - let blockhash = Hash::default(); - let program_client: Arc> = Arc::new( - ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction), - ); - config.program_client = program_client; - let _result = exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &mint.to_string(), - "10", - &Pubkey::new_unique().to_string(), - "--blockhash", - &blockhash.to_string(), - "--nonce", - &Pubkey::new_unique().to_string(), - "--nonce-authority", - &Pubkey::new_unique().to_string(), - "--sign-only", - "--mint-decimals", - &format!("{}", TEST_DECIMALS), - "--from", - &Pubkey::new_unique().to_string(), - "--owner", - &Pubkey::new_unique().to_string(), - "--transfer-hook-account", - &format!("{}:readonly", Pubkey::new_unique()), - "--transfer-hook-account", - &format!("{}:writable", Pubkey::new_unique()), - "--transfer-hook-account", - &format!("{}:readonly-signer", Pubkey::new_unique()), - "--transfer-hook-account", - &format!("{}:writable-signer", Pubkey::new_unique()), - ], - ) - .await - .unwrap(); - - config.program_client = real_program_client; - let _result_with_disable = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::SetTransferHook.into(), - &mint.to_string(), - "--disable", - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - - assert_eq!(extension.program_id, None.try_into().unwrap()); -} - -async fn transfer_hook_with_transfer_fee(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let mut config = test_config_with_default_signer(test_validator, payer, &program_id); - let transfer_hook_program_id = Pubkey::new_unique(); - - let transfer_fee_basis_points = 100; - let maximum_fee: u64 = 10_000_000_000; - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--transfer-hook", - &transfer_hook_program_id.to_string(), - "--transfer-fee", - &transfer_fee_basis_points.to_string(), - &maximum_fee.to_string(), - ], - ) - .await; - - // Check that the transfer-hook extension is correctly configured - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - assert_eq!( - extension.program_id, - Some(transfer_hook_program_id).try_into().unwrap() - ); - - // Check that the transfer-fee extension is correctly configured - let extension = mint_state.get_extension::().unwrap(); - assert_eq!( - u16::from(extension.older_transfer_fee.transfer_fee_basis_points), - transfer_fee_basis_points - ); - assert_eq!( - u64::from(extension.older_transfer_fee.maximum_fee), - maximum_fee - ); - assert_eq!( - u16::from(extension.newer_transfer_fee.transfer_fee_basis_points), - transfer_fee_basis_points - ); - assert_eq!( - u64::from(extension.newer_transfer_fee.maximum_fee), - maximum_fee - ); - - // Make sure that parsing transfer hook accounts and expected-fee works - let blockhash = Hash::default(); - let program_client: Arc> = Arc::new( - ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction), - ); - config.program_client = program_client; - - let _result = exec_test_cmd( - &config, - &[ - "spl-token", - CommandName::Transfer.into(), - &mint.to_string(), - "100", - &Pubkey::new_unique().to_string(), - "--blockhash", - &blockhash.to_string(), - "--nonce", - &Pubkey::new_unique().to_string(), - "--nonce-authority", - &Pubkey::new_unique().to_string(), - "--sign-only", - "--mint-decimals", - &format!("{}", TEST_DECIMALS), - "--from", - &Pubkey::new_unique().to_string(), - "--owner", - &Pubkey::new_unique().to_string(), - "--transfer-hook-account", - &format!("{}:readonly", Pubkey::new_unique()), - "--transfer-hook-account", - &format!("{}:writable", Pubkey::new_unique()), - "--transfer-hook-account", - &format!("{}:readonly-signer", Pubkey::new_unique()), - "--transfer-hook-account", - &format!("{}:writable-signer", Pubkey::new_unique()), - "--expected-fee", - "1", - "--program-id", - &program_id.to_string(), - ], - ) - .await - .unwrap(); -} - -async fn metadata(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, &program_id); - let name = "this"; - let symbol = "is"; - let uri = "METADATA!"; - - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--enable-metadata", - ], - ) - .await; - - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - - let extension = mint_state.get_extension::().unwrap(); - assert_eq!(extension.metadata_address, Some(mint).try_into().unwrap()); - - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::InitializeMetadata.into(), - &mint.to_string(), - name, - symbol, - uri, - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(fetched_metadata.name, name); - assert_eq!(fetched_metadata.symbol, symbol); - assert_eq!(fetched_metadata.uri, uri); - assert_eq!(fetched_metadata.mint, mint); - assert_eq!( - fetched_metadata.update_authority, - Some(payer.pubkey()).try_into().unwrap() - ); - assert_eq!(fetched_metadata.additional_metadata, []); - - // update canonical field - let new_value = "THIS!"; - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateMetadata.into(), - &mint.to_string(), - "NAME", - new_value, - ], - ) - .await - .unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(fetched_metadata.name, new_value); - - // add new field - let field = "My field!"; - let value = "Try and stop me"; - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateMetadata.into(), - &mint.to_string(), - field, - value, - ], - ) - .await - .unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!( - fetched_metadata.additional_metadata, - [(field.to_string(), value.to_string())] - ); - - // remove it - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateMetadata.into(), - &mint.to_string(), - field, - "--remove", - ], - ) - .await - .unwrap(); - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(fetched_metadata.additional_metadata, []); - - // fail to remove name - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateMetadata.into(), - &mint.to_string(), - "name", - "--remove", - ], - ) - .await - .unwrap_err(); - - // update authority - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &mint.to_string(), - "metadata", - &mint.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let fetched_metadata = mint_state - .get_variable_len_extension::() - .unwrap(); - assert_eq!( - fetched_metadata.update_authority, - Some(mint).try_into().unwrap() - ); -} - -async fn group(test_validator: &TestValidator, payer: &Keypair) { - let program_id = spl_token_2022::id(); - let config = test_config_with_default_signer(test_validator, payer, &program_id); - let max_size = 10; - - // Create token - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--enable-group", - ], - ) - .await; - - let value: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - let mint = Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - - // Initialize the group - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::InitializeGroup.into(), - &mint.to_string(), - &max_size.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - - let extension = mint_state.get_extension::().unwrap(); - assert_eq!( - extension.update_authority, - Some(payer.pubkey()).try_into().unwrap() - ); - assert_eq!(extension.max_size, max_size.into()); - - let extension_pointer = mint_state.get_extension::().unwrap(); - assert_eq!( - extension_pointer.group_address, - Some(mint).try_into().unwrap() - ); - - let new_max_size = 12; - - // Update token-group max-size - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::UpdateGroupMaxSize.into(), - &mint.to_string(), - &new_max_size.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - - let updated_mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - - let updated_extension = updated_mint_state.get_extension::().unwrap(); - assert_eq!(updated_extension.max_size, new_max_size.into()); - - // Create member token - let result = process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::CreateToken.into(), - "--program-id", - &program_id.to_string(), - "--enable-member", - ], - ) - .await - .unwrap(); - - let value: serde_json::Value = serde_json::from_str(&result).unwrap(); - let member_mint = - Pubkey::from_str(value["commandOutput"]["address"].as_str().unwrap()).unwrap(); - - // Initialize it as a member of the group - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::InitializeMember.into(), - &member_mint.to_string(), - &mint.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let group_mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = group_mint_state.get_extension::().unwrap(); - assert_eq!(u64::from(extension.size), 1); - - let account = config.rpc_client.get_account(&member_mint).await.unwrap(); - let member_mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = member_mint_state - .get_extension::() - .unwrap(); - assert_eq!(extension.group, mint); - assert_eq!(extension.mint, member_mint); - assert_eq!(u64::from(extension.member_number), 1); - - // update authority - process_test_command( - &config, - payer, - &[ - "spl-token", - CommandName::Authorize.into(), - &mint.to_string(), - "group", - &mint.to_string(), - ], - ) - .await - .unwrap(); - - let account = config.rpc_client.get_account(&mint).await.unwrap(); - let mint_state = StateWithExtensionsOwned::::unpack(account.data).unwrap(); - let extension = mint_state.get_extension::().unwrap(); - assert_eq!(extension.update_authority, Some(mint).try_into().unwrap()); -} - -async fn compute_budget(test_validator: &TestValidator, payer: &Keypair) { - for program_id in VALID_TOKEN_PROGRAM_IDS.iter() { - let mut config = test_config_with_default_signer(test_validator, payer, program_id); - config.compute_unit_price = Some(42); - config.compute_unit_limit = ComputeUnitLimit::Static(40_000); - run_transfer_test(&config, payer).await; - } -} diff --git a/token/cli/tests/config.rs b/token/cli/tests/config.rs deleted file mode 100644 index 78c765abe0d..00000000000 --- a/token/cli/tests/config.rs +++ /dev/null @@ -1,10 +0,0 @@ -use assert_cmd::cmd::Command; - -#[test] -fn invalid_config_will_cause_commands_to_fail() { - let mut cmd = Command::cargo_bin("spl-token").unwrap(); - cmd.args(["address", "--config", "~/nonexistent/config.yml"]); - cmd.assert() - .stderr("error: Could not find config file `~/nonexistent/config.yml`\n"); - cmd.assert().code(1).failure(); -} diff --git a/token/client/Cargo.toml b/token/client/Cargo.toml deleted file mode 100644 index 94547634576..00000000000 --- a/token/client/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -authors = ["Solana Labs Maintainers "] -description = "SPL-Token Rust Client" -edition = "2021" -license = "Apache-2.0" -name = "spl-token-client" -repository = "https://github.com/solana-labs/solana-program-library" -version = "0.13.0" - -[dependencies] -async-trait = "0.1" -bincode = "1.3.2" -bytemuck = "1.21.0" -futures = "0.3.31" -futures-util = "0.3" -solana-banks-interface = "2.1.0" -solana-cli-output = { version = "2.1.0", optional = true } -solana-program-test = "2.1.0" -solana-rpc-client = "2.1.0" -solana-rpc-client-api = "2.1.0" -solana-sdk = "2.1.0" -spl-associated-token-account-client = { version = "2.0.0", path = "../../associated-token-account/client" } -spl-elgamal-registry = { version = "0.1.0", path = "../confidential-transfer/elgamal-registry"} -spl-memo = { version = "6.0", features = ["no-entrypoint"] } -spl-record = { version = "0.3.0", path = "../../record/program", features = ["no-entrypoint"] } -spl-token = { version = "7.0", path = "../program", features = [ - "no-entrypoint", -] } -spl-token-confidential-transfer-proof-extraction = { version = "0.2.0", path = "../confidential-transfer/proof-extraction" } -spl-token-confidential-transfer-proof-generation = { version = "0.2.0", path = "../confidential-transfer/proof-generation" } -spl-token-2022 = { version = "6.0.0", path = "../program-2022" } -spl-token-group-interface = { version = "0.5.0", path = "../../token-group/interface" } -spl-token-metadata-interface = { version = "0.6.0", path = "../../token-metadata/interface" } -spl-transfer-hook-interface = { version = "0.9.0", path = "../transfer-hook/interface" } -thiserror = "2.0" - -[features] -default = ["display"] -display = ["dep:solana-cli-output"] diff --git a/token/client/src/client.rs b/token/client/src/client.rs deleted file mode 100644 index 12f3faf17a4..00000000000 --- a/token/client/src/client.rs +++ /dev/null @@ -1,426 +0,0 @@ -use { - async_trait::async_trait, - solana_banks_interface::BanksTransactionResultWithSimulation, - solana_program_test::{tokio::sync::Mutex, BanksClient, ProgramTestContext}, - solana_rpc_client::nonblocking::rpc_client::RpcClient, - solana_rpc_client_api::response::RpcSimulateTransactionResult, - solana_sdk::{ - account::Account, hash::Hash, pubkey::Pubkey, signature::Signature, - transaction::Transaction, - }, - std::{fmt, future::Future, pin::Pin, sync::Arc}, -}; - -type BoxFuture<'a, T> = Pin + Send + 'a>>; - -/// Basic trait for sending transactions to validator. -pub trait SendTransaction { - type Output; -} - -/// Basic trait for simulating transactions in a validator. -pub trait SimulateTransaction { - type SimulationOutput: SimulationResult; -} - -/// Trait for the output of a simulation -pub trait SimulationResult { - fn get_compute_units_consumed(&self) -> ProgramClientResult; -} - -/// Extends basic `SendTransaction` trait with function `send` where client is -/// `&mut BanksClient`. Required for `ProgramBanksClient`. -pub trait SendTransactionBanksClient: SendTransaction { - fn send<'a>( - &self, - client: &'a mut BanksClient, - transaction: Transaction, - ) -> BoxFuture<'a, ProgramClientResult>; -} - -/// Extends basic `SimulateTransaction` trait with function `simulation` where -/// client is `&mut BanksClient`. Required for `ProgramBanksClient`. -pub trait SimulateTransactionBanksClient: SimulateTransaction { - fn simulate<'a>( - &self, - client: &'a mut BanksClient, - transaction: Transaction, - ) -> BoxFuture<'a, ProgramClientResult>; -} - -/// Send transaction to validator using `BanksClient::process_transaction`. -#[derive(Debug, Clone, Copy, Default)] -pub struct ProgramBanksClientProcessTransaction; - -impl SendTransaction for ProgramBanksClientProcessTransaction { - type Output = (); -} - -impl SendTransactionBanksClient for ProgramBanksClientProcessTransaction { - fn send<'a>( - &self, - client: &'a mut BanksClient, - transaction: Transaction, - ) -> BoxFuture<'a, ProgramClientResult> { - Box::pin(async move { - client - .process_transaction(transaction) - .await - .map_err(Into::into) - }) - } -} - -impl SimulationResult for BanksTransactionResultWithSimulation { - fn get_compute_units_consumed(&self) -> ProgramClientResult { - self.simulation_details - .as_ref() - .map(|x| x.units_consumed) - .ok_or("No simulation results found".into()) - } -} - -impl SimulateTransaction for ProgramBanksClientProcessTransaction { - type SimulationOutput = BanksTransactionResultWithSimulation; -} - -impl SimulateTransactionBanksClient for ProgramBanksClientProcessTransaction { - fn simulate<'a>( - &self, - client: &'a mut BanksClient, - transaction: Transaction, - ) -> BoxFuture<'a, ProgramClientResult> { - Box::pin(async move { - client - .simulate_transaction(transaction) - .await - .map_err(Into::into) - }) - } -} - -/// Extends basic `SendTransaction` trait with function `send` where client is -/// `&RpcClient`. Required for `ProgramRpcClient`. -pub trait SendTransactionRpc: SendTransaction { - fn send<'a>( - &self, - client: &'a RpcClient, - transaction: &'a Transaction, - ) -> BoxFuture<'a, ProgramClientResult>; -} - -/// Extends basic `SimulateTransaction` trait with function `simulate` where -/// client is `&RpcClient`. Required for `ProgramRpcClient`. -pub trait SimulateTransactionRpc: SimulateTransaction { - fn simulate<'a>( - &self, - client: &'a RpcClient, - transaction: &'a Transaction, - ) -> BoxFuture<'a, ProgramClientResult>; -} - -#[derive(Debug, Clone, Copy, Default)] -pub struct ProgramRpcClientSendTransaction; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RpcClientResponse { - Signature(Signature), - Transaction(Transaction), - Simulation(RpcSimulateTransactionResult), -} - -impl SendTransaction for ProgramRpcClientSendTransaction { - type Output = RpcClientResponse; -} - -impl SendTransactionRpc for ProgramRpcClientSendTransaction { - fn send<'a>( - &self, - client: &'a RpcClient, - transaction: &'a Transaction, - ) -> BoxFuture<'a, ProgramClientResult> { - Box::pin(async move { - if !transaction.is_signed() { - return Err("Cannot send transaction: not fully signed".into()); - } - - client - .send_and_confirm_transaction(transaction) - .await - .map(RpcClientResponse::Signature) - .map_err(Into::into) - }) - } -} - -impl SimulationResult for RpcClientResponse { - fn get_compute_units_consumed(&self) -> ProgramClientResult { - match self { - // `Transaction` is the result of an offline simulation. The error - // should be properly handled by a caller that supports offline - // signing - Self::Signature(_) | Self::Transaction(_) => Err("Not a simulation result".into()), - Self::Simulation(simulation_result) => simulation_result - .units_consumed - .ok_or("No simulation results found".into()), - } - } -} - -impl SimulateTransaction for ProgramRpcClientSendTransaction { - type SimulationOutput = RpcClientResponse; -} - -impl SimulateTransactionRpc for ProgramRpcClientSendTransaction { - fn simulate<'a>( - &self, - client: &'a RpcClient, - transaction: &'a Transaction, - ) -> BoxFuture<'a, ProgramClientResult> { - Box::pin(async move { - client - .simulate_transaction(transaction) - .await - .map(|r| RpcClientResponse::Simulation(r.value)) - .map_err(Into::into) - }) - } -} - -pub type ProgramClientError = Box; -pub type ProgramClientResult = Result; - -/// Generic client interface for programs. -#[async_trait] -pub trait ProgramClient -where - ST: SendTransaction + SimulateTransaction, -{ - async fn get_minimum_balance_for_rent_exemption( - &self, - data_len: usize, - ) -> ProgramClientResult; - - async fn get_latest_blockhash(&self) -> ProgramClientResult; - - async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult; - - async fn get_account(&self, address: Pubkey) -> ProgramClientResult>; - - async fn simulate_transaction( - &self, - transaction: &Transaction, - ) -> ProgramClientResult; -} - -enum ProgramBanksClientContext { - Client(Arc>), - Context(Arc>), -} - -/// Program client for `BanksClient` from crate `solana-program-test`. -pub struct ProgramBanksClient { - context: ProgramBanksClientContext, - send: ST, -} - -impl fmt::Debug for ProgramBanksClient { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ProgramBanksClient").finish() - } -} - -impl ProgramBanksClient { - fn new(context: ProgramBanksClientContext, send: ST) -> Self { - Self { context, send } - } - - pub fn new_from_client(client: Arc>, send: ST) -> Self { - Self::new(ProgramBanksClientContext::Client(client), send) - } - - pub fn new_from_context(context: Arc>, send: ST) -> Self { - Self::new(ProgramBanksClientContext::Context(context), send) - } - - async fn run_in_lock(&self, f: F) -> O - where - for<'a> F: Fn(&'a mut BanksClient) -> BoxFuture<'a, O>, - { - match &self.context { - ProgramBanksClientContext::Client(client) => { - let mut lock = client.lock().await; - f(&mut lock).await - } - ProgramBanksClientContext::Context(context) => { - let mut lock = context.lock().await; - f(&mut lock.banks_client).await - } - } - } -} - -#[async_trait] -impl ProgramClient for ProgramBanksClient -where - ST: SendTransactionBanksClient + SimulateTransactionBanksClient + Send + Sync, -{ - async fn get_minimum_balance_for_rent_exemption( - &self, - data_len: usize, - ) -> ProgramClientResult { - self.run_in_lock(|client| { - Box::pin(async move { - let rent = client.get_rent().await?; - Ok(rent.minimum_balance(data_len)) - }) - }) - .await - } - - async fn get_latest_blockhash(&self) -> ProgramClientResult { - self.run_in_lock(|client| { - Box::pin(async move { client.get_latest_blockhash().await.map_err(Into::into) }) - }) - .await - } - - async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult { - self.run_in_lock(|client| { - let transaction = transaction.clone(); - self.send.send(client, transaction) - }) - .await - } - - async fn simulate_transaction( - &self, - transaction: &Transaction, - ) -> ProgramClientResult { - self.run_in_lock(|client| { - let transaction = transaction.clone(); - self.send.simulate(client, transaction) - }) - .await - } - - async fn get_account(&self, address: Pubkey) -> ProgramClientResult> { - self.run_in_lock(|client| { - Box::pin(async move { client.get_account(address).await.map_err(Into::into) }) - }) - .await - } -} - -/// Program client for `RpcClient` from crate `solana-client`. -pub struct ProgramRpcClient { - client: Arc, - send: ST, -} - -impl fmt::Debug for ProgramRpcClient { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ProgramRpcClient").finish() - } -} - -impl ProgramRpcClient { - pub fn new(client: Arc, send: ST) -> Self { - Self { client, send } - } -} - -#[async_trait] -impl ProgramClient for ProgramRpcClient -where - ST: SendTransactionRpc + SimulateTransactionRpc + Send + Sync, -{ - async fn get_minimum_balance_for_rent_exemption( - &self, - data_len: usize, - ) -> ProgramClientResult { - self.client - .get_minimum_balance_for_rent_exemption(data_len) - .await - .map_err(Into::into) - } - - async fn get_latest_blockhash(&self) -> ProgramClientResult { - self.client.get_latest_blockhash().await.map_err(Into::into) - } - - async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult { - self.send.send(&self.client, transaction).await - } - - async fn simulate_transaction( - &self, - transaction: &Transaction, - ) -> ProgramClientResult { - self.send.simulate(&self.client, transaction).await - } - - async fn get_account(&self, address: Pubkey) -> ProgramClientResult> { - Ok(self - .client - .get_account_with_commitment(&address, self.client.commitment()) - .await? - .value) - } -} - -/// Program client for offline signing. -pub struct ProgramOfflineClient { - blockhash: Hash, - _send: ST, -} - -impl fmt::Debug for ProgramOfflineClient { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ProgramOfflineClient").finish() - } -} - -impl ProgramOfflineClient { - pub fn new(blockhash: Hash, send: ST) -> Self { - Self { - blockhash, - _send: send, - } - } -} - -#[async_trait] -impl ProgramClient for ProgramOfflineClient -where - ST: SendTransaction - + SimulateTransaction - + Send - + Sync, -{ - async fn get_minimum_balance_for_rent_exemption( - &self, - _data_len: usize, - ) -> ProgramClientResult { - Err("Unable to fetch minimum balance for rent exemption in offline mode".into()) - } - - async fn get_latest_blockhash(&self) -> ProgramClientResult { - Ok(self.blockhash) - } - - async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult { - Ok(RpcClientResponse::Transaction(transaction.clone())) - } - - async fn simulate_transaction( - &self, - transaction: &Transaction, - ) -> ProgramClientResult { - Ok(RpcClientResponse::Transaction(transaction.clone())) - } - - async fn get_account(&self, _address: Pubkey) -> ProgramClientResult> { - Err("Unable to fetch account in offline mode".into()) - } -} diff --git a/token/client/src/lib.rs b/token/client/src/lib.rs deleted file mode 100644 index e2ea2307cb4..00000000000 --- a/token/client/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -pub mod client; -pub mod output; -pub mod token; - -pub use spl_token_2022; diff --git a/token/client/src/output.rs b/token/client/src/output.rs deleted file mode 100644 index e3436ca435d..00000000000 --- a/token/client/src/output.rs +++ /dev/null @@ -1,59 +0,0 @@ -#![cfg(feature = "display")] - -use {crate::client::RpcClientResponse, solana_cli_output::display::writeln_transaction, std::fmt}; - -impl fmt::Display for RpcClientResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - RpcClientResponse::Signature(signature) => writeln!(f, "Signature: {}", signature), - RpcClientResponse::Transaction(transaction) => { - writeln!(f, "Transaction:")?; - writeln_transaction(f, &transaction.clone().into(), None, " ", None, None) - } - RpcClientResponse::Simulation(result) => { - writeln!(f, "Simulation:")?; - // maybe implement another formatter on simulation result? - writeln!(f, "{result:?}") - } - } - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - solana_sdk::{ - hash::Hash, - pubkey::Pubkey, - signature::{Signature, Signer, SIGNATURE_BYTES}, - signer::keypair::Keypair, - system_instruction, - transaction::Transaction, - }, - }; - - #[test] - fn display_signature() { - let signature_bytes = [202u8; SIGNATURE_BYTES]; - let signature = RpcClientResponse::Signature(Signature::from(signature_bytes)); - println!("{}", signature); - } - - #[test] - fn display_transaction() { - let payer = Keypair::new(); - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::transfer( - &payer.pubkey(), - &Pubkey::new_unique(), - 10, - )], - Some(&payer.pubkey()), - &[&payer], - Hash::default(), - ); - let transaction = RpcClientResponse::Transaction(transaction); - println!("{}", transaction); - } -} diff --git a/token/client/src/token.rs b/token/client/src/token.rs deleted file mode 100644 index 3d9f14777ff..00000000000 --- a/token/client/src/token.rs +++ /dev/null @@ -1,3623 +0,0 @@ -use { - crate::client::{ - ProgramClient, ProgramClientError, SendTransaction, SimulateTransaction, SimulationResult, - }, - bytemuck::{bytes_of, Pod}, - futures::future::join_all, - futures_util::TryFutureExt, - solana_program_test::tokio::time, - solana_sdk::{ - account::Account as BaseAccount, - compute_budget::ComputeBudgetInstruction, - hash::Hash, - instruction::{AccountMeta, Instruction}, - message::Message, - packet::PACKET_DATA_SIZE, - program_error::ProgramError, - program_pack::Pack, - pubkey::Pubkey, - signature::Signature, - signer::{signers::Signers, Signer, SignerError}, - system_instruction, - transaction::Transaction, - }, - spl_associated_token_account_client::{ - address::get_associated_token_address_with_program_id, - instruction::{ - create_associated_token_account, create_associated_token_account_idempotent, - }, - }, - spl_record::state::RecordData, - spl_token_2022::{ - extension::{ - confidential_transfer::{ - self, - account_info::{ - ApplyPendingBalanceAccountInfo, EmptyAccountAccountInfo, TransferAccountInfo, - WithdrawAccountInfo, - }, - ConfidentialTransferAccount, DecryptableBalance, - }, - confidential_transfer_fee::{ - self, account_info::WithheldTokensInfo, ConfidentialTransferFeeAmount, - ConfidentialTransferFeeConfig, - }, - cpi_guard, default_account_state, group_member_pointer, group_pointer, - interest_bearing_mint, memo_transfer, metadata_pointer, pausable, scaled_ui_amount, - transfer_fee, transfer_hook, BaseStateWithExtensions, Extension, ExtensionType, - StateWithExtensionsOwned, - }, - instruction, offchain, - solana_zk_sdk::{ - encryption::{ - auth_encryption::AeKey, - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey}, - pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - }, - zk_elgamal_proof_program::{ - self, - instruction::{close_context_state, ContextStateInfo}, - proof_data::*, - state::ProofContextState, - }, - }, - state::{Account, AccountState, Mint, Multisig}, - }, - spl_token_confidential_transfer_proof_extraction::instruction::{ - zk_proof_type_to_instruction, ProofData, ProofLocation, - }, - spl_token_confidential_transfer_proof_generation::{ - transfer::TransferProofData, transfer_with_fee::TransferWithFeeProofData, - withdraw::WithdrawProofData, - }, - spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, - spl_token_metadata_interface::state::{Field, TokenMetadata}, - std::{ - fmt, io, - mem::size_of, - sync::{Arc, RwLock}, - time::{Duration, Instant}, - }, - thiserror::Error, -}; - -#[derive(Error, Debug)] -pub enum TokenError { - #[error("client error: {0}")] - Client(ProgramClientError), - #[error("program error: {0}")] - Program(#[from] ProgramError), - #[error("account not found")] - AccountNotFound, - #[error("invalid account owner")] - AccountInvalidOwner, - #[error("invalid account mint")] - AccountInvalidMint, - #[error("invalid associated account address")] - AccountInvalidAssociatedAddress, - #[error("invalid auxiliary account address")] - AccountInvalidAuxiliaryAddress, - #[error("proof generation")] - ProofGeneration, - #[error("maximum deposit transfer amount exceeded")] - MaximumDepositTransferAmountExceeded, - #[error("encryption key error")] - Key(SignerError), - #[error("account decryption failed")] - AccountDecryption, - #[error("not enough funds in account")] - NotEnoughFunds, - #[error("missing memo signer")] - MissingMemoSigner, - #[error("decimals required, but missing")] - MissingDecimals, - #[error("decimals specified, but incorrect")] - InvalidDecimals, -} -impl PartialEq for TokenError { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - // TODO not great, but workable for tests - // currently missing: proof error, signer error - (Self::Client(ref a), Self::Client(ref b)) => a.to_string() == b.to_string(), - (Self::Program(ref a), Self::Program(ref b)) => a == b, - (Self::AccountNotFound, Self::AccountNotFound) => true, - (Self::AccountInvalidOwner, Self::AccountInvalidOwner) => true, - (Self::AccountInvalidMint, Self::AccountInvalidMint) => true, - (Self::AccountInvalidAssociatedAddress, Self::AccountInvalidAssociatedAddress) => true, - (Self::AccountInvalidAuxiliaryAddress, Self::AccountInvalidAuxiliaryAddress) => true, - (Self::ProofGeneration, Self::ProofGeneration) => true, - ( - Self::MaximumDepositTransferAmountExceeded, - Self::MaximumDepositTransferAmountExceeded, - ) => true, - (Self::AccountDecryption, Self::AccountDecryption) => true, - (Self::NotEnoughFunds, Self::NotEnoughFunds) => true, - (Self::MissingMemoSigner, Self::MissingMemoSigner) => true, - (Self::MissingDecimals, Self::MissingDecimals) => true, - (Self::InvalidDecimals, Self::InvalidDecimals) => true, - _ => false, - } - } -} - -/// Encapsulates initializing an extension -#[derive(Clone, Debug, PartialEq)] -pub enum ExtensionInitializationParams { - ConfidentialTransferMint { - authority: Option, - auto_approve_new_accounts: bool, - auditor_elgamal_pubkey: Option, - }, - DefaultAccountState { - state: AccountState, - }, - MintCloseAuthority { - close_authority: Option, - }, - TransferFeeConfig { - transfer_fee_config_authority: Option, - withdraw_withheld_authority: Option, - transfer_fee_basis_points: u16, - maximum_fee: u64, - }, - InterestBearingConfig { - rate_authority: Option, - rate: i16, - }, - NonTransferable, - PermanentDelegate { - delegate: Pubkey, - }, - TransferHook { - authority: Option, - program_id: Option, - }, - MetadataPointer { - authority: Option, - metadata_address: Option, - }, - ConfidentialTransferFeeConfig { - authority: Option, - withdraw_withheld_authority_elgamal_pubkey: PodElGamalPubkey, - }, - GroupPointer { - authority: Option, - group_address: Option, - }, - GroupMemberPointer { - authority: Option, - member_address: Option, - }, - ScaledUiAmountConfig { - authority: Option, - multiplier: f64, - }, - PausableConfig { - authority: Pubkey, - }, -} -impl ExtensionInitializationParams { - /// Get the extension type associated with the init params - pub fn extension(&self) -> ExtensionType { - match self { - Self::ConfidentialTransferMint { .. } => ExtensionType::ConfidentialTransferMint, - Self::DefaultAccountState { .. } => ExtensionType::DefaultAccountState, - Self::MintCloseAuthority { .. } => ExtensionType::MintCloseAuthority, - Self::TransferFeeConfig { .. } => ExtensionType::TransferFeeConfig, - Self::InterestBearingConfig { .. } => ExtensionType::InterestBearingConfig, - Self::NonTransferable => ExtensionType::NonTransferable, - Self::PermanentDelegate { .. } => ExtensionType::PermanentDelegate, - Self::TransferHook { .. } => ExtensionType::TransferHook, - Self::MetadataPointer { .. } => ExtensionType::MetadataPointer, - Self::ConfidentialTransferFeeConfig { .. } => { - ExtensionType::ConfidentialTransferFeeConfig - } - Self::GroupPointer { .. } => ExtensionType::GroupPointer, - Self::GroupMemberPointer { .. } => ExtensionType::GroupMemberPointer, - Self::ScaledUiAmountConfig { .. } => ExtensionType::ScaledUiAmount, - Self::PausableConfig { .. } => ExtensionType::Pausable, - } - } - /// Generate an appropriate initialization instruction for the given mint - pub fn instruction( - self, - token_program_id: &Pubkey, - mint: &Pubkey, - ) -> Result { - match self { - Self::ConfidentialTransferMint { - authority, - auto_approve_new_accounts, - auditor_elgamal_pubkey, - } => confidential_transfer::instruction::initialize_mint( - token_program_id, - mint, - authority, - auto_approve_new_accounts, - auditor_elgamal_pubkey, - ), - Self::DefaultAccountState { state } => { - default_account_state::instruction::initialize_default_account_state( - token_program_id, - mint, - &state, - ) - } - Self::MintCloseAuthority { close_authority } => { - instruction::initialize_mint_close_authority( - token_program_id, - mint, - close_authority.as_ref(), - ) - } - Self::TransferFeeConfig { - transfer_fee_config_authority, - withdraw_withheld_authority, - transfer_fee_basis_points, - maximum_fee, - } => transfer_fee::instruction::initialize_transfer_fee_config( - token_program_id, - mint, - transfer_fee_config_authority.as_ref(), - withdraw_withheld_authority.as_ref(), - transfer_fee_basis_points, - maximum_fee, - ), - Self::InterestBearingConfig { - rate_authority, - rate, - } => interest_bearing_mint::instruction::initialize( - token_program_id, - mint, - rate_authority, - rate, - ), - Self::NonTransferable => { - instruction::initialize_non_transferable_mint(token_program_id, mint) - } - Self::PermanentDelegate { delegate } => { - instruction::initialize_permanent_delegate(token_program_id, mint, &delegate) - } - Self::TransferHook { - authority, - program_id, - } => transfer_hook::instruction::initialize( - token_program_id, - mint, - authority, - program_id, - ), - Self::MetadataPointer { - authority, - metadata_address, - } => metadata_pointer::instruction::initialize( - token_program_id, - mint, - authority, - metadata_address, - ), - Self::ConfidentialTransferFeeConfig { - authority, - withdraw_withheld_authority_elgamal_pubkey, - } => { - confidential_transfer_fee::instruction::initialize_confidential_transfer_fee_config( - token_program_id, - mint, - authority, - &withdraw_withheld_authority_elgamal_pubkey, - ) - } - Self::GroupPointer { - authority, - group_address, - } => group_pointer::instruction::initialize( - token_program_id, - mint, - authority, - group_address, - ), - Self::GroupMemberPointer { - authority, - member_address, - } => group_member_pointer::instruction::initialize( - token_program_id, - mint, - authority, - member_address, - ), - Self::ScaledUiAmountConfig { - authority, - multiplier, - } => scaled_ui_amount::instruction::initialize( - token_program_id, - mint, - authority, - multiplier, - ), - Self::PausableConfig { authority } => { - pausable::instruction::initialize(token_program_id, mint, &authority) - } - } - } -} - -pub type TokenResult = Result; - -#[derive(Debug)] -struct TokenMemo { - text: String, - signers: Vec, -} -impl TokenMemo { - pub fn to_instruction(&self) -> Instruction { - spl_memo::build_memo( - self.text.as_bytes(), - &self.signers.iter().collect::>(), - ) - } -} - -#[derive(Debug, Clone)] -pub enum ComputeUnitLimit { - Default, - Simulated, - Static(u32), -} - -pub enum ProofAccount { - ContextAccount(Pubkey), - RecordAccount(Pubkey, u32), -} - -pub struct ProofAccountWithCiphertext { - pub proof_account: ProofAccount, - pub ciphertext_lo: PodElGamalCiphertext, - pub ciphertext_hi: PodElGamalCiphertext, -} - -pub struct Token { - client: Arc>, - pubkey: Pubkey, /* token mint */ - decimals: Option, - payer: Arc, - program_id: Pubkey, - nonce_account: Option, - nonce_authority: Option>, - nonce_blockhash: Option, - memo: Arc>>, - transfer_hook_accounts: Option>, - compute_unit_price: Option, - compute_unit_limit: ComputeUnitLimit, -} - -impl fmt::Debug for Token { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Token") - .field("pubkey", &self.pubkey) - .field("decimals", &self.decimals) - .field("payer", &self.payer.pubkey()) - .field("program_id", &self.program_id) - .field("nonce_account", &self.nonce_account) - .field( - "nonce_authority", - &self.nonce_authority.as_ref().map(|s| s.pubkey()), - ) - .field("nonce_blockhash", &self.nonce_blockhash) - .field("memo", &self.memo.read().unwrap()) - .field("transfer_hook_accounts", &self.transfer_hook_accounts) - .field("compute_unit_price", &self.compute_unit_price) - .field("compute_unit_limit", &self.compute_unit_limit) - .finish() - } -} - -fn native_mint(program_id: &Pubkey) -> Pubkey { - if program_id == &spl_token_2022::id() { - spl_token_2022::native_mint::id() - } else if program_id == &spl_token::id() { - spl_token::native_mint::id() - } else { - panic!("Unrecognized token program id: {}", program_id); - } -} - -fn native_mint_decimals(program_id: &Pubkey) -> u8 { - if program_id == &spl_token_2022::id() { - spl_token_2022::native_mint::DECIMALS - } else if program_id == &spl_token::id() { - spl_token::native_mint::DECIMALS - } else { - panic!("Unrecognized token program id: {}", program_id); - } -} - -impl Token -where - T: SendTransaction + SimulateTransaction, -{ - pub fn new( - client: Arc>, - program_id: &Pubkey, - address: &Pubkey, - decimals: Option, - payer: Arc, - ) -> Self { - Token { - client, - pubkey: *address, - decimals, - payer, - program_id: *program_id, - nonce_account: None, - nonce_authority: None, - nonce_blockhash: None, - memo: Arc::new(RwLock::new(None)), - transfer_hook_accounts: None, - compute_unit_price: None, - compute_unit_limit: ComputeUnitLimit::Default, - } - } - - pub fn new_native( - client: Arc>, - program_id: &Pubkey, - payer: Arc, - ) -> Self { - Self::new( - client, - program_id, - &native_mint(program_id), - Some(native_mint_decimals(program_id)), - payer, - ) - } - - pub fn is_native(&self) -> bool { - self.pubkey == native_mint(&self.program_id) - } - - /// Get token address. - pub fn get_address(&self) -> &Pubkey { - &self.pubkey - } - - pub fn with_payer(mut self, payer: Arc) -> Self { - self.payer = payer; - self - } - - pub fn with_nonce( - mut self, - nonce_account: &Pubkey, - nonce_authority: Arc, - nonce_blockhash: &Hash, - ) -> Self { - self.nonce_account = Some(*nonce_account); - self.nonce_authority = Some(nonce_authority); - self.nonce_blockhash = Some(*nonce_blockhash); - self.transfer_hook_accounts = Some(vec![]); - self - } - - pub fn with_transfer_hook_accounts(mut self, transfer_hook_accounts: Vec) -> Self { - self.transfer_hook_accounts = Some(transfer_hook_accounts); - self - } - - pub fn with_compute_unit_price(mut self, compute_unit_price: u64) -> Self { - self.compute_unit_price = Some(compute_unit_price); - self - } - - pub fn with_compute_unit_limit(mut self, compute_unit_limit: ComputeUnitLimit) -> Self { - self.compute_unit_limit = compute_unit_limit; - self - } - - pub fn with_memo>(&self, memo: M, signers: Vec) -> &Self { - let mut w_memo = self.memo.write().unwrap(); - *w_memo = Some(TokenMemo { - text: memo.as_ref().to_string(), - signers, - }); - self - } - - pub async fn get_new_latest_blockhash(&self) -> TokenResult { - let blockhash = self - .client - .get_latest_blockhash() - .await - .map_err(TokenError::Client)?; - let start = Instant::now(); - let mut num_retries = 0; - while start.elapsed().as_secs() < 5 { - let new_blockhash = self - .client - .get_latest_blockhash() - .await - .map_err(TokenError::Client)?; - if new_blockhash != blockhash { - return Ok(new_blockhash); - } - - time::sleep(Duration::from_millis(200)).await; - num_retries += 1; - } - - Err(TokenError::Client(Box::new(io::Error::new( - io::ErrorKind::Other, - format!( - "Unable to get new blockhash after {}ms (retried {} times), stuck at {}", - start.elapsed().as_millis(), - num_retries, - blockhash - ), - )))) - } - - fn get_multisig_signers<'a>( - &self, - authority: &Pubkey, - signing_pubkeys: &'a [Pubkey], - ) -> Vec<&'a Pubkey> { - if signing_pubkeys == [*authority] { - vec![] - } else { - signing_pubkeys.iter().collect::>() - } - } - - /// Helper function to add a compute unit limit instruction to a given set - /// of instructions - async fn add_compute_unit_limit_from_simulation( - &self, - instructions: &mut Vec, - blockhash: &Hash, - ) -> TokenResult<()> { - // add a max compute unit limit instruction for the simulation - const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; - instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( - MAX_COMPUTE_UNIT_LIMIT, - )); - - let transaction = Transaction::new_unsigned(Message::new_with_blockhash( - instructions, - Some(&self.payer.pubkey()), - blockhash, - )); - let simulation_result = self - .client - .simulate_transaction(&transaction) - .await - .map_err(TokenError::Client)?; - let units_consumed = simulation_result - .get_compute_units_consumed() - .map_err(TokenError::Client)?; - // Overwrite the compute unit limit instruction with the actual units consumed - let compute_unit_limit = - u32::try_from(units_consumed).map_err(|x| TokenError::Client(x.into()))?; - instructions - .last_mut() - .expect("Compute budget instruction was added earlier") - .data = ComputeBudgetInstruction::set_compute_unit_limit(compute_unit_limit).data; - Ok(()) - } - - async fn construct_tx( - &self, - token_instructions: &[Instruction], - signing_keypairs: &S, - ) -> TokenResult { - let mut instructions = vec![]; - let payer_key = self.payer.pubkey(); - let fee_payer = Some(&payer_key); - - { - let mut w_memo = self.memo.write().unwrap(); - if let Some(memo) = w_memo.take() { - let signing_pubkeys = signing_keypairs.pubkeys(); - if !memo - .signers - .iter() - .all(|signer| signing_pubkeys.contains(signer)) - { - return Err(TokenError::MissingMemoSigner); - } - - instructions.push(memo.to_instruction()); - } - } - - instructions.extend_from_slice(token_instructions); - - let blockhash = if let (Some(nonce_account), Some(nonce_authority), Some(nonce_blockhash)) = ( - self.nonce_account, - &self.nonce_authority, - self.nonce_blockhash, - ) { - let nonce_instruction = system_instruction::advance_nonce_account( - &nonce_account, - &nonce_authority.pubkey(), - ); - instructions.insert(0, nonce_instruction); - nonce_blockhash - } else { - self.client - .get_latest_blockhash() - .await - .map_err(TokenError::Client)? - }; - - if let Some(compute_unit_price) = self.compute_unit_price { - instructions.push(ComputeBudgetInstruction::set_compute_unit_price( - compute_unit_price, - )); - } - - // The simulation to find out the compute unit usage must be run after - // all instructions have been added to the transaction, so be sure to - // keep this instruction as the last one before creating and sending the - // transaction. - match self.compute_unit_limit { - ComputeUnitLimit::Default => {} - ComputeUnitLimit::Simulated => { - self.add_compute_unit_limit_from_simulation(&mut instructions, &blockhash) - .await?; - } - ComputeUnitLimit::Static(compute_unit_limit) => { - instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( - compute_unit_limit, - )); - } - } - - let message = Message::new_with_blockhash(&instructions, fee_payer, &blockhash); - let mut transaction = Transaction::new_unsigned(message); - let signing_pubkeys = signing_keypairs.pubkeys(); - - if !signing_pubkeys.contains(&self.payer.pubkey()) { - transaction - .try_partial_sign(&vec![self.payer.clone()], blockhash) - .map_err(|error| TokenError::Client(error.into()))?; - } - if let Some(nonce_authority) = &self.nonce_authority { - let nonce_authority_pubkey = nonce_authority.pubkey(); - if nonce_authority_pubkey != self.payer.pubkey() - && !signing_pubkeys.contains(&nonce_authority_pubkey) - { - transaction - .try_partial_sign(&vec![nonce_authority.clone()], blockhash) - .map_err(|error| TokenError::Client(error.into()))?; - } - } - transaction - .try_partial_sign(signing_keypairs, blockhash) - .map_err(|error| TokenError::Client(error.into()))?; - - Ok(transaction) - } - - pub async fn simulate_ixs( - &self, - token_instructions: &[Instruction], - signing_keypairs: &S, - ) -> TokenResult { - let transaction = self - .construct_tx(token_instructions, signing_keypairs) - .await?; - - self.client - .simulate_transaction(&transaction) - .await - .map_err(TokenError::Client) - } - - pub async fn process_ixs( - &self, - token_instructions: &[Instruction], - signing_keypairs: &S, - ) -> TokenResult { - let transaction = self - .construct_tx(token_instructions, signing_keypairs) - .await?; - - self.client - .send_transaction(&transaction) - .await - .map_err(TokenError::Client) - } - - #[allow(clippy::too_many_arguments)] - pub async fn create_mint<'a, S: Signers>( - &self, - mint_authority: &'a Pubkey, - freeze_authority: Option<&'a Pubkey>, - extension_initialization_params: Vec, - signing_keypairs: &S, - ) -> TokenResult { - let decimals = self.decimals.ok_or(TokenError::MissingDecimals)?; - - let extension_types = extension_initialization_params - .iter() - .map(|e| e.extension()) - .collect::>(); - let space = ExtensionType::try_calculate_account_len::(&extension_types)?; - - let mut instructions = vec![system_instruction::create_account( - &self.payer.pubkey(), - &self.pubkey, - self.client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?, - space as u64, - &self.program_id, - )]; - - for params in extension_initialization_params { - instructions.push(params.instruction(&self.program_id, &self.pubkey)?); - } - - instructions.push(instruction::initialize_mint( - &self.program_id, - &self.pubkey, - mint_authority, - freeze_authority, - decimals, - )?); - - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Create native mint - pub async fn create_native_mint( - client: Arc>, - program_id: &Pubkey, - payer: Arc, - ) -> TokenResult { - let token = Self::new_native(client, program_id, payer); - token - .process_ixs::<[&dyn Signer; 0]>( - &[instruction::create_native_mint( - program_id, - &token.payer.pubkey(), - )?], - &[], - ) - .await?; - - Ok(token) - } - - /// Create multisig - pub async fn create_multisig( - &self, - account: &dyn Signer, - multisig_members: &[&Pubkey], - minimum_signers: u8, - ) -> TokenResult { - let instructions = vec![ - system_instruction::create_account( - &self.payer.pubkey(), - &account.pubkey(), - self.client - .get_minimum_balance_for_rent_exemption(Multisig::LEN) - .await - .map_err(TokenError::Client)?, - Multisig::LEN as u64, - &self.program_id, - ), - instruction::initialize_multisig( - &self.program_id, - &account.pubkey(), - multisig_members, - minimum_signers, - )?, - ]; - - self.process_ixs(&instructions, &[account]).await - } - - /// Get the address for the associated token account. - pub fn get_associated_token_address(&self, owner: &Pubkey) -> Pubkey { - get_associated_token_address_with_program_id(owner, &self.pubkey, &self.program_id) - } - - /// Create and initialize the associated account. - pub async fn create_associated_token_account(&self, owner: &Pubkey) -> TokenResult { - self.process_ixs::<[&dyn Signer; 0]>( - &[create_associated_token_account( - &self.payer.pubkey(), - owner, - &self.pubkey, - &self.program_id, - )], - &[], - ) - .await - } - - /// Create and initialize a new token account. - pub async fn create_auxiliary_token_account( - &self, - account: &dyn Signer, - owner: &Pubkey, - ) -> TokenResult { - self.create_auxiliary_token_account_with_extension_space(account, owner, vec![]) - .await - } - - /// Create and initialize a new token account. - pub async fn create_auxiliary_token_account_with_extension_space( - &self, - account: &dyn Signer, - owner: &Pubkey, - extensions: Vec, - ) -> TokenResult { - let state = self.get_mint_info().await?; - let mint_extensions: Vec = state.get_extension_types()?; - let mut required_extensions = - ExtensionType::get_required_init_account_extensions(&mint_extensions); - for extension_type in extensions.into_iter() { - if !required_extensions.contains(&extension_type) { - required_extensions.push(extension_type); - } - } - let space = ExtensionType::try_calculate_account_len::(&required_extensions)?; - let mut instructions = vec![system_instruction::create_account( - &self.payer.pubkey(), - &account.pubkey(), - self.client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?, - space as u64, - &self.program_id, - )]; - - if required_extensions.contains(&ExtensionType::ImmutableOwner) { - instructions.push(instruction::initialize_immutable_owner( - &self.program_id, - &account.pubkey(), - )?) - } - - instructions.push(instruction::initialize_account( - &self.program_id, - &account.pubkey(), - &self.pubkey, - owner, - )?); - - self.process_ixs(&instructions, &[account]).await - } - - /// Retrieve a raw account - pub async fn get_account(&self, account: Pubkey) -> TokenResult { - self.client - .get_account(account) - .await - .map_err(TokenError::Client)? - .ok_or(TokenError::AccountNotFound) - } - - fn unpack_mint_info( - &self, - account: BaseAccount, - ) -> TokenResult> { - if account.owner != self.program_id { - return Err(TokenError::AccountInvalidOwner); - } - - let mint_result = - StateWithExtensionsOwned::::unpack(account.data).map_err(Into::into); - - if let (Ok(mint), Some(decimals)) = (&mint_result, self.decimals) { - if decimals != mint.base.decimals { - return Err(TokenError::InvalidDecimals); - } - } - - mint_result - } - - /// Retrieve mint information. - pub async fn get_mint_info(&self) -> TokenResult> { - let account = self.get_account(self.pubkey).await?; - self.unpack_mint_info(account) - } - - /// Retrieve account information. - pub async fn get_account_info( - &self, - account: &Pubkey, - ) -> TokenResult> { - let account = self.get_account(*account).await?; - if account.owner != self.program_id { - return Err(TokenError::AccountInvalidOwner); - } - let account = StateWithExtensionsOwned::::unpack(account.data)?; - if account.base.mint != *self.get_address() { - return Err(TokenError::AccountInvalidMint); - } - - Ok(account) - } - - /// Retrieve the associated account or create one if not found. - pub async fn get_or_create_associated_account_info( - &self, - owner: &Pubkey, - ) -> TokenResult> { - let account = self.get_associated_token_address(owner); - match self.get_account_info(&account).await { - Ok(account) => Ok(account), - // AccountInvalidOwner is possible if account already received some lamports. - Err(TokenError::AccountNotFound) | Err(TokenError::AccountInvalidOwner) => { - self.create_associated_token_account(owner).await?; - self.get_account_info(&account).await - } - Err(error) => Err(error), - } - } - - /// Assign a new authority to the account. - pub async fn set_authority( - &self, - account: &Pubkey, - authority: &Pubkey, - new_authority: Option<&Pubkey>, - authority_type: instruction::AuthorityType, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[instruction::set_authority( - &self.program_id, - account, - new_authority, - authority_type, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Mint new tokens - pub async fn mint_to( - &self, - destination: &Pubkey, - authority: &Pubkey, - amount: u64, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let instructions = if let Some(decimals) = self.decimals { - [instruction::mint_to_checked( - &self.program_id, - &self.pubkey, - destination, - authority, - &multisig_signers, - amount, - decimals, - )?] - } else { - [instruction::mint_to( - &self.program_id, - &self.pubkey, - destination, - authority, - &multisig_signers, - amount, - )?] - }; - - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Transfer tokens to another account - #[allow(clippy::too_many_arguments)] - pub async fn transfer( - &self, - source: &Pubkey, - destination: &Pubkey, - authority: &Pubkey, - amount: u64, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let fetch_account_data_fn = |address| { - self.client - .get_account(address) - .map_ok(|opt| opt.map(|acc| acc.data)) - }; - - let instruction = if let Some(decimals) = self.decimals { - if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts { - let mut instruction = instruction::transfer_checked( - &self.program_id, - source, - self.get_address(), - destination, - authority, - &multisig_signers, - amount, - decimals, - )?; - instruction.accounts.extend(transfer_hook_accounts.clone()); - instruction - } else { - offchain::create_transfer_checked_instruction_with_extra_metas( - &self.program_id, - source, - self.get_address(), - destination, - authority, - &multisig_signers, - amount, - decimals, - fetch_account_data_fn, - ) - .await - .map_err(|_| TokenError::AccountNotFound)? - } - } else { - #[allow(deprecated)] - instruction::transfer( - &self.program_id, - source, - destination, - authority, - &multisig_signers, - amount, - )? - }; - - self.process_ixs(&[instruction], signing_keypairs).await - } - - /// Transfer tokens to an associated account, creating it if it does not - /// exist - #[allow(clippy::too_many_arguments)] - pub async fn create_recipient_associated_account_and_transfer( - &self, - source: &Pubkey, - destination: &Pubkey, - destination_owner: &Pubkey, - authority: &Pubkey, - amount: u64, - fee: Option, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let fetch_account_data_fn = |address| { - self.client - .get_account(address) - .map_ok(|opt| opt.map(|acc| acc.data)) - }; - - if *destination != self.get_associated_token_address(destination_owner) { - return Err(TokenError::AccountInvalidAssociatedAddress); - } - - let mut instructions = vec![ - (create_associated_token_account_idempotent( - &self.payer.pubkey(), - destination_owner, - &self.pubkey, - &self.program_id, - )), - ]; - - if let Some(fee) = fee { - let decimals = self.decimals.ok_or(TokenError::MissingDecimals)?; - instructions.push(transfer_fee::instruction::transfer_checked_with_fee( - &self.program_id, - source, - &self.pubkey, - destination, - authority, - &multisig_signers, - amount, - decimals, - fee, - )?); - } else if let Some(decimals) = self.decimals { - instructions.push( - if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts { - let mut instruction = instruction::transfer_checked( - &self.program_id, - source, - self.get_address(), - destination, - authority, - &multisig_signers, - amount, - decimals, - )?; - instruction.accounts.extend(transfer_hook_accounts.clone()); - instruction - } else { - offchain::create_transfer_checked_instruction_with_extra_metas( - &self.program_id, - source, - self.get_address(), - destination, - authority, - &multisig_signers, - amount, - decimals, - fetch_account_data_fn, - ) - .await - .map_err(|_| TokenError::AccountNotFound)? - }, - ); - } else { - #[allow(deprecated)] - instructions.push(instruction::transfer( - &self.program_id, - source, - destination, - authority, - &multisig_signers, - amount, - )?); - } - - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Transfer tokens to another account, given an expected fee - #[allow(clippy::too_many_arguments)] - pub async fn transfer_with_fee( - &self, - source: &Pubkey, - destination: &Pubkey, - authority: &Pubkey, - amount: u64, - fee: u64, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - let decimals = self.decimals.ok_or(TokenError::MissingDecimals)?; - - let fetch_account_data_fn = |address| { - self.client - .get_account(address) - .map_ok(|opt| opt.map(|acc| acc.data)) - }; - - let instruction = if let Some(transfer_hook_accounts) = &self.transfer_hook_accounts { - let mut instruction = transfer_fee::instruction::transfer_checked_with_fee( - &self.program_id, - source, - self.get_address(), - destination, - authority, - &multisig_signers, - amount, - decimals, - fee, - )?; - instruction.accounts.extend(transfer_hook_accounts.clone()); - instruction - } else { - offchain::create_transfer_checked_with_fee_instruction_with_extra_metas( - &self.program_id, - source, - self.get_address(), - destination, - authority, - &multisig_signers, - amount, - decimals, - fee, - fetch_account_data_fn, - ) - .await - .map_err(|_| TokenError::AccountNotFound)? - }; - - self.process_ixs(&[instruction], signing_keypairs).await - } - - /// Burn tokens from account - pub async fn burn( - &self, - source: &Pubkey, - authority: &Pubkey, - amount: u64, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let instructions = if let Some(decimals) = self.decimals { - [instruction::burn_checked( - &self.program_id, - source, - &self.pubkey, - authority, - &multisig_signers, - amount, - decimals, - )?] - } else { - [instruction::burn( - &self.program_id, - source, - &self.pubkey, - authority, - &multisig_signers, - amount, - )?] - }; - - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Approve a delegate to spend tokens - pub async fn approve( - &self, - source: &Pubkey, - delegate: &Pubkey, - authority: &Pubkey, - amount: u64, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let instructions = if let Some(decimals) = self.decimals { - [instruction::approve_checked( - &self.program_id, - source, - &self.pubkey, - delegate, - authority, - &multisig_signers, - amount, - decimals, - )?] - } else { - [instruction::approve( - &self.program_id, - source, - delegate, - authority, - &multisig_signers, - amount, - )?] - }; - - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Revoke a delegate - pub async fn revoke( - &self, - source: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[instruction::revoke( - &self.program_id, - source, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Close an empty account and reclaim its lamports - pub async fn close_account( - &self, - account: &Pubkey, - lamports_destination: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let mut instructions = vec![instruction::close_account( - &self.program_id, - account, - lamports_destination, - authority, - &multisig_signers, - )?]; - - if let Ok(Some(destination_account)) = self.client.get_account(*lamports_destination).await - { - if let Ok(destination_obj) = - StateWithExtensionsOwned::::unpack(destination_account.data) - { - if destination_obj.base.is_native() { - instructions.push(instruction::sync_native( - &self.program_id, - lamports_destination, - )?); - } - } - } - - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Close an account, reclaiming its lamports and tokens - pub async fn empty_and_close_account( - &self, - account_to_close: &Pubkey, - lamports_destination: &Pubkey, - tokens_destination: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - // this implicitly validates that the mint on self is correct - let account_state = self.get_account_info(account_to_close).await?; - - let mut instructions = vec![]; - - if !self.is_native() && account_state.base.amount > 0 { - // if a separate close authority is being used, it must be a delegate also - if let Some(decimals) = self.decimals { - instructions.push(instruction::transfer_checked( - &self.program_id, - account_to_close, - &self.pubkey, - tokens_destination, - authority, - &multisig_signers, - account_state.base.amount, - decimals, - )?); - } else { - #[allow(deprecated)] - instructions.push(instruction::transfer( - &self.program_id, - account_to_close, - tokens_destination, - authority, - &multisig_signers, - account_state.base.amount, - )?); - } - } - - instructions.push(instruction::close_account( - &self.program_id, - account_to_close, - lamports_destination, - authority, - &multisig_signers, - )?); - - if let Ok(Some(destination_account)) = self.client.get_account(*lamports_destination).await - { - if let Ok(destination_obj) = - StateWithExtensionsOwned::::unpack(destination_account.data) - { - if destination_obj.base.is_native() { - instructions.push(instruction::sync_native( - &self.program_id, - lamports_destination, - )?); - } - } - } - - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Freeze a token account - pub async fn freeze( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[instruction::freeze_account( - &self.program_id, - account, - &self.pubkey, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Thaw / unfreeze a token account - pub async fn thaw( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[instruction::thaw_account( - &self.program_id, - account, - &self.pubkey, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Wrap lamports into native account - pub async fn wrap( - &self, - account: &Pubkey, - owner: &Pubkey, - lamports: u64, - signing_keypairs: &S, - ) -> TokenResult { - // mutable owner for Tokenkeg, immutable otherwise - let immutable_owner = self.program_id != spl_token::id(); - let instructions = self.wrap_ixs(account, owner, lamports, immutable_owner)?; - - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Wrap lamports into a native account that can always have its ownership - /// changed - pub async fn wrap_with_mutable_ownership( - &self, - account: &Pubkey, - owner: &Pubkey, - lamports: u64, - signing_keypairs: &S, - ) -> TokenResult { - let instructions = self.wrap_ixs(account, owner, lamports, false)?; - - self.process_ixs(&instructions, signing_keypairs).await - } - - fn wrap_ixs( - &self, - account: &Pubkey, - owner: &Pubkey, - lamports: u64, - immutable_owner: bool, - ) -> TokenResult> { - if !self.is_native() { - return Err(TokenError::AccountInvalidMint); - } - - let mut instructions = vec![]; - if *account == self.get_associated_token_address(owner) { - instructions.push(system_instruction::transfer(owner, account, lamports)); - instructions.push(create_associated_token_account( - &self.payer.pubkey(), - owner, - &self.pubkey, - &self.program_id, - )); - } else { - let extensions = if immutable_owner { - vec![ExtensionType::ImmutableOwner] - } else { - vec![] - }; - let space = ExtensionType::try_calculate_account_len::(&extensions)?; - - instructions.push(system_instruction::create_account( - &self.payer.pubkey(), - account, - lamports, - space as u64, - &self.program_id, - )); - - if immutable_owner { - instructions.push(instruction::initialize_immutable_owner( - &self.program_id, - account, - )?) - } - - instructions.push(instruction::initialize_account( - &self.program_id, - account, - &self.pubkey, - owner, - )?); - }; - - Ok(instructions) - } - - /// Sync native account lamports - pub async fn sync_native(&self, account: &Pubkey) -> TokenResult { - self.process_ixs::<[&dyn Signer; 0]>( - &[instruction::sync_native(&self.program_id, account)?], - &[], - ) - .await - } - - /// Set transfer fee - pub async fn set_transfer_fee( - &self, - authority: &Pubkey, - transfer_fee_basis_points: u16, - maximum_fee: u64, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[transfer_fee::instruction::set_transfer_fee( - &self.program_id, - &self.pubkey, - authority, - &multisig_signers, - transfer_fee_basis_points, - maximum_fee, - )?], - signing_keypairs, - ) - .await - } - - /// Set default account state on mint - pub async fn set_default_account_state( - &self, - authority: &Pubkey, - state: &AccountState, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[ - default_account_state::instruction::update_default_account_state( - &self.program_id, - &self.pubkey, - authority, - &multisig_signers, - state, - )?, - ], - signing_keypairs, - ) - .await - } - - /// Harvest withheld tokens to mint - pub async fn harvest_withheld_tokens_to_mint( - &self, - sources: &[&Pubkey], - ) -> TokenResult { - self.process_ixs::<[&dyn Signer; 0]>( - &[transfer_fee::instruction::harvest_withheld_tokens_to_mint( - &self.program_id, - &self.pubkey, - sources, - )?], - &[], - ) - .await - } - - /// Withdraw withheld tokens from mint - pub async fn withdraw_withheld_tokens_from_mint( - &self, - destination: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[ - transfer_fee::instruction::withdraw_withheld_tokens_from_mint( - &self.program_id, - &self.pubkey, - destination, - authority, - &multisig_signers, - )?, - ], - signing_keypairs, - ) - .await - } - - /// Withdraw withheld tokens from accounts - pub async fn withdraw_withheld_tokens_from_accounts( - &self, - destination: &Pubkey, - authority: &Pubkey, - sources: &[&Pubkey], - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[ - transfer_fee::instruction::withdraw_withheld_tokens_from_accounts( - &self.program_id, - &self.pubkey, - destination, - authority, - &multisig_signers, - sources, - )?, - ], - signing_keypairs, - ) - .await - } - - /// Reallocate a token account to be large enough for a set of - /// `ExtensionType`s - pub async fn reallocate( - &self, - account: &Pubkey, - authority: &Pubkey, - extension_types: &[ExtensionType], - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[instruction::reallocate( - &self.program_id, - account, - &self.payer.pubkey(), - authority, - &multisig_signers, - extension_types, - )?], - signing_keypairs, - ) - .await - } - - /// Require memos on transfers into this account - pub async fn enable_required_transfer_memos( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[memo_transfer::instruction::enable_required_transfer_memos( - &self.program_id, - account, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Stop requiring memos on transfers into this account - pub async fn disable_required_transfer_memos( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[memo_transfer::instruction::disable_required_transfer_memos( - &self.program_id, - account, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Pause transferring, minting, and burning on the mint - pub async fn pause( - &self, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[pausable::instruction::pause( - &self.program_id, - self.get_address(), - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Resume transferring, minting, and burning on the mint - pub async fn resume( - &self, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[pausable::instruction::resume( - &self.program_id, - self.get_address(), - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Prevent unsafe usage of token account through CPI - pub async fn enable_cpi_guard( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[cpi_guard::instruction::enable_cpi_guard( - &self.program_id, - account, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Stop preventing unsafe usage of token account through CPI - pub async fn disable_cpi_guard( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[cpi_guard::instruction::disable_cpi_guard( - &self.program_id, - account, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Update interest rate - pub async fn update_interest_rate( - &self, - authority: &Pubkey, - new_rate: i16, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[interest_bearing_mint::instruction::update_rate( - &self.program_id, - self.get_address(), - authority, - &multisig_signers, - new_rate, - )?], - signing_keypairs, - ) - .await - } - - /// Update multiplier - pub async fn update_multiplier( - &self, - authority: &Pubkey, - new_multiplier: f64, - new_multiplier_effective_timestamp: i64, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[scaled_ui_amount::instruction::update_multiplier( - &self.program_id, - self.get_address(), - authority, - &multisig_signers, - new_multiplier, - new_multiplier_effective_timestamp, - )?], - signing_keypairs, - ) - .await - } - - /// Update transfer hook program id - pub async fn update_transfer_hook_program_id( - &self, - authority: &Pubkey, - new_program_id: Option, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[transfer_hook::instruction::update( - &self.program_id, - self.get_address(), - authority, - &multisig_signers, - new_program_id, - )?], - signing_keypairs, - ) - .await - } - - /// Update metadata pointer address - pub async fn update_metadata_address( - &self, - authority: &Pubkey, - new_metadata_address: Option, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[metadata_pointer::instruction::update( - &self.program_id, - self.get_address(), - authority, - &multisig_signers, - new_metadata_address, - )?], - signing_keypairs, - ) - .await - } - - /// Update group pointer address - pub async fn update_group_address( - &self, - authority: &Pubkey, - new_group_address: Option, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[group_pointer::instruction::update( - &self.program_id, - self.get_address(), - authority, - &multisig_signers, - new_group_address, - )?], - signing_keypairs, - ) - .await - } - - /// Update group member pointer address - pub async fn update_group_member_address( - &self, - authority: &Pubkey, - new_member_address: Option, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[group_member_pointer::instruction::update( - &self.program_id, - self.get_address(), - authority, - &multisig_signers, - new_member_address, - )?], - signing_keypairs, - ) - .await - } - - /// Update confidential transfer mint - pub async fn confidential_transfer_update_mint( - &self, - authority: &Pubkey, - auto_approve_new_account: bool, - auditor_elgamal_pubkey: Option, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[confidential_transfer::instruction::update_mint( - &self.program_id, - &self.pubkey, - authority, - &multisig_signers, - auto_approve_new_account, - auditor_elgamal_pubkey, - )?], - signing_keypairs, - ) - .await - } - - /// Configures confidential transfers for a token account. If the maximum - /// pending balance credit counter for the extension is not provided, - /// then it is set to be a default value of `2^16`. - #[allow(clippy::too_many_arguments)] - pub async fn confidential_transfer_configure_token_account( - &self, - account: &Pubkey, - authority: &Pubkey, - proof_account: Option<&ProofAccount>, - maximum_pending_balance_credit_counter: Option, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - signing_keypairs: &S, - ) -> TokenResult { - const DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER: u64 = 65536; - - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let maximum_pending_balance_credit_counter = maximum_pending_balance_credit_counter - .unwrap_or(DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER); - - let proof_data = if proof_account.is_some() { - None - } else { - Some( - confidential_transfer::instruction::PubkeyValidityProofData::new(elgamal_keypair) - .map_err(|_| TokenError::ProofGeneration)?, - ) - }; - - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, - // which is guaranteed by the previous check - let proof_location = Self::confidential_transfer_create_proof_location( - proof_data.as_ref(), - proof_account, - 1, - ) - .unwrap(); - - let decryptable_balance = aes_key.encrypt(0).into(); - - self.process_ixs( - &confidential_transfer::instruction::configure_account( - &self.program_id, - account, - &self.pubkey, - &decryptable_balance, - maximum_pending_balance_credit_counter, - authority, - &multisig_signers, - proof_location, - )?, - signing_keypairs, - ) - .await - } - - /// Configures confidential transfers for a token account using an ElGamal - /// registry account - pub async fn confidential_transfer_configure_token_account_with_registry( - &self, - account: &Pubkey, - elgamal_registry_account: &Pubkey, - payer: Option<&Pubkey>, - ) -> TokenResult { - self.process_ixs::<[&dyn Signer; 0]>( - &[ - confidential_transfer::instruction::configure_account_with_registry( - &self.program_id, - account, - &self.pubkey, - elgamal_registry_account, - payer, - )?, - ], - &[], - ) - .await - } - - /// Approves a token account for confidential transfers - pub async fn confidential_transfer_approve_account( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[confidential_transfer::instruction::approve_account( - &self.program_id, - account, - &self.pubkey, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Prepare a token account with the confidential transfer extension for - /// closing - pub async fn confidential_transfer_empty_account( - &self, - account: &Pubkey, - authority: &Pubkey, - proof_account: Option<&ProofAccount>, - account_info: Option, - elgamal_keypair: &ElGamalKeypair, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let account_info = if let Some(account_info) = account_info { - account_info - } else { - let account = self.get_account_info(account).await?; - let confidential_transfer_account = - account.get_extension::()?; - EmptyAccountAccountInfo::new(confidential_transfer_account) - }; - - let proof_data = if proof_account.is_some() { - None - } else { - Some( - account_info - .generate_proof_data(elgamal_keypair) - .map_err(|_| TokenError::ProofGeneration)?, - ) - }; - - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, - // which is guaranteed by the previous check - let proof_location = Self::confidential_transfer_create_proof_location( - proof_data.as_ref(), - proof_account, - 1, - ) - .unwrap(); - - self.process_ixs( - &confidential_transfer::instruction::empty_account( - &self.program_id, - account, - authority, - &multisig_signers, - proof_location, - )?, - signing_keypairs, - ) - .await - } - - /// Deposit SPL Tokens into the pending balance of a confidential token - /// account - pub async fn confidential_transfer_deposit( - &self, - account: &Pubkey, - authority: &Pubkey, - amount: u64, - decimals: u8, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[confidential_transfer::instruction::deposit( - &self.program_id, - account, - &self.pubkey, - amount, - decimals, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Withdraw SPL Tokens from the available balance of a confidential token - /// account - #[allow(clippy::too_many_arguments)] - pub async fn confidential_transfer_withdraw( - &self, - account: &Pubkey, - authority: &Pubkey, - equality_proof_account: Option<&ProofAccount>, - range_proof_account: Option<&ProofAccount>, - withdraw_amount: u64, - decimals: u8, - account_info: Option, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let account_info = if let Some(account_info) = account_info { - account_info - } else { - let account = self.get_account_info(account).await?; - let confidential_transfer_account = - account.get_extension::()?; - WithdrawAccountInfo::new(confidential_transfer_account) - }; - - let (equality_proof_data, range_proof_data) = - if equality_proof_account.is_some() && range_proof_account.is_some() { - (None, None) - } else { - let WithdrawProofData { - equality_proof_data, - range_proof_data, - } = account_info - .generate_proof_data(withdraw_amount, elgamal_keypair, aes_key) - .map_err(|_| TokenError::ProofGeneration)?; - - // if proof accounts are none, then proof data must be included as instruction - // data - let equality_proof_data = equality_proof_account - .is_none() - .then_some(equality_proof_data); - let range_proof_data = range_proof_account.is_none().then_some(range_proof_data); - - (equality_proof_data, range_proof_data) - }; - - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, - // which is guaranteed by the previous check - let equality_proof_location = Self::confidential_transfer_create_proof_location( - equality_proof_data.as_ref(), - equality_proof_account, - 1, - ) - .unwrap(); - - let range_proof_location = Self::confidential_transfer_create_proof_location( - range_proof_data.as_ref(), - range_proof_account, - 2, - ) - .unwrap(); - - let new_decryptable_available_balance = account_info - .new_decryptable_available_balance(withdraw_amount, aes_key) - .map_err(|_| TokenError::AccountDecryption)? - .into(); - - self.process_ixs( - &confidential_transfer::instruction::withdraw( - &self.program_id, - account, - &self.pubkey, - withdraw_amount, - decimals, - &new_decryptable_available_balance, - authority, - &multisig_signers, - equality_proof_location, - range_proof_location, - )?, - signing_keypairs, - ) - .await - } - - /// Transfer tokens confidentially - #[allow(clippy::too_many_arguments)] - pub async fn confidential_transfer_transfer( - &self, - source_account: &Pubkey, - destination_account: &Pubkey, - source_authority: &Pubkey, - equality_proof_account: Option<&ProofAccount>, - ciphertext_validity_proof_account_with_ciphertext: Option<&ProofAccountWithCiphertext>, - range_proof_account: Option<&ProofAccount>, - transfer_amount: u64, - account_info: Option, - source_elgamal_keypair: &ElGamalKeypair, - source_aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(source_authority, &signing_pubkeys); - - let account_info = if let Some(account_info) = account_info { - account_info - } else { - let account = self.get_account_info(source_account).await?; - let confidential_transfer_account = - account.get_extension::()?; - TransferAccountInfo::new(confidential_transfer_account) - }; - - let (equality_proof_data, ciphertext_validity_proof_data_with_ciphertext, range_proof_data) = - if equality_proof_account.is_some() - && ciphertext_validity_proof_account_with_ciphertext.is_some() - && range_proof_account.is_some() - { - (None, None, None) - } else { - let TransferProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - } = account_info - .generate_split_transfer_proof_data( - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ) - .map_err(|_| TokenError::ProofGeneration)?; - - // if proof accounts are none, then proof data must be included as instruction - // data - let equality_proof_data = equality_proof_account - .is_none() - .then_some(equality_proof_data); - let ciphertext_validity_proof_data_with_ciphertext = - ciphertext_validity_proof_account_with_ciphertext - .is_none() - .then_some(ciphertext_validity_proof_data_with_ciphertext); - let range_proof_data = range_proof_account.is_none().then_some(range_proof_data); - - ( - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - ) - }; - - let (transfer_amount_auditor_ciphertext_lo, transfer_amount_auditor_ciphertext_hi) = - if let Some(proof_data_with_ciphertext) = ciphertext_validity_proof_data_with_ciphertext - { - ( - proof_data_with_ciphertext.ciphertext_lo, - proof_data_with_ciphertext.ciphertext_hi, - ) - } else { - // unwrap is safe as long as either `proof_data_with_ciphertext`, - // `proof_account_with_ciphertext` is `Some(..)`, which is guaranteed by the - // previous check - ( - ciphertext_validity_proof_account_with_ciphertext - .unwrap() - .ciphertext_lo, - ciphertext_validity_proof_account_with_ciphertext - .unwrap() - .ciphertext_hi, - ) - }; - - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, - // which is guaranteed by the previous check - let equality_proof_location = Self::confidential_transfer_create_proof_location( - equality_proof_data.as_ref(), - equality_proof_account, - 1, - ) - .unwrap(); - let ciphertext_validity_proof_data = - ciphertext_validity_proof_data_with_ciphertext.map(|data| data.proof_data); - let ciphertext_validity_proof_location = Self::confidential_transfer_create_proof_location( - ciphertext_validity_proof_data.as_ref(), - ciphertext_validity_proof_account_with_ciphertext.map(|account| &account.proof_account), - 2, - ) - .unwrap(); - let range_proof_location = Self::confidential_transfer_create_proof_location( - range_proof_data.as_ref(), - range_proof_account, - 3, - ) - .unwrap(); - - let new_decryptable_available_balance = account_info - .new_decryptable_available_balance(transfer_amount, source_aes_key) - .map_err(|_| TokenError::AccountDecryption)? - .into(); - - let mut instructions = confidential_transfer::instruction::transfer( - &self.program_id, - source_account, - self.get_address(), - destination_account, - &new_decryptable_available_balance, - &transfer_amount_auditor_ciphertext_lo, - &transfer_amount_auditor_ciphertext_hi, - source_authority, - &multisig_signers, - equality_proof_location, - ciphertext_validity_proof_location, - range_proof_location, - )?; - offchain::add_extra_account_metas( - &mut instructions[0], - source_account, - self.get_address(), - destination_account, - source_authority, - u64::MAX, - |address| { - self.client - .get_account(address) - .map_ok(|opt| opt.map(|acc| acc.data)) - }, - ) - .await - .map_err(|_| TokenError::AccountNotFound)?; - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Create a record account containing zero-knowledge proof needed for a - /// confidential transfer. - pub async fn confidential_transfer_create_record_account< - S1: Signer, - S2: Signer, - ZK: Pod + ZkProofData, - U: Pod, - >( - &self, - record_account: &Pubkey, - record_authority: &Pubkey, - proof_data: &ZK, - record_account_signer: &S1, - record_authority_signer: &S2, - ) -> TokenResult> { - let proof_data = bytes_of(proof_data); - let space = proof_data - .len() - .saturating_add(RecordData::WRITABLE_START_INDEX); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - - // A closure that constructs a vector of instructions needed to create and write - // to record accounts. The closure is defined as a convenience function - // to be fed into the function `calculate_record_max_chunk_size`. - let create_record_instructions = |first_instruction: bool, bytes: &[u8], offset: u64| { - let mut ixs = vec![]; - if first_instruction { - ixs.push(system_instruction::create_account( - &self.payer.pubkey(), - record_account, - rent, - space as u64, - &spl_record::id(), - )); - ixs.push(spl_record::instruction::initialize( - record_account, - record_authority, - )); - } - ixs.push(spl_record::instruction::write( - record_account, - record_authority, - offset, - bytes, - )); - ixs - }; - let first_chunk_size = calculate_record_max_chunk_size(create_record_instructions, true); - let (first_chunk, rest) = if space <= first_chunk_size { - (proof_data, &[] as &[u8]) - } else { - proof_data.split_at(first_chunk_size) - }; - - let first_ixs = create_record_instructions(true, first_chunk, 0); - let first_ixs_signers: [&dyn Signer; 2] = [record_account_signer, record_authority_signer]; - self.process_ixs(&first_ixs, &first_ixs_signers).await?; - - let subsequent_chunk_size = - calculate_record_max_chunk_size(create_record_instructions, false); - let mut record_offset = first_chunk_size; - let mut ixs_batch = vec![]; - for chunk in rest.chunks(subsequent_chunk_size) { - ixs_batch.push(create_record_instructions( - false, - chunk, - record_offset as u64, - )); - record_offset = record_offset.saturating_add(chunk.len()); - } - - let futures = ixs_batch - .into_iter() - .map(|ixs| async move { self.process_ixs(&ixs, &[record_authority_signer]).await }) - .collect::>(); - - join_all(futures).await.into_iter().collect() - } - - /// Close a record account. - pub async fn confidential_transfer_close_record_account( - &self, - record_account: &Pubkey, - lamport_destination_account: &Pubkey, - record_account_authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - self.process_ixs( - &[spl_record::instruction::close_account( - record_account, - record_account_authority, - lamport_destination_account, - )], - signing_keypairs, - ) - .await - } - - /// Create a context state account containing zero-knowledge proof needed - /// for a confidential transfer instruction. - pub async fn confidential_transfer_create_context_state_account< - S: Signers, - ZK: Pod + ZkProofData, - U: Pod, - >( - &self, - context_state_account: &Pubkey, - context_state_authority: &Pubkey, - proof_data: &ZK, - split_account_creation_and_proof_verification: bool, - signing_keypairs: &S, - ) -> TokenResult { - let instruction_type = zk_proof_type_to_instruction(ZK::PROOF_TYPE)?; - let space = size_of::>(); - let rent = self - .client - .get_minimum_balance_for_rent_exemption(space) - .await - .map_err(TokenError::Client)?; - - let context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - - // Some proof instructions are right at the transaction size limit, but in the - // future it might be able to support the transfer too - if split_account_creation_and_proof_verification { - self.process_ixs( - &[system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_elgamal_proof_program::id(), - )], - signing_keypairs, - ) - .await?; - - let blockhash = self - .client - .get_latest_blockhash() - .await - .map_err(TokenError::Client)?; - - let transaction = Transaction::new_signed_with_payer( - &[instruction_type.encode_verify_proof(Some(context_state_info), proof_data)], - Some(&self.payer.pubkey()), - &[self.payer.as_ref()], - blockhash, - ); - - self.client - .send_transaction(&transaction) - .await - .map_err(TokenError::Client) - } else { - self.process_ixs( - &[ - system_instruction::create_account( - &self.payer.pubkey(), - context_state_account, - rent, - space as u64, - &zk_elgamal_proof_program::id(), - ), - instruction_type.encode_verify_proof(Some(context_state_info), proof_data), - ], - signing_keypairs, - ) - .await - } - } - - /// Close a ZK Token proof program context state - pub async fn confidential_transfer_close_context_state_account( - &self, - context_state_account: &Pubkey, - lamport_destination_account: &Pubkey, - context_state_authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let context_state_info = ContextStateInfo { - context_state_account, - context_state_authority, - }; - - self.process_ixs( - &[close_context_state( - context_state_info, - lamport_destination_account, - )], - signing_keypairs, - ) - .await - } - - /// Transfer tokens confidentially with fee - #[allow(clippy::too_many_arguments)] - pub async fn confidential_transfer_transfer_with_fee( - &self, - source_account: &Pubkey, - destination_account: &Pubkey, - source_authority: &Pubkey, - equality_proof_account: Option<&ProofAccount>, - transfer_amount_ciphertext_validity_proof_account_with_ciphertext: Option< - &ProofAccountWithCiphertext, - >, - percentage_with_cap_proof_account: Option<&ProofAccount>, - fee_ciphertext_validity_proof_account: Option<&ProofAccount>, - range_proof_account: Option<&ProofAccount>, - transfer_amount: u64, - account_info: Option, - source_elgamal_keypair: &ElGamalKeypair, - source_aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey, - fee_rate_basis_points: u16, - maximum_fee: u64, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(source_authority, &signing_pubkeys); - - let account_info = if let Some(account_info) = account_info { - account_info - } else { - let account = self.get_account_info(source_account).await?; - let confidential_transfer_account = - account.get_extension::()?; - TransferAccountInfo::new(confidential_transfer_account) - }; - - let ( - equality_proof_data, - transfer_amount_ciphertext_validity_proof_data_with_ciphertext, - percentage_with_cap_proof_data, - fee_ciphertext_validity_proof_data, - range_proof_data, - ) = if equality_proof_account.is_some() - && transfer_amount_ciphertext_validity_proof_account_with_ciphertext.is_some() - && percentage_with_cap_proof_account.is_some() - && fee_ciphertext_validity_proof_account.is_some() - && range_proof_account.is_some() - { - // is all proofs come from accounts, then skip proof generation - (None, None, None, None, None) - } else { - let TransferWithFeeProofData { - equality_proof_data, - transfer_amount_ciphertext_validity_proof_data_with_ciphertext, - percentage_with_cap_proof_data, - fee_ciphertext_validity_proof_data, - range_proof_data, - } = account_info - .generate_split_transfer_with_fee_proof_data( - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - fee_rate_basis_points, - maximum_fee, - ) - .map_err(|_| TokenError::ProofGeneration)?; - - let equality_proof_data = equality_proof_account - .is_none() - .then_some(equality_proof_data); - let transfer_amount_ciphertext_validity_proof_data_with_ciphertext = - transfer_amount_ciphertext_validity_proof_account_with_ciphertext - .is_none() - .then_some(transfer_amount_ciphertext_validity_proof_data_with_ciphertext); - let percentage_with_cap_proof_data = percentage_with_cap_proof_account - .is_none() - .then_some(percentage_with_cap_proof_data); - let fee_ciphertext_validity_proof_data = fee_ciphertext_validity_proof_account - .is_none() - .then_some(fee_ciphertext_validity_proof_data); - let range_proof_data = range_proof_account.is_none().then_some(range_proof_data); - - ( - equality_proof_data, - transfer_amount_ciphertext_validity_proof_data_with_ciphertext, - percentage_with_cap_proof_data, - fee_ciphertext_validity_proof_data, - range_proof_data, - ) - }; - - let (transfer_amount_auditor_ciphertext_lo, transfer_amount_auditor_ciphertext_hi) = - if let Some(proof_data_with_ciphertext) = - transfer_amount_ciphertext_validity_proof_data_with_ciphertext - { - ( - proof_data_with_ciphertext.ciphertext_lo, - proof_data_with_ciphertext.ciphertext_hi, - ) - } else { - // unwrap is safe as long as either `proof_data_with_ciphertext`, - // `proof_account_with_ciphertext` is `Some(..)`, which is guaranteed by the - // previous check - ( - transfer_amount_ciphertext_validity_proof_account_with_ciphertext - .unwrap() - .ciphertext_lo, - transfer_amount_ciphertext_validity_proof_account_with_ciphertext - .unwrap() - .ciphertext_hi, - ) - }; - - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, - // which is guaranteed by the previous check - let equality_proof_location = Self::confidential_transfer_create_proof_location( - equality_proof_data.as_ref(), - equality_proof_account, - 1, - ) - .unwrap(); - let transfer_amount_ciphertext_validity_proof_data = - transfer_amount_ciphertext_validity_proof_data_with_ciphertext - .map(|data| data.proof_data); - let transfer_amount_ciphertext_validity_proof_location = - Self::confidential_transfer_create_proof_location( - transfer_amount_ciphertext_validity_proof_data.as_ref(), - transfer_amount_ciphertext_validity_proof_account_with_ciphertext - .map(|account| &account.proof_account), - 2, - ) - .unwrap(); - let fee_sigma_proof_location = Self::confidential_transfer_create_proof_location( - percentage_with_cap_proof_data.as_ref(), - percentage_with_cap_proof_account, - 3, - ) - .unwrap(); - let fee_ciphertext_validity_proof_location = - Self::confidential_transfer_create_proof_location( - fee_ciphertext_validity_proof_data.as_ref(), - fee_ciphertext_validity_proof_account, - 4, - ) - .unwrap(); - let range_proof_location = Self::confidential_transfer_create_proof_location( - range_proof_data.as_ref(), - range_proof_account, - 5, - ) - .unwrap(); - - let new_decryptable_available_balance = account_info - .new_decryptable_available_balance(transfer_amount, source_aes_key) - .map_err(|_| TokenError::AccountDecryption)? - .into(); - - let mut instructions = confidential_transfer::instruction::transfer_with_fee( - &self.program_id, - source_account, - self.get_address(), - destination_account, - &new_decryptable_available_balance, - &transfer_amount_auditor_ciphertext_lo, - &transfer_amount_auditor_ciphertext_hi, - source_authority, - &multisig_signers, - equality_proof_location, - transfer_amount_ciphertext_validity_proof_location, - fee_sigma_proof_location, - fee_ciphertext_validity_proof_location, - range_proof_location, - )?; - offchain::add_extra_account_metas( - &mut instructions[0], - source_account, - self.get_address(), - destination_account, - source_authority, - u64::MAX, - |address| { - self.client - .get_account(address) - .map_ok(|opt| opt.map(|acc| acc.data)) - }, - ) - .await - .map_err(|_| TokenError::AccountNotFound)?; - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Applies the confidential transfer pending balance to the available - /// balance - pub async fn confidential_transfer_apply_pending_balance( - &self, - account: &Pubkey, - authority: &Pubkey, - account_info: Option, - elgamal_secret_key: &ElGamalSecretKey, - aes_key: &AeKey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - let account_info = if let Some(account_info) = account_info { - account_info - } else { - let account = self.get_account_info(account).await?; - let confidential_transfer_account = - account.get_extension::()?; - ApplyPendingBalanceAccountInfo::new(confidential_transfer_account) - }; - - let expected_pending_balance_credit_counter = account_info.pending_balance_credit_counter(); - let new_decryptable_available_balance = account_info - .new_decryptable_available_balance(elgamal_secret_key, aes_key) - .map_err(|_| TokenError::AccountDecryption)? - .into(); - - self.process_ixs( - &[confidential_transfer::instruction::apply_pending_balance( - &self.program_id, - account, - expected_pending_balance_credit_counter, - &new_decryptable_available_balance, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Enable confidential transfer `Deposit` and `Transfer` instructions for a - /// token account - pub async fn confidential_transfer_enable_confidential_credits( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[ - confidential_transfer::instruction::enable_confidential_credits( - &self.program_id, - account, - authority, - &multisig_signers, - )?, - ], - signing_keypairs, - ) - .await - } - - /// Disable confidential transfer `Deposit` and `Transfer` instructions for - /// a token account - pub async fn confidential_transfer_disable_confidential_credits( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[ - confidential_transfer::instruction::disable_confidential_credits( - &self.program_id, - account, - authority, - &multisig_signers, - )?, - ], - signing_keypairs, - ) - .await - } - - /// Enable a confidential extension token account to receive - /// non-confidential payments - pub async fn confidential_transfer_enable_non_confidential_credits( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[ - confidential_transfer::instruction::enable_non_confidential_credits( - &self.program_id, - account, - authority, - &multisig_signers, - )?, - ], - signing_keypairs, - ) - .await - } - - /// Disable non-confidential payments for a confidential extension token - /// account - pub async fn confidential_transfer_disable_non_confidential_credits( - &self, - account: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[ - confidential_transfer::instruction::disable_non_confidential_credits( - &self.program_id, - account, - authority, - &multisig_signers, - )?, - ], - signing_keypairs, - ) - .await - } - - /// Withdraw withheld confidential tokens from mint - #[allow(clippy::too_many_arguments)] - pub async fn confidential_transfer_withdraw_withheld_tokens_from_mint( - &self, - destination_account: &Pubkey, - withdraw_withheld_authority: &Pubkey, - proof_account: Option<&ProofAccount>, - withheld_tokens_info: Option, - withdraw_withheld_authority_elgamal_keypair: &ElGamalKeypair, - destination_elgamal_pubkey: &ElGamalPubkey, - new_decryptable_available_balance: &DecryptableBalance, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = - self.get_multisig_signers(withdraw_withheld_authority, &signing_pubkeys); - - let account_info = if let Some(account_info) = withheld_tokens_info { - account_info - } else { - let mint_info = self.get_mint_info().await?; - let confidential_transfer_fee_config = - mint_info.get_extension::()?; - WithheldTokensInfo::new(&confidential_transfer_fee_config.withheld_amount) - }; - - let proof_data = if proof_account.is_some() { - None - } else { - Some( - account_info - .generate_proof_data( - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - ) - .map_err(|_| TokenError::ProofGeneration)?, - ) - }; - - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, - // which is guaranteed by the previous check - let proof_location = Self::confidential_transfer_create_proof_location( - proof_data.as_ref(), - proof_account, - 1, - ) - .unwrap(); - - self.process_ixs( - &confidential_transfer_fee::instruction::withdraw_withheld_tokens_from_mint( - &self.program_id, - &self.pubkey, - destination_account, - new_decryptable_available_balance, - withdraw_withheld_authority, - &multisig_signers, - proof_location, - )?, - signing_keypairs, - ) - .await - } - - /// Withdraw withheld confidential tokens from accounts - #[allow(clippy::too_many_arguments)] - pub async fn confidential_transfer_withdraw_withheld_tokens_from_accounts( - &self, - destination_account: &Pubkey, - withdraw_withheld_authority: &Pubkey, - proof_account: Option<&ProofAccount>, - withheld_tokens_info: Option, - withdraw_withheld_authority_elgamal_keypair: &ElGamalKeypair, - destination_elgamal_pubkey: &ElGamalPubkey, - new_decryptable_available_balance: &DecryptableBalance, - sources: &[&Pubkey], - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = - self.get_multisig_signers(withdraw_withheld_authority, &signing_pubkeys); - - let account_info = if let Some(account_info) = withheld_tokens_info { - account_info - } else { - let futures = sources.iter().map(|source| self.get_account_info(source)); - let sources_extensions = join_all(futures).await; - - let mut aggregate_withheld_amount = ElGamalCiphertext::default(); - for source_extension in sources_extensions { - let withheld_amount: ElGamalCiphertext = source_extension? - .get_extension::()? - .withheld_amount - .try_into() - .map_err(|_| TokenError::AccountDecryption)?; - aggregate_withheld_amount = aggregate_withheld_amount + withheld_amount; - } - - WithheldTokensInfo::new(&aggregate_withheld_amount.into()) - }; - - let proof_data = if proof_account.is_some() { - None - } else { - Some( - account_info - .generate_proof_data( - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - ) - .map_err(|_| TokenError::ProofGeneration)?, - ) - }; - - // cannot panic as long as either `proof_data` or `proof_account` is `Some(..)`, - // which is guaranteed by the previous check - let proof_location = Self::confidential_transfer_create_proof_location( - proof_data.as_ref(), - proof_account, - 1, - ) - .unwrap(); - - self.process_ixs( - &confidential_transfer_fee::instruction::withdraw_withheld_tokens_from_accounts( - &self.program_id, - &self.pubkey, - destination_account, - new_decryptable_available_balance, - withdraw_withheld_authority, - &multisig_signers, - sources, - proof_location, - )?, - signing_keypairs, - ) - .await - } - - /// Harvest withheld confidential tokens to mint - pub async fn confidential_transfer_harvest_withheld_tokens_to_mint( - &self, - sources: &[&Pubkey], - ) -> TokenResult { - self.process_ixs::<[&dyn Signer; 0]>( - &[ - confidential_transfer_fee::instruction::harvest_withheld_tokens_to_mint( - &self.program_id, - &self.pubkey, - sources, - )?, - ], - &[], - ) - .await - } - - /// Enable harvest of confidential fees to mint - pub async fn confidential_transfer_enable_harvest_to_mint( - &self, - withdraw_withheld_authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = - self.get_multisig_signers(withdraw_withheld_authority, &signing_pubkeys); - - self.process_ixs( - &[ - confidential_transfer_fee::instruction::enable_harvest_to_mint( - &self.program_id, - &self.pubkey, - withdraw_withheld_authority, - &multisig_signers, - )?, - ], - signing_keypairs, - ) - .await - } - - /// Disable harvest of confidential fees to mint - pub async fn confidential_transfer_disable_harvest_to_mint( - &self, - withdraw_withheld_authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = - self.get_multisig_signers(withdraw_withheld_authority, &signing_pubkeys); - - self.process_ixs( - &[ - confidential_transfer_fee::instruction::disable_harvest_to_mint( - &self.program_id, - &self.pubkey, - withdraw_withheld_authority, - &multisig_signers, - )?, - ], - signing_keypairs, - ) - .await - } - - // Creates `ProofLocation` from proof data and `ProofAccount`. If both - // `proof_data` and `proof_account` are `None`, then the result is `None`. - fn confidential_transfer_create_proof_location<'a, ZK: ZkProofData, U: Pod>( - proof_data: Option<&'a ZK>, - proof_account: Option<&'a ProofAccount>, - instruction_offset: i8, - ) -> Option> { - if let Some(proof_data) = proof_data { - Some(ProofLocation::InstructionOffset( - instruction_offset.try_into().unwrap(), - ProofData::InstructionData(proof_data), - )) - } else if let Some(proof_account) = proof_account { - match proof_account { - ProofAccount::ContextAccount(context_state_account) => { - Some(ProofLocation::ContextStateAccount(context_state_account)) - } - ProofAccount::RecordAccount(record_account, data_offset) => { - Some(ProofLocation::InstructionOffset( - instruction_offset.try_into().unwrap(), - ProofData::RecordAccount(record_account, *data_offset), - )) - } - } - } else { - None - } - } - - pub async fn withdraw_excess_lamports( - &self, - source: &Pubkey, - destination: &Pubkey, - authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let signing_pubkeys = signing_keypairs.pubkeys(); - let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); - - self.process_ixs( - &[spl_token_2022::instruction::withdraw_excess_lamports( - &self.program_id, - source, - destination, - authority, - &multisig_signers, - )?], - signing_keypairs, - ) - .await - } - - /// Initialize token-metadata on a mint - pub async fn token_metadata_initialize( - &self, - update_authority: &Pubkey, - mint_authority: &Pubkey, - name: String, - symbol: String, - uri: String, - signing_keypairs: &S, - ) -> TokenResult { - self.process_ixs( - &[spl_token_metadata_interface::instruction::initialize( - &self.program_id, - &self.pubkey, - update_authority, - &self.pubkey, - mint_authority, - name, - symbol, - uri, - )], - signing_keypairs, - ) - .await - } - - async fn get_additional_rent_for_new_metadata( - &self, - token_metadata: &TokenMetadata, - ) -> TokenResult { - let account = self.get_account(self.pubkey).await?; - let account_lamports = account.lamports; - let mint_state = self.unpack_mint_info(account)?; - let new_account_len = mint_state - .try_get_new_account_len_for_variable_len_extension::(token_metadata)?; - let new_rent_exempt_minimum = self - .client - .get_minimum_balance_for_rent_exemption(new_account_len) - .await - .map_err(TokenError::Client)?; - Ok(new_rent_exempt_minimum.saturating_sub(account_lamports)) - } - - /// Initialize token-metadata on a mint - #[allow(clippy::too_many_arguments)] - pub async fn token_metadata_initialize_with_rent_transfer( - &self, - payer: &Pubkey, - update_authority: &Pubkey, - mint_authority: &Pubkey, - name: String, - symbol: String, - uri: String, - signing_keypairs: &S, - ) -> TokenResult { - let token_metadata = TokenMetadata { - name, - symbol, - uri, - ..Default::default() - }; - let additional_lamports = self - .get_additional_rent_for_new_metadata(&token_metadata) - .await?; - let mut instructions = vec![]; - if additional_lamports > 0 { - instructions.push(system_instruction::transfer( - payer, - &self.pubkey, - additional_lamports, - )); - } - instructions.push(spl_token_metadata_interface::instruction::initialize( - &self.program_id, - &self.pubkey, - update_authority, - &self.pubkey, - mint_authority, - token_metadata.name, - token_metadata.symbol, - token_metadata.uri, - )); - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Update a token-metadata field on a mint - pub async fn token_metadata_update_field( - &self, - update_authority: &Pubkey, - field: Field, - value: String, - signing_keypairs: &S, - ) -> TokenResult { - self.process_ixs( - &[spl_token_metadata_interface::instruction::update_field( - &self.program_id, - &self.pubkey, - update_authority, - field, - value, - )], - signing_keypairs, - ) - .await - } - - async fn get_additional_rent_for_updated_metadata( - &self, - field: Field, - value: String, - ) -> TokenResult { - let account = self.get_account(self.pubkey).await?; - let account_lamports = account.lamports; - let mint_state = self.unpack_mint_info(account)?; - let mut token_metadata = mint_state.get_variable_len_extension::()?; - token_metadata.update(field, value); - let new_account_len = mint_state - .try_get_new_account_len_for_variable_len_extension::(&token_metadata)?; - let new_rent_exempt_minimum = self - .client - .get_minimum_balance_for_rent_exemption(new_account_len) - .await - .map_err(TokenError::Client)?; - Ok(new_rent_exempt_minimum.saturating_sub(account_lamports)) - } - - /// Update a token-metadata field on a mint. Includes a transfer for any - /// additional rent-exempt SOL required. - #[allow(clippy::too_many_arguments)] - pub async fn token_metadata_update_field_with_rent_transfer( - &self, - payer: &Pubkey, - update_authority: &Pubkey, - field: Field, - value: String, - transfer_lamports: Option, - signing_keypairs: &S, - ) -> TokenResult { - let additional_lamports = if let Some(transfer_lamports) = transfer_lamports { - transfer_lamports - } else { - self.get_additional_rent_for_updated_metadata(field.clone(), value.clone()) - .await? - }; - let mut instructions = vec![]; - if additional_lamports > 0 { - instructions.push(system_instruction::transfer( - payer, - &self.pubkey, - additional_lamports, - )); - } - instructions.push(spl_token_metadata_interface::instruction::update_field( - &self.program_id, - &self.pubkey, - update_authority, - field, - value, - )); - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Update the token-metadata authority in a mint - pub async fn token_metadata_update_authority( - &self, - current_authority: &Pubkey, - new_authority: Option, - signing_keypairs: &S, - ) -> TokenResult { - self.process_ixs( - &[spl_token_metadata_interface::instruction::update_authority( - &self.program_id, - &self.pubkey, - current_authority, - new_authority.try_into()?, - )], - signing_keypairs, - ) - .await - } - - /// Remove a token-metadata field on a mint - pub async fn token_metadata_remove_key( - &self, - update_authority: &Pubkey, - key: String, - idempotent: bool, - signing_keypairs: &S, - ) -> TokenResult { - self.process_ixs( - &[spl_token_metadata_interface::instruction::remove_key( - &self.program_id, - &self.pubkey, - update_authority, - key, - idempotent, - )], - signing_keypairs, - ) - .await - } - - /// Initialize token-group on a mint - pub async fn token_group_initialize( - &self, - mint_authority: &Pubkey, - update_authority: &Pubkey, - max_size: u64, - signing_keypairs: &S, - ) -> TokenResult { - self.process_ixs( - &[spl_token_group_interface::instruction::initialize_group( - &self.program_id, - &self.pubkey, - &self.pubkey, - mint_authority, - Some(*update_authority), - max_size, - )], - signing_keypairs, - ) - .await - } - - async fn get_additional_rent_for_fixed_len_extension( - &self, - ) -> TokenResult { - let account = self.get_account(self.pubkey).await?; - let account_lamports = account.lamports; - let mint_state = self.unpack_mint_info(account)?; - if mint_state.get_extension::().is_ok() { - Ok(0) - } else { - let new_account_len = mint_state.try_get_new_account_len::()?; - let new_rent_exempt_minimum = self - .client - .get_minimum_balance_for_rent_exemption(new_account_len) - .await - .map_err(TokenError::Client)?; - Ok(new_rent_exempt_minimum.saturating_sub(account_lamports)) - } - } - - /// Initialize token-group on a mint - pub async fn token_group_initialize_with_rent_transfer( - &self, - payer: &Pubkey, - mint_authority: &Pubkey, - update_authority: &Pubkey, - max_size: u64, - signing_keypairs: &S, - ) -> TokenResult { - let additional_lamports = self - .get_additional_rent_for_fixed_len_extension::() - .await?; - let mut instructions = vec![]; - if additional_lamports > 0 { - instructions.push(system_instruction::transfer( - payer, - &self.pubkey, - additional_lamports, - )); - } - instructions.push(spl_token_group_interface::instruction::initialize_group( - &self.program_id, - &self.pubkey, - &self.pubkey, - mint_authority, - Some(*update_authority), - max_size, - )); - self.process_ixs(&instructions, signing_keypairs).await - } - - /// Update a token-group max size on a mint - pub async fn token_group_update_max_size( - &self, - update_authority: &Pubkey, - new_max_size: u64, - signing_keypairs: &S, - ) -> TokenResult { - self.process_ixs( - &[ - spl_token_group_interface::instruction::update_group_max_size( - &self.program_id, - &self.pubkey, - update_authority, - new_max_size, - ), - ], - signing_keypairs, - ) - .await - } - - /// Update the token-group authority in a mint - pub async fn token_group_update_authority( - &self, - current_authority: &Pubkey, - new_authority: Option, - signing_keypairs: &S, - ) -> TokenResult { - self.process_ixs( - &[ - spl_token_group_interface::instruction::update_group_authority( - &self.program_id, - &self.pubkey, - current_authority, - new_authority, - ), - ], - signing_keypairs, - ) - .await - } - - /// Initialize a token-group member on a mint - pub async fn token_group_initialize_member( - &self, - mint_authority: &Pubkey, - group_mint: &Pubkey, - group_update_authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - self.process_ixs( - &[spl_token_group_interface::instruction::initialize_member( - &self.program_id, - &self.pubkey, - &self.pubkey, - mint_authority, - group_mint, - group_update_authority, - )], - signing_keypairs, - ) - .await - } - - /// Initialize a token-group member on a mint - #[allow(clippy::too_many_arguments)] - pub async fn token_group_initialize_member_with_rent_transfer( - &self, - payer: &Pubkey, - mint_authority: &Pubkey, - group_mint: &Pubkey, - group_update_authority: &Pubkey, - signing_keypairs: &S, - ) -> TokenResult { - let additional_lamports = self - .get_additional_rent_for_fixed_len_extension::() - .await?; - let mut instructions = vec![]; - if additional_lamports > 0 { - instructions.push(system_instruction::transfer( - payer, - &self.pubkey, - additional_lamports, - )); - } - instructions.push(spl_token_group_interface::instruction::initialize_member( - &self.program_id, - &self.pubkey, - &self.pubkey, - mint_authority, - group_mint, - group_update_authority, - )); - self.process_ixs(&instructions, signing_keypairs).await - } -} - -/// Calculates the maximum chunk size for a zero-knowledge proof record -/// instruction to fit inside a single transaction. -fn calculate_record_max_chunk_size( - create_record_instructions: F, - first_instruction: bool, -) -> usize -where - F: Fn(bool, &[u8], u64) -> Vec, -{ - let ixs = create_record_instructions(first_instruction, &[], 0); - let message = Message::new_with_blockhash(&ixs, Some(&Pubkey::default()), &Hash::default()); - let tx_size = bincode::serialized_size(&Transaction { - signatures: vec![Signature::default(); message.header.num_required_signatures as usize], - message, - }) - .unwrap() as usize; - PACKET_DATA_SIZE.saturating_sub(tx_size).saturating_sub(1) -} diff --git a/token/client/tests/program-test.rs b/token/client/tests/program-test.rs deleted file mode 100644 index f48ae2d9c27..00000000000 --- a/token/client/tests/program-test.rs +++ /dev/null @@ -1,311 +0,0 @@ -use { - solana_program_test::{ - tokio::{self, sync::Mutex}, - ProgramTest, - }, - solana_sdk::{ - program_option::COption, - signer::{keypair::Keypair, Signer}, - }, - spl_token_2022::{instruction, state}, - spl_token_client::{ - client::{ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient}, - token::Token, - }, - std::sync::Arc, -}; - -struct TestContext { - pub decimals: u8, - pub mint_authority: Keypair, - pub token: Token, - - pub alice: Keypair, - pub bob: Keypair, -} - -impl TestContext { - async fn new() -> Self { - let program_test = ProgramTest::default(); - let ctx = program_test.start_with_context().await; - let ctx = Arc::new(Mutex::new(ctx)); - - let payer = keypair_clone(&ctx.lock().await.payer); - - let client: Arc> = - Arc::new(ProgramBanksClient::new_from_context( - Arc::clone(&ctx), - ProgramBanksClientProcessTransaction, - )); - - let decimals: u8 = 6; - - let mint_account = Keypair::new(); - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - - let token = Token::new( - Arc::clone(&client), - &spl_token_2022::id(), - &mint_account.pubkey(), - Some(decimals), - Arc::new(keypair_clone(&payer)), - ); - - token - .create_mint(&mint_authority_pubkey, None, vec![], &[&mint_account]) - .await - .expect("failed to create mint"); - - Self { - decimals, - mint_authority, - token, - - alice: Keypair::new(), - bob: Keypair::new(), - } - } -} - -fn keypair_clone(kp: &Keypair) -> Keypair { - Keypair::from_bytes(&kp.to_bytes()).expect("failed to copy keypair") -} - -#[tokio::test] -async fn associated_token_account() { - let TestContext { token, alice, .. } = TestContext::new().await; - - token - .create_associated_token_account(&alice.pubkey()) - .await - .expect("failed to create associated token account"); - let alice_vault = token.get_associated_token_address(&alice.pubkey()); - - assert_eq!( - token.get_associated_token_address(&alice.pubkey()), - alice_vault - ); - - assert_eq!( - token - .get_account_info(&alice_vault) - .await - .expect("failed to get account info") - .base, - state::Account { - mint: *token.get_address(), - owner: alice.pubkey(), - amount: 0, - delegate: COption::None, - state: state::AccountState::Initialized, - is_native: COption::None, - delegated_amount: 0, - close_authority: COption::None, - } - ); -} - -#[tokio::test] -async fn get_or_create_associated_token_account() { - let TestContext { token, alice, .. } = TestContext::new().await; - - assert_eq!( - token - .get_or_create_associated_account_info(&alice.pubkey()) - .await - .expect("failed to get account info") - .base, - state::Account { - mint: *token.get_address(), - owner: alice.pubkey(), - amount: 0, - delegate: COption::None, - state: state::AccountState::Initialized, - is_native: COption::None, - delegated_amount: 0, - close_authority: COption::None, - } - ); -} - -#[tokio::test] -async fn set_authority() { - let TestContext { - mint_authority, - token, - alice, - bob, - .. - } = TestContext::new().await; - - let alice_vault = Keypair::new(); - token - .create_auxiliary_token_account(&alice_vault, &alice.pubkey()) - .await - .expect("failed to create token account"); - let alice_vault = alice_vault.pubkey(); - - token - .mint_to( - &alice_vault, - &mint_authority.pubkey(), - 1, - &[&mint_authority], - ) - .await - .expect("failed to mint token"); - - token - .set_authority( - token.get_address(), - &mint_authority.pubkey(), - None, - instruction::AuthorityType::MintTokens, - &[&mint_authority], - ) - .await - .expect("failed to set authority"); - - let mint = token - .get_mint_info() - .await - .expect("failed to get mint info"); - assert!(mint.base.mint_authority.is_none()); - - // TODO: compare - // Err(Client(TransactionError(InstructionError(0, Custom(5))))) - assert!(token - .mint_to( - &alice_vault, - &mint_authority.pubkey(), - 2, - &[&mint_authority] - ) - .await - .is_err()); - - token - .set_authority( - &alice_vault, - &alice.pubkey(), - Some(&bob.pubkey()), - instruction::AuthorityType::AccountOwner, - &[&alice], - ) - .await - .expect("failed to set_authority"); - - assert_eq!( - token - .get_account_info(&alice_vault) - .await - .expect("failed to get account info") - .base - .owner, - bob.pubkey(), - ); -} - -#[tokio::test] -async fn mint_to() { - let TestContext { - decimals, - mint_authority, - token, - alice, - .. - } = TestContext::new().await; - - token - .create_associated_token_account(&alice.pubkey()) - .await - .expect("failed to create associated token account"); - let alice_vault = token.get_associated_token_address(&alice.pubkey()); - - let mint_amount = 10 * u64::pow(10, decimals as u32); - token - .mint_to( - &alice_vault, - &mint_authority.pubkey(), - mint_amount, - &[&mint_authority], - ) - .await - .expect("failed to mint token"); - - assert_eq!( - token - .get_account_info(&alice_vault) - .await - .expect("failed to get account") - .base - .amount, - mint_amount - ); -} - -#[tokio::test] -async fn transfer() { - let TestContext { - decimals, - mint_authority, - token, - alice, - bob, - .. - } = TestContext::new().await; - - token - .create_associated_token_account(&alice.pubkey()) - .await - .expect("failed to create associated token account"); - let alice_vault = token.get_associated_token_address(&alice.pubkey()); - token - .create_associated_token_account(&bob.pubkey()) - .await - .expect("failed to create associated token account"); - let bob_vault = token.get_associated_token_address(&bob.pubkey()); - - let mint_amount = 10 * u64::pow(10, decimals as u32); - token - .mint_to( - &alice_vault, - &mint_authority.pubkey(), - mint_amount, - &[&mint_authority], - ) - .await - .expect("failed to mint token"); - - let transfer_amount = mint_amount.overflowing_div(3).0; - token - .transfer( - &alice_vault, - &bob_vault, - &alice.pubkey(), - transfer_amount, - &[&alice], - ) - .await - .expect("failed to transfer"); - - assert_eq!( - token - .get_account_info(&alice_vault) - .await - .expect("failed to get account") - .base - .amount, - mint_amount - transfer_amount - ); - assert_eq!( - token - .get_account_info(&bob_vault) - .await - .expect("failed to get account") - .base - .amount, - transfer_amount - ); -} diff --git a/token/confidential-transfer/ciphertext-arithmetic/Cargo.toml b/token/confidential-transfer/ciphertext-arithmetic/Cargo.toml deleted file mode 100644 index 865ee202d0d..00000000000 --- a/token/confidential-transfer/ciphertext-arithmetic/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "spl-token-confidential-transfer-ciphertext-arithmetic" -version = "0.2.0" -description = "Solana Program Library Confidential Transfer Ciphertext Arithmetic" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -base64 = "0.22.1" -bytemuck = "1.21.0" -solana-curve25519 = "2.1.0" -solana-zk-sdk = "2.1.0" - -[dev-dependencies] -spl-token-confidential-transfer-proof-generation = { version = "0.2.0", path = "../proof-generation" } -curve25519-dalek = "4.1.3" - -[lib] -crate-type = ["cdylib", "lib"] diff --git a/token/confidential-transfer/ciphertext-arithmetic/src/lib.rs b/token/confidential-transfer/ciphertext-arithmetic/src/lib.rs deleted file mode 100644 index e8b23d55340..00000000000 --- a/token/confidential-transfer/ciphertext-arithmetic/src/lib.rs +++ /dev/null @@ -1,334 +0,0 @@ -use { - base64::{engine::general_purpose::STANDARD, Engine}, - bytemuck::bytes_of, - solana_curve25519::{ - ristretto::{add_ristretto, multiply_ristretto, subtract_ristretto, PodRistrettoPoint}, - scalar::PodScalar, - }, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalCiphertext, - std::str::FromStr, -}; - -const SHIFT_BITS: usize = 16; - -const G: PodRistrettoPoint = PodRistrettoPoint([ - 226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165, - 130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118, -]); - -/// Add two ElGamal ciphertexts -pub fn add( - left_ciphertext: &PodElGamalCiphertext, - right_ciphertext: &PodElGamalCiphertext, -) -> Option { - let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext); - let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext); - - let result_commitment = add_ristretto(&left_commitment, &right_commitment)?; - let result_handle = add_ristretto(&left_handle, &right_handle)?; - - Some(ristretto_to_elgamal_ciphertext( - &result_commitment, - &result_handle, - )) -} - -/// Multiply an ElGamal ciphertext by a scalar -pub fn multiply( - scalar: &PodScalar, - ciphertext: &PodElGamalCiphertext, -) -> Option { - let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext); - - let result_commitment = multiply_ristretto(scalar, &commitment)?; - let result_handle = multiply_ristretto(scalar, &handle)?; - - Some(ristretto_to_elgamal_ciphertext( - &result_commitment, - &result_handle, - )) -} - -/// Compute `left_ciphertext + (right_ciphertext_lo + 2^16 * -/// right_ciphertext_hi)` -pub fn add_with_lo_hi( - left_ciphertext: &PodElGamalCiphertext, - right_ciphertext_lo: &PodElGamalCiphertext, - right_ciphertext_hi: &PodElGamalCiphertext, -) -> Option { - let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS); - let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?; - let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?; - add(left_ciphertext, &combined_right_ciphertext) -} - -/// Subtract two ElGamal ciphertexts -pub fn subtract( - left_ciphertext: &PodElGamalCiphertext, - right_ciphertext: &PodElGamalCiphertext, -) -> Option { - let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext); - let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext); - - let result_commitment = subtract_ristretto(&left_commitment, &right_commitment)?; - let result_handle = subtract_ristretto(&left_handle, &right_handle)?; - - Some(ristretto_to_elgamal_ciphertext( - &result_commitment, - &result_handle, - )) -} - -/// Compute `left_ciphertext - (right_ciphertext_lo + 2^16 * -/// right_ciphertext_hi)` -pub fn subtract_with_lo_hi( - left_ciphertext: &PodElGamalCiphertext, - right_ciphertext_lo: &PodElGamalCiphertext, - right_ciphertext_hi: &PodElGamalCiphertext, -) -> Option { - let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS); - let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?; - let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?; - subtract(left_ciphertext, &combined_right_ciphertext) -} - -/// Add a constant amount to a ciphertext -pub fn add_to(ciphertext: &PodElGamalCiphertext, amount: u64) -> Option { - let amount_scalar = u64_to_scalar(amount); - let amount_point = multiply_ristretto(&amount_scalar, &G)?; - - let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext); - - let result_commitment = add_ristretto(&commitment, &amount_point)?; - - Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle)) -} - -/// Subtract a constant amount to a ciphertext -pub fn subtract_from( - ciphertext: &PodElGamalCiphertext, - amount: u64, -) -> Option { - let amount_scalar = u64_to_scalar(amount); - let amount_point = multiply_ristretto(&amount_scalar, &G)?; - - let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext); - - let result_commitment = subtract_ristretto(&commitment, &amount_point)?; - - Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle)) -} - -/// Convert a `u64` amount into a curve-25519 scalar -fn u64_to_scalar(amount: u64) -> PodScalar { - let mut amount_bytes = [0u8; 32]; - amount_bytes[..8].copy_from_slice(&amount.to_le_bytes()); - PodScalar(amount_bytes) -} - -/// Convert a `PodElGamalCiphertext` into a tuple of commitment and decrypt -/// handle `PodRistrettoPoint` -fn elgamal_ciphertext_to_ristretto( - ciphertext: &PodElGamalCiphertext, -) -> (PodRistrettoPoint, PodRistrettoPoint) { - let ciphertext_bytes = bytes_of(ciphertext); // must be of length 64 by type - let commitment_bytes = ciphertext_bytes[..32].try_into().unwrap(); - let handle_bytes = ciphertext_bytes[32..64].try_into().unwrap(); - ( - PodRistrettoPoint(commitment_bytes), - PodRistrettoPoint(handle_bytes), - ) -} - -/// Convert a pair of `PodRistrettoPoint` to a `PodElGamalCiphertext` -/// interpreting the first as the commitment and the second as the handle -fn ristretto_to_elgamal_ciphertext( - commitment: &PodRistrettoPoint, - handle: &PodRistrettoPoint, -) -> PodElGamalCiphertext { - let mut ciphertext_bytes = [0u8; 64]; - ciphertext_bytes[..32].copy_from_slice(bytes_of(commitment)); - ciphertext_bytes[32..64].copy_from_slice(bytes_of(handle)); - // Unfortunately, the `solana-zk-sdk` does not exporse a constructor interface - // to construct `PodRistrettoPoint` from bytes. As a work-around, encode the - // bytes as base64 string and then convert the string to a - // `PodElGamalCiphertext`. - let ciphertext_string = STANDARD.encode(ciphertext_bytes); - FromStr::from_str(&ciphertext_string).unwrap() -} - -#[cfg(test)] -mod tests { - use { - super::*, - bytemuck::Zeroable, - curve25519_dalek::scalar::Scalar, - solana_zk_sdk::encryption::{ - elgamal::{ElGamalCiphertext, ElGamalKeypair}, - pedersen::{Pedersen, PedersenOpening}, - pod::{elgamal::PodDecryptHandle, pedersen::PodPedersenCommitment}, - }, - spl_token_confidential_transfer_proof_generation::try_split_u64, - }; - - const TWO_16: u64 = 65536; - - #[test] - fn test_zero_ct() { - let spendable_balance = PodElGamalCiphertext::zeroed(); - let spendable_ct: ElGamalCiphertext = spendable_balance.try_into().unwrap(); - - // spendable_ct should be an encryption of 0 for any public key when - // `PedersenOpen::default()` is used - let keypair = ElGamalKeypair::new_rand(); - let public = keypair.pubkey(); - let balance: u64 = 0; - assert_eq!( - spendable_ct, - public.encrypt_with(balance, &PedersenOpening::default()) - ); - - // homomorphism should work like any other ciphertext - let open = PedersenOpening::new_rand(); - let transfer_amount_ciphertext = public.encrypt_with(55_u64, &open); - let transfer_amount_pod: PodElGamalCiphertext = transfer_amount_ciphertext.into(); - - let sum = add(&spendable_balance, &transfer_amount_pod).unwrap(); - - let expected: PodElGamalCiphertext = public.encrypt_with(55_u64, &open).into(); - assert_eq!(expected, sum); - } - - #[test] - fn test_add_to() { - let spendable_balance = PodElGamalCiphertext::zeroed(); - - let added_ciphertext = add_to(&spendable_balance, 55).unwrap(); - - let keypair = ElGamalKeypair::new_rand(); - let public = keypair.pubkey(); - let expected: PodElGamalCiphertext = public - .encrypt_with(55_u64, &PedersenOpening::default()) - .into(); - - assert_eq!(expected, added_ciphertext); - } - - #[test] - fn test_subtract_from() { - let amount = 77_u64; - let keypair = ElGamalKeypair::new_rand(); - let public = keypair.pubkey(); - let open = PedersenOpening::new_rand(); - let encrypted_amount: PodElGamalCiphertext = public.encrypt_with(amount, &open).into(); - - let subtracted_ciphertext = subtract_from(&encrypted_amount, 55).unwrap(); - - let expected: PodElGamalCiphertext = public.encrypt_with(22_u64, &open).into(); - - assert_eq!(expected, subtracted_ciphertext); - } - - #[test] - fn test_transfer_arithmetic() { - // transfer amount - let transfer_amount: u64 = 55; - let (amount_lo, amount_hi) = try_split_u64(transfer_amount, 16).unwrap(); - - // generate public keys - let source_keypair = ElGamalKeypair::new_rand(); - let source_pubkey = source_keypair.pubkey(); - - let destination_keypair = ElGamalKeypair::new_rand(); - let destination_pubkey = destination_keypair.pubkey(); - - let auditor_keypair = ElGamalKeypair::new_rand(); - let auditor_pubkey = auditor_keypair.pubkey(); - - // commitments associated with TransferRangeProof - let (commitment_lo, opening_lo) = Pedersen::new(amount_lo); - let (commitment_hi, opening_hi) = Pedersen::new(amount_hi); - - let commitment_lo: PodPedersenCommitment = commitment_lo.into(); - let commitment_hi: PodPedersenCommitment = commitment_hi.into(); - - // decryption handles associated with TransferValidityProof - let source_handle_lo: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_lo).into(); - let destination_handle_lo: PodDecryptHandle = - destination_pubkey.decrypt_handle(&opening_lo).into(); - let _auditor_handle_lo: PodDecryptHandle = - auditor_pubkey.decrypt_handle(&opening_lo).into(); - - let source_handle_hi: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_hi).into(); - let destination_handle_hi: PodDecryptHandle = - destination_pubkey.decrypt_handle(&opening_hi).into(); - let _auditor_handle_hi: PodDecryptHandle = - auditor_pubkey.decrypt_handle(&opening_hi).into(); - - // source spendable and recipient pending - let source_opening = PedersenOpening::new_rand(); - let destination_opening = PedersenOpening::new_rand(); - - let source_spendable_ciphertext: PodElGamalCiphertext = - source_pubkey.encrypt_with(77_u64, &source_opening).into(); - let destination_pending_ciphertext: PodElGamalCiphertext = destination_pubkey - .encrypt_with(77_u64, &destination_opening) - .into(); - - // program arithmetic for the source account - let commitment_lo_point = PodRistrettoPoint(bytes_of(&commitment_lo).try_into().unwrap()); - let source_handle_lo_point = - PodRistrettoPoint(bytes_of(&source_handle_lo).try_into().unwrap()); - - let commitment_hi_point = PodRistrettoPoint(bytes_of(&commitment_hi).try_into().unwrap()); - let source_handle_hi_point = - PodRistrettoPoint(bytes_of(&source_handle_hi).try_into().unwrap()); - - let source_ciphertext_lo = - ristretto_to_elgamal_ciphertext(&commitment_lo_point, &source_handle_lo_point); - let source_ciphertext_hi = - ristretto_to_elgamal_ciphertext(&commitment_hi_point, &source_handle_hi_point); - - let final_source_spendable = subtract_with_lo_hi( - &source_spendable_ciphertext, - &source_ciphertext_lo, - &source_ciphertext_hi, - ) - .unwrap(); - - let final_source_opening = - source_opening - (opening_lo.clone() + opening_hi.clone() * Scalar::from(TWO_16)); - let expected_source: PodElGamalCiphertext = source_pubkey - .encrypt_with(22_u64, &final_source_opening) - .into(); - assert_eq!(expected_source, final_source_spendable); - - // program arithmetic for the destination account - let destination_handle_lo_point = - PodRistrettoPoint(bytes_of(&destination_handle_lo).try_into().unwrap()); - let destination_handle_hi_point = - PodRistrettoPoint(bytes_of(&destination_handle_hi).try_into().unwrap()); - - let destination_ciphertext_lo = - ristretto_to_elgamal_ciphertext(&commitment_lo_point, &destination_handle_lo_point); - let destination_ciphertext_hi = - ristretto_to_elgamal_ciphertext(&commitment_hi_point, &destination_handle_hi_point); - - let final_destination_pending_ciphertext = add_with_lo_hi( - &destination_pending_ciphertext, - &destination_ciphertext_lo, - &destination_ciphertext_hi, - ) - .unwrap(); - - let final_destination_opening = - destination_opening + (opening_lo + opening_hi * Scalar::from(TWO_16)); - let expected_destination_ciphertext: PodElGamalCiphertext = destination_pubkey - .encrypt_with(132_u64, &final_destination_opening) - .into(); - assert_eq!( - expected_destination_ciphertext, - final_destination_pending_ciphertext - ); - } -} diff --git a/token/confidential-transfer/elgamal-registry/Cargo.toml b/token/confidential-transfer/elgamal-registry/Cargo.toml deleted file mode 100644 index 6524babd167..00000000000 --- a/token/confidential-transfer/elgamal-registry/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "spl-elgamal-registry" -version = "0.1.0" -description = "Solana ElGamal Registry Program" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -bytemuck = { version = "1.21.0", features = ["derive"] } -solana-program = "2.1.0" -solana-zk-sdk = "2.1.0" -spl-pod = { version = "0.5.0", path = "../../../libraries/pod" } -spl-token-confidential-transfer-proof-extraction = { version = "0.2.0", path = "../proof-extraction" } - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[lints] -workspace = true diff --git a/token/confidential-transfer/elgamal-registry/src/entrypoint.rs b/token/confidential-transfer/elgamal-registry/src/entrypoint.rs deleted file mode 100644 index 62a02f2a465..00000000000 --- a/token/confidential-transfer/elgamal-registry/src/entrypoint.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Program entrypoint - -#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] - -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - crate::processor::process_instruction(program_id, accounts, instruction_data) -} diff --git a/token/confidential-transfer/elgamal-registry/src/instruction.rs b/token/confidential-transfer/elgamal-registry/src/instruction.rs deleted file mode 100644 index 0fa33c9ca1e..00000000000 --- a/token/confidential-transfer/elgamal-registry/src/instruction.rs +++ /dev/null @@ -1,194 +0,0 @@ -use { - crate::{get_elgamal_registry_address, id}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - system_program, sysvar, - }, - solana_zk_sdk::zk_elgamal_proof_program::{ - instruction::ProofInstruction, proof_data::PubkeyValidityProofData, - }, - spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation}, -}; - -#[derive(Clone, Debug, PartialEq)] -#[repr(u8)] -pub enum RegistryInstruction { - /// Initialize an ElGamal public key registry. - /// - /// 0. `[writable]` The account to be created - /// 1. `[signer]` The wallet address (will also be the owner address for the - /// registry account) - /// 2. `[]` System program - /// 3. `[]` Instructions sysvar if `VerifyPubkeyValidity` is included in the - /// same transaction or context state account if `VerifyPubkeyValidity` - /// is pre-verified into a context state account. - /// 4. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - CreateRegistry { - /// Relative location of the `ProofInstruction::PubkeyValidityProof` - /// instruction to the `CreateElGamalRegistry` instruction in the - /// transaction. If the offset is `0`, then use a context state account - /// for the proof. - proof_instruction_offset: i8, - }, - /// Update an ElGamal public key registry with a new ElGamal public key. - /// - /// 0. `[writable]` The ElGamal registry account - /// 1. `[]` Instructions sysvar if `VerifyPubkeyValidity` is included in the - /// same transaction or context state account if `VerifyPubkeyValidity` - /// is pre-verified into a context state account. - /// 2. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 3. `[signer]` The owner of the ElGamal public key registry - UpdateRegistry { - /// Relative location of the `ProofInstruction::PubkeyValidityProof` - /// instruction to the `UpdateElGamalRegistry` instruction in the - /// transaction. If the offset is `0`, then use a context state account - /// for the proof. - proof_instruction_offset: i8, - }, -} - -impl RegistryInstruction { - /// Unpacks a byte buffer into a `RegistryInstruction` - pub fn unpack(input: &[u8]) -> Result { - let (&tag, rest) = input - .split_first() - .ok_or(ProgramError::InvalidInstructionData)?; - - Ok(match tag { - 0 => { - let proof_instruction_offset = - *rest.first().ok_or(ProgramError::InvalidInstructionData)?; - Self::CreateRegistry { - proof_instruction_offset: proof_instruction_offset as i8, - } - } - 1 => { - let proof_instruction_offset = - *rest.first().ok_or(ProgramError::InvalidInstructionData)?; - Self::UpdateRegistry { - proof_instruction_offset: proof_instruction_offset as i8, - } - } - _ => return Err(ProgramError::InvalidInstructionData), - }) - } - - /// Packs a `RegistryInstruction` into a byte buffer. - pub fn pack(&self) -> Vec { - let mut buf = vec![]; - match self { - Self::CreateRegistry { - proof_instruction_offset, - } => { - buf.push(0); - buf.extend_from_slice(&proof_instruction_offset.to_le_bytes()); - } - Self::UpdateRegistry { - proof_instruction_offset, - } => { - buf.push(1); - buf.extend_from_slice(&proof_instruction_offset.to_le_bytes()); - } - }; - buf - } -} - -/// Create a `RegistryInstruction::CreateRegistry` instruction -pub fn create_registry( - owner_address: &Pubkey, - proof_location: ProofLocation, -) -> Result, ProgramError> { - let elgamal_registry_address = get_elgamal_registry_address(owner_address, &id()); - - let mut accounts = vec![ - AccountMeta::new(elgamal_registry_address, false), - AccountMeta::new_readonly(*owner_address, true), - AccountMeta::new_readonly(system_program::id(), false), - ]; - let proof_instruction_offset = proof_instruction_offset(&mut accounts, proof_location); - - let mut instructions = vec![Instruction { - program_id: id(), - accounts, - data: RegistryInstruction::CreateRegistry { - proof_instruction_offset, - } - .pack(), - }]; - append_zk_elgamal_proof(&mut instructions, proof_location)?; - Ok(instructions) -} - -/// Create a `RegistryInstruction::UpdateRegistry` instruction -pub fn update_registry( - owner_address: &Pubkey, - proof_location: ProofLocation, -) -> Result, ProgramError> { - let elgamal_registry_address = get_elgamal_registry_address(owner_address, &id()); - - let mut accounts = vec![AccountMeta::new(elgamal_registry_address, false)]; - let proof_instruction_offset = proof_instruction_offset(&mut accounts, proof_location); - accounts.push(AccountMeta::new_readonly(*owner_address, true)); - - let mut instructions = vec![Instruction { - program_id: id(), - accounts, - data: RegistryInstruction::UpdateRegistry { - proof_instruction_offset, - } - .pack(), - }]; - append_zk_elgamal_proof(&mut instructions, proof_location)?; - Ok(instructions) -} - -/// Takes a `ProofLocation`, updates the list of accounts, and returns a -/// suitable proof location -fn proof_instruction_offset( - accounts: &mut Vec, - proof_location: ProofLocation, -) -> i8 { - match proof_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - } -} - -/// Takes a `RegistryInstruction` and appends the pubkey validity proof -/// instruction -fn append_zk_elgamal_proof( - instructions: &mut Vec, - proof_data_location: ProofLocation, -) -> Result<(), ProgramError> { - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 1 { - return Err(ProgramError::InvalidArgument); - } - match proof_data { - ProofData::InstructionData(data) => instructions - .push(ProofInstruction::VerifyPubkeyValidity.encode_verify_proof(None, data)), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyPubkeyValidity - .encode_verify_proof_from_account(None, address, offset), - ), - } - } - Ok(()) -} diff --git a/token/confidential-transfer/elgamal-registry/src/lib.rs b/token/confidential-transfer/elgamal-registry/src/lib.rs deleted file mode 100644 index 895f42daad2..00000000000 --- a/token/confidential-transfer/elgamal-registry/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -mod entrypoint; -pub mod instruction; -pub mod processor; -pub mod state; - -use solana_program::pubkey::Pubkey; - -/// Seed for the ElGamal registry program-derived address -pub const REGISTRY_ADDRESS_SEED: &[u8] = b"elgamal-registry"; - -/// Derives the ElGamal registry account address and seed for the given wallet -/// address -pub fn get_elgamal_registry_address_and_bump_seed( - wallet_address: &Pubkey, - program_id: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[REGISTRY_ADDRESS_SEED, wallet_address.as_ref()], - program_id, - ) -} - -/// Derives the ElGamal registry account address for the given wallet address -pub fn get_elgamal_registry_address(wallet_address: &Pubkey, program_id: &Pubkey) -> Pubkey { - get_elgamal_registry_address_and_bump_seed(wallet_address, program_id).0 -} - -solana_program::declare_id!("regVYJW7tcT8zipN5YiBvHsvR5jXW1uLFxaHSbugABg"); diff --git a/token/confidential-transfer/elgamal-registry/src/processor.rs b/token/confidential-transfer/elgamal-registry/src/processor.rs deleted file mode 100644 index 53be973c636..00000000000 --- a/token/confidential-transfer/elgamal-registry/src/processor.rs +++ /dev/null @@ -1,166 +0,0 @@ -use { - crate::{ - get_elgamal_registry_address_and_bump_seed, - instruction::RegistryInstruction, - state::{ElGamalRegistry, ELGAMAL_REGISTRY_ACCOUNT_LEN}, - REGISTRY_ADDRESS_SEED, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program::invoke_signed, - program_error::ProgramError, - pubkey::Pubkey, - rent::Rent, - system_instruction, - sysvar::Sysvar, - }, - solana_zk_sdk::zk_elgamal_proof_program::proof_data::pubkey_validity::{ - PubkeyValidityProofContext, PubkeyValidityProofData, - }, - spl_pod::bytemuck::pod_from_bytes_mut, - spl_token_confidential_transfer_proof_extraction::instruction::verify_and_extract_context, -}; - -/// Processes `CreateRegistry` instruction -pub fn process_create_registry_account( - program_id: &Pubkey, - accounts: &[AccountInfo], - proof_instruction_offset: i64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let elgamal_registry_account_info = next_account_info(account_info_iter)?; - let wallet_account_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - - if !wallet_account_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - - // zero-knowledge proof certifies that the supplied ElGamal public key is valid - let proof_context = verify_and_extract_context::< - PubkeyValidityProofData, - PubkeyValidityProofContext, - >(account_info_iter, proof_instruction_offset, None)?; - - let (elgamal_registry_account_address, bump_seed) = - get_elgamal_registry_address_and_bump_seed(wallet_account_info.key, program_id); - if elgamal_registry_account_address != *elgamal_registry_account_info.key { - msg!("Error: ElGamal registry account address does not match seed derivation"); - return Err(ProgramError::InvalidSeeds); - } - - let elgamal_registry_account_seeds: &[&[_]] = &[ - REGISTRY_ADDRESS_SEED, - wallet_account_info.key.as_ref(), - &[bump_seed], - ]; - let rent = Rent::get()?; - - create_pda_account( - &rent, - ELGAMAL_REGISTRY_ACCOUNT_LEN, - program_id, - system_program_info, - elgamal_registry_account_info, - elgamal_registry_account_seeds, - )?; - - let elgamal_registry_account_data = &mut elgamal_registry_account_info.data.borrow_mut(); - let elgamal_registry_account = - pod_from_bytes_mut::(elgamal_registry_account_data)?; - elgamal_registry_account.owner = *wallet_account_info.key; - elgamal_registry_account.elgamal_pubkey = proof_context.pubkey; - - Ok(()) -} - -/// Processes `UpdateRegistry` instruction -pub fn process_update_registry_account( - _program_id: &Pubkey, - accounts: &[AccountInfo], - proof_instruction_offset: i64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let elgamal_registry_account_info = next_account_info(account_info_iter)?; - let elgamal_registry_account_data = &mut elgamal_registry_account_info.data.borrow_mut(); - let elgamal_registry_account = - pod_from_bytes_mut::(elgamal_registry_account_data)?; - - // zero-knowledge proof certifies that the supplied ElGamal public key is valid - let proof_context = verify_and_extract_context::< - PubkeyValidityProofData, - PubkeyValidityProofContext, - >(account_info_iter, proof_instruction_offset, None)?; - - let owner_info = next_account_info(account_info_iter)?; - validate_owner(owner_info, &elgamal_registry_account.owner)?; - - elgamal_registry_account.elgamal_pubkey = proof_context.pubkey; - Ok(()) -} - -/// Instruction processor -pub fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - let instruction = RegistryInstruction::unpack(input)?; - match instruction { - RegistryInstruction::CreateRegistry { - proof_instruction_offset, - } => { - msg!("ElGamalRegistryInstruction::CreateRegistry"); - process_create_registry_account(program_id, accounts, proof_instruction_offset as i64) - } - RegistryInstruction::UpdateRegistry { - proof_instruction_offset, - } => { - msg!("ElGamalRegistryInstruction::UpdateRegistry"); - process_update_registry_account(program_id, accounts, proof_instruction_offset as i64) - } - } -} - -fn validate_owner(owner_info: &AccountInfo, expected_owner: &Pubkey) -> ProgramResult { - if expected_owner != owner_info.key { - return Err(ProgramError::InvalidAccountOwner); - } - if !owner_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - Ok(()) -} - -/// Allocate ElGamal registry account using Program Derived Address for the -/// given seeds -pub fn create_pda_account<'a>( - rent: &Rent, - space: usize, - owner: &Pubkey, - system_program: &AccountInfo<'a>, - new_pda_account: &AccountInfo<'a>, - new_pda_signer_seeds: &[&[u8]], -) -> ProgramResult { - let required_lamports = rent - .minimum_balance(space) - .saturating_sub(new_pda_account.lamports()); - - if required_lamports > 0 { - return Err(ProgramError::AccountNotRentExempt); - } - - invoke_signed( - &system_instruction::allocate(new_pda_account.key, space as u64), - &[new_pda_account.clone(), system_program.clone()], - &[new_pda_signer_seeds], - )?; - - invoke_signed( - &system_instruction::assign(new_pda_account.key, owner), - &[new_pda_account.clone(), system_program.clone()], - &[new_pda_signer_seeds], - ) -} diff --git a/token/confidential-transfer/elgamal-registry/src/state.rs b/token/confidential-transfer/elgamal-registry/src/state.rs deleted file mode 100644 index 55e77689df6..00000000000 --- a/token/confidential-transfer/elgamal-registry/src/state.rs +++ /dev/null @@ -1,18 +0,0 @@ -use { - bytemuck::{Pod, Zeroable}, - solana_program::pubkey::Pubkey, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, -}; - -pub const ELGAMAL_REGISTRY_ACCOUNT_LEN: usize = 64; - -/// ElGamal public key registry. It contains an ElGamal public key that is -/// associated with a wallet account, but independent of any specific mint. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct ElGamalRegistry { - /// The owner of the registry - pub owner: Pubkey, - /// The ElGamal public key associated with an account - pub elgamal_pubkey: PodElGamalPubkey, -} diff --git a/token/confidential-transfer/proof-extraction/Cargo.toml b/token/confidential-transfer/proof-extraction/Cargo.toml deleted file mode 100644 index d0354c84a94..00000000000 --- a/token/confidential-transfer/proof-extraction/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "spl-token-confidential-transfer-proof-extraction" -version = "0.2.0" -description = "Solana Program Library Confidential Transfer Proof Extraction" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -bytemuck = "1.21.0" -solana-curve25519 = "2.1.0" -solana-program = "2.1.0" -solana-zk-sdk = "2.1.0" -spl-pod = { version = "0.5.0", path = "../../../libraries/pod" } -thiserror = "2.0.9" - -[lib] -crate-type = ["cdylib", "lib"] diff --git a/token/confidential-transfer/proof-extraction/src/burn.rs b/token/confidential-transfer/proof-extraction/src/burn.rs deleted file mode 100644 index dbfa6d1e81a..00000000000 --- a/token/confidential-transfer/proof-extraction/src/burn.rs +++ /dev/null @@ -1,130 +0,0 @@ -use { - crate::{encryption::PodBurnAmountCiphertext, errors::TokenProofExtractionError}, - solana_zk_sdk::{ - encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, - CiphertextCommitmentEqualityProofContext, - }, - }, -}; - -/// The public keys associated with a confidential burn -pub struct BurnPubkeys { - pub source: PodElGamalPubkey, - pub auditor: PodElGamalPubkey, - pub supply: PodElGamalPubkey, -} - -/// The proof context information needed to process a confidential burn -/// instruction -pub struct BurnProofContext { - pub burn_amount_ciphertext_lo: PodBurnAmountCiphertext, - pub burn_amount_ciphertext_hi: PodBurnAmountCiphertext, - pub burn_pubkeys: BurnPubkeys, - pub remaining_balance_ciphertext: PodElGamalCiphertext, -} - -impl BurnProofContext { - pub fn verify_and_extract( - equality_proof_context: &CiphertextCommitmentEqualityProofContext, - ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, - range_proof_context: &BatchedRangeProofContext, - ) -> Result { - // The equality proof context consists of the source ElGamal public key, the new - // source available balance ciphertext, and the new source available - // balance commitment. The public key should be checked with ciphertext - // validity proof context for consistency and the commitment should be - // checked with range proof for consistency. The public key and - // the cihpertext should be returned as part of `BurnProofContext`. - let CiphertextCommitmentEqualityProofContext { - pubkey: source_elgamal_pubkey_from_equality_proof, - ciphertext: remaining_balance_ciphertext, - commitment: remaining_balance_commitment, - } = equality_proof_context; - - // The ciphertext validity proof context consists of the source ElGamal public - // key, the auditor ElGamal public key, and the grouped ElGamal - // ciphertexts for the low and high bits of the burn amount. The source - // ElGamal public key should be checked with equality - // proof for consistency and the rest of the data should be returned as part of - // `BurnProofContext`. - let BatchedGroupedCiphertext3HandlesValidityProofContext { - first_pubkey: source_elgamal_pubkey_from_validity_proof, - second_pubkey: auditor_elgamal_pubkey, - third_pubkey: supply_elgamal_pubkey, - grouped_ciphertext_lo: burn_amount_ciphertext_lo, - grouped_ciphertext_hi: burn_amount_ciphertext_hi, - } = ciphertext_validity_proof_context; - - // The range proof context consists of the Pedersen commitments and bit-lengths - // for which the range proof is proved. The commitments must consist of - // three commitments pertaining to the new source available balance, the - // low bits of the burn amount, and high bits of the burn - // amount. These commitments must be checked for bit lengths `64`, `16`, - // and `32`. - let BatchedRangeProofContext { - commitments: range_proof_commitments, - bit_lengths: range_proof_bit_lengths, - } = range_proof_context; - - // check that the source pubkey is consistent between equality and ciphertext - // validity proofs - if source_elgamal_pubkey_from_equality_proof != source_elgamal_pubkey_from_validity_proof { - return Err(TokenProofExtractionError::ElGamalPubkeyMismatch); - } - - // check that the range proof was created for the correct set of Pedersen - // commitments - let burn_amount_commitment_lo = burn_amount_ciphertext_lo.extract_commitment(); - let burn_amount_commitment_hi = burn_amount_ciphertext_hi.extract_commitment(); - - let expected_commitments = [ - *remaining_balance_commitment, - burn_amount_commitment_lo, - burn_amount_commitment_hi, - ]; - - if !range_proof_commitments - .iter() - .zip(expected_commitments.iter()) - .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) - { - return Err(TokenProofExtractionError::PedersenCommitmentMismatch); - } - - // check that the range proof was created for the correct number of bits - const REMAINING_BALANCE_BIT_LENGTH: u8 = 64; - const BURN_AMOUNT_LO_BIT_LENGTH: u8 = 16; - const BURN_AMOUNT_HI_BIT_LENGTH: u8 = 32; - const PADDING_BIT_LENGTH: u8 = 16; - let expected_bit_lengths = [ - REMAINING_BALANCE_BIT_LENGTH, - BURN_AMOUNT_LO_BIT_LENGTH, - BURN_AMOUNT_HI_BIT_LENGTH, - PADDING_BIT_LENGTH, - ] - .iter(); - - if !range_proof_bit_lengths - .iter() - .zip(expected_bit_lengths) - .all(|(proof_len, expected_len)| proof_len == expected_len) - { - return Err(TokenProofExtractionError::RangeProofLengthMismatch); - } - - let burn_pubkeys = BurnPubkeys { - source: *source_elgamal_pubkey_from_equality_proof, - auditor: *auditor_elgamal_pubkey, - supply: *supply_elgamal_pubkey, - }; - - Ok(BurnProofContext { - burn_amount_ciphertext_lo: PodBurnAmountCiphertext(*burn_amount_ciphertext_lo), - burn_amount_ciphertext_hi: PodBurnAmountCiphertext(*burn_amount_ciphertext_hi), - burn_pubkeys, - remaining_balance_ciphertext: *remaining_balance_ciphertext, - }) - } -} diff --git a/token/confidential-transfer/proof-extraction/src/encryption.rs b/token/confidential-transfer/proof-extraction/src/encryption.rs deleted file mode 100644 index 383108d41b1..00000000000 --- a/token/confidential-transfer/proof-extraction/src/encryption.rs +++ /dev/null @@ -1,69 +0,0 @@ -use { - crate::errors::TokenProofExtractionError, - solana_zk_sdk::encryption::pod::{ - elgamal::PodElGamalCiphertext, - grouped_elgamal::{ - PodGroupedElGamalCiphertext2Handles, PodGroupedElGamalCiphertext3Handles, - }, - }, -}; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct PodTransferAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); - -impl PodTransferAmountCiphertext { - pub fn try_extract_ciphertext( - &self, - index: usize, - ) -> Result { - self.0 - .try_extract_ciphertext(index) - .map_err(|_| TokenProofExtractionError::CiphertextExtraction) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct PodFeeCiphertext(pub(crate) PodGroupedElGamalCiphertext2Handles); - -impl PodFeeCiphertext { - pub fn try_extract_ciphertext( - &self, - index: usize, - ) -> Result { - self.0 - .try_extract_ciphertext(index) - .map_err(|_| TokenProofExtractionError::CiphertextExtraction) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct PodBurnAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); - -impl PodBurnAmountCiphertext { - pub fn try_extract_ciphertext( - &self, - index: usize, - ) -> Result { - self.0 - .try_extract_ciphertext(index) - .map_err(|_| TokenProofExtractionError::CiphertextExtraction) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct PodMintAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles); - -impl PodMintAmountCiphertext { - pub fn try_extract_ciphertext( - &self, - index: usize, - ) -> Result { - self.0 - .try_extract_ciphertext(index) - .map_err(|_| TokenProofExtractionError::CiphertextExtraction) - } -} diff --git a/token/confidential-transfer/proof-extraction/src/errors.rs b/token/confidential-transfer/proof-extraction/src/errors.rs deleted file mode 100644 index e710dec7159..00000000000 --- a/token/confidential-transfer/proof-extraction/src/errors.rs +++ /dev/null @@ -1,17 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Clone, Debug, Eq, PartialEq)] -pub enum TokenProofExtractionError { - #[error("ElGamal pubkey mismatch")] - ElGamalPubkeyMismatch, - #[error("Pedersen commitment mismatch")] - PedersenCommitmentMismatch, - #[error("Range proof length mismatch")] - RangeProofLengthMismatch, - #[error("Fee parameters mismatch")] - FeeParametersMismatch, - #[error("Curve arithmetic failed")] - CurveArithmetic, - #[error("Ciphertext extraction failed")] - CiphertextExtraction, -} diff --git a/token/confidential-transfer/proof-extraction/src/instruction.rs b/token/confidential-transfer/proof-extraction/src/instruction.rs deleted file mode 100644 index 247071a90f1..00000000000 --- a/token/confidential-transfer/proof-extraction/src/instruction.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Utility functions to simplify the handling of ZK ElGamal proof program -//! instruction data in SPL crates - -use { - bytemuck::Pod, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - instruction::{AccountMeta, Instruction}, - msg, - program_error::ProgramError, - pubkey::Pubkey, - sysvar::{self, instructions::get_instruction_relative}, - }, - solana_zk_sdk::zk_elgamal_proof_program::{ - self, - instruction::ProofInstruction, - proof_data::{ProofType, ZkProofData}, - state::ProofContextState, - }, - spl_pod::bytemuck::pod_from_bytes, - std::{num::NonZeroI8, slice::Iter}, -}; - -/// Checks that the supplied program ID is correct for the ZK ElGamal proof -/// program -pub fn check_zk_elgamal_proof_program_account( - zk_elgamal_proof_program_id: &Pubkey, -) -> ProgramResult { - if zk_elgamal_proof_program_id != &solana_zk_sdk::zk_elgamal_proof_program::id() { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} - -/// If a proof is to be read from a record account, the proof instruction data -/// must be 5 bytes: 1 byte for the proof type and 4 bytes for the `u32` offset -const INSTRUCTION_DATA_LENGTH_WITH_RECORD_ACCOUNT: usize = 5; - -/// Decodes the proof context data associated with a zero-knowledge proof -/// instruction. -pub fn decode_proof_instruction_context, U: Pod>( - account_info_iter: &mut Iter<'_, AccountInfo<'_>>, - expected: ProofInstruction, - instruction: &Instruction, -) -> Result { - if instruction.program_id != zk_elgamal_proof_program::id() - || ProofInstruction::instruction_type(&instruction.data) != Some(expected) - { - msg!("Unexpected proof instruction"); - return Err(ProgramError::InvalidInstructionData); - } - - // If the instruction data size is exactly 5 bytes, then interpret it as an - // offset byte for a record account. This behavior is identical to that of - // the ZK ElGamal proof program. - if instruction.data.len() == INSTRUCTION_DATA_LENGTH_WITH_RECORD_ACCOUNT { - let record_account = next_account_info(account_info_iter)?; - - // first byte is the proof type - let start_offset = u32::from_le_bytes(instruction.data[1..].try_into().unwrap()) as usize; - let end_offset = start_offset - .checked_add(std::mem::size_of::()) - .ok_or(ProgramError::InvalidAccountData)?; - - let record_account_data = record_account.data.borrow(); - let raw_proof_data = record_account_data - .get(start_offset..end_offset) - .ok_or(ProgramError::AccountDataTooSmall)?; - - bytemuck::try_from_bytes::(raw_proof_data) - .map(|proof_data| *ZkProofData::context_data(proof_data)) - .map_err(|_| ProgramError::InvalidAccountData) - } else { - ProofInstruction::proof_data::(&instruction.data) - .map(|proof_data| *ZkProofData::context_data(proof_data)) - .ok_or(ProgramError::InvalidInstructionData) - } -} - -/// A proof location type meant to be used for arguments to instruction -/// constructors. -#[derive(Clone, Copy)] -pub enum ProofLocation<'a, T> { - /// The proof is included in the same transaction of a corresponding - /// token-2022 instruction. - InstructionOffset(NonZeroI8, ProofData<'a, T>), - /// The proof is pre-verified into a context state account. - ContextStateAccount(&'a Pubkey), -} - -impl<'a, T> ProofLocation<'a, T> { - /// Returns true if the proof location is an instruction offset - pub fn is_instruction_offset(&self) -> bool { - match self { - Self::InstructionOffset(_, _) => true, - Self::ContextStateAccount(_) => false, - } - } -} - -/// A proof data type to distinguish between proof data included as part of -/// zk-token proof instruction data and proof data stored in a record account. -#[derive(Clone, Copy)] -pub enum ProofData<'a, T> { - /// The proof data - InstructionData(&'a T), - /// The address of a record account containing the proof data and its byte - /// offset - RecordAccount(&'a Pubkey, u32), -} - -/// Verify zero-knowledge proof and return the corresponding proof context. -pub fn verify_and_extract_context<'a, T: Pod + ZkProofData, U: Pod>( - account_info_iter: &mut Iter<'_, AccountInfo<'a>>, - proof_instruction_offset: i64, - sysvar_account_info: Option<&'_ AccountInfo<'a>>, -) -> Result { - if proof_instruction_offset == 0 { - // interpret `account_info` as a context state account - let context_state_account_info = next_account_info(account_info_iter)?; - check_zk_elgamal_proof_program_account(context_state_account_info.owner)?; - let context_state_account_data = context_state_account_info.data.borrow(); - let context_state = pod_from_bytes::>(&context_state_account_data)?; - - if context_state.proof_type != T::PROOF_TYPE.into() { - return Err(ProgramError::InvalidInstructionData); - } - - Ok(context_state.proof_context) - } else { - // if sysvar account is not provided, then get the sysvar account - let sysvar_account_info = if let Some(sysvar_account_info) = sysvar_account_info { - sysvar_account_info - } else { - next_account_info(account_info_iter)? - }; - let zkp_instruction = - get_instruction_relative(proof_instruction_offset, sysvar_account_info)?; - let expected_proof_type = zk_proof_type_to_instruction(T::PROOF_TYPE)?; - Ok(decode_proof_instruction_context::( - account_info_iter, - expected_proof_type, - &zkp_instruction, - )?) - } -} - -/// Processes a proof location for instruction creation. Adds relevant accounts -/// to supplied account vector -/// -/// If the proof location is an instruction offset the corresponding proof -/// instruction is created and added to the `proof_instructions` vector. -pub fn process_proof_location( - accounts: &mut Vec, - expected_instruction_offset: &mut i8, - proof_instructions: &mut Vec, - proof_location: ProofLocation, - push_sysvar_to_accounts: bool, - proof_instruction_type: ProofInstruction, -) -> Result -where - T: Pod + ZkProofData, - U: Pod, -{ - match proof_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if &proof_instruction_offset != expected_instruction_offset { - return Err(ProgramError::InvalidInstructionData); - } - - if push_sysvar_to_accounts { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - } - match proof_data { - ProofData::InstructionData(data) => proof_instructions - .push(proof_instruction_type.encode_verify_proof::(None, data)), - ProofData::RecordAccount(address, offset) => { - accounts.push(AccountMeta::new_readonly(*address, false)); - proof_instructions.push( - proof_instruction_type - .encode_verify_proof_from_account(None, address, offset), - ) - } - }; - *expected_instruction_offset = expected_instruction_offset - .checked_add(1) - .ok_or(ProgramError::InvalidInstructionData)?; - Ok(proof_instruction_offset) - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - Ok(0) - } - } -} - -/// Converts a zk proof type to a corresponding ZK ElGamal proof program -/// instruction that verifies the proof. -pub fn zk_proof_type_to_instruction( - proof_type: ProofType, -) -> Result { - match proof_type { - ProofType::ZeroCiphertext => Ok(ProofInstruction::VerifyZeroCiphertext), - ProofType::CiphertextCiphertextEquality => { - Ok(ProofInstruction::VerifyCiphertextCiphertextEquality) - } - ProofType::PubkeyValidity => Ok(ProofInstruction::VerifyPubkeyValidity), - ProofType::BatchedRangeProofU64 => Ok(ProofInstruction::VerifyBatchedRangeProofU64), - ProofType::BatchedRangeProofU128 => Ok(ProofInstruction::VerifyBatchedRangeProofU128), - ProofType::BatchedRangeProofU256 => Ok(ProofInstruction::VerifyBatchedRangeProofU256), - ProofType::CiphertextCommitmentEquality => { - Ok(ProofInstruction::VerifyCiphertextCommitmentEquality) - } - ProofType::GroupedCiphertext2HandlesValidity => { - Ok(ProofInstruction::VerifyGroupedCiphertext2HandlesValidity) - } - ProofType::BatchedGroupedCiphertext2HandlesValidity => { - Ok(ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity) - } - ProofType::PercentageWithCap => Ok(ProofInstruction::VerifyPercentageWithCap), - ProofType::GroupedCiphertext3HandlesValidity => { - Ok(ProofInstruction::VerifyGroupedCiphertext3HandlesValidity) - } - ProofType::BatchedGroupedCiphertext3HandlesValidity => { - Ok(ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity) - } - ProofType::Uninitialized => Err(ProgramError::InvalidInstructionData), - } -} diff --git a/token/confidential-transfer/proof-extraction/src/lib.rs b/token/confidential-transfer/proof-extraction/src/lib.rs deleted file mode 100644 index 82dcff0d31b..00000000000 --- a/token/confidential-transfer/proof-extraction/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod burn; -pub mod encryption; -pub mod errors; -pub mod instruction; -pub mod mint; -pub mod transfer; -pub mod transfer_with_fee; -pub mod withdraw; diff --git a/token/confidential-transfer/proof-extraction/src/mint.rs b/token/confidential-transfer/proof-extraction/src/mint.rs deleted file mode 100644 index 97f67f91ae7..00000000000 --- a/token/confidential-transfer/proof-extraction/src/mint.rs +++ /dev/null @@ -1,130 +0,0 @@ -use { - crate::{encryption::PodMintAmountCiphertext, errors::TokenProofExtractionError}, - solana_zk_sdk::{ - encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, - CiphertextCommitmentEqualityProofContext, - }, - }, -}; - -/// The public keys associated with a confidential mint -pub struct MintPubkeys { - pub destination: PodElGamalPubkey, - pub auditor: PodElGamalPubkey, - pub supply: PodElGamalPubkey, -} - -/// The proof context information needed to process a confidential mint -/// instruction -pub struct MintProofContext { - pub mint_amount_ciphertext_lo: PodMintAmountCiphertext, - pub mint_amount_ciphertext_hi: PodMintAmountCiphertext, - pub mint_pubkeys: MintPubkeys, - pub new_supply_ciphertext: PodElGamalCiphertext, -} - -impl MintProofContext { - pub fn verify_and_extract( - equality_proof_context: &CiphertextCommitmentEqualityProofContext, - ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, - range_proof_context: &BatchedRangeProofContext, - ) -> Result { - // The equality proof context consists of the supply ElGamal public key, the new - // supply ciphertext, and the new supply commitment. The supply ElGamal - // public key should be checked with ciphertext validity proof for - // consistency and the new supply commitment should be checked with - // range proof for consistency. The new supply ciphertext should be - // returned as part of `MintProofContext`. - let CiphertextCommitmentEqualityProofContext { - pubkey: supply_elgamal_pubkey_from_equality_proof, - ciphertext: new_supply_ciphertext, - commitment: new_supply_commitment, - } = equality_proof_context; - - // The ciphertext validity proof context consists of the destination ElGamal - // public key, the auditor ElGamal public key, and the grouped ElGamal - // ciphertexts for the low and high bits of the mint amount. These - // fields should be returned as part of `MintProofContext`. - let BatchedGroupedCiphertext3HandlesValidityProofContext { - first_pubkey: destination_elgamal_pubkey, - second_pubkey: auditor_elgamal_pubkey, - third_pubkey: supply_elgamal_pubkey_from_ciphertext_validity_proof, - grouped_ciphertext_lo: mint_amount_ciphertext_lo, - grouped_ciphertext_hi: mint_amount_ciphertext_hi, - } = ciphertext_validity_proof_context; - - // The range proof context consists of the Pedersen commitments and bit-lengths - // for which the range proof is proved. The commitments must consist of - // two commitments pertaining to the - // low bits of the mint amount, and high bits of the mint - // amount. These commitments must be checked for bit lengths `16` and - // and `32`. - let BatchedRangeProofContext { - commitments: range_proof_commitments, - bit_lengths: range_proof_bit_lengths, - } = range_proof_context; - - // check that the supply pubkey is consistent between equality and ciphertext - // validity proofs - if supply_elgamal_pubkey_from_equality_proof - != supply_elgamal_pubkey_from_ciphertext_validity_proof - { - return Err(TokenProofExtractionError::ElGamalPubkeyMismatch); - } - - // check that the range proof was created for the correct set of Pedersen - // commitments - let mint_amount_commitment_lo = mint_amount_ciphertext_lo.extract_commitment(); - let mint_amount_commitment_hi = mint_amount_ciphertext_hi.extract_commitment(); - - let expected_commitments = [ - *new_supply_commitment, - mint_amount_commitment_lo, - mint_amount_commitment_hi, - ]; - - if !range_proof_commitments - .iter() - .zip(expected_commitments.iter()) - .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) - { - return Err(TokenProofExtractionError::PedersenCommitmentMismatch); - } - - // check that the range proof was created for the correct number of bits - const NEW_SUPPLY_BIT_LENGTH: u8 = 64; - const MINT_AMOUNT_LO_BIT_LENGTH: u8 = 16; - const MINT_AMOUNT_HI_BIT_LENGTH: u8 = 32; - const PADDING_BIT_LENGTH: u8 = 16; - let expected_bit_lengths = [ - NEW_SUPPLY_BIT_LENGTH, - MINT_AMOUNT_LO_BIT_LENGTH, - MINT_AMOUNT_HI_BIT_LENGTH, - PADDING_BIT_LENGTH, - ] - .iter(); - - if !range_proof_bit_lengths - .iter() - .zip(expected_bit_lengths) - .all(|(proof_len, expected_len)| proof_len == expected_len) - { - return Err(TokenProofExtractionError::RangeProofLengthMismatch); - } - - let mint_pubkeys = MintPubkeys { - destination: *destination_elgamal_pubkey, - auditor: *auditor_elgamal_pubkey, - supply: *supply_elgamal_pubkey_from_equality_proof, - }; - - Ok(MintProofContext { - mint_amount_ciphertext_lo: PodMintAmountCiphertext(*mint_amount_ciphertext_lo), - mint_amount_ciphertext_hi: PodMintAmountCiphertext(*mint_amount_ciphertext_hi), - mint_pubkeys, - new_supply_ciphertext: *new_supply_ciphertext, - }) - } -} diff --git a/token/confidential-transfer/proof-extraction/src/transfer.rs b/token/confidential-transfer/proof-extraction/src/transfer.rs deleted file mode 100644 index ccc055bebaf..00000000000 --- a/token/confidential-transfer/proof-extraction/src/transfer.rs +++ /dev/null @@ -1,137 +0,0 @@ -use { - crate::{encryption::PodTransferAmountCiphertext, errors::TokenProofExtractionError}, - solana_zk_sdk::{ - encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, - CiphertextCommitmentEqualityProofContext, - }, - }, -}; - -/// The transfer public keys associated with a transfer. -pub struct TransferPubkeys { - /// Source ElGamal public key - pub source: PodElGamalPubkey, - /// Destination ElGamal public key - pub destination: PodElGamalPubkey, - /// Auditor ElGamal public key - pub auditor: PodElGamalPubkey, -} - -/// The proof context information needed to process a [Transfer] instruction. -pub struct TransferProofContext { - /// Ciphertext containing the low 16 bits of the transfer amount - pub ciphertext_lo: PodTransferAmountCiphertext, - /// Ciphertext containing the high 32 bits of the transfer amount - pub ciphertext_hi: PodTransferAmountCiphertext, - /// The transfer public keys associated with a transfer - pub transfer_pubkeys: TransferPubkeys, - /// The new source available balance ciphertext - pub new_source_ciphertext: PodElGamalCiphertext, -} - -impl TransferProofContext { - pub fn verify_and_extract( - equality_proof_context: &CiphertextCommitmentEqualityProofContext, - ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, - range_proof_context: &BatchedRangeProofContext, - ) -> Result { - // The equality proof context consists of the source ElGamal public key, the new - // source available balance ciphertext, and the new source available - // commitment. The public key and ciphertext should be returned as parts - // of `TransferProofContextInfo` and the commitment should be checked - // with range proof for consistency. - let CiphertextCommitmentEqualityProofContext { - pubkey: source_pubkey_from_equality_proof, - ciphertext: new_source_ciphertext, - commitment: new_source_commitment, - } = equality_proof_context; - - // The ciphertext validity proof context consists of the destination ElGamal - // public key, auditor ElGamal public key, and the transfer amount - // ciphertexts. All of these fields should be returned as part of - // `TransferProofContextInfo`. In addition, the commitments pertaining - // to the transfer amount ciphertexts should be checked with range proof for - // consistency. - let BatchedGroupedCiphertext3HandlesValidityProofContext { - first_pubkey: source_pubkey_from_validity_proof, - second_pubkey: destination_pubkey, - third_pubkey: auditor_pubkey, - grouped_ciphertext_lo: transfer_amount_ciphertext_lo, - grouped_ciphertext_hi: transfer_amount_ciphertext_hi, - } = ciphertext_validity_proof_context; - - // The range proof context consists of the Pedersen commitments and bit-lengths - // for which the range proof is proved. The commitments must consist of - // three commitments pertaining to the new source available balance, the - // low bits of the transfer amount, and high bits of the transfer - // amount. These commitments must be checked for bit lengths `64`, `16`, - // and `32`. - let BatchedRangeProofContext { - commitments: range_proof_commitments, - bit_lengths: range_proof_bit_lengths, - } = range_proof_context; - - // check that the source pubkey is consistent between equality and ciphertext - // validity proofs - if source_pubkey_from_equality_proof != source_pubkey_from_validity_proof { - return Err(TokenProofExtractionError::ElGamalPubkeyMismatch); - } - - // check that the range proof was created for the correct set of Pedersen - // commitments - let transfer_amount_commitment_lo = transfer_amount_ciphertext_lo.extract_commitment(); - let transfer_amount_commitment_hi = transfer_amount_ciphertext_hi.extract_commitment(); - - let expected_commitments = [ - *new_source_commitment, - transfer_amount_commitment_lo, - transfer_amount_commitment_hi, - ]; - - if !range_proof_commitments - .iter() - .zip(expected_commitments.iter()) - .all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment) - { - return Err(TokenProofExtractionError::PedersenCommitmentMismatch); - } - - // check that the range proof was created for the correct number of bits - const REMAINING_BALANCE_BIT_LENGTH: u8 = 64; - const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16; - const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32; - const PADDING_BIT_LENGTH: u8 = 16; - let expected_bit_lengths = [ - REMAINING_BALANCE_BIT_LENGTH, - TRANSFER_AMOUNT_LO_BIT_LENGTH, - TRANSFER_AMOUNT_HI_BIT_LENGTH, - PADDING_BIT_LENGTH, - ] - .iter(); - - if !range_proof_bit_lengths - .iter() - .zip(expected_bit_lengths) - .all(|(proof_len, expected_len)| proof_len == expected_len) - { - return Err(TokenProofExtractionError::RangeProofLengthMismatch); - } - - let transfer_pubkeys = TransferPubkeys { - source: *source_pubkey_from_equality_proof, - destination: *destination_pubkey, - auditor: *auditor_pubkey, - }; - - let context_info = TransferProofContext { - ciphertext_lo: PodTransferAmountCiphertext(*transfer_amount_ciphertext_lo), - ciphertext_hi: PodTransferAmountCiphertext(*transfer_amount_ciphertext_hi), - transfer_pubkeys, - new_source_ciphertext: *new_source_ciphertext, - }; - - Ok(context_info) - } -} diff --git a/token/confidential-transfer/proof-extraction/src/transfer_with_fee.rs b/token/confidential-transfer/proof-extraction/src/transfer_with_fee.rs deleted file mode 100644 index e89ae0bd4c3..00000000000 --- a/token/confidential-transfer/proof-extraction/src/transfer_with_fee.rs +++ /dev/null @@ -1,322 +0,0 @@ -use { - crate::{ - encryption::{PodFeeCiphertext, PodTransferAmountCiphertext}, - errors::TokenProofExtractionError, - }, - bytemuck::bytes_of, - solana_curve25519::{ - ristretto::{self, PodRistrettoPoint}, - scalar::PodScalar, - }, - solana_zk_sdk::{ - encryption::pod::{ - elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - pedersen::PodPedersenCommitment, - }, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext2HandlesValidityProofContext, - BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext, - CiphertextCommitmentEqualityProofContext, PercentageWithCapProofContext, - }, - }, -}; - -const MAX_FEE_BASIS_POINTS: u64 = 10_000; -const REMAINING_BALANCE_BIT_LENGTH: u8 = 64; -const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16; -const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32; -const DELTA_BIT_LENGTH: u8 = 48; -const FEE_AMOUNT_LO_BIT_LENGTH: u8 = 16; -const FEE_AMOUNT_HI_BIT_LENGTH: u8 = 32; - -/// The transfer public keys associated with a transfer with fee. -pub struct TransferWithFeePubkeys { - /// Source ElGamal public key - pub source: PodElGamalPubkey, - /// Destination ElGamal public key - pub destination: PodElGamalPubkey, - /// Auditor ElGamal public key - pub auditor: PodElGamalPubkey, - /// Withdraw withheld authority public key - pub withdraw_withheld_authority: PodElGamalPubkey, -} - -/// The proof context information needed to process a [Transfer] instruction -/// with fee. -pub struct TransferWithFeeProofContext { - /// Group encryption of the low 16 bits of the transfer amount - pub ciphertext_lo: PodTransferAmountCiphertext, - /// Group encryption of the high 48 bits of the transfer amount - pub ciphertext_hi: PodTransferAmountCiphertext, - /// The public encryption keys associated with the transfer: source, - /// destination, auditor, and withdraw withheld authority - pub transfer_with_fee_pubkeys: TransferWithFeePubkeys, - /// The final spendable ciphertext after the transfer, - pub new_source_ciphertext: PodElGamalCiphertext, - /// The transfer fee encryption of the low 16 bits of the transfer fee - /// amount - pub fee_ciphertext_lo: PodFeeCiphertext, - /// The transfer fee encryption of the hi 32 bits of the transfer fee amount - pub fee_ciphertext_hi: PodFeeCiphertext, -} - -impl TransferWithFeeProofContext { - pub fn verify_and_extract( - equality_proof_context: &CiphertextCommitmentEqualityProofContext, - transfer_amount_ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext, - fee_sigma_proof_context: &PercentageWithCapProofContext, - fee_ciphertext_validity_proof_context: &BatchedGroupedCiphertext2HandlesValidityProofContext, - range_proof_context: &BatchedRangeProofContext, - expected_fee_rate_basis_points: u16, - expected_maximum_fee: u64, - ) -> Result { - // The equality proof context consists of the source ElGamal public key, the new - // source available balance ciphertext, and the new source available - // commitment. The public key and ciphertext should be returned as part - // of `TransferWithFeeProofContextInfo` and the commitment should be - // checked with range proof for consistency. - let CiphertextCommitmentEqualityProofContext { - pubkey: source_pubkey_from_equality_proof, - ciphertext: new_source_ciphertext, - commitment: new_source_commitment, - } = equality_proof_context; - - // The transfer amount ciphertext validity proof context consists of the - // destination ElGamal public key, auditor ElGamal public key, and the - // transfer amount ciphertexts. All of these fields should be returned - // as part of `TransferWithFeeProofContextInfo`. In addition, the - // commitments pertaining to the transfer amount ciphertexts should be - // checked with range proof for consistency. - let BatchedGroupedCiphertext3HandlesValidityProofContext { - first_pubkey: source_pubkey_from_validity_proof, - second_pubkey: destination_pubkey, - third_pubkey: auditor_pubkey, - grouped_ciphertext_lo: transfer_amount_ciphertext_lo, - grouped_ciphertext_hi: transfer_amount_ciphertext_hi, - } = transfer_amount_ciphertext_validity_proof_context; - - // The fee sigma proof context consists of the fee commitment, delta commitment, - // claimed commitment, and max fee. The fee and claimed commitment - // should be checked with range proof for consistency. The delta - // commitment should be checked whether it is properly generated with - // respect to the fee parameters. The max fee should be checked for - // consistency with the fee parameters. - let PercentageWithCapProofContext { - percentage_commitment: fee_commitment, - delta_commitment, - claimed_commitment, - max_value: proof_maximum_fee, - } = fee_sigma_proof_context; - - let proof_maximum_fee: u64 = (*proof_maximum_fee).into(); - if expected_maximum_fee != proof_maximum_fee { - return Err(TokenProofExtractionError::FeeParametersMismatch); - } - - // The transfer fee ciphertext validity proof context consists of the - // destination ElGamal public key, withdraw withheld authority ElGamal - // public key, and the transfer fee ciphertexts. The rest of the fields - // should be return as part of `TransferWithFeeProofContextInfo`. In - // addition, the destination public key should be checked for - // consistency with the destination public key contained in the transfer amount - // ciphertext validity proof, and the commitments pertaining to the transfer fee - // amount ciphertexts should be checked with range proof for - // consistency. - let BatchedGroupedCiphertext2HandlesValidityProofContext { - first_pubkey: destination_pubkey_from_transfer_fee_validity_proof, - second_pubkey: withdraw_withheld_authority_pubkey, - grouped_ciphertext_lo: fee_ciphertext_lo, - grouped_ciphertext_hi: fee_ciphertext_hi, - } = fee_ciphertext_validity_proof_context; - - if destination_pubkey != destination_pubkey_from_transfer_fee_validity_proof { - return Err(TokenProofExtractionError::ElGamalPubkeyMismatch); - } - - // The range proof context consists of the Pedersen commitments and bit-lengths - // for which the range proof is proved. The commitments must consist of - // seven commitments pertaining to - // - the new source available balance (64 bits) - // - the low bits of the transfer amount (16 bits) - // - the high bits of the transfer amount (32 bits) - // - the delta amount for the fee (48 bits) - // - the complement of the delta amount for the fee (48 bits) - // - the low bits of the fee amount (16 bits) - // - the high bits of the fee amount (32 bits) - let BatchedRangeProofContext { - commitments: range_proof_commitments, - bit_lengths: range_proof_bit_lengths, - } = range_proof_context; - - // check that the range proof was created for the correct set of Pedersen - // commitments - let transfer_amount_commitment_lo = transfer_amount_ciphertext_lo.extract_commitment(); - let transfer_amount_commitment_hi = transfer_amount_ciphertext_hi.extract_commitment(); - - let fee_commitment_lo = fee_ciphertext_lo.extract_commitment(); - let fee_commitment_hi = fee_ciphertext_hi.extract_commitment(); - - let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS); - let max_fee_basis_points_commitment = - ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &G) - .ok_or(TokenProofExtractionError::CurveArithmetic)?; - let claimed_complement_commitment = ristretto::subtract_ristretto( - &max_fee_basis_points_commitment, - &commitment_to_ristretto(claimed_commitment), - ) - .ok_or(TokenProofExtractionError::CurveArithmetic)?; - - let expected_commitments = [ - bytes_of(new_source_commitment), - bytes_of(&transfer_amount_commitment_lo), - bytes_of(&transfer_amount_commitment_hi), - bytes_of(claimed_commitment), - bytes_of(&claimed_complement_commitment), - bytes_of(&fee_commitment_lo), - bytes_of(&fee_commitment_hi), - ]; - - if !range_proof_commitments - .iter() - .zip(expected_commitments.into_iter()) - .all(|(proof_commitment, expected_commitment)| { - bytes_of(proof_commitment) == expected_commitment - }) - { - return Err(TokenProofExtractionError::PedersenCommitmentMismatch); - } - - // check that the range proof was created for the correct number of bits - let expected_bit_lengths = [ - REMAINING_BALANCE_BIT_LENGTH, - TRANSFER_AMOUNT_LO_BIT_LENGTH, - TRANSFER_AMOUNT_HI_BIT_LENGTH, - DELTA_BIT_LENGTH, - DELTA_BIT_LENGTH, - FEE_AMOUNT_LO_BIT_LENGTH, - FEE_AMOUNT_HI_BIT_LENGTH, - ] - .iter(); - - if !range_proof_bit_lengths - .iter() - .zip(expected_bit_lengths) - .all(|(proof_len, expected_len)| proof_len == expected_len) - { - return Err(TokenProofExtractionError::RangeProofLengthMismatch); - } - - // check consistency between fee sigma and fee ciphertext validity proofs - let sigma_proof_fee_commitment_point: PodRistrettoPoint = - commitment_to_ristretto(fee_commitment); - let validity_proof_fee_point = combine_lo_hi_pedersen_points( - &commitment_to_ristretto(&fee_commitment_lo), - &commitment_to_ristretto(&fee_commitment_hi), - ) - .ok_or(TokenProofExtractionError::CurveArithmetic)?; - - if source_pubkey_from_equality_proof != source_pubkey_from_validity_proof { - return Err(TokenProofExtractionError::ElGamalPubkeyMismatch); - } - - if validity_proof_fee_point != sigma_proof_fee_commitment_point { - return Err(TokenProofExtractionError::FeeParametersMismatch); - } - - verify_delta_commitment( - &transfer_amount_commitment_lo, - &transfer_amount_commitment_hi, - fee_commitment, - delta_commitment, - expected_fee_rate_basis_points, - )?; - - // create transfer with fee proof context info and return - let transfer_with_fee_pubkeys = TransferWithFeePubkeys { - source: *source_pubkey_from_equality_proof, - destination: *destination_pubkey, - auditor: *auditor_pubkey, - withdraw_withheld_authority: *withdraw_withheld_authority_pubkey, - }; - - Ok(Self { - ciphertext_lo: PodTransferAmountCiphertext(*transfer_amount_ciphertext_lo), - ciphertext_hi: PodTransferAmountCiphertext(*transfer_amount_ciphertext_hi), - transfer_with_fee_pubkeys, - new_source_ciphertext: *new_source_ciphertext, - fee_ciphertext_lo: PodFeeCiphertext(*fee_ciphertext_lo), - fee_ciphertext_hi: PodFeeCiphertext(*fee_ciphertext_hi), - }) - } -} - -/// Ristretto generator point for curve-25519 -const G: PodRistrettoPoint = PodRistrettoPoint([ - 226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165, - 130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118, -]); - -/// Convert a `u64` amount into a curve-25519 scalar -fn u64_to_scalar(amount: u64) -> PodScalar { - let mut bytes = [0u8; 32]; - bytes[..8].copy_from_slice(&amount.to_le_bytes()); - PodScalar(bytes) -} - -/// Convert a `u16` amount into a curve-25519 scalar -fn u16_to_scalar(amount: u16) -> PodScalar { - let mut bytes = [0u8; 32]; - bytes[..2].copy_from_slice(&amount.to_le_bytes()); - PodScalar(bytes) -} - -fn commitment_to_ristretto(commitment: &PodPedersenCommitment) -> PodRistrettoPoint { - let mut bytes = [0u8; 32]; - bytes.copy_from_slice(bytes_of(commitment)); - PodRistrettoPoint(bytes) -} - -/// Combine lo and hi Pedersen commitment points -fn combine_lo_hi_pedersen_points( - point_lo: &PodRistrettoPoint, - point_hi: &PodRistrettoPoint, -) -> Option { - const SCALING_CONSTANT: u64 = 65536; - let scaling_constant_scalar = u64_to_scalar(SCALING_CONSTANT); - let scaled_point_hi = ristretto::multiply_ristretto(&scaling_constant_scalar, point_hi)?; - ristretto::add_ristretto(point_lo, &scaled_point_hi) -} - -/// Compute fee delta commitment -fn verify_delta_commitment( - transfer_amount_commitment_lo: &PodPedersenCommitment, - transfer_amount_commitment_hi: &PodPedersenCommitment, - fee_commitment: &PodPedersenCommitment, - proof_delta_commitment: &PodPedersenCommitment, - transfer_fee_basis_points: u16, -) -> Result<(), TokenProofExtractionError> { - let transfer_amount_point = combine_lo_hi_pedersen_points( - &commitment_to_ristretto(transfer_amount_commitment_lo), - &commitment_to_ristretto(transfer_amount_commitment_hi), - ) - .ok_or(TokenProofExtractionError::CurveArithmetic)?; - let transfer_fee_basis_points_scalar = u16_to_scalar(transfer_fee_basis_points); - let scaled_transfer_amount_point = - ristretto::multiply_ristretto(&transfer_fee_basis_points_scalar, &transfer_amount_point) - .ok_or(TokenProofExtractionError::CurveArithmetic)?; - - let max_fee_basis_points_scalar = u64_to_scalar(MAX_FEE_BASIS_POINTS); - let fee_point: PodRistrettoPoint = commitment_to_ristretto(fee_commitment); - let scaled_fee_point = ristretto::multiply_ristretto(&max_fee_basis_points_scalar, &fee_point) - .ok_or(TokenProofExtractionError::CurveArithmetic)?; - - let expected_delta_commitment_point = - ristretto::subtract_ristretto(&scaled_fee_point, &scaled_transfer_amount_point) - .ok_or(TokenProofExtractionError::CurveArithmetic)?; - - let proof_delta_commitment_point = commitment_to_ristretto(proof_delta_commitment); - if expected_delta_commitment_point != proof_delta_commitment_point { - return Err(TokenProofExtractionError::CurveArithmetic); - } - Ok(()) -} diff --git a/token/confidential-transfer/proof-extraction/src/withdraw.rs b/token/confidential-transfer/proof-extraction/src/withdraw.rs deleted file mode 100644 index 7fc5fb17dc5..00000000000 --- a/token/confidential-transfer/proof-extraction/src/withdraw.rs +++ /dev/null @@ -1,53 +0,0 @@ -use { - crate::errors::TokenProofExtractionError, - solana_zk_sdk::{ - encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - zk_elgamal_proof_program::proof_data::{ - BatchedRangeProofContext, CiphertextCommitmentEqualityProofContext, - }, - }, -}; - -const REMAINING_BALANCE_BIT_LENGTH: u8 = 64; - -pub struct WithdrawProofContext { - pub source_pubkey: PodElGamalPubkey, - pub remaining_balance_ciphertext: PodElGamalCiphertext, -} - -impl WithdrawProofContext { - pub fn verify_and_extract( - equality_proof_context: &CiphertextCommitmentEqualityProofContext, - range_proof_context: &BatchedRangeProofContext, - ) -> Result { - let CiphertextCommitmentEqualityProofContext { - pubkey: source_pubkey, - ciphertext: remaining_balance_ciphertext, - commitment: remaining_balance_commitment, - } = equality_proof_context; - - let BatchedRangeProofContext { - commitments: range_proof_commitments, - bit_lengths: range_proof_bit_lengths, - } = range_proof_context; - - if range_proof_commitments.is_empty() - || range_proof_commitments[0] != *remaining_balance_commitment - { - return Err(TokenProofExtractionError::PedersenCommitmentMismatch); - } - - if range_proof_bit_lengths.is_empty() - || range_proof_bit_lengths[0] != REMAINING_BALANCE_BIT_LENGTH - { - return Err(TokenProofExtractionError::RangeProofLengthMismatch); - } - - let context_info = WithdrawProofContext { - source_pubkey: *source_pubkey, - remaining_balance_ciphertext: *remaining_balance_ciphertext, - }; - - Ok(context_info) - } -} diff --git a/token/confidential-transfer/proof-generation/Cargo.toml b/token/confidential-transfer/proof-generation/Cargo.toml deleted file mode 100644 index f9856ef5038..00000000000 --- a/token/confidential-transfer/proof-generation/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "spl-token-confidential-transfer-proof-generation" -version = "0.2.0" -description = "Solana Program Library Confidential Transfer Proof Generation" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -curve25519-dalek = "4.1.3" -solana-zk-sdk = "2.1.0" -thiserror = "2.0.9" - -[dev-dependencies] - -[lib] -crate-type = ["cdylib", "lib"] - -[lints] -workspace = true diff --git a/token/confidential-transfer/proof-generation/src/burn.rs b/token/confidential-transfer/proof-generation/src/burn.rs deleted file mode 100644 index 7d7b788fb11..00000000000 --- a/token/confidential-transfer/proof-generation/src/burn.rs +++ /dev/null @@ -1,169 +0,0 @@ -use { - crate::{ - encryption::BurnAmountCiphertext, errors::TokenProofGenerationError, - try_combine_lo_hi_ciphertexts, try_split_u64, CiphertextValidityProofWithAuditorCiphertext, - }, - solana_zk_sdk::{ - encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, - pedersen::Pedersen, - }, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, - CiphertextCommitmentEqualityProofData, ZkProofData, - }, - }, -}; - -const REMAINING_BALANCE_BIT_LENGTH: usize = 64; -const BURN_AMOUNT_LO_BIT_LENGTH: usize = 16; -const BURN_AMOUNT_HI_BIT_LENGTH: usize = 32; -/// The padding bit length in range proofs to make the bit-length power-of-2 -const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16; - -/// The proof data required for a confidential burn instruction -pub struct BurnProofData { - pub equality_proof_data: CiphertextCommitmentEqualityProofData, - pub ciphertext_validity_proof_data_with_ciphertext: - CiphertextValidityProofWithAuditorCiphertext, - pub range_proof_data: BatchedRangeProofU128Data, -} - -pub fn burn_split_proof_data( - current_available_balance_ciphertext: &ElGamalCiphertext, - current_decryptable_available_balance: &AeCiphertext, - burn_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - source_aes_key: &AeKey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - supply_elgamal_pubkey: &ElGamalPubkey, -) -> Result { - let default_auditor_pubkey = ElGamalPubkey::default(); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey); - - // split the burn amount into low and high bits - let (burn_amount_lo, burn_amount_hi) = try_split_u64(burn_amount, BURN_AMOUNT_LO_BIT_LENGTH) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // encrypt the burn amount under the source and auditor's ElGamal public key - let (burn_amount_ciphertext_lo, burn_amount_opening_lo) = BurnAmountCiphertext::new( - burn_amount_lo, - source_elgamal_keypair.pubkey(), - auditor_elgamal_pubkey, - supply_elgamal_pubkey, - ); - - let (burn_amount_ciphertext_hi, burn_amount_opening_hi) = BurnAmountCiphertext::new( - burn_amount_hi, - source_elgamal_keypair.pubkey(), - auditor_elgamal_pubkey, - supply_elgamal_pubkey, - ); - - // decrypt the current available balance at the source - let current_decrypted_available_balance = current_decryptable_available_balance - .decrypt(source_aes_key) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // compute the remaining balance ciphertext - let burn_amount_ciphertext_source_lo = burn_amount_ciphertext_lo - .0 - .to_elgamal_ciphertext(0) - .unwrap(); - let burn_amount_ciphertext_source_hi = burn_amount_ciphertext_hi - .0 - .to_elgamal_ciphertext(0) - .unwrap(); - - #[allow(clippy::arithmetic_side_effects)] - let new_available_balance_ciphertext = current_available_balance_ciphertext - - try_combine_lo_hi_ciphertexts( - &burn_amount_ciphertext_source_lo, - &burn_amount_ciphertext_source_hi, - BURN_AMOUNT_LO_BIT_LENGTH, - ) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // compute the remaining balance at the source - let remaining_balance = current_decrypted_available_balance - .checked_sub(burn_amount) - .ok_or(TokenProofGenerationError::NotEnoughFunds)?; - - let (new_available_balance_commitment, new_available_balance_opening) = - Pedersen::new(remaining_balance); - - // generate equality proof data - let equality_proof_data = CiphertextCommitmentEqualityProofData::new( - source_elgamal_keypair, - &new_available_balance_ciphertext, - &new_available_balance_commitment, - &new_available_balance_opening, - remaining_balance, - ) - .map_err(TokenProofGenerationError::from)?; - - // generate ciphertext validity data - let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new( - source_elgamal_keypair.pubkey(), - auditor_elgamal_pubkey, - supply_elgamal_pubkey, - &burn_amount_ciphertext_lo.0, - &burn_amount_ciphertext_hi.0, - burn_amount_lo, - burn_amount_hi, - &burn_amount_opening_lo, - &burn_amount_opening_hi, - ) - .map_err(TokenProofGenerationError::from)?; - - let burn_amount_auditor_ciphertext_lo = ciphertext_validity_proof_data - .context_data() - .grouped_ciphertext_lo - .try_extract_ciphertext(2) - .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?; - - let burn_amount_auditor_ciphertext_hi = ciphertext_validity_proof_data - .context_data() - .grouped_ciphertext_hi - .try_extract_ciphertext(2) - .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?; - - let ciphertext_validity_proof_data_with_ciphertext = - CiphertextValidityProofWithAuditorCiphertext { - proof_data: ciphertext_validity_proof_data, - ciphertext_lo: burn_amount_auditor_ciphertext_lo, - ciphertext_hi: burn_amount_auditor_ciphertext_hi, - }; - - // generate range proof data - let (padding_commitment, padding_opening) = Pedersen::new(0_u64); - let range_proof_data = BatchedRangeProofU128Data::new( - vec![ - &new_available_balance_commitment, - burn_amount_ciphertext_lo.get_commitment(), - burn_amount_ciphertext_hi.get_commitment(), - &padding_commitment, - ], - vec![remaining_balance, burn_amount_lo, burn_amount_hi, 0], - vec![ - REMAINING_BALANCE_BIT_LENGTH, - BURN_AMOUNT_LO_BIT_LENGTH, - BURN_AMOUNT_HI_BIT_LENGTH, - RANGE_PROOF_PADDING_BIT_LENGTH, - ], - vec![ - &new_available_balance_opening, - &burn_amount_opening_lo, - &burn_amount_opening_hi, - &padding_opening, - ], - ) - .map_err(TokenProofGenerationError::from)?; - - Ok(BurnProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - }) -} diff --git a/token/confidential-transfer/proof-generation/src/encryption.rs b/token/confidential-transfer/proof-generation/src/encryption.rs deleted file mode 100644 index 3f3f875aa44..00000000000 --- a/token/confidential-transfer/proof-generation/src/encryption.rs +++ /dev/null @@ -1,140 +0,0 @@ -use solana_zk_sdk::encryption::{ - elgamal::{DecryptHandle, ElGamalPubkey}, - grouped_elgamal::{GroupedElGamal, GroupedElGamalCiphertext}, - pedersen::{PedersenCommitment, PedersenOpening}, -}; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct TransferAmountCiphertext(pub(crate) GroupedElGamalCiphertext<3>); - -impl TransferAmountCiphertext { - pub fn new( - amount: u64, - source_pubkey: &ElGamalPubkey, - destination_pubkey: &ElGamalPubkey, - auditor_pubkey: &ElGamalPubkey, - ) -> (Self, PedersenOpening) { - let opening = PedersenOpening::new_rand(); - let grouped_ciphertext = GroupedElGamal::<3>::encrypt_with( - [source_pubkey, destination_pubkey, auditor_pubkey], - amount, - &opening, - ); - - (Self(grouped_ciphertext), opening) - } - - pub fn get_commitment(&self) -> &PedersenCommitment { - &self.0.commitment - } - - pub fn get_source_handle(&self) -> &DecryptHandle { - // `TransferAmountCiphertext` is a wrapper for `GroupedElGamalCiphertext<3>`, - // which holds exactly three decryption handles. - self.0.handles.first().unwrap() - } - - pub fn get_destination_handle(&self) -> &DecryptHandle { - // `TransferAmountCiphertext` is a wrapper for `GroupedElGamalCiphertext<3>`, - // which holds exactly three decryption handles. - self.0.handles.get(1).unwrap() - } - - pub fn get_auditor_handle(&self) -> &DecryptHandle { - // `TransferAmountCiphertext` is a wrapper for `GroupedElGamalCiphertext<3>`, - // which holds exactly three decryption handles. - self.0.handles.get(2).unwrap() - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(C)] -#[cfg(not(target_os = "solana"))] -pub struct FeeCiphertext(pub(crate) GroupedElGamalCiphertext<2>); - -#[cfg(not(target_os = "solana"))] -impl FeeCiphertext { - pub fn new( - amount: u64, - destination_pubkey: &ElGamalPubkey, - withdraw_withheld_authority_pubkey: &ElGamalPubkey, - ) -> (Self, PedersenOpening) { - let opening = PedersenOpening::new_rand(); - let grouped_ciphertext = GroupedElGamal::<2>::encrypt_with( - [destination_pubkey, withdraw_withheld_authority_pubkey], - amount, - &opening, - ); - - (Self(grouped_ciphertext), opening) - } - - pub fn get_commitment(&self) -> &PedersenCommitment { - &self.0.commitment - } - - pub fn get_destination_handle(&self) -> &DecryptHandle { - // `FeeEncryption` is a wrapper for `GroupedElGamalCiphertext<2>`, which holds - // exactly two decryption handles. - self.0.handles.first().unwrap() - } - - pub fn get_withdraw_withheld_authority_handle(&self) -> &DecryptHandle { - // `FeeEncryption` is a wrapper for `GroupedElGamalCiphertext<2>`, which holds - // exactly two decryption handles. - self.0.handles.get(1).unwrap() - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct BurnAmountCiphertext(pub(crate) GroupedElGamalCiphertext<3>); - -impl BurnAmountCiphertext { - pub fn new( - amount: u64, - source_pubkey: &ElGamalPubkey, - auditor_pubkey: &ElGamalPubkey, - supply_pubkey: &ElGamalPubkey, - ) -> (Self, PedersenOpening) { - let opening = PedersenOpening::new_rand(); - let grouped_ciphertext = GroupedElGamal::<3>::encrypt_with( - [source_pubkey, auditor_pubkey, supply_pubkey], - amount, - &opening, - ); - - (Self(grouped_ciphertext), opening) - } - - pub fn get_commitment(&self) -> &PedersenCommitment { - &self.0.commitment - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(C)] -pub struct MintAmountCiphertext(pub(crate) GroupedElGamalCiphertext<3>); - -impl MintAmountCiphertext { - pub fn new( - amount: u64, - source_pubkey: &ElGamalPubkey, - auditor_pubkey: &ElGamalPubkey, - supply_pubkey: &ElGamalPubkey, - ) -> (Self, PedersenOpening) { - let opening = PedersenOpening::new_rand(); - let grouped_ciphertext = GroupedElGamal::<3>::encrypt_with( - [source_pubkey, auditor_pubkey, supply_pubkey], - amount, - &opening, - ); - - (Self(grouped_ciphertext), opening) - } - - pub fn get_commitment(&self) -> &PedersenCommitment { - &self.0.commitment - } -} diff --git a/token/confidential-transfer/proof-generation/src/errors.rs b/token/confidential-transfer/proof-generation/src/errors.rs deleted file mode 100644 index 9f27f6ebb42..00000000000 --- a/token/confidential-transfer/proof-generation/src/errors.rs +++ /dev/null @@ -1,15 +0,0 @@ -use {solana_zk_sdk::zk_elgamal_proof_program::errors::ProofGenerationError, thiserror::Error}; - -#[derive(Error, Clone, Debug, Eq, PartialEq)] -pub enum TokenProofGenerationError { - #[error("inner proof generation failed")] - ProofGeneration(#[from] ProofGenerationError), - #[error("not enough funds in account")] - NotEnoughFunds, - #[error("illegal amount bit length")] - IllegalAmountBitLength, - #[error("fee calculation failed")] - FeeCalculation, - #[error("ciphertext extraction failed")] - CiphertextExtraction, -} diff --git a/token/confidential-transfer/proof-generation/src/lib.rs b/token/confidential-transfer/proof-generation/src/lib.rs deleted file mode 100644 index 02155735183..00000000000 --- a/token/confidential-transfer/proof-generation/src/lib.rs +++ /dev/null @@ -1,108 +0,0 @@ -use { - curve25519_dalek::scalar::Scalar, - solana_zk_sdk::{ - encryption::{ - elgamal::ElGamalCiphertext, - pedersen::{PedersenCommitment, PedersenOpening}, - pod::elgamal::PodElGamalCiphertext, - }, - zk_elgamal_proof_program::proof_data::BatchedGroupedCiphertext3HandlesValidityProofData, - }, -}; - -pub mod burn; -pub mod encryption; -pub mod errors; -pub mod mint; -pub mod transfer; -pub mod transfer_with_fee; -pub mod withdraw; - -/// The low bit length of the encrypted transfer amount -pub const TRANSFER_AMOUNT_LO_BITS: usize = 16; -/// The high bit length of the encrypted transfer amount -pub const TRANSFER_AMOUNT_HI_BITS: usize = 32; -/// The bit length of the encrypted remaining balance in a token account -pub const REMAINING_BALANCE_BIT_LENGTH: usize = 64; - -/// Takes in a 64-bit number `amount` and a bit length `bit_length`. It returns: -/// - the `bit_length` low bits of `amount` interpreted as `u64` -/// - the `(64 - bit_length)` high bits of `amount` interpreted as `u64` -pub fn try_split_u64(amount: u64, bit_length: usize) -> Option<(u64, u64)> { - match bit_length { - 0 => Some((0, amount)), - 1..=63 => { - let bit_length_complement = u64::BITS.checked_sub(bit_length as u32).unwrap(); - // shifts are safe as long as `bit_length` and `bit_length_complement` < 64 - let lo = amount - .checked_shl(bit_length_complement)? - .checked_shr(bit_length_complement)?; - let hi = amount.checked_shr(bit_length as u32)?; - Some((lo, hi)) - } - 64 => Some((amount, 0)), - _ => None, - } -} - -/// Combine two numbers that are interpreted as the low and high bits of a -/// target number. The `bit_length` parameter specifies the number of bits that -/// `amount_hi` is to be shifted by. -pub fn try_combine_lo_hi_u64(amount_lo: u64, amount_hi: u64, bit_length: usize) -> Option { - match bit_length { - 0 => Some(amount_hi), - 1..=63 => { - // shifts are safe as long as `bit_length` < 64 - amount_hi - .checked_shl(bit_length as u32)? - .checked_add(amount_hi) - } - 64 => Some(amount_lo), - _ => None, - } -} - -#[allow(clippy::arithmetic_side_effects)] -pub fn try_combine_lo_hi_ciphertexts( - ciphertext_lo: &ElGamalCiphertext, - ciphertext_hi: &ElGamalCiphertext, - bit_length: usize, -) -> Option { - let two_power = 1_u64.checked_shl(bit_length as u32)?; - Some(ciphertext_lo + ciphertext_hi * Scalar::from(two_power)) -} - -#[allow(clippy::arithmetic_side_effects)] -pub fn try_combine_lo_hi_commitments( - comm_lo: &PedersenCommitment, - comm_hi: &PedersenCommitment, - bit_length: usize, -) -> Option { - let two_power = 1_u64.checked_shl(bit_length as u32)?; - Some(comm_lo + comm_hi * Scalar::from(two_power)) -} - -#[allow(clippy::arithmetic_side_effects)] -pub fn try_combine_lo_hi_openings( - opening_lo: &PedersenOpening, - opening_hi: &PedersenOpening, - bit_length: usize, -) -> Option { - let two_power = 1_u64.checked_shl(bit_length as u32)?; - Some(opening_lo + opening_hi * Scalar::from(two_power)) -} - -/// A type that wraps a ciphertext validity proof along with two `lo` and `hi` -/// ciphertexts. -/// -/// Ciphertext validity proof data contains grouped ElGamal ciphertexts (`lo` -/// and `hi`) and a proof containing the validity of these ciphertexts. Token -/// client-side logic often requires a function to extract specific forms of -/// the grouped ElGamal ciphertexts. This type is a convenience type that -/// contains the proof data and the extracted ciphertexts. -#[derive(Clone, Copy)] -pub struct CiphertextValidityProofWithAuditorCiphertext { - pub proof_data: BatchedGroupedCiphertext3HandlesValidityProofData, - pub ciphertext_lo: PodElGamalCiphertext, - pub ciphertext_hi: PodElGamalCiphertext, -} diff --git a/token/confidential-transfer/proof-generation/src/mint.rs b/token/confidential-transfer/proof-generation/src/mint.rs deleted file mode 100644 index 8e03e6d6c1a..00000000000 --- a/token/confidential-transfer/proof-generation/src/mint.rs +++ /dev/null @@ -1,162 +0,0 @@ -use { - crate::{ - encryption::MintAmountCiphertext, errors::TokenProofGenerationError, - try_combine_lo_hi_ciphertexts, try_split_u64, CiphertextValidityProofWithAuditorCiphertext, - }, - solana_zk_sdk::{ - encryption::{ - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, - pedersen::Pedersen, - }, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, - CiphertextCommitmentEqualityProofData, ZkProofData, - }, - }, -}; - -const NEW_SUPPLY_BIT_LENGTH: usize = 64; -const MINT_AMOUNT_LO_BIT_LENGTH: usize = 16; -const MINT_AMOUNT_HI_BIT_LENGTH: usize = 32; -/// The padding bit length in range proofs to make the bit-length power-of-2 -const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16; - -/// The proof data required for a confidential mint instruction -pub struct MintProofData { - pub equality_proof_data: CiphertextCommitmentEqualityProofData, - pub ciphertext_validity_proof_data_with_ciphertext: - CiphertextValidityProofWithAuditorCiphertext, - pub range_proof_data: BatchedRangeProofU128Data, -} - -pub fn mint_split_proof_data( - current_supply_ciphertext: &ElGamalCiphertext, - mint_amount: u64, - current_supply: u64, - supply_elgamal_keypair: &ElGamalKeypair, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, -) -> Result { - let default_auditor_pubkey = ElGamalPubkey::default(); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey); - - // split the mint amount into low and high bits - let (mint_amount_lo, mint_amount_hi) = try_split_u64(mint_amount, MINT_AMOUNT_LO_BIT_LENGTH) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // encrypt the mint amount under the destination and auditor's ElGamal public - // keys - let (mint_amount_grouped_ciphertext_lo, mint_amount_opening_lo) = MintAmountCiphertext::new( - mint_amount_lo, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - supply_elgamal_keypair.pubkey(), - ); - - let (mint_amount_grouped_ciphertext_hi, mint_amount_opening_hi) = MintAmountCiphertext::new( - mint_amount_hi, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - supply_elgamal_keypair.pubkey(), - ); - - // compute the new supply ciphertext - let mint_amount_ciphertext_supply_lo = mint_amount_grouped_ciphertext_lo - .0 - .to_elgamal_ciphertext(2) - .unwrap(); - let mint_amount_ciphertext_supply_hi = mint_amount_grouped_ciphertext_hi - .0 - .to_elgamal_ciphertext(2) - .unwrap(); - - #[allow(clippy::arithmetic_side_effects)] - let new_supply_ciphertext = current_supply_ciphertext - + try_combine_lo_hi_ciphertexts( - &mint_amount_ciphertext_supply_lo, - &mint_amount_ciphertext_supply_hi, - MINT_AMOUNT_LO_BIT_LENGTH, - ) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // compute the new supply - let new_supply = current_supply - .checked_add(mint_amount) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - let (new_supply_commitment, new_supply_opening) = Pedersen::new(new_supply); - - // generate equality proof data - let equality_proof_data = CiphertextCommitmentEqualityProofData::new( - supply_elgamal_keypair, - &new_supply_ciphertext, - &new_supply_commitment, - &new_supply_opening, - new_supply, - ) - .map_err(TokenProofGenerationError::from)?; - - // generate ciphertext validity proof data - let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new( - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - supply_elgamal_keypair.pubkey(), - &mint_amount_grouped_ciphertext_lo.0, - &mint_amount_grouped_ciphertext_hi.0, - mint_amount_lo, - mint_amount_hi, - &mint_amount_opening_lo, - &mint_amount_opening_hi, - ) - .map_err(TokenProofGenerationError::from)?; - - let mint_amount_auditor_ciphertext_lo = ciphertext_validity_proof_data - .context_data() - .grouped_ciphertext_lo - .try_extract_ciphertext(2) - .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?; - - let mint_amount_auditor_ciphertext_hi = ciphertext_validity_proof_data - .context_data() - .grouped_ciphertext_hi - .try_extract_ciphertext(2) - .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?; - - let ciphertext_validity_proof_data_with_ciphertext = - CiphertextValidityProofWithAuditorCiphertext { - proof_data: ciphertext_validity_proof_data, - ciphertext_lo: mint_amount_auditor_ciphertext_lo, - ciphertext_hi: mint_amount_auditor_ciphertext_hi, - }; - - // generate range proof data - let (padding_commitment, padding_opening) = Pedersen::new(0_u64); - let range_proof_data = BatchedRangeProofU128Data::new( - vec![ - &new_supply_commitment, - mint_amount_grouped_ciphertext_lo.get_commitment(), - mint_amount_grouped_ciphertext_hi.get_commitment(), - &padding_commitment, - ], - vec![new_supply, mint_amount_lo, mint_amount_hi, 0], - vec![ - NEW_SUPPLY_BIT_LENGTH, - MINT_AMOUNT_LO_BIT_LENGTH, - MINT_AMOUNT_HI_BIT_LENGTH, - RANGE_PROOF_PADDING_BIT_LENGTH, - ], - vec![ - &new_supply_opening, - &mint_amount_opening_lo, - &mint_amount_opening_hi, - &padding_opening, - ], - ) - .map_err(TokenProofGenerationError::from)?; - - Ok(MintProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - }) -} diff --git a/token/confidential-transfer/proof-generation/src/transfer.rs b/token/confidential-transfer/proof-generation/src/transfer.rs deleted file mode 100644 index 4d59cc95f4d..00000000000 --- a/token/confidential-transfer/proof-generation/src/transfer.rs +++ /dev/null @@ -1,178 +0,0 @@ -use { - crate::{ - encryption::TransferAmountCiphertext, errors::TokenProofGenerationError, - try_combine_lo_hi_ciphertexts, try_split_u64, CiphertextValidityProofWithAuditorCiphertext, - REMAINING_BALANCE_BIT_LENGTH, TRANSFER_AMOUNT_HI_BITS, TRANSFER_AMOUNT_LO_BITS, - }, - solana_zk_sdk::{ - encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, - pedersen::Pedersen, - }, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, - CiphertextCommitmentEqualityProofData, ZkProofData, - }, - }, -}; - -/// The padding bit length in range proofs that are used for a confidential -/// token transfer -const RANGE_PROOF_PADDING_BIT_LENGTH: usize = 16; - -/// The proof data required for a confidential transfer instruction when the -/// mint is not extended for fees -pub struct TransferProofData { - pub equality_proof_data: CiphertextCommitmentEqualityProofData, - pub ciphertext_validity_proof_data_with_ciphertext: - CiphertextValidityProofWithAuditorCiphertext, - pub range_proof_data: BatchedRangeProofU128Data, -} - -pub fn transfer_split_proof_data( - current_available_balance: &ElGamalCiphertext, - current_decryptable_available_balance: &AeCiphertext, - transfer_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, -) -> Result { - let default_auditor_pubkey = ElGamalPubkey::default(); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey); - - // Split the transfer amount into the low and high bit components - let (transfer_amount_lo, transfer_amount_hi) = - try_split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // Encrypt the `lo` and `hi` transfer amounts - let (transfer_amount_grouped_ciphertext_lo, transfer_amount_opening_lo) = - TransferAmountCiphertext::new( - transfer_amount_lo, - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ); - - let (transfer_amount_grouped_ciphertext_hi, transfer_amount_opening_hi) = - TransferAmountCiphertext::new( - transfer_amount_hi, - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ); - - // Decrypt the current available balance at the source - let current_decrypted_available_balance = current_decryptable_available_balance - .decrypt(aes_key) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // Compute the remaining balance at the source - let new_decrypted_available_balance = current_decrypted_available_balance - .checked_sub(transfer_amount) - .ok_or(TokenProofGenerationError::NotEnoughFunds)?; - - // Create a new Pedersen commitment for the remaining balance at the source - let (new_available_balance_commitment, new_source_opening) = - Pedersen::new(new_decrypted_available_balance); - - // Compute the remaining balance at the source as ElGamal ciphertexts - let transfer_amount_source_ciphertext_lo = transfer_amount_grouped_ciphertext_lo - .0 - .to_elgamal_ciphertext(0) - .unwrap(); - let transfer_amount_source_ciphertext_hi = transfer_amount_grouped_ciphertext_hi - .0 - .to_elgamal_ciphertext(0) - .unwrap(); - - #[allow(clippy::arithmetic_side_effects)] - let new_available_balance_ciphertext = current_available_balance - - try_combine_lo_hi_ciphertexts( - &transfer_amount_source_ciphertext_lo, - &transfer_amount_source_ciphertext_hi, - TRANSFER_AMOUNT_LO_BITS, - ) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // generate equality proof data - let equality_proof_data = CiphertextCommitmentEqualityProofData::new( - source_elgamal_keypair, - &new_available_balance_ciphertext, - &new_available_balance_commitment, - &new_source_opening, - new_decrypted_available_balance, - ) - .map_err(TokenProofGenerationError::from)?; - - // generate ciphertext validity data - let ciphertext_validity_proof_data = BatchedGroupedCiphertext3HandlesValidityProofData::new( - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - &transfer_amount_grouped_ciphertext_lo.0, - &transfer_amount_grouped_ciphertext_hi.0, - transfer_amount_lo, - transfer_amount_hi, - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - ) - .map_err(TokenProofGenerationError::from)?; - - let transfer_amount_auditor_ciphertext_lo = ciphertext_validity_proof_data - .context_data() - .grouped_ciphertext_lo - .try_extract_ciphertext(2) - .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?; - - let transfer_amount_auditor_ciphertext_hi = ciphertext_validity_proof_data - .context_data() - .grouped_ciphertext_hi - .try_extract_ciphertext(2) - .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?; - - let ciphertext_validity_proof_data_with_ciphertext = - CiphertextValidityProofWithAuditorCiphertext { - proof_data: ciphertext_validity_proof_data, - ciphertext_lo: transfer_amount_auditor_ciphertext_lo, - ciphertext_hi: transfer_amount_auditor_ciphertext_hi, - }; - - // generate range proof data - let (padding_commitment, padding_opening) = Pedersen::new(0_u64); - let range_proof_data = BatchedRangeProofU128Data::new( - vec![ - &new_available_balance_commitment, - transfer_amount_grouped_ciphertext_lo.get_commitment(), - transfer_amount_grouped_ciphertext_hi.get_commitment(), - &padding_commitment, - ], - vec![ - new_decrypted_available_balance, - transfer_amount_lo, - transfer_amount_hi, - 0, - ], - vec![ - REMAINING_BALANCE_BIT_LENGTH, - TRANSFER_AMOUNT_LO_BITS, - TRANSFER_AMOUNT_HI_BITS, - RANGE_PROOF_PADDING_BIT_LENGTH, - ], - vec![ - &new_source_opening, - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - &padding_opening, - ], - ) - .map_err(TokenProofGenerationError::from)?; - - Ok(TransferProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - }) -} diff --git a/token/confidential-transfer/proof-generation/src/transfer_with_fee.rs b/token/confidential-transfer/proof-generation/src/transfer_with_fee.rs deleted file mode 100644 index 5e5939f6184..00000000000 --- a/token/confidential-transfer/proof-generation/src/transfer_with_fee.rs +++ /dev/null @@ -1,361 +0,0 @@ -use { - crate::{ - encryption::{FeeCiphertext, TransferAmountCiphertext}, - errors::TokenProofGenerationError, - try_combine_lo_hi_ciphertexts, try_combine_lo_hi_commitments, try_combine_lo_hi_openings, - try_split_u64, CiphertextValidityProofWithAuditorCiphertext, TRANSFER_AMOUNT_HI_BITS, - TRANSFER_AMOUNT_LO_BITS, - }, - curve25519_dalek::scalar::Scalar, - solana_zk_sdk::{ - encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, - grouped_elgamal::GroupedElGamal, - pedersen::{Pedersen, PedersenCommitment, PedersenOpening}, - }, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext2HandlesValidityProofData, - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU256Data, - CiphertextCommitmentEqualityProofData, PercentageWithCapProofData, ZkProofData, - }, - }, -}; - -const MAX_FEE_BASIS_POINTS: u64 = 10_000; -const ONE_IN_BASIS_POINTS: u128 = MAX_FEE_BASIS_POINTS as u128; - -const FEE_AMOUNT_LO_BITS: usize = 16; -const FEE_AMOUNT_HI_BITS: usize = 32; - -const REMAINING_BALANCE_BIT_LENGTH: usize = 64; -const DELTA_BIT_LENGTH: usize = 48; - -/// The proof data required for a confidential transfer instruction when the -/// mint is extended for fees -pub struct TransferWithFeeProofData { - pub equality_proof_data: CiphertextCommitmentEqualityProofData, - pub transfer_amount_ciphertext_validity_proof_data_with_ciphertext: - CiphertextValidityProofWithAuditorCiphertext, - pub percentage_with_cap_proof_data: PercentageWithCapProofData, - pub fee_ciphertext_validity_proof_data: BatchedGroupedCiphertext2HandlesValidityProofData, - pub range_proof_data: BatchedRangeProofU256Data, -} - -#[allow(clippy::too_many_arguments)] -pub fn transfer_with_fee_split_proof_data( - current_available_balance: &ElGamalCiphertext, - current_decryptable_available_balance: &AeCiphertext, - transfer_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey, - fee_rate_basis_points: u16, - maximum_fee: u64, -) -> Result { - let default_auditor_pubkey = ElGamalPubkey::default(); - let auditor_elgamal_pubkey = auditor_elgamal_pubkey.unwrap_or(&default_auditor_pubkey); - - // Split the transfer amount into the low and high bit components - let (transfer_amount_lo, transfer_amount_hi) = - try_split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // Encrypt the `lo` and `hi` transfer amounts - let (transfer_amount_grouped_ciphertext_lo, transfer_amount_opening_lo) = - TransferAmountCiphertext::new( - transfer_amount_lo, - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ); - - let (transfer_amount_grouped_ciphertext_hi, transfer_amount_opening_hi) = - TransferAmountCiphertext::new( - transfer_amount_hi, - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ); - - // Decrypt the current available balance at the source - let current_decrypted_available_balance = current_decryptable_available_balance - .decrypt(aes_key) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // Compute the remaining balance at the source - let new_decrypted_available_balance = current_decrypted_available_balance - .checked_sub(transfer_amount) - .ok_or(TokenProofGenerationError::NotEnoughFunds)?; - - // Create a new Pedersen commitment for the remaining balance at the source - let (new_available_balance_commitment, new_source_opening) = - Pedersen::new(new_decrypted_available_balance); - - // Compute the remaining balance at the source as ElGamal ciphertexts - let transfer_amount_source_ciphertext_lo = transfer_amount_grouped_ciphertext_lo - .0 - .to_elgamal_ciphertext(0) - .unwrap(); - - let transfer_amount_source_ciphertext_hi = transfer_amount_grouped_ciphertext_hi - .0 - .to_elgamal_ciphertext(0) - .unwrap(); - - #[allow(clippy::arithmetic_side_effects)] - let new_available_balance_ciphertext = current_available_balance - - try_combine_lo_hi_ciphertexts( - &transfer_amount_source_ciphertext_lo, - &transfer_amount_source_ciphertext_hi, - TRANSFER_AMOUNT_LO_BITS, - ) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // generate equality proof data - let equality_proof_data = CiphertextCommitmentEqualityProofData::new( - source_elgamal_keypair, - &new_available_balance_ciphertext, - &new_available_balance_commitment, - &new_source_opening, - new_decrypted_available_balance, - ) - .map_err(TokenProofGenerationError::from)?; - - // generate ciphertext validity data - let transfer_amount_ciphertext_validity_proof_data = - BatchedGroupedCiphertext3HandlesValidityProofData::new( - source_elgamal_keypair.pubkey(), - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - &transfer_amount_grouped_ciphertext_lo.0, - &transfer_amount_grouped_ciphertext_hi.0, - transfer_amount_lo, - transfer_amount_hi, - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - ) - .map_err(TokenProofGenerationError::from)?; - - let transfer_amount_auditor_ciphertext_lo = transfer_amount_ciphertext_validity_proof_data - .context_data() - .grouped_ciphertext_lo - .try_extract_ciphertext(2) - .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?; - - let transfer_amount_auditor_ciphertext_hi = transfer_amount_ciphertext_validity_proof_data - .context_data() - .grouped_ciphertext_hi - .try_extract_ciphertext(2) - .map_err(|_| TokenProofGenerationError::CiphertextExtraction)?; - - let transfer_amount_ciphertext_validity_proof_data_with_ciphertext = - CiphertextValidityProofWithAuditorCiphertext { - proof_data: transfer_amount_ciphertext_validity_proof_data, - ciphertext_lo: transfer_amount_auditor_ciphertext_lo, - ciphertext_hi: transfer_amount_auditor_ciphertext_hi, - }; - - // calculate fee - let transfer_fee_basis_points = fee_rate_basis_points; - let transfer_fee_maximum_fee = maximum_fee; - let (raw_fee_amount, delta_fee) = calculate_fee(transfer_amount, transfer_fee_basis_points) - .ok_or(TokenProofGenerationError::FeeCalculation)?; - - // if raw fee is greater than the maximum fee, then use the maximum fee for the - // fee amount - let fee_amount = std::cmp::min(transfer_fee_maximum_fee, raw_fee_amount); - - // split and encrypt fee - let (fee_amount_lo, fee_amount_hi) = try_split_u64(fee_amount, FEE_AMOUNT_LO_BITS) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - let (fee_ciphertext_lo, fee_opening_lo) = FeeCiphertext::new( - fee_amount_lo, - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - ); - let (fee_ciphertext_hi, fee_opening_hi) = FeeCiphertext::new( - fee_amount_hi, - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - ); - - // create combined commitments and openings to be used to generate proofs - let combined_transfer_amount_commitment = try_combine_lo_hi_commitments( - transfer_amount_grouped_ciphertext_lo.get_commitment(), - transfer_amount_grouped_ciphertext_hi.get_commitment(), - TRANSFER_AMOUNT_LO_BITS, - ) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - let combined_transfer_amount_opening = try_combine_lo_hi_openings( - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - TRANSFER_AMOUNT_LO_BITS, - ) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - let combined_fee_commitment = try_combine_lo_hi_commitments( - fee_ciphertext_lo.get_commitment(), - fee_ciphertext_hi.get_commitment(), - FEE_AMOUNT_LO_BITS, - ) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - let combined_fee_opening = - try_combine_lo_hi_openings(&fee_opening_lo, &fee_opening_hi, FEE_AMOUNT_LO_BITS) - .ok_or(TokenProofGenerationError::IllegalAmountBitLength)?; - - // compute claimed and real delta commitment - let (claimed_commitment, claimed_opening) = Pedersen::new(delta_fee); - let (delta_commitment, delta_opening) = compute_delta_commitment_and_opening( - ( - &combined_transfer_amount_commitment, - &combined_transfer_amount_opening, - ), - (&combined_fee_commitment, &combined_fee_opening), - transfer_fee_basis_points, - ); - - // generate fee sigma proof - let percentage_with_cap_proof_data = PercentageWithCapProofData::new( - &combined_fee_commitment, - &combined_fee_opening, - fee_amount, - &delta_commitment, - &delta_opening, - delta_fee, - &claimed_commitment, - &claimed_opening, - transfer_fee_maximum_fee, - ) - .map_err(TokenProofGenerationError::from)?; - - // encrypt the fee amount under the destination and withdraw withheld authority - // ElGamal public key - let fee_destination_withdraw_withheld_authority_ciphertext_lo = GroupedElGamal::encrypt_with( - [ - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - ], - fee_amount_lo, - &fee_opening_lo, - ); - let fee_destination_withdraw_withheld_authority_ciphertext_hi = GroupedElGamal::encrypt_with( - [ - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - ], - fee_amount_hi, - &fee_opening_hi, - ); - - // generate fee ciphertext validity data - let fee_ciphertext_validity_proof_data = - BatchedGroupedCiphertext2HandlesValidityProofData::new( - destination_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - &fee_destination_withdraw_withheld_authority_ciphertext_lo, - &fee_destination_withdraw_withheld_authority_ciphertext_hi, - fee_amount_lo, - fee_amount_hi, - &fee_opening_lo, - &fee_opening_hi, - ) - .map_err(TokenProofGenerationError::from)?; - - // generate range proof data - let delta_fee_complement = MAX_FEE_BASIS_POINTS - .checked_sub(delta_fee) - .ok_or(TokenProofGenerationError::FeeCalculation)?; - - let max_fee_basis_points_commitment = - Pedersen::with(MAX_FEE_BASIS_POINTS, &PedersenOpening::default()); - #[allow(clippy::arithmetic_side_effects)] - let claimed_complement_commitment = max_fee_basis_points_commitment - claimed_commitment; - #[allow(clippy::arithmetic_side_effects)] - let claimed_complement_opening = PedersenOpening::default() - &claimed_opening; - - let range_proof_data = BatchedRangeProofU256Data::new( - vec![ - &new_available_balance_commitment, - transfer_amount_grouped_ciphertext_lo.get_commitment(), - transfer_amount_grouped_ciphertext_hi.get_commitment(), - &claimed_commitment, - &claimed_complement_commitment, - fee_ciphertext_lo.get_commitment(), - fee_ciphertext_hi.get_commitment(), - ], - vec![ - new_decrypted_available_balance, - transfer_amount_lo, - transfer_amount_hi, - delta_fee, - delta_fee_complement, - fee_amount_lo, - fee_amount_hi, - ], - vec![ - REMAINING_BALANCE_BIT_LENGTH, - TRANSFER_AMOUNT_LO_BITS, - TRANSFER_AMOUNT_HI_BITS, - DELTA_BIT_LENGTH, - DELTA_BIT_LENGTH, - FEE_AMOUNT_LO_BITS, - FEE_AMOUNT_HI_BITS, - ], - vec![ - &new_source_opening, - &transfer_amount_opening_lo, - &transfer_amount_opening_hi, - &claimed_opening, - &claimed_complement_opening, - &fee_opening_lo, - &fee_opening_hi, - ], - ) - .map_err(TokenProofGenerationError::from)?; - - Ok(TransferWithFeeProofData { - equality_proof_data, - transfer_amount_ciphertext_validity_proof_data_with_ciphertext, - percentage_with_cap_proof_data, - fee_ciphertext_validity_proof_data, - range_proof_data, - }) -} - -fn calculate_fee(transfer_amount: u64, fee_rate_basis_points: u16) -> Option<(u64, u64)> { - let numerator = (transfer_amount as u128).checked_mul(fee_rate_basis_points as u128)?; - - // Warning: Division may involve CPU opcodes that have variable execution times. - // This non-constant-time execution of the fee calculation can theoretically - // reveal information about the transfer amount. For transfers that involve - // extremely sensitive data, additional care should be put into how the fees - // are calculated. - let fee = numerator - .checked_add(ONE_IN_BASIS_POINTS)? - .checked_sub(1)? - .checked_div(ONE_IN_BASIS_POINTS)?; - - let delta_fee = fee - .checked_mul(ONE_IN_BASIS_POINTS)? - .checked_sub(numerator)?; - - Some((fee as u64, delta_fee as u64)) -} - -#[allow(clippy::arithmetic_side_effects)] -fn compute_delta_commitment_and_opening( - (combined_commitment, combined_opening): (&PedersenCommitment, &PedersenOpening), - (combined_fee_commitment, combined_fee_opening): (&PedersenCommitment, &PedersenOpening), - fee_rate_basis_points: u16, -) -> (PedersenCommitment, PedersenOpening) { - let fee_rate_scalar = Scalar::from(fee_rate_basis_points); - let delta_commitment = combined_fee_commitment * Scalar::from(MAX_FEE_BASIS_POINTS) - - combined_commitment * fee_rate_scalar; - let delta_opening = combined_fee_opening * Scalar::from(MAX_FEE_BASIS_POINTS) - - combined_opening * fee_rate_scalar; - - (delta_commitment, delta_opening) -} diff --git a/token/confidential-transfer/proof-generation/src/withdraw.rs b/token/confidential-transfer/proof-generation/src/withdraw.rs deleted file mode 100644 index ece1fedc58e..00000000000 --- a/token/confidential-transfer/proof-generation/src/withdraw.rs +++ /dev/null @@ -1,63 +0,0 @@ -use { - crate::errors::TokenProofGenerationError, - solana_zk_sdk::{ - encryption::{ - elgamal::{ElGamal, ElGamalCiphertext, ElGamalKeypair}, - pedersen::Pedersen, - }, - zk_elgamal_proof_program::proof_data::{ - BatchedRangeProofU64Data, CiphertextCommitmentEqualityProofData, - }, - }, -}; - -const REMAINING_BALANCE_BIT_LENGTH: usize = 64; - -/// Proof data required for a withdraw instruction -pub struct WithdrawProofData { - pub equality_proof_data: CiphertextCommitmentEqualityProofData, - pub range_proof_data: BatchedRangeProofU64Data, -} - -pub fn withdraw_proof_data( - current_available_balance: &ElGamalCiphertext, - current_balance: u64, - withdraw_amount: u64, - elgamal_keypair: &ElGamalKeypair, -) -> Result { - // Calculate the remaining balance after withdraw - let remaining_balance = current_balance - .checked_sub(withdraw_amount) - .ok_or(TokenProofGenerationError::NotEnoughFunds)?; - - // Generate a Pedersen commitment for the remaining balance - let (remaining_balance_commitment, remaining_balance_opening) = - Pedersen::new(remaining_balance); - - // Compute the remaining balance ciphertext - #[allow(clippy::arithmetic_side_effects)] - let remaining_balance_ciphertext = current_available_balance - ElGamal::encode(withdraw_amount); - - // Generate proof data - let equality_proof_data = CiphertextCommitmentEqualityProofData::new( - elgamal_keypair, - &remaining_balance_ciphertext, - &remaining_balance_commitment, - &remaining_balance_opening, - remaining_balance, - ) - .map_err(TokenProofGenerationError::from)?; - - let range_proof_data = BatchedRangeProofU64Data::new( - vec![&remaining_balance_commitment], - vec![remaining_balance], - vec![REMAINING_BALANCE_BIT_LENGTH], - vec![&remaining_balance_opening], - ) - .map_err(TokenProofGenerationError::from)?; - - Ok(WithdrawProofData { - equality_proof_data, - range_proof_data, - }) -} diff --git a/token/confidential-transfer/proof-tests/Cargo.toml b/token/confidential-transfer/proof-tests/Cargo.toml deleted file mode 100644 index fb4caef4103..00000000000 --- a/token/confidential-transfer/proof-tests/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "spl-token-confidential-transfer-proof-test" -version = "0.0.1" -description = "Solana Program Library Confidential Transfer Proof Test" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dev-dependencies] -curve25519-dalek = "4.1.3" -solana-zk-sdk = "2.1.0" -thiserror = "2.0.9" -spl-token-confidential-transfer-proof-extraction = { version = "0.2.0", path = "../proof-extraction" } -spl-token-confidential-transfer-proof-generation = { version = "0.2.0", path = "../proof-generation" } diff --git a/token/confidential-transfer/proof-tests/tests/proof_test.rs b/token/confidential-transfer/proof-tests/tests/proof_test.rs deleted file mode 100644 index b9444cd40e4..00000000000 --- a/token/confidential-transfer/proof-tests/tests/proof_test.rs +++ /dev/null @@ -1,311 +0,0 @@ -use { - solana_zk_sdk::{ - encryption::{auth_encryption::AeKey, elgamal::ElGamalKeypair}, - zk_elgamal_proof_program::proof_data::ZkProofData, - }, - spl_token_confidential_transfer_proof_extraction::{ - burn::BurnProofContext, mint::MintProofContext, transfer::TransferProofContext, - transfer_with_fee::TransferWithFeeProofContext, withdraw::WithdrawProofContext, - }, - spl_token_confidential_transfer_proof_generation::{ - burn::{burn_split_proof_data, BurnProofData}, - mint::{mint_split_proof_data, MintProofData}, - transfer::{transfer_split_proof_data, TransferProofData}, - transfer_with_fee::{transfer_with_fee_split_proof_data, TransferWithFeeProofData}, - withdraw::{withdraw_proof_data, WithdrawProofData}, - }, -}; - -#[test] -fn test_transfer_correctness() { - test_transfer_proof_validity(0, 0); - test_transfer_proof_validity(1, 0); - test_transfer_proof_validity(1, 1); - test_transfer_proof_validity(65535, 65535); // 2^16 - 1 - test_transfer_proof_validity(65536, 65536); // 2^16 - test_transfer_proof_validity(281474976710655, 281474976710655); // 2^48 - 1 -} - -fn test_transfer_proof_validity(spendable_balance: u64, transfer_amount: u64) { - let source_keypair = ElGamalKeypair::new_rand(); - - let aes_key = AeKey::new_rand(); - - let destination_keypair = ElGamalKeypair::new_rand(); - let destination_pubkey = destination_keypair.pubkey(); - - let auditor_keypair = ElGamalKeypair::new_rand(); - let auditor_pubkey = auditor_keypair.pubkey(); - - let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance); - let decryptable_balance = aes_key.encrypt(spendable_balance); - - let TransferProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - } = transfer_split_proof_data( - &spendable_ciphertext, - &decryptable_balance, - transfer_amount, - &source_keypair, - &aes_key, - destination_pubkey, - Some(auditor_pubkey), - ) - .unwrap(); - - equality_proof_data.verify_proof().unwrap(); - ciphertext_validity_proof_data_with_ciphertext - .proof_data - .verify_proof() - .unwrap(); - range_proof_data.verify_proof().unwrap(); - - TransferProofContext::verify_and_extract( - equality_proof_data.context_data(), - ciphertext_validity_proof_data_with_ciphertext - .proof_data - .context_data(), - range_proof_data.context_data(), - ) - .unwrap(); -} - -#[test] -fn test_transfer_with_fee_correctness() { - test_transfer_with_fee_proof_validity(0, 0, 0, 0); - test_transfer_with_fee_proof_validity(0, 0, 0, 1); - test_transfer_with_fee_proof_validity(0, 0, 1, 0); - test_transfer_with_fee_proof_validity(0, 0, 1, 1); - test_transfer_with_fee_proof_validity(1, 0, 0, 0); - test_transfer_with_fee_proof_validity(1, 1, 0, 0); - - test_transfer_with_fee_proof_validity(100, 100, 5, 10); - test_transfer_with_fee_proof_validity(100, 100, 5, 1); - - test_transfer_with_fee_proof_validity(65535, 65535, 5, 10); - test_transfer_with_fee_proof_validity(65535, 65535, 5, 1); - - test_transfer_with_fee_proof_validity(65536, 65536, 5, 10); - test_transfer_with_fee_proof_validity(65536, 65536, 5, 1); - - test_transfer_with_fee_proof_validity(281474976710655, 281474976710655, 5, 10); // 2^48 - 1 - test_transfer_with_fee_proof_validity(281474976710655, 281474976710655, 5, 1); -} - -fn test_transfer_with_fee_proof_validity( - spendable_balance: u64, - transfer_amount: u64, - fee_rate_basis_points: u16, - maximum_fee: u64, -) { - let source_keypair = ElGamalKeypair::new_rand(); - let aes_key = AeKey::new_rand(); - - let destination_keypair = ElGamalKeypair::new_rand(); - let destination_pubkey = destination_keypair.pubkey(); - - let auditor_keypair = ElGamalKeypair::new_rand(); - let auditor_pubkey = auditor_keypair.pubkey(); - - let withdraw_withheld_authority_keyupair = ElGamalKeypair::new_rand(); - let withdraw_withheld_authority_pubkey = withdraw_withheld_authority_keyupair.pubkey(); - - let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance); - let decryptable_balance = aes_key.encrypt(spendable_balance); - - let TransferWithFeeProofData { - equality_proof_data, - transfer_amount_ciphertext_validity_proof_data_with_ciphertext, - percentage_with_cap_proof_data, - fee_ciphertext_validity_proof_data, - range_proof_data, - } = transfer_with_fee_split_proof_data( - &spendable_ciphertext, - &decryptable_balance, - transfer_amount, - &source_keypair, - &aes_key, - destination_pubkey, - Some(auditor_pubkey), - withdraw_withheld_authority_pubkey, - fee_rate_basis_points, - maximum_fee, - ) - .unwrap(); - - equality_proof_data.verify_proof().unwrap(); - transfer_amount_ciphertext_validity_proof_data_with_ciphertext - .proof_data - .verify_proof() - .unwrap(); - percentage_with_cap_proof_data.verify_proof().unwrap(); - fee_ciphertext_validity_proof_data.verify_proof().unwrap(); - range_proof_data.verify_proof().unwrap(); - - TransferWithFeeProofContext::verify_and_extract( - equality_proof_data.context_data(), - transfer_amount_ciphertext_validity_proof_data_with_ciphertext - .proof_data - .context_data(), - percentage_with_cap_proof_data.context_data(), - fee_ciphertext_validity_proof_data.context_data(), - range_proof_data.context_data(), - fee_rate_basis_points, - maximum_fee, - ) - .unwrap(); -} - -#[test] -fn test_withdraw_proof_correctness() { - test_withdraw_validity(0, 0); - test_withdraw_validity(77, 55); - test_withdraw_validity(65535, 65535); - test_withdraw_validity(65536, 65536); - test_withdraw_validity(281474976710655, 281474976710655); -} - -fn test_withdraw_validity(spendable_balance: u64, withdraw_amount: u64) { - let keypair = ElGamalKeypair::new_rand(); - - let spendable_ciphertext = keypair.pubkey().encrypt(spendable_balance); - - let WithdrawProofData { - equality_proof_data, - range_proof_data, - } = withdraw_proof_data( - &spendable_ciphertext, - spendable_balance, - withdraw_amount, - &keypair, - ) - .unwrap(); - - equality_proof_data.verify_proof().unwrap(); - range_proof_data.verify_proof().unwrap(); - - WithdrawProofContext::verify_and_extract( - equality_proof_data.context_data(), - range_proof_data.context_data(), - ) - .unwrap(); -} - -#[test] -fn test_mint_proof_correctness() { - test_mint_validity(0, 0); - test_mint_validity(1, 0); - test_mint_validity(65535, 0); - test_mint_validity(65536, 0); - test_mint_validity(281474976710655, 0); - - test_mint_validity(0, 65535); - test_mint_validity(1, 65535); - test_mint_validity(65535, 65535); - test_mint_validity(65536, 65535); - test_mint_validity(281474976710655, 65535); - - test_mint_validity(0, 281474976710655); - test_mint_validity(1, 281474976710655); - test_mint_validity(65535, 281474976710655); - test_mint_validity(65536, 281474976710655); - test_mint_validity(281474976710655, 281474976710655); -} - -fn test_mint_validity(mint_amount: u64, supply: u64) { - let destination_keypair = ElGamalKeypair::new_rand(); - let destination_pubkey = destination_keypair.pubkey(); - - let auditor_keypair = ElGamalKeypair::new_rand(); - let auditor_pubkey = auditor_keypair.pubkey(); - - let supply_keypair = ElGamalKeypair::new_rand(); - - let supply_ciphertext = supply_keypair.pubkey().encrypt(supply); - - let MintProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - } = mint_split_proof_data( - &supply_ciphertext, - mint_amount, - supply, - &supply_keypair, - destination_pubkey, - Some(auditor_pubkey), - ) - .unwrap(); - - equality_proof_data.verify_proof().unwrap(); - ciphertext_validity_proof_data_with_ciphertext - .proof_data - .verify_proof() - .unwrap(); - range_proof_data.verify_proof().unwrap(); - - MintProofContext::verify_and_extract( - equality_proof_data.context_data(), - ciphertext_validity_proof_data_with_ciphertext - .proof_data - .context_data(), - range_proof_data.context_data(), - ) - .unwrap(); -} - -#[test] -fn test_burn_proof_correctness() { - test_burn_validity(0, 0); - test_burn_validity(77, 55); - test_burn_validity(65535, 65535); - test_burn_validity(65536, 65536); - test_burn_validity(281474976710655, 281474976710655); -} - -fn test_burn_validity(spendable_balance: u64, burn_amount: u64) { - let source_keypair = ElGamalKeypair::new_rand(); - let aes_key = AeKey::new_rand(); - - let auditor_keypair = ElGamalKeypair::new_rand(); - let auditor_pubkey = auditor_keypair.pubkey(); - - let supply_keypair = ElGamalKeypair::new_rand(); - let supply_pubkey = supply_keypair.pubkey(); - - let spendable_balance_ciphertext = source_keypair.pubkey().encrypt(spendable_balance); - let decryptable_balance = aes_key.encrypt(spendable_balance); - - let BurnProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - } = burn_split_proof_data( - &spendable_balance_ciphertext, - &decryptable_balance, - burn_amount, - &source_keypair, - &aes_key, - Some(auditor_pubkey), - supply_pubkey, - ) - .unwrap(); - - equality_proof_data.verify_proof().unwrap(); - ciphertext_validity_proof_data_with_ciphertext - .proof_data - .verify_proof() - .unwrap(); - range_proof_data.verify_proof().unwrap(); - - BurnProofContext::verify_and_extract( - equality_proof_data.context_data(), - ciphertext_validity_proof_data_with_ciphertext - .proof_data - .context_data(), - range_proof_data.context_data(), - ) - .unwrap(); -} diff --git a/token/js/.eslintignore b/token/js/.eslintignore deleted file mode 100644 index 6da325effab..00000000000 --- a/token/js/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -docs -lib -test-ledger - -package-lock.json diff --git a/token/js/.eslintrc b/token/js/.eslintrc deleted file mode 100644 index 5aef10a4729..00000000000 --- a/token/js/.eslintrc +++ /dev/null @@ -1,34 +0,0 @@ -{ - "root": true, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - "plugin:require-extensions/recommended" - ], - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint", - "prettier", - "require-extensions" - ], - "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/consistent-type-imports": "error" - }, - "overrides": [ - { - "files": [ - "examples/**/*", - "test/**/*" - ], - "rules": { - "require-extensions/require-extensions": "off", - "require-extensions/require-index": "off" - } - } - ] -} diff --git a/token/js/.gitignore b/token/js/.gitignore deleted file mode 100644 index 21f33db819c..00000000000 --- a/token/js/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -.idea -.vscode -.DS_Store - -node_modules - -pnpm-lock.yaml -yarn.lock - -docs -lib -test-ledger -*.tsbuildinfo diff --git a/token/js/.mocharc.json b/token/js/.mocharc.json deleted file mode 100644 index 451c14c3016..00000000000 --- a/token/js/.mocharc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extension": ["ts"], - "node-option": ["experimental-specifier-resolution=node", "loader=ts-node/esm"], - "timeout": 5000 -} diff --git a/token/js/.nojekyll b/token/js/.nojekyll deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/token/js/LICENSE b/token/js/LICENSE deleted file mode 100644 index d6456956733..00000000000 --- a/token/js/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/token/js/README.md b/token/js/README.md deleted file mode 100644 index cc9f8299d36..00000000000 --- a/token/js/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# `@solana/spl-token` - -A TypeScript library for interacting with the SPL Token and Token-2022 programs. - -> [!IMPORTANT] -> If you are using [@solana/web3.js version 2](https://github.com/solana-labs/solana-web3.js/?tab=readme-ov-file#whats-new-in-version-20) -> , you should use the `@solana-program/token` and `@solana-program/token-2022` -> packages instead. - -## Links - -- [TypeScript Docs](https://solana-labs.github.io/solana-program-library/token/js/) -- [FAQs (Frequently Asked Questions)](#faqs) -- [Install](#install) -- [Build from Source](#build-from-source) - -## FAQs - -### How can I get support? - -Please ask questions in the Solana Stack Exchange: https://solana.stackexchange.com/ - -If you've found a bug or you'd like to request a feature, please -[open an issue](https://github.com/solana-labs/solana-program-library/issues/new). - -### No export named Token - -Please see [upgrading from 0.1.x](#upgrading-from-01x). - -## Install - -```shell -npm install --save @solana/spl-token @solana/web3.js@1 -``` -_OR_ -```shell -yarn add @solana/spl-token @solana/web3.js@1 -``` - -## Build from Source - -0. Prerequisites - -* Node 16+ -* PNPM - -If you have Node 16+, you can [activate PNPM with Corepack](https://pnpm.io/installation#using-corepack). - -1. Clone the project: -```shell -git clone https://github.com/solana-labs/solana-program-library.git -``` - -2. Navigate to the root of the repository: -```shell -cd solana-program-library -``` - -3. Install the dependencies: -```shell -pnpm install -``` - -4. Build the libraries in the repository: -```shell -pnpm run build -``` - -5. Navigate to the SPL Token library: -```shell -cd token/js -``` - -6. Build the on-chain programs: -```shell -pnpm run test:build-programs -``` - -7. Run the tests: -```shell -pnpm run test -``` - -8. Run the example: -```shell -pnpm run example -``` - -## Upgrading - -### Upgrading from 0.2.0 - -There are no breaking changes from 0.2.0, only new functionality for Token-2022. - -### Upgrading from 0.1.x - -When upgrading from spl-token 0.1.x, you may see the following error in your code: - -``` -import {TOKEN_PROGRAM_ID, Token, AccountLayout} from '@solana/spl-token'; - ^^^^^ -SyntaxError: The requested module '@solana/spl-token' does not provide an export named 'Token' -``` - -The `@solana/spl-token` library as of version 0.2.0 does not have the `Token` -class. Instead the actions are split up and exported separately. - -To use the old version, install it with: - -``` -npm install @solana/spl-token@0.1.8 -``` - -Otherwise you can find documentation on how to use new versions on the -[SPL docs](https://spl.solana.com/token) or -[Solana Cookbook](https://solanacookbook.com/references/token.html). diff --git a/token/js/examples/cpiGuard.ts b/token/js/examples/cpiGuard.ts deleted file mode 100644 index b7182910e1d..00000000000 --- a/token/js/examples/cpiGuard.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - SystemProgram, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; -import { - createMint, - createEnableCpiGuardInstruction, - createInitializeAccountInstruction, - disableCpiGuard, - enableCpiGuard, - getAccountLen, - ExtensionType, - TOKEN_2022_PROGRAM_ID, -} from '../src'; - -(async () => { - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const payer = Keypair.generate(); - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const mintAuthority = Keypair.generate(); - const decimals = 9; - const mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - decimals, - undefined, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const accountLen = getAccountLen([ExtensionType.CpiGuard]); - const lamports = await connection.getMinimumBalanceForRentExemption(accountLen); - - const owner = Keypair.generate(); - const destinationKeypair = Keypair.generate(); - const destination = destinationKeypair.publicKey; - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: destination, - space: accountLen, - lamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializeAccountInstruction(destination, mint, owner.publicKey, TOKEN_2022_PROGRAM_ID), - createEnableCpiGuardInstruction(destination, owner.publicKey, [], TOKEN_2022_PROGRAM_ID), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, owner, destinationKeypair], undefined); - - await disableCpiGuard(connection, payer, destination, owner, [], undefined, TOKEN_2022_PROGRAM_ID); - - await enableCpiGuard(connection, payer, destination, owner, [], undefined, TOKEN_2022_PROGRAM_ID); -})(); diff --git a/token/js/examples/createMintAndTransferTokens.ts b/token/js/examples/createMintAndTransferTokens.ts deleted file mode 100644 index da6437d73f2..00000000000 --- a/token/js/examples/createMintAndTransferTokens.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js'; -import { createMint, getOrCreateAssociatedTokenAccount, mintTo, transfer } from '../src'; // @FIXME: replace with @solana/spl-token - -(async () => { - // Connect to cluster - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - // Generate a new wallet keypair and airdrop SOL - const fromWallet = Keypair.generate(); - const fromAirdropSignature = await connection.requestAirdrop(fromWallet.publicKey, LAMPORTS_PER_SOL); - - // Wait for airdrop confirmation - await connection.confirmTransaction({ - signature: fromAirdropSignature, - ...(await connection.getLatestBlockhash()), - }); - - // Generate a new wallet to receive newly minted token - const toWallet = Keypair.generate(); - - // Create new token mint - const mint = await createMint(connection, fromWallet, fromWallet.publicKey, null, 9); - - // Get the token account of the fromWallet address, and if it does not exist, create it - const fromTokenAccount = await getOrCreateAssociatedTokenAccount( - connection, - fromWallet, - mint, - fromWallet.publicKey, - ); - - // Get the token account of the toWallet address, and if it does not exist, create it - const toTokenAccount = await getOrCreateAssociatedTokenAccount(connection, fromWallet, mint, toWallet.publicKey); - - // Mint 1 new token to the "fromTokenAccount" account we just created - let signature = await mintTo( - connection, - fromWallet, - mint, - fromTokenAccount.address, - fromWallet.publicKey, - 1000000000, - [], - ); - console.log('mint tx:', signature); - - // Transfer the new token to the "toTokenAccount" we just created - signature = await transfer( - connection, - fromWallet, - fromTokenAccount.address, - toTokenAccount.address, - fromWallet.publicKey, - 1000000000, - [], - ); - console.log('transfer tx:', signature); -})(); diff --git a/token/js/examples/createMintCloseAuthority.ts b/token/js/examples/createMintCloseAuthority.ts deleted file mode 100644 index 330a35946ea..00000000000 --- a/token/js/examples/createMintCloseAuthority.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - closeAccount, - createInitializeMintInstruction, - createInitializeMintCloseAuthorityInstruction, - getMintLen, - ExtensionType, - TOKEN_2022_PROGRAM_ID, -} from '../src'; -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - SystemProgram, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; - -(async () => { - const payer = Keypair.generate(); - - const mintKeypair = Keypair.generate(); - const mint = mintKeypair.publicKey; - const mintAuthority = Keypair.generate(); - const freezeAuthority = Keypair.generate(); - const closeAuthority = Keypair.generate(); - - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const extensions = [ExtensionType.MintCloseAuthority]; - const mintLen = getMintLen(extensions); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializeMintCloseAuthorityInstruction(mint, closeAuthority.publicKey, TOKEN_2022_PROGRAM_ID), - createInitializeMintInstruction( - mint, - 9, - mintAuthority.publicKey, - freezeAuthority.publicKey, - TOKEN_2022_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined); - - console.log(mint.toBase58()); - - await closeAccount(connection, payer, mint, payer.publicKey, closeAuthority, [], undefined, TOKEN_2022_PROGRAM_ID); -})(); diff --git a/token/js/examples/defaultAccountState.ts b/token/js/examples/defaultAccountState.ts deleted file mode 100644 index e160abe88bb..00000000000 --- a/token/js/examples/defaultAccountState.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - SystemProgram, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; -import { - AccountState, - createInitializeMintInstruction, - createInitializeDefaultAccountStateInstruction, - getMintLen, - updateDefaultAccountState, - ExtensionType, - TOKEN_2022_PROGRAM_ID, -} from '../src'; - -(async () => { - const payer = Keypair.generate(); - - const mintAuthority = Keypair.generate(); - const freezeAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - const mint = mintKeypair.publicKey; - - const extensions = [ExtensionType.DefaultAccountState]; - const mintLen = getMintLen(extensions); - const decimals = 9; - - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const defaultState = AccountState.Frozen; - - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializeDefaultAccountStateInstruction(mint, defaultState, TOKEN_2022_PROGRAM_ID), - createInitializeMintInstruction( - mint, - decimals, - mintAuthority.publicKey, - freezeAuthority.publicKey, - TOKEN_2022_PROGRAM_ID, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined); - - await updateDefaultAccountState( - connection, - payer, - mint, - AccountState.Initialized, - freezeAuthority, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); -})(); diff --git a/token/js/examples/immutableOwner.ts b/token/js/examples/immutableOwner.ts deleted file mode 100644 index 53e4c774233..00000000000 --- a/token/js/examples/immutableOwner.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - SystemProgram, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; -import { - createAccount, - createMint, - createInitializeImmutableOwnerInstruction, - createInitializeAccountInstruction, - getAccountLen, - ExtensionType, - TOKEN_2022_PROGRAM_ID, -} from '../src'; - -(async () => { - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const payer = Keypair.generate(); - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const mintAuthority = Keypair.generate(); - const decimals = 9; - const mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - decimals, - undefined, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const accountLen = getAccountLen([ExtensionType.ImmutableOwner]); - const lamports = await connection.getMinimumBalanceForRentExemption(accountLen); - - const owner = Keypair.generate(); - const accountKeypair = Keypair.generate(); - const account = accountKeypair.publicKey; - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: account, - space: accountLen, - lamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializeImmutableOwnerInstruction(account, TOKEN_2022_PROGRAM_ID), - createInitializeAccountInstruction(account, mint, owner.publicKey, TOKEN_2022_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, accountKeypair], undefined); - - // create associated token account - await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TOKEN_2022_PROGRAM_ID); -})(); diff --git a/token/js/examples/interestBearing.ts b/token/js/examples/interestBearing.ts deleted file mode 100644 index 280c706b36c..00000000000 --- a/token/js/examples/interestBearing.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js'; -import { createInterestBearingMint, updateRateInterestBearingMint, TOKEN_2022_PROGRAM_ID } from '../src'; - -(async () => { - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const payer = Keypair.generate(); - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const mintAuthority = Keypair.generate(); - const freezeAuthority = Keypair.generate(); - const rateAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - const rate = 10; - const decimals = 9; - const mint = await createInterestBearingMint( - connection, - payer, - mintAuthority.publicKey, - freezeAuthority.publicKey, - rateAuthority.publicKey, - rate, - decimals, - mintKeypair, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const updateRate = 50; - await updateRateInterestBearingMint( - connection, - payer, - mint, - rateAuthority, - updateRate, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); -})(); diff --git a/token/js/examples/memoTransfer.ts b/token/js/examples/memoTransfer.ts deleted file mode 100644 index 708ea7424d4..00000000000 --- a/token/js/examples/memoTransfer.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - SystemProgram, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; -import { createMemoInstruction } from '@solana/spl-memo'; -import { - createAssociatedTokenAccount, - createMint, - createEnableRequiredMemoTransfersInstruction, - createInitializeAccountInstruction, - createTransferInstruction, - disableRequiredMemoTransfers, - enableRequiredMemoTransfers, - getAccountLen, - mintTo, - ExtensionType, - TOKEN_2022_PROGRAM_ID, -} from '../src'; - -(async () => { - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const payer = Keypair.generate(); - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const mintAuthority = Keypair.generate(); - const decimals = 9; - const mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - decimals, - undefined, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const accountLen = getAccountLen([ExtensionType.MemoTransfer]); - const lamports = await connection.getMinimumBalanceForRentExemption(accountLen); - - const owner = Keypair.generate(); - const destinationKeypair = Keypair.generate(); - const destination = destinationKeypair.publicKey; - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: destination, - space: accountLen, - lamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializeAccountInstruction(destination, mint, owner.publicKey, TOKEN_2022_PROGRAM_ID), - createEnableRequiredMemoTransfersInstruction(destination, owner.publicKey, [], TOKEN_2022_PROGRAM_ID), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, owner, destinationKeypair], undefined); - - await disableRequiredMemoTransfers(connection, payer, destination, owner, [], undefined, TOKEN_2022_PROGRAM_ID); - - await enableRequiredMemoTransfers(connection, payer, destination, owner, [], undefined, TOKEN_2022_PROGRAM_ID); - - const sourceTokenAccount = await createAssociatedTokenAccount( - connection, - payer, - mint, - payer.publicKey, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - await mintTo(connection, payer, mint, sourceTokenAccount, mintAuthority, 100, [], undefined, TOKEN_2022_PROGRAM_ID); - - const transferTransaction = new Transaction().add( - createMemoInstruction('Hello, memo-transfer!', [payer.publicKey]), - createTransferInstruction(sourceTokenAccount, destination, payer.publicKey, 100, [], TOKEN_2022_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transferTransaction, [payer], undefined); -})(); diff --git a/token/js/examples/metadata.ts b/token/js/examples/metadata.ts deleted file mode 100644 index 92f09aaf61c..00000000000 --- a/token/js/examples/metadata.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - clusterApiUrl, - Connection, - Keypair, - LAMPORTS_PER_SOL, - sendAndConfirmTransaction, - SystemProgram, - Transaction, -} from '@solana/web3.js'; -import { - createInitializeMetadataPointerInstruction, - createInitializeMintInstruction, - ExtensionType, - getMintLen, - LENGTH_SIZE, - TOKEN_2022_PROGRAM_ID, - TYPE_SIZE, -} from '@solana/spl-token'; -import type { TokenMetadata } from '@solana/spl-token-metadata'; -import { - createInitializeInstruction, - pack, - createUpdateFieldInstruction, - createRemoveKeyInstruction, -} from '@solana/spl-token-metadata'; - -(async () => { - const payer = Keypair.generate(); - - const mint = Keypair.generate(); - const decimals = 9; - - const metadata: TokenMetadata = { - mint: mint.publicKey, - name: 'TOKEN_NAME', - symbol: 'SMBL', - uri: 'URI', - additionalMetadata: [['new-field', 'new-value']], - }; - - const mintLen = getMintLen([ExtensionType.MetadataPointer]); - - const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length; - - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ - signature: airdropSignature, - ...(await connection.getLatestBlockhash()), - }); - - const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen + metadataLen); - const mintTransaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint.publicKey, - space: mintLen, - lamports: mintLamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializeMetadataPointerInstruction( - mint.publicKey, - payer.publicKey, - mint.publicKey, - TOKEN_2022_PROGRAM_ID, - ), - createInitializeMintInstruction(mint.publicKey, decimals, payer.publicKey, null, TOKEN_2022_PROGRAM_ID), - createInitializeInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - mint: mint.publicKey, - metadata: mint.publicKey, - name: metadata.name, - symbol: metadata.symbol, - uri: metadata.uri, - mintAuthority: payer.publicKey, - updateAuthority: payer.publicKey, - }), - - // add a custom field - createUpdateFieldInstruction({ - metadata: mint.publicKey, - updateAuthority: payer.publicKey, - programId: TOKEN_2022_PROGRAM_ID, - field: metadata.additionalMetadata[0][0], - value: metadata.additionalMetadata[0][1], - }), - - // update a field - createUpdateFieldInstruction({ - metadata: mint.publicKey, - updateAuthority: payer.publicKey, - programId: TOKEN_2022_PROGRAM_ID, - field: 'name', - value: 'YourToken', - }), - - // remove a field - createRemoveKeyInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - metadata: mint.publicKey, - updateAuthority: payer.publicKey, - key: 'new-field', - idempotent: true, // If false the operation will fail if the field does not exist in the metadata - }), - ); - const sig = await sendAndConfirmTransaction(connection, mintTransaction, [payer, mint]); - console.log('Signature:', sig); -})(); diff --git a/token/js/examples/nonTransferable.ts b/token/js/examples/nonTransferable.ts deleted file mode 100644 index 13df064fc1a..00000000000 --- a/token/js/examples/nonTransferable.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - SystemProgram, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; -import { - createInitializeNonTransferableMintInstruction, - createInitializeMintInstruction, - getMintLen, - ExtensionType, - TOKEN_2022_PROGRAM_ID, -} from '../src'; - -(async () => { - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const payer = Keypair.generate(); - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const mintAuthority = Keypair.generate(); - const decimals = 9; - - const mintKeypair = Keypair.generate(); - const mint = mintKeypair.publicKey; - const mintLen = getMintLen([ExtensionType.NonTransferable]); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializeNonTransferableMintInstruction(mint, TOKEN_2022_PROGRAM_ID), - createInitializeMintInstruction(mint, decimals, mintAuthority.publicKey, null, TOKEN_2022_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined); -})(); diff --git a/token/js/examples/permanentDelegate.ts b/token/js/examples/permanentDelegate.ts deleted file mode 100644 index a1c43e63a21..00000000000 --- a/token/js/examples/permanentDelegate.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - SystemProgram, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; - -import { - ExtensionType, - createInitializeMintInstruction, - createInitializePermanentDelegateInstruction, - mintTo, - createAccount, - getMintLen, - transferChecked, - TOKEN_2022_PROGRAM_ID, -} from '../src'; - -(async () => { - const payer = Keypair.generate(); - - const mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - const mint = mintKeypair.publicKey; - const permanentDelegate = Keypair.generate(); - - const extensions = [ExtensionType.PermanentDelegate]; - const mintLen = getMintLen(extensions); - const decimals = 9; - - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen); - const mintTransaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports: mintLamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializePermanentDelegateInstruction(mint, permanentDelegate.publicKey, TOKEN_2022_PROGRAM_ID), - createInitializeMintInstruction(mint, decimals, mintAuthority.publicKey, null, TOKEN_2022_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, mintTransaction, [payer, mintKeypair], undefined); - - const mintAmount = BigInt(1_000_000_000); - const owner = Keypair.generate(); - const sourceAccount = await createAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - await mintTo( - connection, - payer, - mint, - sourceAccount, - mintAuthority, - mintAmount, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const accountKeypair = Keypair.generate(); - const destinationAccount = await createAccount( - connection, - payer, - mint, - owner.publicKey, - accountKeypair, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - // Transfer by signing with the permanent delegate - const signature = await transferChecked( - connection, - payer, - sourceAccount, - mint, - destinationAccount, - permanentDelegate, - mintAmount, - decimals, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); -})(); diff --git a/token/js/examples/reallocate.ts b/token/js/examples/reallocate.ts deleted file mode 100644 index 3ee07efd3f5..00000000000 --- a/token/js/examples/reallocate.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; -import { - createAccount, - createMint, - createEnableRequiredMemoTransfersInstruction, - createReallocateInstruction, - ExtensionType, - TOKEN_2022_PROGRAM_ID, -} from '../src'; - -(async () => { - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const payer = Keypair.generate(); - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const mintAuthority = Keypair.generate(); - const decimals = 9; - const mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - decimals, - undefined, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const owner = Keypair.generate(); - const account = await createAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const extensions = [ExtensionType.MemoTransfer]; - const transaction = new Transaction().add( - createReallocateInstruction( - account, - payer.publicKey, - extensions, - owner.publicKey, - undefined, - TOKEN_2022_PROGRAM_ID, - ), - createEnableRequiredMemoTransfersInstruction(account, owner.publicKey, [], TOKEN_2022_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, owner], undefined); -})(); diff --git a/token/js/examples/transferFee.ts b/token/js/examples/transferFee.ts deleted file mode 100644 index 2cc31fddedf..00000000000 --- a/token/js/examples/transferFee.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - SystemProgram, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; - -import { - ExtensionType, - createInitializeMintInstruction, - mintTo, - createAccount, - getMintLen, - getTransferFeeAmount, - unpackAccount, - TOKEN_2022_PROGRAM_ID, -} from '../src'; - -import { - createInitializeTransferFeeConfigInstruction, - harvestWithheldTokensToMint, - transferCheckedWithFee, - withdrawWithheldTokensFromAccounts, - withdrawWithheldTokensFromMint, -} from '../src/extensions/transferFee/index'; - -(async () => { - const payer = Keypair.generate(); - - const mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - const mint = mintKeypair.publicKey; - const transferFeeConfigAuthority = Keypair.generate(); - const withdrawWithheldAuthority = Keypair.generate(); - - const extensions = [ExtensionType.TransferFeeConfig]; - - const mintLen = getMintLen(extensions); - const decimals = 9; - const feeBasisPoints = 50; - const maxFee = BigInt(5_000); - - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen); - const mintTransaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports: mintLamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializeTransferFeeConfigInstruction( - mint, - transferFeeConfigAuthority.publicKey, - withdrawWithheldAuthority.publicKey, - feeBasisPoints, - maxFee, - TOKEN_2022_PROGRAM_ID, - ), - createInitializeMintInstruction(mint, decimals, mintAuthority.publicKey, null, TOKEN_2022_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, mintTransaction, [payer, mintKeypair], undefined); - - const mintAmount = BigInt(1_000_000_000); - const owner = Keypair.generate(); - const sourceAccount = await createAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - await mintTo( - connection, - payer, - mint, - sourceAccount, - mintAuthority, - mintAmount, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const accountKeypair = Keypair.generate(); - const destinationAccount = await createAccount( - connection, - payer, - mint, - owner.publicKey, - accountKeypair, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const transferAmount = BigInt(1_000_000); - const fee = (transferAmount * BigInt(feeBasisPoints)) / BigInt(10_000); - await transferCheckedWithFee( - connection, - payer, - sourceAccount, - mint, - destinationAccount, - owner, - transferAmount, - decimals, - fee, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const allAccounts = await connection.getProgramAccounts(TOKEN_2022_PROGRAM_ID, { - commitment: 'confirmed', - filters: [ - { - memcmp: { - offset: 0, - bytes: mint.toString(), - }, - }, - ], - }); - const accountsToWithdrawFrom = []; - for (const accountInfo of allAccounts) { - const account = unpackAccount(accountInfo.pubkey, accountInfo.account, TOKEN_2022_PROGRAM_ID); - const transferFeeAmount = getTransferFeeAmount(account); - if (transferFeeAmount !== null && transferFeeAmount.withheldAmount > BigInt(0)) { - accountsToWithdrawFrom.push(accountInfo.pubkey); - } - } - - await withdrawWithheldTokensFromAccounts( - connection, - payer, - mint, - destinationAccount, - withdrawWithheldAuthority, - [], - accountsToWithdrawFrom, - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - await harvestWithheldTokensToMint(connection, payer, mint, [destinationAccount], undefined, TOKEN_2022_PROGRAM_ID); - - await withdrawWithheldTokensFromMint( - connection, - payer, - mint, - destinationAccount, - withdrawWithheldAuthority, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); -})(); diff --git a/token/js/examples/transferHook.ts b/token/js/examples/transferHook.ts deleted file mode 100644 index 1850b757d12..00000000000 --- a/token/js/examples/transferHook.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - clusterApiUrl, - sendAndConfirmTransaction, - Connection, - Keypair, - PublicKey, - SystemProgram, - Transaction, - LAMPORTS_PER_SOL, -} from '@solana/web3.js'; - -import { - ExtensionType, - createInitializeMintInstruction, - createInitializeTransferHookInstruction, - getMintLen, - TOKEN_2022_PROGRAM_ID, - updateTransferHook, - transferCheckedWithTransferHook, - getAssociatedTokenAddressSync, - ASSOCIATED_TOKEN_PROGRAM_ID, -} from '../src'; - -(async () => { - const payer = Keypair.generate(); - - const mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - const mint = mintKeypair.publicKey; - - const sender = Keypair.generate(); - const recipient = Keypair.generate(); - - const extensions = [ExtensionType.TransferHook]; - const mintLen = getMintLen(extensions); - const decimals = 9; - const transferHookPogramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj'); - const newTransferHookProgramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj'); - - const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); - - const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL); - await connection.confirmTransaction({ signature: airdropSignature, ...(await connection.getLatestBlockhash()) }); - - const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen); - const mintTransaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports: mintLamports, - programId: TOKEN_2022_PROGRAM_ID, - }), - createInitializeTransferHookInstruction(mint, payer.publicKey, transferHookPogramId, TOKEN_2022_PROGRAM_ID), - createInitializeMintInstruction(mint, decimals, mintAuthority.publicKey, null, TOKEN_2022_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, mintTransaction, [payer, mintKeypair], undefined); - - await updateTransferHook( - connection, - payer, - mint, - newTransferHookProgramId, - payer.publicKey, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const senderAta = getAssociatedTokenAddressSync( - mint, - sender.publicKey, - false, - TOKEN_2022_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - const recipientAta = getAssociatedTokenAddressSync( - mint, - recipient.publicKey, - false, - TOKEN_2022_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - - await transferCheckedWithTransferHook( - connection, - payer, - senderAta, - mint, - recipientAta, - sender, - BigInt(1000000000), - 9, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); -})(); diff --git a/token/js/package.json b/token/js/package.json deleted file mode 100644 index 1d8883bf1e5..00000000000 --- a/token/js/package.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "name": "@solana/spl-token", - "description": "SPL Token Program JS API", - "version": "0.4.9", - "author": "Solana Labs Maintainers ", - "repository": "https://github.com/solana-labs/solana-program-library", - "license": "Apache-2.0", - "type": "module", - "sideEffects": false, - "engines": { - "node": ">=16" - }, - "files": [ - "lib", - "src", - "LICENSE", - "README.md" - ], - "publishConfig": { - "access": "public" - }, - "main": "./lib/cjs/index.js", - "module": "./lib/esm/index.js", - "types": "./lib/types/index.d.ts", - "exports": { - "types": "./lib/types/index.d.ts", - "require": "./lib/cjs/index.js", - "import": "./lib/esm/index.js" - }, - "scripts": { - "nuke": "shx rm -rf node_modules package-lock.json || true", - "reinstall": "npm run nuke && npm install", - "clean": "shx rm -rf lib **/*.tsbuildinfo || true", - "build": "tsc --build --verbose tsconfig.all.json", - "postbuild": "shx echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", - "build:program": "cargo build-sbf --manifest-path=../program/Cargo.toml && cargo build-sbf --manifest-path=../program-2022/Cargo.toml && cargo build-sbf --manifest-path=../../associated-token-account/program/Cargo.toml && cargo build-sbf --no-default-features --manifest-path=../transfer-hook/example/Cargo.toml", - "watch": "tsc --build --verbose --watch tsconfig.all.json", - "release": "npm run clean && npm run build", - "lint": "eslint --max-warnings 0 .", - "lint:fix": "eslint --fix .", - "example": "node --experimental-specifier-resolution=node --loader ts-node/esm examples/createMintAndTransferTokens.ts", - "test": "npm run test:unit && npm run test:e2e-built && npm run test:e2e-native && npm run test:e2e-2022", - "test:unit": "mocha test/unit", - "test:e2e-built": "start-server-and-test 'solana-test-validator --bpf-program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA ../../target/deploy/spl_token.so --reset --quiet' http://127.0.0.1:8899/health 'mocha test/e2e'", - "test:e2e-2022": "TEST_PROGRAM_ID=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb start-server-and-test 'solana-test-validator --bpf-program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL ../../target/deploy/spl_associated_token_account.so --bpf-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb ../../target/deploy/spl_token_2022.so --bpf-program TokenHookExampLe8smaVNrxTBezWTRbEwxwb1Zykrb ../../target/deploy/spl_transfer_hook_example.so --reset --quiet' http://127.0.0.1:8899/health 'mocha test/e2e*'", - "test:e2e-native": "start-server-and-test 'solana-test-validator --reset --quiet' http://127.0.0.1:8899/health 'mocha test/e2e'", - "test:build-programs": "cargo build-sbf --manifest-path ../program/Cargo.toml && cargo build-sbf --manifest-path ../program-2022/Cargo.toml && cargo build-sbf --manifest-path ../../associated-token-account/program/Cargo.toml", - "deploy": "npm run deploy:docs", - "docs": "shx rm -rf docs && typedoc && shx cp .nojekyll docs/", - "deploy:docs": "npm run docs && gh-pages --dest token/js --dist docs --dotfiles" - }, - "peerDependencies": { - "@solana/web3.js": "^1.95.5" - }, - "dependencies": { - "@solana/buffer-layout": "^4.0.0", - "@solana/buffer-layout-utils": "^0.2.0", - "@solana/spl-token-group": "^0.0.7", - "@solana/spl-token-metadata": "^0.1.6", - "buffer": "^6.0.3" - }, - "devDependencies": { - "@solana/codecs-strings": "2.0.0", - "@solana/spl-memo": "0.2.5", - "@solana/web3.js": "^1.95.5", - "@types/chai-as-promised": "^8.0.1", - "@types/chai": "^5.0.1", - "@types/mocha": "^10.0.10", - "@types/node": "^22.10.5", - "@types/node-fetch": "^2.6.12", - "@typescript-eslint/eslint-plugin": "^8.4.0", - "@typescript-eslint/parser": "^8.4.0", - "chai": "^5.1.2", - "chai-as-promised": "^8.0.1", - "eslint": "^8.57.0", - "eslint-plugin-require-extensions": "^0.1.1", - "gh-pages": "^6.3.0", - "mocha": "^11.0.1", - "process": "^0.11.10", - "shx": "^0.3.4", - "start-server-and-test": "^2.0.9", - "ts-node": "^10.9.2", - "typedoc": "^0.27.6", - "typescript": "^5.7.2" - } -} diff --git a/token/js/src/actions/amountToUiAmount.ts b/token/js/src/actions/amountToUiAmount.ts deleted file mode 100644 index d6a7a204ff5..00000000000 --- a/token/js/src/actions/amountToUiAmount.ts +++ /dev/null @@ -1,271 +0,0 @@ -import type { Connection, Signer, TransactionError } from '@solana/web3.js'; -import { PublicKey, Transaction } from '@solana/web3.js'; -import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'; -import { createAmountToUiAmountInstruction } from '../instructions/amountToUiAmount.js'; -import { unpackMint } from '../state/mint.js'; -import { getInterestBearingMintConfigState } from '../extensions/interestBearingMint/state.js'; - -/** - * Amount as a string using mint-prescribed decimals - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint for the account - * @param amount Amount of tokens to be converted to Ui Amount - * @param programId SPL Token program account - * - * @return Ui Amount generated - */ -export async function amountToUiAmount( - connection: Connection, - payer: Signer, - mint: PublicKey, - amount: number | bigint, - programId = TOKEN_PROGRAM_ID, -): Promise { - const transaction = new Transaction().add(createAmountToUiAmountInstruction(mint, amount, programId)); - const { returnData, err } = (await connection.simulateTransaction(transaction, [payer], false)).value; - if (returnData?.data) { - return Buffer.from(returnData.data[0], returnData.data[1]).toString('utf-8'); - } - return err; -} - -/** - * Calculates the exponent for the interest rate formula. - * @param t1 - The start time in seconds. - * @param t2 - The end time in seconds. - * @param r - The interest rate in basis points. - * @returns The calculated exponent. - */ -function calculateExponentForTimesAndRate(t1: number, t2: number, r: number) { - const ONE_IN_BASIS_POINTS = 10000; - const SECONDS_PER_YEAR = 60 * 60 * 24 * 365.24; - const timespan = t2 - t1; - const numerator = r * timespan; - const exponent = numerator / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS); - return Math.exp(exponent); -} - -/** - * Retrieves the current timestamp from the Solana clock sysvar. - * @param connection - The Solana connection object. - * @returns A promise that resolves to the current timestamp in seconds. - * @throws An error if the sysvar clock cannot be fetched or parsed. - */ -async function getSysvarClockTimestamp(connection: Connection): Promise { - const info = await connection.getParsedAccountInfo(new PublicKey('SysvarC1ock11111111111111111111111111111111')); - if (!info) { - throw new Error('Failed to fetch sysvar clock'); - } - if (typeof info.value === 'object' && info.value && 'data' in info.value && 'parsed' in info.value.data) { - return info.value.data.parsed.info.unixTimestamp; - } - throw new Error('Failed to parse sysvar clock'); -} - -/** - * Convert amount to UiAmount for a mint with interest bearing extension without simulating a transaction - * This implements the same logic as the CPI instruction available in /token/program-2022/src/extension/interest_bearing_mint/mod.rs - * In general to calculate compounding interest over a period of time, the formula is: - * A = P * e^(r * t) where - * A = final amount after interest - * P = principal amount (initial investment) - * r = annual interest rate (as a decimal, e.g., 5% = 0.05) - * t = time in years - * e = mathematical constant (~2.718) - * - * In this case, we are calculating the total scale factor for the interest bearing extension which is the product of two exponential functions: - * totalScale = e^(r1 * t1) * e^(r2 * t2) - * where r1 and r2 are the interest rates before and after the last update, and t1 and t2 are the times in years between - * the initialization timestamp and the last update timestamp, and between the last update timestamp and the current timestamp. - * - * @param amount Amount of tokens to be converted - * @param decimals Number of decimals of the mint - * @param currentTimestamp Current timestamp in seconds - * @param lastUpdateTimestamp Last time the interest rate was updated in seconds - * @param initializationTimestamp Time the interest bearing extension was initialized in seconds - * @param preUpdateAverageRate Interest rate in basis points (1 basis point = 0.01%) before last update - * @param currentRate Current interest rate in basis points - * - * @return Amount scaled by accrued interest as a string with appropriate decimal places - */ -export function amountToUiAmountWithoutSimulation( - amount: bigint, - decimals: number, - currentTimestamp: number, // in seconds - lastUpdateTimestamp: number, - initializationTimestamp: number, - preUpdateAverageRate: number, - currentRate: number, -): string { - // Calculate pre-update exponent - // e^(preUpdateAverageRate * (lastUpdateTimestamp - initializationTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS)) - const preUpdateExp = calculateExponentForTimesAndRate( - initializationTimestamp, - lastUpdateTimestamp, - preUpdateAverageRate, - ); - - // Calculate post-update exponent - // e^(currentRate * (currentTimestamp - lastUpdateTimestamp) / (SECONDS_PER_YEAR * ONE_IN_BASIS_POINTS)) - const postUpdateExp = calculateExponentForTimesAndRate(lastUpdateTimestamp, currentTimestamp, currentRate); - - // Calculate total scale - const totalScale = preUpdateExp * postUpdateExp; - // Scale the amount by the total interest factor - const scaledAmount = Number(amount) * totalScale; - - // Calculate the decimal factor (e.g. 100 for 2 decimals) - const decimalFactor = Math.pow(10, decimals); - - // Convert to UI amount by: - // 1. Truncating to remove any remaining decimals - // 2. Dividing by decimal factor to get final UI amount - // 3. Converting to string - return (Math.trunc(scaledAmount) / decimalFactor).toString(); -} - -/** - * Convert amount to UiAmount for a mint without simulating a transaction - * This implements the same logic as `process_amount_to_ui_amount` in /token/program-2022/src/processor.rs - * and `process_amount_to_ui_amount` in /token/program/src/processor.rs - * - * @param connection Connection to use - * @param mint Mint to use for calculations - * @param amount Amount of tokens to be converted to Ui Amount - * - * @return Ui Amount generated - */ -export async function amountToUiAmountForMintWithoutSimulation( - connection: Connection, - mint: PublicKey, - amount: bigint, -): Promise { - const accountInfo = await connection.getAccountInfo(mint); - const programId = accountInfo?.owner; - if (programId !== TOKEN_PROGRAM_ID && programId !== TOKEN_2022_PROGRAM_ID) { - throw new Error('Invalid program ID'); - } - - const mintInfo = unpackMint(mint, accountInfo, programId); - - const interestBearingMintConfigState = getInterestBearingMintConfigState(mintInfo); - if (!interestBearingMintConfigState) { - const amountNumber = Number(amount); - const decimalsFactor = Math.pow(10, mintInfo.decimals); - return (amountNumber / decimalsFactor).toString(); - } - - const timestamp = await getSysvarClockTimestamp(connection); - - return amountToUiAmountWithoutSimulation( - amount, - mintInfo.decimals, - timestamp, - Number(interestBearingMintConfigState.lastUpdateTimestamp), - Number(interestBearingMintConfigState.initializationTimestamp), - interestBearingMintConfigState.preUpdateAverageRate, - interestBearingMintConfigState.currentRate, - ); -} - -/** - * Convert an amount with interest back to the original amount without interest - * This implements the same logic as the CPI instruction available in /token/program-2022/src/extension/interest_bearing_mint/mod.rs - * - * @param uiAmount UI Amount (principal plus continuously compounding interest) to be converted back to original principal - * @param decimals Number of decimals for the mint - * @param currentTimestamp Current timestamp in seconds - * @param lastUpdateTimestamp Last time the interest rate was updated in seconds - * @param initializationTimestamp Time the interest bearing extension was initialized in seconds - * @param preUpdateAverageRate Interest rate in basis points (hundredths of a percent) before the last update - * @param currentRate Current interest rate in basis points - * - * In general to calculate the principal from the UI amount, the formula is: - * P = A / (e^(r * t)) where - * P = principal - * A = UI amount - * r = annual interest rate (as a decimal, e.g., 5% = 0.05) - * t = time in years - * - * In this case, we are calculating the principal by dividing the UI amount by the total scale factor which is the product of two exponential functions: - * totalScale = e^(r1 * t1) * e^(r2 * t2) - * where r1 is the pre-update average rate, r2 is the current rate, t1 is the time in years between the initialization timestamp and the last update timestamp, - * and t2 is the time in years between the last update timestamp and the current timestamp. - * then to calculate the principal, we divide the UI amount by the total scale factor: - * P = A / totalScale - * - * @return Original amount (principal) without interest - */ -export function uiAmountToAmountWithoutSimulation( - uiAmount: string, - decimals: number, - currentTimestamp: number, // in seconds - lastUpdateTimestamp: number, - initializationTimestamp: number, - preUpdateAverageRate: number, - currentRate: number, -): bigint { - const uiAmountNumber = parseFloat(uiAmount); - const decimalsFactor = Math.pow(10, decimals); - const uiAmountScaled = uiAmountNumber * decimalsFactor; - - // Calculate pre-update exponent - const preUpdateExp = calculateExponentForTimesAndRate( - initializationTimestamp, - lastUpdateTimestamp, - preUpdateAverageRate, - ); - - // Calculate post-update exponent - const postUpdateExp = calculateExponentForTimesAndRate(lastUpdateTimestamp, currentTimestamp, currentRate); - - // Calculate total scale - const totalScale = preUpdateExp * postUpdateExp; - - // Calculate original principal by dividing the UI amount (principal + interest) by the total scale - const originalPrincipal = uiAmountScaled / totalScale; - return BigInt(Math.trunc(originalPrincipal)); -} - -/** - * Convert a UI amount back to the raw amount - * - * @param connection Connection to use - * @param mint Mint to use for calculations - * @param uiAmount UI Amount to be converted back to raw amount - * - * - * @return Raw amount - */ -export async function uiAmountToAmountForMintWithoutSimulation( - connection: Connection, - mint: PublicKey, - uiAmount: string, -): Promise { - const accountInfo = await connection.getAccountInfo(mint); - const programId = accountInfo?.owner; - if (programId !== TOKEN_PROGRAM_ID && programId !== TOKEN_2022_PROGRAM_ID) { - throw new Error('Invalid program ID'); - } - - const mintInfo = unpackMint(mint, accountInfo, programId); - const interestBearingMintConfigState = getInterestBearingMintConfigState(mintInfo); - if (!interestBearingMintConfigState) { - const uiAmountScaled = parseFloat(uiAmount) * Math.pow(10, mintInfo.decimals); - return BigInt(Math.trunc(uiAmountScaled)); - } - - const timestamp = await getSysvarClockTimestamp(connection); - - return uiAmountToAmountWithoutSimulation( - uiAmount, - mintInfo.decimals, - timestamp, - Number(interestBearingMintConfigState.lastUpdateTimestamp), - Number(interestBearingMintConfigState.initializationTimestamp), - interestBearingMintConfigState.preUpdateAverageRate, - interestBearingMintConfigState.currentRate, - ); -} diff --git a/token/js/src/actions/approve.ts b/token/js/src/actions/approve.ts deleted file mode 100644 index 1e8f091e8d3..00000000000 --- a/token/js/src/actions/approve.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createApproveInstruction } from '../instructions/approve.js'; -import { getSigners } from './internal.js'; - -/** - * Approve a delegate to transfer up to a maximum number of tokens from an account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Address of the token account - * @param delegate Account authorized to transfer tokens from the account - * @param owner Owner of the account - * @param amount Maximum number of tokens the delegate may transfer - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function approve( - connection: Connection, - payer: Signer, - account: PublicKey, - delegate: PublicKey, - owner: Signer | PublicKey, - amount: number | bigint, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createApproveInstruction(account, delegate, ownerPublicKey, amount, multiSigners, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/approveChecked.ts b/token/js/src/actions/approveChecked.ts deleted file mode 100644 index 68d0d651db7..00000000000 --- a/token/js/src/actions/approveChecked.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createApproveCheckedInstruction } from '../instructions/approveChecked.js'; -import { getSigners } from './internal.js'; - -/** - * Approve a delegate to transfer up to a maximum number of tokens from an account, asserting the token mint and - * decimals - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Address of the mint - * @param account Address of the account - * @param delegate Account authorized to perform a transfer tokens from the source account - * @param owner Owner of the source account - * @param amount Maximum number of tokens the delegate may transfer - * @param decimals Number of decimals in approve amount - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function approveChecked( - connection: Connection, - payer: Signer, - mint: PublicKey, - account: PublicKey, - delegate: PublicKey, - owner: Signer | PublicKey, - amount: number | bigint, - decimals: number, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createApproveCheckedInstruction( - account, - mint, - delegate, - ownerPublicKey, - amount, - decimals, - multiSigners, - programId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/burn.ts b/token/js/src/actions/burn.ts deleted file mode 100644 index 8bc23303c03..00000000000 --- a/token/js/src/actions/burn.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createBurnInstruction } from '../instructions/burn.js'; -import { getSigners } from './internal.js'; - -/** - * Burn tokens from an account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Account to burn tokens from - * @param mint Mint for the account - * @param owner Account owner - * @param amount Amount to burn - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function burn( - connection: Connection, - payer: Signer, - account: PublicKey, - mint: PublicKey, - owner: Signer | PublicKey, - amount: number | bigint, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createBurnInstruction(account, mint, ownerPublicKey, amount, multiSigners, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/burnChecked.ts b/token/js/src/actions/burnChecked.ts deleted file mode 100644 index b29c8d635c2..00000000000 --- a/token/js/src/actions/burnChecked.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createBurnCheckedInstruction } from '../instructions/burnChecked.js'; -import { getSigners } from './internal.js'; - -/** - * Burn tokens from an account, asserting the token mint and decimals - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Account to burn tokens from - * @param mint Mint for the account - * @param owner Account owner - * @param amount Amount to burn - * @param decimals Number of decimals in amount to burn - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function burnChecked( - connection: Connection, - payer: Signer, - account: PublicKey, - mint: PublicKey, - owner: Signer | PublicKey, - amount: number | bigint, - decimals: number, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createBurnCheckedInstruction(account, mint, ownerPublicKey, amount, decimals, multiSigners, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/closeAccount.ts b/token/js/src/actions/closeAccount.ts deleted file mode 100644 index c6b84654f3f..00000000000 --- a/token/js/src/actions/closeAccount.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createCloseAccountInstruction } from '../instructions/closeAccount.js'; -import { getSigners } from './internal.js'; - -/** - * Close a token account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Account to close - * @param destination Account to receive the remaining balance of the closed account - * @param authority Authority which is allowed to close the account - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function closeAccount( - connection: Connection, - payer: Signer, - account: PublicKey, - destination: PublicKey, - authority: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - createCloseAccountInstruction(account, destination, authorityPublicKey, multiSigners, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/createAccount.ts b/token/js/src/actions/createAccount.ts deleted file mode 100644 index 8b2641cf059..00000000000 --- a/token/js/src/actions/createAccount.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { ConfirmOptions, Connection, Keypair, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { getAccountLenForMint } from '../extensions/extensionType.js'; -import { createInitializeAccountInstruction } from '../instructions/initializeAccount.js'; -import { getMint } from '../state/mint.js'; -import { createAssociatedTokenAccount } from './createAssociatedTokenAccount.js'; - -/** - * Create and initialize a new token account - * - * @param connection Connection to use - * @param payer Payer of the transaction and initialization fees - * @param mint Mint for the account - * @param owner Owner of the new account - * @param keypair Optional keypair, defaulting to the associated token account for the `mint` and `owner` - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Address of the new token account - */ -export async function createAccount( - connection: Connection, - payer: Signer, - mint: PublicKey, - owner: PublicKey, - keypair?: Keypair, - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - // If a keypair isn't provided, create the associated token account and return its address - if (!keypair) return await createAssociatedTokenAccount(connection, payer, mint, owner, confirmOptions, programId); - - // Otherwise, create the account with the provided keypair and return its public key - const mintState = await getMint(connection, mint, confirmOptions?.commitment, programId); - const space = getAccountLenForMint(mintState); - const lamports = await connection.getMinimumBalanceForRentExemption(space); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: keypair.publicKey, - space, - lamports, - programId, - }), - createInitializeAccountInstruction(keypair.publicKey, mint, owner, programId), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, keypair], confirmOptions); - - return keypair.publicKey; -} diff --git a/token/js/src/actions/createAssociatedTokenAccount.ts b/token/js/src/actions/createAssociatedTokenAccount.ts deleted file mode 100644 index 2d84acaed62..00000000000 --- a/token/js/src/actions/createAssociatedTokenAccount.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'; -import { createAssociatedTokenAccountInstruction } from '../instructions/associatedTokenAccount.js'; -import { getAssociatedTokenAddressSync } from '../state/mint.js'; - -/** - * Create and initialize a new associated token account - * - * @param connection Connection to use - * @param payer Payer of the transaction and initialization fees - * @param mint Mint for the account - * @param owner Owner of the new account - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address) - * - * @return Address of the new associated token account - */ -export async function createAssociatedTokenAccount( - connection: Connection, - payer: Signer, - mint: PublicKey, - owner: PublicKey, - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, - allowOwnerOffCurve = false, -): Promise { - const associatedToken = getAssociatedTokenAddressSync( - mint, - owner, - allowOwnerOffCurve, - programId, - associatedTokenProgramId, - ); - - const transaction = new Transaction().add( - createAssociatedTokenAccountInstruction( - payer.publicKey, - associatedToken, - owner, - mint, - programId, - associatedTokenProgramId, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); - - return associatedToken; -} diff --git a/token/js/src/actions/createAssociatedTokenAccountIdempotent.ts b/token/js/src/actions/createAssociatedTokenAccountIdempotent.ts deleted file mode 100644 index 0f38c43a2d1..00000000000 --- a/token/js/src/actions/createAssociatedTokenAccountIdempotent.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'; -import { createAssociatedTokenAccountIdempotentInstruction } from '../instructions/associatedTokenAccount.js'; -import { getAssociatedTokenAddressSync } from '../state/mint.js'; - -/** - * Create and initialize a new associated token account - * The instruction will succeed even if the associated token account already exists - * - * @param connection Connection to use - * @param payer Payer of the transaction and initialization fees - * @param mint Mint for the account - * @param owner Owner of the new account - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address) - * - * @return Address of the new or existing associated token account - */ -export async function createAssociatedTokenAccountIdempotent( - connection: Connection, - payer: Signer, - mint: PublicKey, - owner: PublicKey, - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, - allowOwnerOffCurve = false, -): Promise { - const associatedToken = getAssociatedTokenAddressSync( - mint, - owner, - allowOwnerOffCurve, - programId, - associatedTokenProgramId, - ); - - const transaction = new Transaction().add( - createAssociatedTokenAccountIdempotentInstruction( - payer.publicKey, - associatedToken, - owner, - mint, - programId, - associatedTokenProgramId, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); - - return associatedToken; -} diff --git a/token/js/src/actions/createMint.ts b/token/js/src/actions/createMint.ts deleted file mode 100644 index 4a35e9171de..00000000000 --- a/token/js/src/actions/createMint.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair, sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createInitializeMint2Instruction } from '../instructions/initializeMint2.js'; -import { getMinimumBalanceForRentExemptMint, MINT_SIZE } from '../state/mint.js'; - -/** - * Create and initialize a new mint - * - * @param connection Connection to use - * @param payer Payer of the transaction and initialization fees - * @param mintAuthority Account or multisig that will control minting - * @param freezeAuthority Optional account or multisig that can freeze token accounts - * @param decimals Location of the decimal place - * @param keypair Optional keypair, defaulting to a new random one - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Address of the new mint - */ -export async function createMint( - connection: Connection, - payer: Signer, - mintAuthority: PublicKey, - freezeAuthority: PublicKey | null, - decimals: number, - keypair = Keypair.generate(), - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const lamports = await getMinimumBalanceForRentExemptMint(connection); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: keypair.publicKey, - space: MINT_SIZE, - lamports, - programId, - }), - createInitializeMint2Instruction(keypair.publicKey, decimals, mintAuthority, freezeAuthority, programId), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, keypair], confirmOptions); - - return keypair.publicKey; -} diff --git a/token/js/src/actions/createMultisig.ts b/token/js/src/actions/createMultisig.ts deleted file mode 100644 index 3717ee51d4b..00000000000 --- a/token/js/src/actions/createMultisig.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair, sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createInitializeMultisigInstruction } from '../instructions/initializeMultisig.js'; -import { getMinimumBalanceForRentExemptMultisig, MULTISIG_SIZE } from '../state/multisig.js'; - -/** - * Create and initialize a new multisig - * - * @param connection Connection to use - * @param payer Payer of the transaction and initialization fees - * @param signers Full set of signers - * @param m Number of required signatures - * @param keypair Optional keypair, defaulting to a new random one - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Address of the new multisig - */ -export async function createMultisig( - connection: Connection, - payer: Signer, - signers: PublicKey[], - m: number, - keypair = Keypair.generate(), - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const lamports = await getMinimumBalanceForRentExemptMultisig(connection); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: keypair.publicKey, - space: MULTISIG_SIZE, - lamports, - programId, - }), - createInitializeMultisigInstruction(keypair.publicKey, signers, m, programId), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, keypair], confirmOptions); - - return keypair.publicKey; -} diff --git a/token/js/src/actions/createNativeMint.ts b/token/js/src/actions/createNativeMint.ts deleted file mode 100644 index ec4119cd952..00000000000 --- a/token/js/src/actions/createNativeMint.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ConfirmOptions, Connection, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { NATIVE_MINT_2022, TOKEN_2022_PROGRAM_ID } from '../constants.js'; -import { createCreateNativeMintInstruction } from '../instructions/createNativeMint.js'; - -/** - * Create native mint - * - * @param connection Connection to use - * @param payer Payer of the transaction and initialization fees - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * @param nativeMint Native mint id associated with program - */ -export async function createNativeMint( - connection: Connection, - payer: Signer, - confirmOptions?: ConfirmOptions, - nativeMint = NATIVE_MINT_2022, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const transaction = new Transaction().add( - createCreateNativeMintInstruction(payer.publicKey, nativeMint, programId), - ); - await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); -} diff --git a/token/js/src/actions/createWrappedNativeAccount.ts b/token/js/src/actions/createWrappedNativeAccount.ts deleted file mode 100644 index 9064fb9ad48..00000000000 --- a/token/js/src/actions/createWrappedNativeAccount.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { ConfirmOptions, Connection, Keypair, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js'; -import { ASSOCIATED_TOKEN_PROGRAM_ID, NATIVE_MINT, TOKEN_PROGRAM_ID } from '../constants.js'; -import { createAssociatedTokenAccountInstruction } from '../instructions/associatedTokenAccount.js'; -import { createInitializeAccountInstruction } from '../instructions/initializeAccount.js'; -import { createSyncNativeInstruction } from '../instructions/syncNative.js'; -import { ACCOUNT_SIZE, getMinimumBalanceForRentExemptAccount } from '../state/account.js'; -import { getAssociatedTokenAddressSync } from '../state/mint.js'; -import { createAccount } from './createAccount.js'; - -/** - * Create, initialize, and fund a new wrapped native SOL account - * - * @param connection Connection to use - * @param payer Payer of the transaction and initialization fees - * @param owner Owner of the new token account - * @param amount Number of lamports to wrap - * @param keypair Optional keypair, defaulting to the associated token account for the native mint and `owner` - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Address of the new wrapped native SOL account - */ -export async function createWrappedNativeAccount( - connection: Connection, - payer: Signer, - owner: PublicKey, - amount: number, - keypair?: Keypair, - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, - nativeMint = NATIVE_MINT, -): Promise { - // If the amount provided is explicitly 0 or NaN, just create the account without funding it - if (!amount) return await createAccount(connection, payer, nativeMint, owner, keypair, confirmOptions, programId); - - // If a keypair isn't provided, create the account at the owner's ATA for the native mint and return its address - if (!keypair) { - const associatedToken = getAssociatedTokenAddressSync( - nativeMint, - owner, - false, - programId, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - - const transaction = new Transaction().add( - createAssociatedTokenAccountInstruction( - payer.publicKey, - associatedToken, - owner, - nativeMint, - programId, - ASSOCIATED_TOKEN_PROGRAM_ID, - ), - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: associatedToken, - lamports: amount, - }), - createSyncNativeInstruction(associatedToken, programId), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); - - return associatedToken; - } - - // Otherwise, create the account with the provided keypair and return its public key - const lamports = await getMinimumBalanceForRentExemptAccount(connection); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: keypair.publicKey, - space: ACCOUNT_SIZE, - lamports, - programId, - }), - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: keypair.publicKey, - lamports: amount, - }), - createInitializeAccountInstruction(keypair.publicKey, nativeMint, owner, programId), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, keypair], confirmOptions); - - return keypair.publicKey; -} diff --git a/token/js/src/actions/freezeAccount.ts b/token/js/src/actions/freezeAccount.ts deleted file mode 100644 index 9ec7dae424d..00000000000 --- a/token/js/src/actions/freezeAccount.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createFreezeAccountInstruction } from '../instructions/freezeAccount.js'; -import { getSigners } from './internal.js'; - -/** - * Freeze a token account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Account to freeze - * @param mint Mint for the account - * @param authority Mint freeze authority - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function freezeAccount( - connection: Connection, - payer: Signer, - account: PublicKey, - mint: PublicKey, - authority: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - createFreezeAccountInstruction(account, mint, authorityPublicKey, multiSigners, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/getOrCreateAssociatedTokenAccount.ts b/token/js/src/actions/getOrCreateAssociatedTokenAccount.ts deleted file mode 100644 index 0bdaaf17cd0..00000000000 --- a/token/js/src/actions/getOrCreateAssociatedTokenAccount.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { Commitment, ConfirmOptions, Connection, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenAccountNotFoundError, - TokenInvalidAccountOwnerError, - TokenInvalidMintError, - TokenInvalidOwnerError, -} from '../errors.js'; -import { createAssociatedTokenAccountInstruction } from '../instructions/associatedTokenAccount.js'; -import type { Account } from '../state/account.js'; -import { getAccount } from '../state/account.js'; -import { getAssociatedTokenAddressSync } from '../state/mint.js'; - -/** - * Retrieve the associated token account, or create it if it doesn't exist - * - * @param connection Connection to use - * @param payer Payer of the transaction and initialization fees - * @param mint Mint associated with the account to set or verify - * @param owner Owner of the account to set or verify - * @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address) - * @param commitment Desired level of commitment for querying the state - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * - * @return Address of the new associated token account - */ -export async function getOrCreateAssociatedTokenAccount( - connection: Connection, - payer: Signer, - mint: PublicKey, - owner: PublicKey, - allowOwnerOffCurve = false, - commitment?: Commitment, - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -): Promise { - const associatedToken = getAssociatedTokenAddressSync( - mint, - owner, - allowOwnerOffCurve, - programId, - associatedTokenProgramId, - ); - - // This is the optimal logic, considering TX fee, client-side computation, RPC roundtrips and guaranteed idempotent. - // Sadly we can't do this atomically. - let account: Account; - try { - account = await getAccount(connection, associatedToken, commitment, programId); - } catch (error: unknown) { - // TokenAccountNotFoundError can be possible if the associated address has already received some lamports, - // becoming a system account. Assuming program derived addressing is safe, this is the only case for the - // TokenInvalidAccountOwnerError in this code path. - if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { - // As this isn't atomic, it's possible others can create associated accounts meanwhile. - try { - const transaction = new Transaction().add( - createAssociatedTokenAccountInstruction( - payer.publicKey, - associatedToken, - owner, - mint, - programId, - associatedTokenProgramId, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); - } catch (error: unknown) { - // Ignore all errors; for now there is no API-compatible way to selectively ignore the expected - // instruction error if the associated account exists already. - } - - // Now this should always succeed - account = await getAccount(connection, associatedToken, commitment, programId); - } else { - throw error; - } - } - - if (!account.mint.equals(mint)) throw new TokenInvalidMintError(); - if (!account.owner.equals(owner)) throw new TokenInvalidOwnerError(); - - return account; -} diff --git a/token/js/src/actions/index.ts b/token/js/src/actions/index.ts deleted file mode 100644 index 03f24f25d07..00000000000 --- a/token/js/src/actions/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export * from './amountToUiAmount.js'; -export * from './approve.js'; -export * from './approveChecked.js'; -export * from './burn.js'; -export * from './burnChecked.js'; -export * from './closeAccount.js'; -export * from './createAccount.js'; -export * from './createAssociatedTokenAccount.js'; -export * from './createAssociatedTokenAccountIdempotent.js'; -export * from './createMint.js'; -export * from './createMultisig.js'; -export * from './createNativeMint.js'; -export * from './createWrappedNativeAccount.js'; -export * from './freezeAccount.js'; -export * from './getOrCreateAssociatedTokenAccount.js'; -export * from './mintTo.js'; -export * from './mintToChecked.js'; -export * from './recoverNested.js'; -export * from './revoke.js'; -export * from './setAuthority.js'; -export * from './syncNative.js'; -export * from './thawAccount.js'; -export * from './transfer.js'; -export * from './transferChecked.js'; -export * from './uiAmountToAmount.js'; diff --git a/token/js/src/actions/internal.ts b/token/js/src/actions/internal.ts deleted file mode 100644 index 8af17111659..00000000000 --- a/token/js/src/actions/internal.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; - -/** @internal */ -export function getSigners(signerOrMultisig: Signer | PublicKey, multiSigners: Signer[]): [PublicKey, Signer[]] { - return signerOrMultisig instanceof PublicKey - ? [signerOrMultisig, multiSigners] - : [signerOrMultisig.publicKey, [signerOrMultisig]]; -} diff --git a/token/js/src/actions/mintTo.ts b/token/js/src/actions/mintTo.ts deleted file mode 100644 index 0804d63a0fb..00000000000 --- a/token/js/src/actions/mintTo.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createMintToInstruction } from '../instructions/mintTo.js'; -import { getSigners } from './internal.js'; - -/** - * Mint tokens to an account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint for the account - * @param destination Address of the account to mint to - * @param authority Minting authority - * @param amount Amount to mint - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function mintTo( - connection: Connection, - payer: Signer, - mint: PublicKey, - destination: PublicKey, - authority: Signer | PublicKey, - amount: number | bigint, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - createMintToInstruction(mint, destination, authorityPublicKey, amount, multiSigners, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/mintToChecked.ts b/token/js/src/actions/mintToChecked.ts deleted file mode 100644 index 3b988fe4476..00000000000 --- a/token/js/src/actions/mintToChecked.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createMintToCheckedInstruction } from '../instructions/mintToChecked.js'; -import { getSigners } from './internal.js'; - -/** - * Mint tokens to an account, asserting the token mint and decimals - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint for the account - * @param destination Address of the account to mint to - * @param authority Minting authority - * @param amount Amount to mint - * @param decimals Number of decimals in amount to mint - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function mintToChecked( - connection: Connection, - payer: Signer, - mint: PublicKey, - destination: PublicKey, - authority: Signer | PublicKey, - amount: number | bigint, - decimals: number, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - createMintToCheckedInstruction( - mint, - destination, - authorityPublicKey, - amount, - decimals, - multiSigners, - programId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/recoverNested.ts b/token/js/src/actions/recoverNested.ts deleted file mode 100644 index ac8e50f7f8b..00000000000 --- a/token/js/src/actions/recoverNested.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'; -import { createRecoverNestedInstruction } from '../instructions/associatedTokenAccount.js'; -import { getAssociatedTokenAddressSync } from '../state/mint.js'; - -/** - * Recover funds funds in an associated token account which is owned by an associated token account - * - * @param connection Connection to use - * @param payer Payer of the transaction and initialization fees - * @param owner Owner of original ATA - * @param mint Mint for the original ATA - * @param nestedMint Mint for the nested ATA - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * - * @return Signature of the confirmed transaction - */ -export async function recoverNested( - connection: Connection, - payer: Signer, - owner: Signer, - mint: PublicKey, - nestedMint: PublicKey, - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -): Promise { - const ownerAssociatedToken = getAssociatedTokenAddressSync( - mint, - owner.publicKey, - false, - programId, - associatedTokenProgramId, - ); - - const destinationAssociatedToken = getAssociatedTokenAddressSync( - nestedMint, - owner.publicKey, - false, - programId, - associatedTokenProgramId, - ); - - const nestedAssociatedToken = getAssociatedTokenAddressSync( - nestedMint, - ownerAssociatedToken, - true, - programId, - associatedTokenProgramId, - ); - - const transaction = new Transaction().add( - createRecoverNestedInstruction( - nestedAssociatedToken, - nestedMint, - destinationAssociatedToken, - ownerAssociatedToken, - mint, - owner.publicKey, - programId, - associatedTokenProgramId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, owner], confirmOptions); -} diff --git a/token/js/src/actions/revoke.ts b/token/js/src/actions/revoke.ts deleted file mode 100644 index e89a222bd33..00000000000 --- a/token/js/src/actions/revoke.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createRevokeInstruction } from '../instructions/revoke.js'; -import { getSigners } from './internal.js'; - -/** - * Revoke approval for the transfer of tokens from an account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Address of the token account - * @param owner Owner of the account - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function revoke( - connection: Connection, - payer: Signer, - account: PublicKey, - owner: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createRevokeInstruction(account, ownerPublicKey, multiSigners, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/setAuthority.ts b/token/js/src/actions/setAuthority.ts deleted file mode 100644 index 340b36dc427..00000000000 --- a/token/js/src/actions/setAuthority.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import type { AuthorityType } from '../instructions/setAuthority.js'; -import { createSetAuthorityInstruction } from '../instructions/setAuthority.js'; -import { getSigners } from './internal.js'; - -/** - * Assign a new authority to the account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Address of the account - * @param currentAuthority Current authority of the specified type - * @param authorityType Type of authority to set - * @param newAuthority New authority of the account - * @param multiSigners Signing accounts if `currentAuthority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function setAuthority( - connection: Connection, - payer: Signer, - account: PublicKey, - currentAuthority: Signer | PublicKey, - authorityType: AuthorityType, - newAuthority: PublicKey | null, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [currentAuthorityPublicKey, signers] = getSigners(currentAuthority, multiSigners); - - const transaction = new Transaction().add( - createSetAuthorityInstruction( - account, - currentAuthorityPublicKey, - authorityType, - newAuthority, - multiSigners, - programId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/syncNative.ts b/token/js/src/actions/syncNative.ts deleted file mode 100644 index 7e6a8909324..00000000000 --- a/token/js/src/actions/syncNative.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createSyncNativeInstruction } from '../instructions/syncNative.js'; - -/** - * Sync the balance of a native SPL token account to the underlying system account's lamports - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Native account to sync - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function syncNative( - connection: Connection, - payer: Signer, - account: PublicKey, - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const transaction = new Transaction().add(createSyncNativeInstruction(account, programId)); - - return await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); -} diff --git a/token/js/src/actions/thawAccount.ts b/token/js/src/actions/thawAccount.ts deleted file mode 100644 index 0c2d1c27586..00000000000 --- a/token/js/src/actions/thawAccount.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createThawAccountInstruction } from '../instructions/thawAccount.js'; -import { getSigners } from './internal.js'; - -/** - * Thaw (unfreeze) a token account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Account to thaw - * @param mint Mint for the account - * @param authority Mint freeze authority - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function thawAccount( - connection: Connection, - payer: Signer, - account: PublicKey, - mint: PublicKey, - authority: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - createThawAccountInstruction(account, mint, authorityPublicKey, multiSigners, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/transfer.ts b/token/js/src/actions/transfer.ts deleted file mode 100644 index adb1f94b842..00000000000 --- a/token/js/src/actions/transfer.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createTransferInstruction } from '../instructions/transfer.js'; -import { getSigners } from './internal.js'; - -/** - * Transfer tokens from one account to another - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param source Source account - * @param destination Destination account - * @param owner Owner of the source account - * @param amount Number of tokens to transfer - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function transfer( - connection: Connection, - payer: Signer, - source: PublicKey, - destination: PublicKey, - owner: Signer | PublicKey, - amount: number | bigint, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createTransferInstruction(source, destination, ownerPublicKey, amount, multiSigners, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/transferChecked.ts b/token/js/src/actions/transferChecked.ts deleted file mode 100644 index 356cbdbca32..00000000000 --- a/token/js/src/actions/transferChecked.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createTransferCheckedInstruction } from '../instructions/transferChecked.js'; -import { getSigners } from './internal.js'; - -/** - * Transfer tokens from one account to another, asserting the token mint and decimals - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param source Source account - * @param mint Mint for the account - * @param destination Destination account - * @param owner Owner of the source account - * @param amount Number of tokens to transfer - * @param decimals Number of decimals in transfer amount - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function transferChecked( - connection: Connection, - payer: Signer, - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - owner: Signer | PublicKey, - amount: number | bigint, - decimals: number, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createTransferCheckedInstruction( - source, - mint, - destination, - ownerPublicKey, - amount, - decimals, - multiSigners, - programId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/actions/uiAmountToAmount.ts b/token/js/src/actions/uiAmountToAmount.ts deleted file mode 100644 index e6bdaf61f6a..00000000000 --- a/token/js/src/actions/uiAmountToAmount.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { u64 } from '@solana/buffer-layout-utils'; -import type { Connection, PublicKey, Signer, TransactionError } from '@solana/web3.js'; -import { Transaction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { createUiAmountToAmountInstruction } from '../instructions/uiAmountToAmount.js'; - -/** - * Amount as a string using mint-prescribed decimals - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint for the account - * @param amount Ui Amount of tokens to be converted to Amount - * @param programId SPL Token program account - * - * @return Ui Amount generated - */ -export async function uiAmountToAmount( - connection: Connection, - payer: Signer, - mint: PublicKey, - amount: string, - programId = TOKEN_PROGRAM_ID, -): Promise { - const transaction = new Transaction().add(createUiAmountToAmountInstruction(mint, amount, programId)); - const { returnData, err } = (await connection.simulateTransaction(transaction, [payer], false)).value; - if (returnData) { - const data = Buffer.from(returnData.data[0], returnData.data[1]); - return u64().decode(data); - } - return err; -} diff --git a/token/js/src/constants.ts b/token/js/src/constants.ts deleted file mode 100644 index 717755aff7d..00000000000 --- a/token/js/src/constants.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; - -/** Address of the SPL Token program */ -export const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); - -/** Address of the SPL Token 2022 program */ -export const TOKEN_2022_PROGRAM_ID = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'); - -/** Address of the SPL Associated Token Account program */ -export const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); - -/** Address of the special mint for wrapped native SOL in spl-token */ -export const NATIVE_MINT = new PublicKey('So11111111111111111111111111111111111111112'); - -/** Address of the special mint for wrapped native SOL in spl-token-2022 */ -export const NATIVE_MINT_2022 = new PublicKey('9pan9bMn5HatX4EJdBwg9VgCa7Uz5HL8N1m5D3NdXejP'); - -/** Check that the token program provided is not `Tokenkeg...`, useful when using extensions */ -export function programSupportsExtensions(programId: PublicKey): boolean { - if (programId.equals(TOKEN_PROGRAM_ID)) { - return false; - } else { - return true; - } -} diff --git a/token/js/src/errors.ts b/token/js/src/errors.ts deleted file mode 100644 index 4671200eb51..00000000000 --- a/token/js/src/errors.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** Base class for errors */ -export abstract class TokenError extends Error { - constructor(message?: string) { - super(message); - } -} - -/** Thrown if an account is not found at the expected address */ -export class TokenAccountNotFoundError extends TokenError { - name = 'TokenAccountNotFoundError'; -} - -/** Thrown if a program state account is not a valid Account */ -export class TokenInvalidAccountError extends TokenError { - name = 'TokenInvalidAccountError'; -} - -/** Thrown if a program state account does not contain valid data */ -export class TokenInvalidAccountDataError extends TokenError { - name = 'TokenInvalidAccountDataError'; -} - -/** Thrown if a program state account is not owned by the expected token program */ -export class TokenInvalidAccountOwnerError extends TokenError { - name = 'TokenInvalidAccountOwnerError'; -} - -/** Thrown if the byte length of an program state account doesn't match the expected size */ -export class TokenInvalidAccountSizeError extends TokenError { - name = 'TokenInvalidAccountSizeError'; -} - -/** Thrown if the mint of a token account doesn't match the expected mint */ -export class TokenInvalidMintError extends TokenError { - name = 'TokenInvalidMintError'; -} - -/** Thrown if the owner of a token account doesn't match the expected owner */ -export class TokenInvalidOwnerError extends TokenError { - name = 'TokenInvalidOwnerError'; -} - -/** Thrown if the owner of a token account is a PDA (Program Derived Address) */ -export class TokenOwnerOffCurveError extends TokenError { - name = 'TokenOwnerOffCurveError'; -} - -/** Thrown if an instruction's program is invalid */ -export class TokenInvalidInstructionProgramError extends TokenError { - name = 'TokenInvalidInstructionProgramError'; -} - -/** Thrown if an instruction's keys are invalid */ -export class TokenInvalidInstructionKeysError extends TokenError { - name = 'TokenInvalidInstructionKeysError'; -} - -/** Thrown if an instruction's data is invalid */ -export class TokenInvalidInstructionDataError extends TokenError { - name = 'TokenInvalidInstructionDataError'; -} - -/** Thrown if an instruction's type is invalid */ -export class TokenInvalidInstructionTypeError extends TokenError { - name = 'TokenInvalidInstructionTypeError'; -} - -/** Thrown if the program does not support the desired instruction */ -export class TokenUnsupportedInstructionError extends TokenError { - name = 'TokenUnsupportedInstructionError'; -} - -/** Thrown if the transfer hook extra accounts contains an invalid account index */ -export class TokenTransferHookAccountNotFound extends TokenError { - name = 'TokenTransferHookAccountNotFound'; -} - -/** Thrown if the transfer hook extra accounts contains an invalid seed */ -export class TokenTransferHookInvalidSeed extends TokenError { - name = 'TokenTransferHookInvalidSeed'; -} - -/** Thrown if account data required by an extra account meta seed config could not be fetched */ -export class TokenTransferHookAccountDataNotFound extends TokenError { - name = 'TokenTransferHookAccountDataNotFound'; -} diff --git a/token/js/src/extensions/accountType.ts b/token/js/src/extensions/accountType.ts deleted file mode 100644 index 6389c930f68..00000000000 --- a/token/js/src/extensions/accountType.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum AccountType { - Uninitialized, - Mint, - Account, -} -export const ACCOUNT_TYPE_SIZE = 1; diff --git a/token/js/src/extensions/cpiGuard/actions.ts b/token/js/src/extensions/cpiGuard/actions.ts deleted file mode 100644 index 905d4b8a794..00000000000 --- a/token/js/src/extensions/cpiGuard/actions.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { getSigners } from '../../actions/internal.js'; -import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { createDisableCpiGuardInstruction, createEnableCpiGuardInstruction } from './instructions.js'; - -/** - * Enable CPI Guard on the given account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Account to modify - * @param owner Owner of the account - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function enableCpiGuard( - connection: Connection, - payer: Signer, - account: PublicKey, - owner: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createEnableCpiGuardInstruction(account, ownerPublicKey, signers, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Disable CPI Guard on the given account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Account to modify - * @param owner Owner of the account - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function disableCpiGuard( - connection: Connection, - payer: Signer, - account: PublicKey, - owner: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createDisableCpiGuardInstruction(account, ownerPublicKey, signers, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/extensions/cpiGuard/index.ts b/token/js/src/extensions/cpiGuard/index.ts deleted file mode 100644 index 5e28fd6b10a..00000000000 --- a/token/js/src/extensions/cpiGuard/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './actions.js'; -export * from './instructions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/cpiGuard/instructions.ts b/token/js/src/extensions/cpiGuard/instructions.ts deleted file mode 100644 index 04e5d65f9db..00000000000 --- a/token/js/src/extensions/cpiGuard/instructions.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { TokenUnsupportedInstructionError } from '../../errors.js'; -import { addSigners } from '../../instructions/internal.js'; -import { TokenInstruction } from '../../instructions/types.js'; - -export enum CpiGuardInstruction { - Enable = 0, - Disable = 1, -} - -/** TODO: docs */ -export interface CpiGuardInstructionData { - instruction: TokenInstruction.CpiGuardExtension; - cpiGuardInstruction: CpiGuardInstruction; -} - -/** TODO: docs */ -export const cpiGuardInstructionData = struct([u8('instruction'), u8('cpiGuardInstruction')]); - -/** - * Construct an EnableCpiGuard instruction - * - * @param account Token account to update - * @param authority The account's owner/delegate - * @param signers The signer account(s) - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createEnableCpiGuardInstruction( - account: PublicKey, - authority: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - return createCpiGuardInstruction(CpiGuardInstruction.Enable, account, authority, multiSigners, programId); -} - -/** - * Construct a DisableCpiGuard instruction - * - * @param account Token account to update - * @param authority The account's owner/delegate - * @param signers The signer account(s) - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createDisableCpiGuardInstruction( - account: PublicKey, - authority: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - return createCpiGuardInstruction(CpiGuardInstruction.Disable, account, authority, multiSigners, programId); -} - -function createCpiGuardInstruction( - cpiGuardInstruction: CpiGuardInstruction, - account: PublicKey, - authority: PublicKey, - multiSigners: (Signer | PublicKey)[], - programId: PublicKey, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = addSigners([{ pubkey: account, isSigner: false, isWritable: true }], authority, multiSigners); - - const data = Buffer.alloc(cpiGuardInstructionData.span); - cpiGuardInstructionData.encode( - { - instruction: TokenInstruction.CpiGuardExtension, - cpiGuardInstruction, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} diff --git a/token/js/src/extensions/cpiGuard/state.ts b/token/js/src/extensions/cpiGuard/state.ts deleted file mode 100644 index b9e5682ed9e..00000000000 --- a/token/js/src/extensions/cpiGuard/state.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { struct } from '@solana/buffer-layout'; -import { bool } from '@solana/buffer-layout-utils'; -import type { Account } from '../../state/account.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; - -/** CpiGuard as stored by the program */ -export interface CpiGuard { - /** Lock certain token operations from taking place within CPI for this account */ - lockCpi: boolean; -} - -/** Buffer layout for de/serializing a CPI Guard extension */ -export const CpiGuardLayout = struct([bool('lockCpi')]); - -export const CPI_GUARD_SIZE = CpiGuardLayout.span; - -export function getCpiGuard(account: Account): CpiGuard | null { - const extensionData = getExtensionData(ExtensionType.CpiGuard, account.tlvData); - if (extensionData !== null) { - return CpiGuardLayout.decode(extensionData); - } else { - return null; - } -} diff --git a/token/js/src/extensions/defaultAccountState/actions.ts b/token/js/src/extensions/defaultAccountState/actions.ts deleted file mode 100644 index 09a3ac16f2b..00000000000 --- a/token/js/src/extensions/defaultAccountState/actions.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { getSigners } from '../../actions/internal.js'; -import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import type { AccountState } from '../../state/account.js'; -import { - createInitializeDefaultAccountStateInstruction, - createUpdateDefaultAccountStateInstruction, -} from './instructions.js'; - -/** - * Initialize a default account state on a mint - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint to initialize with extension - * @param state Account state with which to initialize new accounts - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function initializeDefaultAccountState( - connection: Connection, - payer: Signer, - mint: PublicKey, - state: AccountState, - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const transaction = new Transaction().add(createInitializeDefaultAccountStateInstruction(mint, state, programId)); - - return await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); -} - -/** - * Update the default account state on a mint - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint to modify - * @param state New account state to set on created accounts - * @param freezeAuthority Freeze authority of the mint - * @param multiSigners Signing accounts if `freezeAuthority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function updateDefaultAccountState( - connection: Connection, - payer: Signer, - mint: PublicKey, - state: AccountState, - freezeAuthority: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [freezeAuthorityPublicKey, signers] = getSigners(freezeAuthority, multiSigners); - - const transaction = new Transaction().add( - createUpdateDefaultAccountStateInstruction(mint, state, freezeAuthorityPublicKey, signers, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/extensions/defaultAccountState/index.ts b/token/js/src/extensions/defaultAccountState/index.ts deleted file mode 100644 index 5e28fd6b10a..00000000000 --- a/token/js/src/extensions/defaultAccountState/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './actions.js'; -export * from './instructions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/defaultAccountState/instructions.ts b/token/js/src/extensions/defaultAccountState/instructions.ts deleted file mode 100644 index 02b16d06a59..00000000000 --- a/token/js/src/extensions/defaultAccountState/instructions.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { TokenUnsupportedInstructionError } from '../../errors.js'; -import { addSigners } from '../../instructions/internal.js'; -import { TokenInstruction } from '../../instructions/types.js'; -import type { AccountState } from '../../state/account.js'; - -export enum DefaultAccountStateInstruction { - Initialize = 0, - Update = 1, -} - -/** TODO: docs */ -export interface DefaultAccountStateInstructionData { - instruction: TokenInstruction.DefaultAccountStateExtension; - defaultAccountStateInstruction: DefaultAccountStateInstruction; - accountState: AccountState; -} - -/** TODO: docs */ -export const defaultAccountStateInstructionData = struct([ - u8('instruction'), - u8('defaultAccountStateInstruction'), - u8('accountState'), -]); - -/** - * Construct an InitializeDefaultAccountState instruction - * - * @param mint Mint to initialize - * @param accountState Default account state to set on all new accounts - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeDefaultAccountStateInstruction( - mint: PublicKey, - accountState: AccountState, - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - const data = Buffer.alloc(defaultAccountStateInstructionData.span); - defaultAccountStateInstructionData.encode( - { - instruction: TokenInstruction.DefaultAccountStateExtension, - defaultAccountStateInstruction: DefaultAccountStateInstruction.Initialize, - accountState, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** - * Construct an UpdateDefaultAccountState instruction - * - * @param mint Mint to update - * @param accountState Default account state to set on all accounts - * @param freezeAuthority The mint's freeze authority - * @param signers The signer account(s) for a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createUpdateDefaultAccountStateInstruction( - mint: PublicKey, - accountState: AccountState, - freezeAuthority: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - - const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], freezeAuthority, multiSigners); - const data = Buffer.alloc(defaultAccountStateInstructionData.span); - defaultAccountStateInstructionData.encode( - { - instruction: TokenInstruction.DefaultAccountStateExtension, - defaultAccountStateInstruction: DefaultAccountStateInstruction.Update, - accountState, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} diff --git a/token/js/src/extensions/defaultAccountState/state.ts b/token/js/src/extensions/defaultAccountState/state.ts deleted file mode 100644 index bd6cb0950f6..00000000000 --- a/token/js/src/extensions/defaultAccountState/state.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountState } from '../../state/account.js'; -import type { Mint } from '../../state/mint.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; - -/** DefaultAccountState as stored by the program */ -export interface DefaultAccountState { - /** Default AccountState in which new accounts are initialized */ - state: AccountState; -} - -/** Buffer layout for de/serializing a transfer fee config extension */ -export const DefaultAccountStateLayout = struct([u8('state')]); - -export const DEFAULT_ACCOUNT_STATE_SIZE = DefaultAccountStateLayout.span; - -export function getDefaultAccountState(mint: Mint): DefaultAccountState | null { - const extensionData = getExtensionData(ExtensionType.DefaultAccountState, mint.tlvData); - if (extensionData !== null) { - return DefaultAccountStateLayout.decode(extensionData); - } else { - return null; - } -} diff --git a/token/js/src/extensions/extensionType.ts b/token/js/src/extensions/extensionType.ts deleted file mode 100644 index f3a33889c6e..00000000000 --- a/token/js/src/extensions/extensionType.ts +++ /dev/null @@ -1,304 +0,0 @@ -import type { AccountInfo, PublicKey } from '@solana/web3.js'; - -import { ACCOUNT_SIZE } from '../state/account.js'; -import type { Mint } from '../state/mint.js'; -import { MINT_SIZE, unpackMint } from '../state/mint.js'; -import { MULTISIG_SIZE } from '../state/multisig.js'; -import { ACCOUNT_TYPE_SIZE } from './accountType.js'; -import { CPI_GUARD_SIZE } from './cpiGuard/index.js'; -import { DEFAULT_ACCOUNT_STATE_SIZE } from './defaultAccountState/index.js'; -import { TOKEN_GROUP_SIZE, TOKEN_GROUP_MEMBER_SIZE } from './tokenGroup/index.js'; -import { GROUP_MEMBER_POINTER_SIZE } from './groupMemberPointer/state.js'; -import { GROUP_POINTER_SIZE } from './groupPointer/state.js'; -import { IMMUTABLE_OWNER_SIZE } from './immutableOwner.js'; -import { INTEREST_BEARING_MINT_CONFIG_STATE_SIZE } from './interestBearingMint/state.js'; -import { MEMO_TRANSFER_SIZE } from './memoTransfer/index.js'; -import { METADATA_POINTER_SIZE } from './metadataPointer/state.js'; -import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority.js'; -import { NON_TRANSFERABLE_SIZE, NON_TRANSFERABLE_ACCOUNT_SIZE } from './nonTransferable.js'; -import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js'; -import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js'; -import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js'; -import { TOKEN_2022_PROGRAM_ID } from '../constants.js'; - -// Sequence from https://github.com/solana-labs/solana-program-library/blob/master/token/program-2022/src/extension/mod.rs#L903 -export enum ExtensionType { - Uninitialized, - TransferFeeConfig, - TransferFeeAmount, - MintCloseAuthority, - ConfidentialTransferMint, - ConfidentialTransferAccount, - DefaultAccountState, - ImmutableOwner, - MemoTransfer, - NonTransferable, - InterestBearingConfig, - CpiGuard, - PermanentDelegate, - NonTransferableAccount, - TransferHook, - TransferHookAccount, - // ConfidentialTransferFee, // Not implemented yet - // ConfidentialTransferFeeAmount, // Not implemented yet - MetadataPointer = 18, // Remove number once above extensions implemented - TokenMetadata = 19, // Remove number once above extensions implemented - GroupPointer = 20, - TokenGroup = 21, - GroupMemberPointer = 22, - TokenGroupMember = 23, -} - -export const TYPE_SIZE = 2; -export const LENGTH_SIZE = 2; - -function addTypeAndLengthToLen(len: number): number { - return len + TYPE_SIZE + LENGTH_SIZE; -} - -function isVariableLengthExtension(e: ExtensionType): boolean { - switch (e) { - case ExtensionType.TokenMetadata: - return true; - default: - return false; - } -} - -// NOTE: All of these should eventually use their type's Span instead of these -// constants. This is provided for at least creation to work. -export function getTypeLen(e: ExtensionType): number { - switch (e) { - case ExtensionType.Uninitialized: - return 0; - case ExtensionType.TransferFeeConfig: - return TRANSFER_FEE_CONFIG_SIZE; - case ExtensionType.TransferFeeAmount: - return TRANSFER_FEE_AMOUNT_SIZE; - case ExtensionType.MintCloseAuthority: - return MINT_CLOSE_AUTHORITY_SIZE; - case ExtensionType.ConfidentialTransferMint: - return 65; - case ExtensionType.ConfidentialTransferAccount: - return 295; - case ExtensionType.CpiGuard: - return CPI_GUARD_SIZE; - case ExtensionType.DefaultAccountState: - return DEFAULT_ACCOUNT_STATE_SIZE; - case ExtensionType.ImmutableOwner: - return IMMUTABLE_OWNER_SIZE; - case ExtensionType.MemoTransfer: - return MEMO_TRANSFER_SIZE; - case ExtensionType.MetadataPointer: - return METADATA_POINTER_SIZE; - case ExtensionType.NonTransferable: - return NON_TRANSFERABLE_SIZE; - case ExtensionType.InterestBearingConfig: - return INTEREST_BEARING_MINT_CONFIG_STATE_SIZE; - case ExtensionType.PermanentDelegate: - return PERMANENT_DELEGATE_SIZE; - case ExtensionType.NonTransferableAccount: - return NON_TRANSFERABLE_ACCOUNT_SIZE; - case ExtensionType.TransferHook: - return TRANSFER_HOOK_SIZE; - case ExtensionType.TransferHookAccount: - return TRANSFER_HOOK_ACCOUNT_SIZE; - case ExtensionType.GroupPointer: - return GROUP_POINTER_SIZE; - case ExtensionType.GroupMemberPointer: - return GROUP_MEMBER_POINTER_SIZE; - case ExtensionType.TokenGroup: - return TOKEN_GROUP_SIZE; - case ExtensionType.TokenGroupMember: - return TOKEN_GROUP_MEMBER_SIZE; - case ExtensionType.TokenMetadata: - throw Error(`Cannot get type length for variable extension type: ${e}`); - default: - throw Error(`Unknown extension type: ${e}`); - } -} - -export function isMintExtension(e: ExtensionType): boolean { - switch (e) { - case ExtensionType.TransferFeeConfig: - case ExtensionType.MintCloseAuthority: - case ExtensionType.ConfidentialTransferMint: - case ExtensionType.DefaultAccountState: - case ExtensionType.NonTransferable: - case ExtensionType.InterestBearingConfig: - case ExtensionType.PermanentDelegate: - case ExtensionType.TransferHook: - case ExtensionType.MetadataPointer: - case ExtensionType.TokenMetadata: - case ExtensionType.GroupPointer: - case ExtensionType.GroupMemberPointer: - case ExtensionType.TokenGroup: - case ExtensionType.TokenGroupMember: - return true; - case ExtensionType.Uninitialized: - case ExtensionType.TransferFeeAmount: - case ExtensionType.ConfidentialTransferAccount: - case ExtensionType.ImmutableOwner: - case ExtensionType.MemoTransfer: - case ExtensionType.CpiGuard: - case ExtensionType.NonTransferableAccount: - case ExtensionType.TransferHookAccount: - return false; - default: - throw Error(`Unknown extension type: ${e}`); - } -} - -export function isAccountExtension(e: ExtensionType): boolean { - switch (e) { - case ExtensionType.TransferFeeAmount: - case ExtensionType.ConfidentialTransferAccount: - case ExtensionType.ImmutableOwner: - case ExtensionType.MemoTransfer: - case ExtensionType.CpiGuard: - case ExtensionType.NonTransferableAccount: - case ExtensionType.TransferHookAccount: - return true; - case ExtensionType.Uninitialized: - case ExtensionType.TransferFeeConfig: - case ExtensionType.MintCloseAuthority: - case ExtensionType.ConfidentialTransferMint: - case ExtensionType.DefaultAccountState: - case ExtensionType.NonTransferable: - case ExtensionType.InterestBearingConfig: - case ExtensionType.PermanentDelegate: - case ExtensionType.TransferHook: - case ExtensionType.MetadataPointer: - case ExtensionType.TokenMetadata: - case ExtensionType.GroupPointer: - case ExtensionType.GroupMemberPointer: - case ExtensionType.TokenGroup: - case ExtensionType.TokenGroupMember: - return false; - default: - throw Error(`Unknown extension type: ${e}`); - } -} - -export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType { - switch (e) { - case ExtensionType.TransferFeeConfig: - return ExtensionType.TransferFeeAmount; - case ExtensionType.ConfidentialTransferMint: - return ExtensionType.ConfidentialTransferAccount; - case ExtensionType.NonTransferable: - return ExtensionType.NonTransferableAccount; - case ExtensionType.TransferHook: - return ExtensionType.TransferHookAccount; - case ExtensionType.TransferFeeAmount: - case ExtensionType.ConfidentialTransferAccount: - case ExtensionType.CpiGuard: - case ExtensionType.DefaultAccountState: - case ExtensionType.ImmutableOwner: - case ExtensionType.MemoTransfer: - case ExtensionType.MintCloseAuthority: - case ExtensionType.MetadataPointer: - case ExtensionType.TokenMetadata: - case ExtensionType.Uninitialized: - case ExtensionType.InterestBearingConfig: - case ExtensionType.PermanentDelegate: - case ExtensionType.NonTransferableAccount: - case ExtensionType.TransferHookAccount: - case ExtensionType.GroupPointer: - case ExtensionType.GroupMemberPointer: - case ExtensionType.TokenGroup: - case ExtensionType.TokenGroupMember: - return ExtensionType.Uninitialized; - } -} - -function getLen( - extensionTypes: ExtensionType[], - baseSize: number, - variableLengthExtensions: { [E in ExtensionType]?: number } = {}, -): number { - if (extensionTypes.length === 0 && Object.keys(variableLengthExtensions).length === 0) { - return baseSize; - } else { - const accountLength = - ACCOUNT_SIZE + - ACCOUNT_TYPE_SIZE + - extensionTypes - .filter((element, i) => i === extensionTypes.indexOf(element)) - .map(element => addTypeAndLengthToLen(getTypeLen(element))) - .reduce((a, b) => a + b, 0) + - Object.entries(variableLengthExtensions) - .map(([extension, len]) => { - if (!isVariableLengthExtension(Number(extension))) { - throw Error(`Extension ${extension} is not variable length`); - } - return addTypeAndLengthToLen(len); - }) - .reduce((a, b) => a + b, 0); - if (accountLength === MULTISIG_SIZE) { - return accountLength + TYPE_SIZE; - } else { - return accountLength; - } - } -} - -export function getMintLen( - extensionTypes: ExtensionType[], - variableLengthExtensions: { [E in ExtensionType]?: number } = {}, -): number { - return getLen(extensionTypes, MINT_SIZE, variableLengthExtensions); -} - -export function getAccountLen(extensionTypes: ExtensionType[]): number { - // There are currently no variable length extensions for accounts - return getLen(extensionTypes, ACCOUNT_SIZE); -} - -export function getExtensionData(extension: ExtensionType, tlvData: Buffer): Buffer | null { - let extensionTypeIndex = 0; - while (addTypeAndLengthToLen(extensionTypeIndex) <= tlvData.length) { - const entryType = tlvData.readUInt16LE(extensionTypeIndex); - const entryLength = tlvData.readUInt16LE(extensionTypeIndex + TYPE_SIZE); - const typeIndex = addTypeAndLengthToLen(extensionTypeIndex); - if (entryType == extension) { - return tlvData.slice(typeIndex, typeIndex + entryLength); - } - extensionTypeIndex = typeIndex + entryLength; - } - return null; -} - -export function getExtensionTypes(tlvData: Buffer): ExtensionType[] { - const extensionTypes = []; - let extensionTypeIndex = 0; - while (extensionTypeIndex < tlvData.length) { - const entryType = tlvData.readUInt16LE(extensionTypeIndex); - extensionTypes.push(entryType); - const entryLength = tlvData.readUInt16LE(extensionTypeIndex + TYPE_SIZE); - extensionTypeIndex += addTypeAndLengthToLen(entryLength); - } - return extensionTypes; -} - -export function getAccountLenForMint(mint: Mint): number { - const extensionTypes = getExtensionTypes(mint.tlvData); - const accountExtensions = extensionTypes.map(getAccountTypeOfMintType); - return getAccountLen(accountExtensions); -} - -export function getNewAccountLenForExtensionLen( - info: AccountInfo, - address: PublicKey, - extensionType: ExtensionType, - extensionLen: number, - programId = TOKEN_2022_PROGRAM_ID, -): number { - const mint = unpackMint(address, info, programId); - const extensionData = getExtensionData(extensionType, mint.tlvData); - - const currentExtensionLen = extensionData ? addTypeAndLengthToLen(extensionData.length) : 0; - const newExtensionLen = addTypeAndLengthToLen(extensionLen); - - return info.data.length + newExtensionLen - currentExtensionLen; -} diff --git a/token/js/src/extensions/groupMemberPointer/index.ts b/token/js/src/extensions/groupMemberPointer/index.ts deleted file mode 100644 index 8bf2a08d1f9..00000000000 --- a/token/js/src/extensions/groupMemberPointer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './instructions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/groupMemberPointer/instructions.ts b/token/js/src/extensions/groupMemberPointer/instructions.ts deleted file mode 100644 index 21e13a62f06..00000000000 --- a/token/js/src/extensions/groupMemberPointer/instructions.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { Signer } from '@solana/web3.js'; -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_2022_PROGRAM_ID, programSupportsExtensions } from '../../constants.js'; -import { TokenUnsupportedInstructionError } from '../../errors.js'; -import { TokenInstruction } from '../../instructions/types.js'; -import { addSigners } from '../../instructions/internal.js'; - -export enum GroupMemberPointerInstruction { - Initialize = 0, - Update = 1, -} - -export const initializeGroupMemberPointerData = struct<{ - instruction: TokenInstruction.GroupMemberPointerExtension; - groupMemberPointerInstruction: number; - authority: PublicKey; - memberAddress: PublicKey; -}>([ - // prettier-ignore - u8('instruction'), - u8('groupMemberPointerInstruction'), - publicKey('authority'), - publicKey('memberAddress'), -]); - -/** - * Construct an Initialize GroupMemberPointer instruction - * - * @param mint Token mint account - * @param authority Optional Authority that can set the member address - * @param memberAddress Optional Account address that holds the member - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeGroupMemberPointerInstruction( - mint: PublicKey, - authority: PublicKey | null, - memberAddress: PublicKey | null, - programId: PublicKey = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializeGroupMemberPointerData.span); - initializeGroupMemberPointerData.encode( - { - instruction: TokenInstruction.GroupMemberPointerExtension, - groupMemberPointerInstruction: GroupMemberPointerInstruction.Initialize, - authority: authority ?? PublicKey.default, - memberAddress: memberAddress ?? PublicKey.default, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data: data }); -} - -export const updateGroupMemberPointerData = struct<{ - instruction: TokenInstruction.GroupMemberPointerExtension; - groupMemberPointerInstruction: number; - memberAddress: PublicKey; -}>([ - // prettier-ignore - u8('instruction'), - u8('groupMemberPointerInstruction'), - publicKey('memberAddress'), -]); - -export function createUpdateGroupMemberPointerInstruction( - mint: PublicKey, - authority: PublicKey, - memberAddress: PublicKey | null, - multiSigners: (Signer | PublicKey)[] = [], - programId: PublicKey = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - - const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners); - - const data = Buffer.alloc(updateGroupMemberPointerData.span); - updateGroupMemberPointerData.encode( - { - instruction: TokenInstruction.GroupMemberPointerExtension, - groupMemberPointerInstruction: GroupMemberPointerInstruction.Update, - memberAddress: memberAddress ?? PublicKey.default, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data: data }); -} diff --git a/token/js/src/extensions/groupMemberPointer/state.ts b/token/js/src/extensions/groupMemberPointer/state.ts deleted file mode 100644 index ceda2073010..00000000000 --- a/token/js/src/extensions/groupMemberPointer/state.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { struct } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import { PublicKey } from '@solana/web3.js'; -import type { Mint } from '../../state/mint.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; - -/** GroupMemberPointer as stored by the program */ -export interface GroupMemberPointer { - /** Optional authority that can set the member address */ - authority: PublicKey | null; - /** Optional account address that holds the member */ - memberAddress: PublicKey | null; -} - -/** Buffer layout for de/serializing a Group Pointer extension */ -export const GroupMemberPointerLayout = struct<{ authority: PublicKey; memberAddress: PublicKey }>([ - publicKey('authority'), - publicKey('memberAddress'), -]); - -export const GROUP_MEMBER_POINTER_SIZE = GroupMemberPointerLayout.span; - -export function getGroupMemberPointerState(mint: Mint): Partial | null { - const extensionData = getExtensionData(ExtensionType.GroupMemberPointer, mint.tlvData); - if (extensionData !== null) { - const { authority, memberAddress } = GroupMemberPointerLayout.decode(extensionData); - - // Explicitly set None/Zero keys to null - return { - authority: authority.equals(PublicKey.default) ? null : authority, - memberAddress: memberAddress.equals(PublicKey.default) ? null : memberAddress, - }; - } else { - return null; - } -} diff --git a/token/js/src/extensions/groupPointer/index.ts b/token/js/src/extensions/groupPointer/index.ts deleted file mode 100644 index 8bf2a08d1f9..00000000000 --- a/token/js/src/extensions/groupPointer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './instructions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/groupPointer/instructions.ts b/token/js/src/extensions/groupPointer/instructions.ts deleted file mode 100644 index 7ebbf9ae448..00000000000 --- a/token/js/src/extensions/groupPointer/instructions.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { Signer } from '@solana/web3.js'; -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_2022_PROGRAM_ID, programSupportsExtensions } from '../../constants.js'; -import { TokenUnsupportedInstructionError } from '../../errors.js'; -import { TokenInstruction } from '../../instructions/types.js'; -import { addSigners } from '../../instructions/internal.js'; - -export enum GroupPointerInstruction { - Initialize = 0, - Update = 1, -} - -export const initializeGroupPointerData = struct<{ - instruction: TokenInstruction.GroupPointerExtension; - groupPointerInstruction: number; - authority: PublicKey; - groupAddress: PublicKey; -}>([ - // prettier-ignore - u8('instruction'), - u8('groupPointerInstruction'), - publicKey('authority'), - publicKey('groupAddress'), -]); - -/** - * Construct an Initialize GroupPointer instruction - * - * @param mint Token mint account - * @param authority Optional Authority that can set the group address - * @param groupAddress Optional Account address that holds the group - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeGroupPointerInstruction( - mint: PublicKey, - authority: PublicKey | null, - groupAddress: PublicKey | null, - programId: PublicKey = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializeGroupPointerData.span); - initializeGroupPointerData.encode( - { - instruction: TokenInstruction.GroupPointerExtension, - groupPointerInstruction: GroupPointerInstruction.Initialize, - authority: authority ?? PublicKey.default, - groupAddress: groupAddress ?? PublicKey.default, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data: data }); -} - -export const updateGroupPointerData = struct<{ - instruction: TokenInstruction.GroupPointerExtension; - groupPointerInstruction: number; - groupAddress: PublicKey; -}>([ - // prettier-ignore - u8('instruction'), - u8('groupPointerInstruction'), - publicKey('groupAddress'), -]); - -export function createUpdateGroupPointerInstruction( - mint: PublicKey, - authority: PublicKey, - groupAddress: PublicKey | null, - multiSigners: (Signer | PublicKey)[] = [], - programId: PublicKey = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - - const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners); - - const data = Buffer.alloc(updateGroupPointerData.span); - updateGroupPointerData.encode( - { - instruction: TokenInstruction.GroupPointerExtension, - groupPointerInstruction: GroupPointerInstruction.Update, - groupAddress: groupAddress ?? PublicKey.default, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data: data }); -} diff --git a/token/js/src/extensions/groupPointer/state.ts b/token/js/src/extensions/groupPointer/state.ts deleted file mode 100644 index 1f522612cc6..00000000000 --- a/token/js/src/extensions/groupPointer/state.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { struct } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import { PublicKey } from '@solana/web3.js'; -import type { Mint } from '../../state/mint.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; - -/** GroupPointer as stored by the program */ -export interface GroupPointer { - /** Optional authority that can set the group address */ - authority: PublicKey | null; - /** Optional account address that holds the group */ - groupAddress: PublicKey | null; -} - -/** Buffer layout for de/serializing a GroupPointer extension */ -export const GroupPointerLayout = struct<{ authority: PublicKey; groupAddress: PublicKey }>([ - publicKey('authority'), - publicKey('groupAddress'), -]); - -export const GROUP_POINTER_SIZE = GroupPointerLayout.span; - -export function getGroupPointerState(mint: Mint): Partial | null { - const extensionData = getExtensionData(ExtensionType.GroupPointer, mint.tlvData); - if (extensionData !== null) { - const { authority, groupAddress } = GroupPointerLayout.decode(extensionData); - - // Explicitly set None/Zero keys to null - return { - authority: authority.equals(PublicKey.default) ? null : authority, - groupAddress: groupAddress.equals(PublicKey.default) ? null : groupAddress, - }; - } else { - return null; - } -} diff --git a/token/js/src/extensions/immutableOwner.ts b/token/js/src/extensions/immutableOwner.ts deleted file mode 100644 index 72a1ce25245..00000000000 --- a/token/js/src/extensions/immutableOwner.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { struct } from '@solana/buffer-layout'; -import type { Account } from '../state/account.js'; -import { ExtensionType, getExtensionData } from './extensionType.js'; - -/** ImmutableOwner as stored by the program */ -export interface ImmutableOwner {} // eslint-disable-line - -/** Buffer layout for de/serializing an account */ -export const ImmutableOwnerLayout = struct([]); - -export const IMMUTABLE_OWNER_SIZE = ImmutableOwnerLayout.span; - -export function getImmutableOwner(account: Account): ImmutableOwner | null { - const extensionData = getExtensionData(ExtensionType.ImmutableOwner, account.tlvData); - if (extensionData !== null) { - return ImmutableOwnerLayout.decode(extensionData); - } else { - return null; - } -} diff --git a/token/js/src/extensions/index.ts b/token/js/src/extensions/index.ts deleted file mode 100644 index cfa585b9482..00000000000 --- a/token/js/src/extensions/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export * from './accountType.js'; -export * from './cpiGuard/index.js'; -export * from './defaultAccountState/index.js'; -export * from './extensionType.js'; -export * from './groupMemberPointer/index.js'; -export * from './groupPointer/index.js'; -export * from './immutableOwner.js'; -export * from './interestBearingMint/index.js'; -export * from './memoTransfer/index.js'; -export * from './metadataPointer/index.js'; -export * from './tokenGroup/index.js'; -export * from './tokenMetadata/index.js'; -export * from './mintCloseAuthority.js'; -export * from './nonTransferable.js'; -export * from './transferFee/index.js'; -export * from './permanentDelegate.js'; -export * from './transferHook/index.js'; diff --git a/token/js/src/extensions/interestBearingMint/actions.ts b/token/js/src/extensions/interestBearingMint/actions.ts deleted file mode 100644 index 02ad8917454..00000000000 --- a/token/js/src/extensions/interestBearingMint/actions.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair, sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js'; -import { getSigners } from '../../actions/internal.js'; -import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { createInitializeMintInstruction } from '../../instructions/initializeMint.js'; -import { ExtensionType, getMintLen } from '../extensionType.js'; -import { - createInitializeInterestBearingMintInstruction, - createUpdateRateInterestBearingMintInstruction, -} from './instructions.js'; - -/** - * Initialize an interest bearing account on a mint - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mintAuthority Account or multisig that will control minting - * @param freezeAuthority Optional account or multisig that can freeze token accounts - * @param rateAuthority The public key for the account that can update the rate - * @param rate The initial interest rate - * @param decimals Location of the decimal place - * @param keypair Optional keypair, defaulting to a new random one - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Public key of the mint - */ -export async function createInterestBearingMint( - connection: Connection, - payer: Signer, - mintAuthority: PublicKey, - freezeAuthority: PublicKey, - rateAuthority: PublicKey, - rate: number, - decimals: number, - keypair = Keypair.generate(), - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const mintLen = getMintLen([ExtensionType.InterestBearingConfig]); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: keypair.publicKey, - space: mintLen, - lamports, - programId, - }), - createInitializeInterestBearingMintInstruction(keypair.publicKey, rateAuthority, rate, programId), - createInitializeMintInstruction(keypair.publicKey, decimals, mintAuthority, freezeAuthority, programId), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, keypair], confirmOptions); - return keypair.publicKey; -} - -/** - * Update the interest rate of an interest bearing account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Public key of the mint - * @param rateAuthority The public key for the account that can update the rate - * @param rate The initial interest rate - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function updateRateInterestBearingMint( - connection: Connection, - payer: Signer, - mint: PublicKey, - rateAuthority: Signer, - rate: number, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [rateAuthorityPublicKey, signers] = getSigners(rateAuthority, multiSigners); - const transaction = new Transaction().add( - createUpdateRateInterestBearingMintInstruction(mint, rateAuthorityPublicKey, rate, signers, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, rateAuthority, ...signers], confirmOptions); -} diff --git a/token/js/src/extensions/interestBearingMint/index.ts b/token/js/src/extensions/interestBearingMint/index.ts deleted file mode 100644 index 5e28fd6b10a..00000000000 --- a/token/js/src/extensions/interestBearingMint/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './actions.js'; -export * from './instructions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/interestBearingMint/instructions.ts b/token/js/src/extensions/interestBearingMint/instructions.ts deleted file mode 100644 index e5de48eb79b..00000000000 --- a/token/js/src/extensions/interestBearingMint/instructions.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { s16, struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { addSigners } from '../../instructions/internal.js'; -import { TokenInstruction } from '../../instructions/types.js'; - -export enum InterestBearingMintInstruction { - Initialize = 0, - UpdateRate = 1, -} - -export interface InterestBearingMintInitializeInstructionData { - instruction: TokenInstruction.InterestBearingMintExtension; - interestBearingMintInstruction: InterestBearingMintInstruction.Initialize; - rateAuthority: PublicKey; - rate: number; -} - -export interface InterestBearingMintUpdateRateInstructionData { - instruction: TokenInstruction.InterestBearingMintExtension; - interestBearingMintInstruction: InterestBearingMintInstruction.UpdateRate; - rate: number; -} - -export const interestBearingMintInitializeInstructionData = struct([ - u8('instruction'), - u8('interestBearingMintInstruction'), - // TODO: Make this an optional public key - publicKey('rateAuthority'), - s16('rate'), -]); - -export const interestBearingMintUpdateRateInstructionData = struct([ - u8('instruction'), - u8('interestBearingMintInstruction'), - s16('rate'), -]); - -/** - * Construct an InitializeInterestBearingMint instruction - * - * @param mint Mint to initialize - * @param rateAuthority The public key for the account that can update the rate - * @param rate The initial interest rate - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeInterestBearingMintInstruction( - mint: PublicKey, - rateAuthority: PublicKey, - rate: number, - programId = TOKEN_2022_PROGRAM_ID, -) { - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - const data = Buffer.alloc(interestBearingMintInitializeInstructionData.span); - interestBearingMintInitializeInstructionData.encode( - { - instruction: TokenInstruction.InterestBearingMintExtension, - interestBearingMintInstruction: InterestBearingMintInstruction.Initialize, - rateAuthority, - rate, - }, - data, - ); - return new TransactionInstruction({ keys, programId, data }); -} - -/** - * Construct an UpdateRateInterestBearingMint instruction - * - * @param mint Mint to initialize - * @param rateAuthority The public key for the account that can update the rate - * @param rate The updated interest rate - * @param multiSigners Signing accounts if `rateAuthority` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createUpdateRateInterestBearingMintInstruction( - mint: PublicKey, - rateAuthority: PublicKey, - rate: number, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -) { - const keys = addSigners( - [ - { pubkey: mint, isSigner: false, isWritable: true }, - { pubkey: rateAuthority, isSigner: !multiSigners.length, isWritable: false }, - ], - rateAuthority, - multiSigners, - ); - const data = Buffer.alloc(interestBearingMintUpdateRateInstructionData.span); - interestBearingMintUpdateRateInstructionData.encode( - { - instruction: TokenInstruction.InterestBearingMintExtension, - interestBearingMintInstruction: InterestBearingMintInstruction.UpdateRate, - rate, - }, - data, - ); - return new TransactionInstruction({ keys, programId, data }); -} diff --git a/token/js/src/extensions/interestBearingMint/state.ts b/token/js/src/extensions/interestBearingMint/state.ts deleted file mode 100644 index b10cb3ab866..00000000000 --- a/token/js/src/extensions/interestBearingMint/state.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ns64, s16, struct } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { PublicKey } from '@solana/web3.js'; -import type { Mint } from '../../state/mint.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; - -export interface InterestBearingMintConfigState { - rateAuthority: PublicKey; - initializationTimestamp: bigint; - preUpdateAverageRate: number; - lastUpdateTimestamp: bigint; - currentRate: number; -} - -export const InterestBearingMintConfigStateLayout = struct([ - publicKey('rateAuthority'), - ns64('initializationTimestamp'), - s16('preUpdateAverageRate'), - ns64('lastUpdateTimestamp'), - s16('currentRate'), -]); - -export const INTEREST_BEARING_MINT_CONFIG_STATE_SIZE = InterestBearingMintConfigStateLayout.span; - -export function getInterestBearingMintConfigState(mint: Mint): InterestBearingMintConfigState | null { - const extensionData = getExtensionData(ExtensionType.InterestBearingConfig, mint.tlvData); - if (extensionData !== null) { - return InterestBearingMintConfigStateLayout.decode(extensionData); - } - return null; -} diff --git a/token/js/src/extensions/memoTransfer/actions.ts b/token/js/src/extensions/memoTransfer/actions.ts deleted file mode 100644 index 77288b95561..00000000000 --- a/token/js/src/extensions/memoTransfer/actions.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { getSigners } from '../../actions/internal.js'; -import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { - createDisableRequiredMemoTransfersInstruction, - createEnableRequiredMemoTransfersInstruction, -} from './instructions.js'; - -/** - * Enable memo transfers on the given account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Account to modify - * @param owner Owner of the account - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function enableRequiredMemoTransfers( - connection: Connection, - payer: Signer, - account: PublicKey, - owner: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createEnableRequiredMemoTransfersInstruction(account, ownerPublicKey, signers, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Disable memo transfers on the given account - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param account Account to modify - * @param owner Owner of the account - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function disableRequiredMemoTransfers( - connection: Connection, - payer: Signer, - account: PublicKey, - owner: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createDisableRequiredMemoTransfersInstruction(account, ownerPublicKey, signers, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/extensions/memoTransfer/index.ts b/token/js/src/extensions/memoTransfer/index.ts deleted file mode 100644 index 5e28fd6b10a..00000000000 --- a/token/js/src/extensions/memoTransfer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './actions.js'; -export * from './instructions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/memoTransfer/instructions.ts b/token/js/src/extensions/memoTransfer/instructions.ts deleted file mode 100644 index 65ff26528ad..00000000000 --- a/token/js/src/extensions/memoTransfer/instructions.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { TokenUnsupportedInstructionError } from '../../errors.js'; -import { addSigners } from '../../instructions/internal.js'; -import { TokenInstruction } from '../../instructions/types.js'; - -export enum MemoTransferInstruction { - Enable = 0, - Disable = 1, -} - -/** TODO: docs */ -export interface MemoTransferInstructionData { - instruction: TokenInstruction.MemoTransferExtension; - memoTransferInstruction: MemoTransferInstruction; -} - -/** TODO: docs */ -export const memoTransferInstructionData = struct([ - u8('instruction'), - u8('memoTransferInstruction'), -]); - -/** - * Construct an EnableRequiredMemoTransfers instruction - * - * @param account Token account to update - * @param authority The account's owner/delegate - * @param signers The signer account(s) - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createEnableRequiredMemoTransfersInstruction( - account: PublicKey, - authority: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - return createMemoTransferInstruction(MemoTransferInstruction.Enable, account, authority, multiSigners, programId); -} - -/** - * Construct a DisableMemoTransfer instruction - * - * @param account Token account to update - * @param authority The account's owner/delegate - * @param signers The signer account(s) - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createDisableRequiredMemoTransfersInstruction( - account: PublicKey, - authority: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - return createMemoTransferInstruction(MemoTransferInstruction.Disable, account, authority, multiSigners, programId); -} - -function createMemoTransferInstruction( - memoTransferInstruction: MemoTransferInstruction, - account: PublicKey, - authority: PublicKey, - multiSigners: (Signer | PublicKey)[], - programId: PublicKey, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - - const keys = addSigners([{ pubkey: account, isSigner: false, isWritable: true }], authority, multiSigners); - const data = Buffer.alloc(memoTransferInstructionData.span); - memoTransferInstructionData.encode( - { - instruction: TokenInstruction.MemoTransferExtension, - memoTransferInstruction, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} diff --git a/token/js/src/extensions/memoTransfer/state.ts b/token/js/src/extensions/memoTransfer/state.ts deleted file mode 100644 index 0f8a726096a..00000000000 --- a/token/js/src/extensions/memoTransfer/state.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { struct } from '@solana/buffer-layout'; -import { bool } from '@solana/buffer-layout-utils'; -import type { Account } from '../../state/account.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; - -/** MemoTransfer as stored by the program */ -export interface MemoTransfer { - /** Require transfers into this account to be accompanied by a memo */ - requireIncomingTransferMemos: boolean; -} - -/** Buffer layout for de/serializing a memo transfer extension */ -export const MemoTransferLayout = struct([bool('requireIncomingTransferMemos')]); - -export const MEMO_TRANSFER_SIZE = MemoTransferLayout.span; - -export function getMemoTransfer(account: Account): MemoTransfer | null { - const extensionData = getExtensionData(ExtensionType.MemoTransfer, account.tlvData); - if (extensionData !== null) { - return MemoTransferLayout.decode(extensionData); - } else { - return null; - } -} diff --git a/token/js/src/extensions/metadataPointer/index.ts b/token/js/src/extensions/metadataPointer/index.ts deleted file mode 100644 index 8bf2a08d1f9..00000000000 --- a/token/js/src/extensions/metadataPointer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './instructions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/metadataPointer/instructions.ts b/token/js/src/extensions/metadataPointer/instructions.ts deleted file mode 100644 index fe26b32b8f3..00000000000 --- a/token/js/src/extensions/metadataPointer/instructions.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { Signer } from '@solana/web3.js'; -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_2022_PROGRAM_ID, programSupportsExtensions } from '../../constants.js'; -import { TokenUnsupportedInstructionError } from '../../errors.js'; -import { TokenInstruction } from '../../instructions/types.js'; -import { addSigners } from '../../instructions/internal.js'; - -export enum MetadataPointerInstruction { - Initialize = 0, - Update = 1, -} - -export const initializeMetadataPointerData = struct<{ - instruction: TokenInstruction.MetadataPointerExtension; - metadataPointerInstruction: number; - authority: PublicKey; - metadataAddress: PublicKey; -}>([ - // prettier-ignore - u8('instruction'), - u8('metadataPointerInstruction'), - publicKey('authority'), - publicKey('metadataAddress'), -]); - -/** - * Construct an Initialize MetadataPointer instruction - * - * @param mint Token mint account - * @param authority Optional Authority that can set the metadata address - * @param metadataAddress Optional Account address that holds the metadata - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeMetadataPointerInstruction( - mint: PublicKey, - authority: PublicKey | null, - metadataAddress: PublicKey | null, - programId: PublicKey, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializeMetadataPointerData.span); - initializeMetadataPointerData.encode( - { - instruction: TokenInstruction.MetadataPointerExtension, - metadataPointerInstruction: MetadataPointerInstruction.Initialize, - authority: authority ?? PublicKey.default, - metadataAddress: metadataAddress ?? PublicKey.default, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data: data }); -} - -export const updateMetadataPointerData = struct<{ - instruction: TokenInstruction.MetadataPointerExtension; - metadataPointerInstruction: number; - metadataAddress: PublicKey; -}>([ - // prettier-ignore - u8('instruction'), - u8('metadataPointerInstruction'), - publicKey('metadataAddress'), -]); - -export function createUpdateMetadataPointerInstruction( - mint: PublicKey, - authority: PublicKey, - metadataAddress: PublicKey | null, - multiSigners: (Signer | PublicKey)[] = [], - programId: PublicKey = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - - const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners); - - const data = Buffer.alloc(updateMetadataPointerData.span); - updateMetadataPointerData.encode( - { - instruction: TokenInstruction.MetadataPointerExtension, - metadataPointerInstruction: MetadataPointerInstruction.Update, - metadataAddress: metadataAddress ?? PublicKey.default, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data: data }); -} diff --git a/token/js/src/extensions/metadataPointer/state.ts b/token/js/src/extensions/metadataPointer/state.ts deleted file mode 100644 index 75335fb1458..00000000000 --- a/token/js/src/extensions/metadataPointer/state.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { struct } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import { PublicKey } from '@solana/web3.js'; -import type { Mint } from '../../state/mint.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; - -/** MetadataPointer as stored by the program */ -export interface MetadataPointer { - /** Optional authority that can set the metadata address */ - authority: PublicKey | null; - /** Optional Account Address that holds the metadata */ - metadataAddress: PublicKey | null; -} - -/** Buffer layout for de/serializing a Metadata Pointer extension */ -export const MetadataPointerLayout = struct<{ authority: PublicKey; metadataAddress: PublicKey }>([ - publicKey('authority'), - publicKey('metadataAddress'), -]); - -export const METADATA_POINTER_SIZE = MetadataPointerLayout.span; - -export function getMetadataPointerState(mint: Mint): Partial | null { - const extensionData = getExtensionData(ExtensionType.MetadataPointer, mint.tlvData); - if (extensionData !== null) { - const { authority, metadataAddress } = MetadataPointerLayout.decode(extensionData); - - // Explicitly set None/Zero keys to null - return { - authority: authority.equals(PublicKey.default) ? null : authority, - metadataAddress: metadataAddress.equals(PublicKey.default) ? null : metadataAddress, - }; - } else { - return null; - } -} diff --git a/token/js/src/extensions/mintCloseAuthority.ts b/token/js/src/extensions/mintCloseAuthority.ts deleted file mode 100644 index 5f09f451a65..00000000000 --- a/token/js/src/extensions/mintCloseAuthority.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { struct } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { PublicKey } from '@solana/web3.js'; -import type { Mint } from '../state/mint.js'; -import { ExtensionType, getExtensionData } from './extensionType.js'; - -/** MintCloseAuthority as stored by the program */ -export interface MintCloseAuthority { - closeAuthority: PublicKey; -} - -/** Buffer layout for de/serializing a mint */ -export const MintCloseAuthorityLayout = struct([publicKey('closeAuthority')]); - -export const MINT_CLOSE_AUTHORITY_SIZE = MintCloseAuthorityLayout.span; - -export function getMintCloseAuthority(mint: Mint): MintCloseAuthority | null { - const extensionData = getExtensionData(ExtensionType.MintCloseAuthority, mint.tlvData); - if (extensionData !== null) { - return MintCloseAuthorityLayout.decode(extensionData); - } else { - return null; - } -} diff --git a/token/js/src/extensions/nonTransferable.ts b/token/js/src/extensions/nonTransferable.ts deleted file mode 100644 index af9e85168ac..00000000000 --- a/token/js/src/extensions/nonTransferable.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { struct } from '@solana/buffer-layout'; -import type { Account } from '../state/account.js'; -import type { Mint } from '../state/mint.js'; -import { ExtensionType, getExtensionData } from './extensionType.js'; - -/** Non-transferable mint state as stored by the program */ -export interface NonTransferable {} // eslint-disable-line - -/** Non-transferable token account state as stored by the program */ -export interface NonTransferableAccount {} // eslint-disable-line - -/** Buffer layout for de/serializing an account */ -export const NonTransferableLayout = struct([]); - -export const NON_TRANSFERABLE_SIZE = NonTransferableLayout.span; -export const NON_TRANSFERABLE_ACCOUNT_SIZE = NonTransferableLayout.span; - -export function getNonTransferable(mint: Mint): NonTransferable | null { - const extensionData = getExtensionData(ExtensionType.NonTransferable, mint.tlvData); - if (extensionData !== null) { - return NonTransferableLayout.decode(extensionData); - } else { - return null; - } -} - -export function getNonTransferableAccount(account: Account): NonTransferableAccount | null { - const extensionData = getExtensionData(ExtensionType.NonTransferableAccount, account.tlvData); - if (extensionData !== null) { - return NonTransferableLayout.decode(extensionData); - } else { - return null; - } -} diff --git a/token/js/src/extensions/permanentDelegate.ts b/token/js/src/extensions/permanentDelegate.ts deleted file mode 100644 index 5b1fb1ef542..00000000000 --- a/token/js/src/extensions/permanentDelegate.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { struct } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { PublicKey } from '@solana/web3.js'; -import type { Mint } from '../state/mint.js'; -import { ExtensionType, getExtensionData } from './extensionType.js'; - -/** PermanentDelegate as stored by the program */ -export interface PermanentDelegate { - delegate: PublicKey; -} - -/** Buffer layout for de/serializing a mint */ -export const PermanentDelegateLayout = struct([publicKey('delegate')]); - -export const PERMANENT_DELEGATE_SIZE = PermanentDelegateLayout.span; - -export function getPermanentDelegate(mint: Mint): PermanentDelegate | null { - const extensionData = getExtensionData(ExtensionType.PermanentDelegate, mint.tlvData); - if (extensionData !== null) { - return PermanentDelegateLayout.decode(extensionData); - } else { - return null; - } -} diff --git a/token/js/src/extensions/tokenGroup/actions.ts b/token/js/src/extensions/tokenGroup/actions.ts deleted file mode 100644 index 318c49fe8fe..00000000000 --- a/token/js/src/extensions/tokenGroup/actions.ts +++ /dev/null @@ -1,281 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js'; -import { - createInitializeGroupInstruction, - createUpdateGroupMaxSizeInstruction, - createUpdateGroupAuthorityInstruction, - createInitializeMemberInstruction, - TOKEN_GROUP_SIZE, - TOKEN_GROUP_MEMBER_SIZE, -} from '@solana/spl-token-group'; - -import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { getSigners } from '../../actions/internal.js'; - -/** - * Initialize a new `Group` - * - * Assumes one has already initialized a mint for the group. - * - * @param connection Connection to use - * @param payer Payer of the transaction fee - * @param mint Group mint - * @param mintAuthority Group mint authority - * @param updateAuthority Group update authority - * @param maxSize Maximum number of members in the group - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenGroupInitializeGroup( - connection: Connection, - payer: Signer, - mint: PublicKey, - mintAuthority: PublicKey | Signer, - updateAuthority: PublicKey | null, - maxSize: bigint, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners); - - const transaction = new Transaction().add( - createInitializeGroupInstruction({ - programId, - group: mint, - mint, - mintAuthority: mintAuthorityPublicKey, - updateAuthority, - maxSize, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Initialize a new `Group` with rent transfer. - * - * Assumes one has already initialized a mint for the group. - * - * @param connection Connection to use - * @param payer Payer of the transaction fee - * @param mint Group mint - * @param mintAuthority Group mint authority - * @param updateAuthority Group update authority - * @param maxSize Maximum number of members in the group - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenGroupInitializeGroupWithRentTransfer( - connection: Connection, - payer: Signer, - mint: PublicKey, - mintAuthority: PublicKey | Signer, - updateAuthority: PublicKey | null, - maxSize: bigint, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners); - - const lamports = await connection.getMinimumBalanceForRentExemption(TOKEN_GROUP_SIZE); - - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: mint, - lamports, - }), - createInitializeGroupInstruction({ - programId, - group: mint, - mint, - mintAuthority: mintAuthorityPublicKey, - updateAuthority, - maxSize, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Update the max size of a `Group` - * - * @param connection Connection to use - * @param payer Payer of the transaction fee - * @param mint Group mint - * @param updateAuthority Group update authority - * @param maxSize Maximum number of members in the group - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenGroupUpdateGroupMaxSize( - connection: Connection, - payer: Signer, - mint: PublicKey, - updateAuthority: PublicKey | Signer, - maxSize: bigint, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [updateAuthorityPublicKey, signers] = getSigners(updateAuthority, multiSigners); - - const transaction = new Transaction().add( - createUpdateGroupMaxSizeInstruction({ - programId, - group: mint, - updateAuthority: updateAuthorityPublicKey, - maxSize, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Update the authority of a `Group` - * - * @param connection Connection to use - * @param payer Payer of the transaction fee - * @param mint Group mint - * @param updateAuthority Group update authority - * @param newAuthority New authority for the token group, or unset - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenGroupUpdateGroupAuthority( - connection: Connection, - payer: Signer, - mint: PublicKey, - updateAuthority: PublicKey | Signer, - newAuthority: PublicKey | null, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [updateAuthorityPublicKey, signers] = getSigners(updateAuthority, multiSigners); - - const transaction = new Transaction().add( - createUpdateGroupAuthorityInstruction({ - programId, - group: mint, - currentAuthority: updateAuthorityPublicKey, - newAuthority, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Initialize a new `Member` of a `Group` - * - * Assumes the `Group` has already been initialized, - * as well as the mint for the member. - * - * @param connection Connection to use - * @param payer Payer of the transaction fee - * @param mint Member mint - * @param mintAuthority Member mint authority - * @param group Group mint - * @param groupUpdateAuthority Group update authority - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenGroupMemberInitialize( - connection: Connection, - payer: Signer, - mint: PublicKey, - mintAuthority: PublicKey | Signer, - group: PublicKey, - groupUpdateAuthority: PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners); - - const transaction = new Transaction().add( - createInitializeMemberInstruction({ - programId, - member: mint, - memberMint: mint, - memberMintAuthority: mintAuthorityPublicKey, - group, - groupUpdateAuthority, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Initialize a new `Member` of a `Group` with rent transfer. - * - * Assumes the `Group` has already been initialized, - * as well as the mint for the member. - * - * @param connection Connection to use - * @param payer Payer of the transaction fee - * @param mint Member mint - * @param mintAuthority Member mint authority - * @param group Group mint - * @param groupUpdateAuthority Group update authority - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenGroupMemberInitializeWithRentTransfer( - connection: Connection, - payer: Signer, - mint: PublicKey, - mintAuthority: PublicKey | Signer, - group: PublicKey, - groupUpdateAuthority: PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners); - - const lamports = await connection.getMinimumBalanceForRentExemption(TOKEN_GROUP_MEMBER_SIZE); - - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: mint, - lamports, - }), - createInitializeMemberInstruction({ - programId, - member: mint, - memberMint: mint, - memberMintAuthority: mintAuthorityPublicKey, - group, - groupUpdateAuthority, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/extensions/tokenGroup/index.ts b/token/js/src/extensions/tokenGroup/index.ts deleted file mode 100644 index 898210857d0..00000000000 --- a/token/js/src/extensions/tokenGroup/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './actions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/tokenGroup/state.ts b/token/js/src/extensions/tokenGroup/state.ts deleted file mode 100644 index a0822357c41..00000000000 --- a/token/js/src/extensions/tokenGroup/state.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { struct, u32 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import { PublicKey } from '@solana/web3.js'; -import { - unpackTokenGroup, - unpackTokenGroupMember, - type TokenGroup, - type TokenGroupMember, -} from '@solana/spl-token-group'; -import type { Mint } from '../../state/mint.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; - -export { TOKEN_GROUP_SIZE, TOKEN_GROUP_MEMBER_SIZE } from '@solana/spl-token-group'; - -export function getTokenGroupState(mint: Mint): Partial | null { - const extensionData = getExtensionData(ExtensionType.TokenGroup, mint.tlvData); - if (extensionData !== null) { - const { updateAuthority, mint, size, maxSize } = unpackTokenGroup(extensionData); - - // Explicitly set None/Zero keys to null - return { - updateAuthority: updateAuthority?.equals(PublicKey.default) ? undefined : updateAuthority, - mint, - size, - maxSize, - }; - } else { - return null; - } -} - -export function getTokenGroupMemberState(mint: Mint): Partial | null { - const extensionData = getExtensionData(ExtensionType.TokenGroupMember, mint.tlvData); - if (extensionData !== null) { - const { mint, group, memberNumber } = unpackTokenGroupMember(extensionData); - - return { - mint, - group, - memberNumber, - }; - } else { - return null; - } -} diff --git a/token/js/src/extensions/tokenMetadata/actions.ts b/token/js/src/extensions/tokenMetadata/actions.ts deleted file mode 100644 index c8838877998..00000000000 --- a/token/js/src/extensions/tokenMetadata/actions.ts +++ /dev/null @@ -1,382 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, SystemProgram, Transaction } from '@solana/web3.js'; -import type { Field, TokenMetadata } from '@solana/spl-token-metadata'; -import { - createInitializeInstruction, - createRemoveKeyInstruction, - createUpdateAuthorityInstruction, - createUpdateFieldInstruction, - pack, - unpack, -} from '@solana/spl-token-metadata'; - -import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { getSigners } from '../../actions/internal.js'; -import { ExtensionType, getExtensionData, getNewAccountLenForExtensionLen } from '../extensionType.js'; -import { updateTokenMetadata } from './state.js'; -import { TokenAccountNotFoundError } from '../../errors.js'; -import { unpackMint } from '../../state/index.js'; - -async function getAdditionalRentForNewMetadata( - connection: Connection, - address: PublicKey, - tokenMetadata: TokenMetadata, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const info = await connection.getAccountInfo(address); - if (!info) { - throw new TokenAccountNotFoundError(); - } - - const extensionLen = pack(tokenMetadata).length; - const newAccountLen = getNewAccountLenForExtensionLen( - info, - address, - ExtensionType.TokenMetadata, - extensionLen, - programId, - ); - - if (newAccountLen <= info.data.length) { - return 0; - } - - const newRentExemptMinimum = await connection.getMinimumBalanceForRentExemption(newAccountLen); - - return newRentExemptMinimum - info.lamports; -} - -async function getAdditionalRentForUpdatedMetadata( - connection: Connection, - address: PublicKey, - field: string | Field, - value: string, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const info = await connection.getAccountInfo(address); - if (!info) { - throw new TokenAccountNotFoundError(); - } - - const mint = unpackMint(address, info, programId); - const extensionData = getExtensionData(ExtensionType.TokenMetadata, mint.tlvData); - if (extensionData === null) { - throw new Error('TokenMetadata extension not initialized'); - } - - const updatedTokenMetadata = updateTokenMetadata(unpack(extensionData), field, value); - const extensionLen = pack(updatedTokenMetadata).length; - - const newAccountLen = getNewAccountLenForExtensionLen( - info, - address, - ExtensionType.TokenMetadata, - extensionLen, - programId, - ); - - if (newAccountLen <= info.data.length) { - return 0; - } - - const newRentExemptMinimum = await connection.getMinimumBalanceForRentExemption(newAccountLen); - - return newRentExemptMinimum - info.lamports; -} - -/** - * Initializes a TLV entry with the basic token-metadata fields. - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint Account - * @param updateAuthority Update Authority - * @param mintAuthority Mint Authority - * @param name Longer name of token - * @param symbol Shortened symbol of token - * @param uri URI pointing to more metadata (image, video, etc) - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenMetadataInitialize( - connection: Connection, - payer: Signer, - mint: PublicKey, - updateAuthority: PublicKey, - mintAuthority: PublicKey | Signer, - name: string, - symbol: string, - uri: string, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners); - - const transaction = new Transaction().add( - createInitializeInstruction({ - programId, - metadata: mint, - updateAuthority, - mint, - mintAuthority: mintAuthorityPublicKey, - name, - symbol, - uri, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Initializes a TLV entry with the basic token-metadata fields, - * Includes a transfer for any additional rent-exempt SOL if required. - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint Account - * @param updateAuthority Update Authority - * @param mintAuthority Mint Authority - * @param name Longer name of token - * @param symbol Shortened symbol of token - * @param uri URI pointing to more metadata (image, video, etc) - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenMetadataInitializeWithRentTransfer( - connection: Connection, - payer: Signer, - mint: PublicKey, - updateAuthority: PublicKey, - mintAuthority: PublicKey | Signer, - name: string, - symbol: string, - uri: string, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [mintAuthorityPublicKey, signers] = getSigners(mintAuthority, multiSigners); - - const transaction = new Transaction(); - - const lamports = await getAdditionalRentForNewMetadata( - connection, - mint, - { - updateAuthority, - mint, - name, - symbol, - uri, - additionalMetadata: [], - }, - programId, - ); - - if (lamports > 0) { - transaction.add(SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: mint, lamports: lamports })); - } - - transaction.add( - createInitializeInstruction({ - programId, - metadata: mint, - updateAuthority, - mint, - mintAuthority: mintAuthorityPublicKey, - name, - symbol, - uri, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Updates a field in a token-metadata account. - * If the field does not exist on the account, it will be created. - * If the field does exist, it will be overwritten. - * - * The field can be one of the required fields (name, symbol, URI), or a - * totally new field denoted by a "key" string. - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint Account - * @param updateAuthority Update Authority - * @param field Field to update in the metadata - * @param value Value to write for the field - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenMetadataUpdateField( - connection: Connection, - payer: Signer, - mint: PublicKey, - updateAuthority: PublicKey | Signer, - field: string | Field, - value: string, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [updateAuthorityPublicKey, signers] = getSigners(updateAuthority, multiSigners); - - const transaction = new Transaction().add( - createUpdateFieldInstruction({ - programId, - metadata: mint, - updateAuthority: updateAuthorityPublicKey, - field, - value, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Updates a field in a token-metadata account. - * If the field does not exist on the account, it will be created. - * If the field does exist, it will be overwritten. - * Includes a transfer for any additional rent-exempt SOL if required. - * - * The field can be one of the required fields (name, symbol, URI), or a - * totally new field denoted by a "key" string. - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint Account - * @param updateAuthority Update Authority - * @param field Field to update in the metadata - * @param value Value to write for the field - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenMetadataUpdateFieldWithRentTransfer( - connection: Connection, - payer: Signer, - mint: PublicKey, - updateAuthority: PublicKey | Signer, - field: string | Field, - value: string, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [updateAuthorityPublicKey, signers] = getSigners(updateAuthority, multiSigners); - - const transaction = new Transaction(); - - const lamports = await getAdditionalRentForUpdatedMetadata(connection, mint, field, value, programId); - - if (lamports > 0) { - transaction.add(SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: mint, lamports: lamports })); - } - - transaction.add( - createUpdateFieldInstruction({ - programId, - metadata: mint, - updateAuthority: updateAuthorityPublicKey, - field, - value, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Remove a field in a token-metadata account. - * - * The field can be one of the required fields (name, symbol, URI), or a - * totally new field denoted by a "key" string. - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint Account - * @param updateAuthority Update Authority - * @param key Key to remove in the additional metadata portion - * @param idempotent When true, instruction will not error if the key does not exist - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenMetadataRemoveKey( - connection: Connection, - payer: Signer, - mint: PublicKey, - updateAuthority: PublicKey | Signer, - key: string, - idempotent: boolean, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [updateAuthorityPublicKey, signers] = getSigners(updateAuthority, multiSigners); - - const transaction = new Transaction().add( - createRemoveKeyInstruction({ - programId, - metadata: mint, - updateAuthority: updateAuthorityPublicKey, - key, - idempotent, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Update authority - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint Account - * @param updateAuthority Update Authority - * @param newAuthority New authority for the token metadata, or unset - * @param multiSigners Signing accounts if `authority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function tokenMetadataUpdateAuthority( - connection: Connection, - payer: Signer, - mint: PublicKey, - updateAuthority: PublicKey | Signer, - newAuthority: PublicKey | null, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [updateAuthorityPublicKey, signers] = getSigners(updateAuthority, multiSigners); - - const transaction = new Transaction().add( - createUpdateAuthorityInstruction({ - programId, - metadata: mint, - oldAuthority: updateAuthorityPublicKey, - newAuthority, - }), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/extensions/tokenMetadata/index.ts b/token/js/src/extensions/tokenMetadata/index.ts deleted file mode 100644 index 898210857d0..00000000000 --- a/token/js/src/extensions/tokenMetadata/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './actions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/tokenMetadata/state.ts b/token/js/src/extensions/tokenMetadata/state.ts deleted file mode 100644 index 95480ca04a2..00000000000 --- a/token/js/src/extensions/tokenMetadata/state.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Commitment, Connection } from '@solana/web3.js'; -import type { PublicKey } from '@solana/web3.js'; -import type { TokenMetadata } from '@solana/spl-token-metadata'; -import { Field, unpack } from '@solana/spl-token-metadata'; - -import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; -import { getMint } from '../../state/mint.js'; - -const getNormalizedTokenMetadataField = (field: Field | string): string => { - if (field === Field.Name || field === 'Name' || field === 'name') { - return 'name'; - } - - if (field === Field.Symbol || field === 'Symbol' || field === 'symbol') { - return 'symbol'; - } - - if (field === Field.Uri || field === 'Uri' || field === 'uri') { - return 'uri'; - } - - return field; -}; - -export function updateTokenMetadata(current: TokenMetadata, key: Field | string, value: string): TokenMetadata { - const field = getNormalizedTokenMetadataField(key); - - if (field === 'mint' || field === 'updateAuthority') { - throw new Error(`Cannot update ${field} via this instruction`); - } - - // Handle updates to default keys - if (['name', 'symbol', 'uri'].includes(field)) { - return { - ...current, - [field]: value, - }; - } - - // Avoid mutating input, make a shallow copy - const additionalMetadata = [...current.additionalMetadata]; - - const i = current.additionalMetadata.findIndex(x => x[0] === field); - - if (i === -1) { - // Key was not found, add it - additionalMetadata.push([field, value]); - } else { - // Key was found, change value - additionalMetadata[i] = [field, value]; - } - - return { - ...current, - additionalMetadata, - }; -} - -/** - * Retrieve Token Metadata Information - * - * @param connection Connection to use - * @param address Mint account - * @param commitment Desired level of commitment for querying the state - * @param programId SPL Token program account - * - * @return Token Metadata information - */ -export async function getTokenMetadata( - connection: Connection, - address: PublicKey, - commitment?: Commitment, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const mintInfo = await getMint(connection, address, commitment, programId); - const data = getExtensionData(ExtensionType.TokenMetadata, mintInfo.tlvData); - - if (data === null) { - return null; - } - - return unpack(data); -} diff --git a/token/js/src/extensions/transferFee/actions.ts b/token/js/src/extensions/transferFee/actions.ts deleted file mode 100644 index fe0ea10042c..00000000000 --- a/token/js/src/extensions/transferFee/actions.ts +++ /dev/null @@ -1,203 +0,0 @@ -import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { getSigners } from '../../actions/internal.js'; -import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { - createHarvestWithheldTokensToMintInstruction, - createSetTransferFeeInstruction, - createTransferCheckedWithFeeInstruction, - createWithdrawWithheldTokensFromAccountsInstruction, - createWithdrawWithheldTokensFromMintInstruction, -} from './instructions.js'; - -/** - * Transfer tokens from one account to another, asserting the transfer fee, token mint, and decimals - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param source Source account - * @param mint Mint for the account - * @param destination Destination account - * @param owner Owner of the source account - * @param amount Number of tokens to transfer - * @param decimals Number of decimals in transfer amount - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function transferCheckedWithFee( - connection: Connection, - payer: Signer, - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - owner: Signer | PublicKey, - amount: bigint, - decimals: number, - fee: bigint, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [ownerPublicKey, signers] = getSigners(owner, multiSigners); - - const transaction = new Transaction().add( - createTransferCheckedWithFeeInstruction( - source, - mint, - destination, - ownerPublicKey, - amount, - decimals, - fee, - multiSigners, - programId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Withdraw withheld tokens from mint - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint The token mint - * @param destination The destination account - * @param authority The mint's withdraw withheld tokens authority - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function withdrawWithheldTokensFromMint( - connection: Connection, - payer: Signer, - mint: PublicKey, - destination: PublicKey, - authority: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - createWithdrawWithheldTokensFromMintInstruction(mint, destination, authorityPublicKey, signers, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Withdraw withheld tokens from accounts - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint The token mint - * @param destination The destination account - * @param authority The mint's withdraw withheld tokens authority - * @param multiSigners Signing accounts if `owner` is a multisig - * @param sources Source accounts from which to withdraw withheld fees - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function withdrawWithheldTokensFromAccounts( - connection: Connection, - payer: Signer, - mint: PublicKey, - destination: PublicKey, - authority: Signer | PublicKey, - multiSigners: Signer[], - sources: PublicKey[], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - createWithdrawWithheldTokensFromAccountsInstruction( - mint, - destination, - authorityPublicKey, - signers, - sources, - programId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Harvest withheld tokens from accounts to the mint - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint The token mint - * @param sources Source accounts from which to withdraw withheld fees - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function harvestWithheldTokensToMint( - connection: Connection, - payer: Signer, - mint: PublicKey, - sources: PublicKey[], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const transaction = new Transaction().add(createHarvestWithheldTokensToMintInstruction(mint, sources, programId)); - - return await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); -} - -/** - * Update transfer fee and maximum fee - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint The token mint - * @param authority The authority of the transfer fee - * @param multiSigners Signing accounts if `owner` is a multisig - * @param transferFeeBasisPoints Amount of transfer collected as fees, expressed as basis points of the transfer amount - * @param maximumFee Maximum fee assessed on transfers - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function setTransferFee( - connection: Connection, - payer: Signer, - mint: PublicKey, - authority: Signer | PublicKey, - multiSigners: Signer[], - transferFeeBasisPoints: number, - maximumFee: bigint, - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - createSetTransferFeeInstruction( - mint, - authorityPublicKey, - signers, - transferFeeBasisPoints, - maximumFee, - programId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/extensions/transferFee/index.ts b/token/js/src/extensions/transferFee/index.ts deleted file mode 100644 index 5e28fd6b10a..00000000000 --- a/token/js/src/extensions/transferFee/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './actions.js'; -export * from './instructions.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/transferFee/instructions.ts b/token/js/src/extensions/transferFee/instructions.ts deleted file mode 100644 index 2db3f791798..00000000000 --- a/token/js/src/extensions/transferFee/instructions.ts +++ /dev/null @@ -1,983 +0,0 @@ -import { struct, u16, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, Signer, PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, - TokenUnsupportedInstructionError, -} from '../../errors.js'; -import { addSigners } from '../../instructions/internal.js'; -import { TokenInstruction } from '../../instructions/types.js'; -import { COptionPublicKeyLayout } from '../../serialization.js'; - -export enum TransferFeeInstruction { - InitializeTransferFeeConfig = 0, - TransferCheckedWithFee = 1, - WithdrawWithheldTokensFromMint = 2, - WithdrawWithheldTokensFromAccounts = 3, - HarvestWithheldTokensToMint = 4, - SetTransferFee = 5, -} - -// InitializeTransferFeeConfig - -/** TODO: docs */ -export interface InitializeTransferFeeConfigInstructionData { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.InitializeTransferFeeConfig; - transferFeeConfigAuthority: PublicKey | null; - withdrawWithheldAuthority: PublicKey | null; - transferFeeBasisPoints: number; - maximumFee: bigint; -} - -/** TODO: docs */ -export const initializeTransferFeeConfigInstructionData = struct([ - u8('instruction'), - u8('transferFeeInstruction'), - new COptionPublicKeyLayout('transferFeeConfigAuthority'), - new COptionPublicKeyLayout('withdrawWithheldAuthority'), - u16('transferFeeBasisPoints'), - u64('maximumFee'), -]); - -/** - * Construct an InitializeTransferFeeConfig instruction - * - * @param mint Token mint account - * @param transferFeeConfigAuthority Optional authority that can update the fees - * @param withdrawWithheldAuthority Optional authority that can withdraw fees - * @param transferFeeBasisPoints Amount of transfer collected as fees, expressed as basis points of the transfer amount - * @param maximumFee Maximum fee assessed on transfers - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeTransferFeeConfigInstruction( - mint: PublicKey, - transferFeeConfigAuthority: PublicKey | null, - withdrawWithheldAuthority: PublicKey | null, - transferFeeBasisPoints: number, - maximumFee: bigint, - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializeTransferFeeConfigInstructionData.span); - initializeTransferFeeConfigInstructionData.encode( - { - instruction: TokenInstruction.TransferFeeExtension, - transferFeeInstruction: TransferFeeInstruction.InitializeTransferFeeConfig, - transferFeeConfigAuthority: transferFeeConfigAuthority, - withdrawWithheldAuthority: withdrawWithheldAuthority, - transferFeeBasisPoints: transferFeeBasisPoints, - maximumFee: maximumFee, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializeTransferFeeConfig instruction */ -export interface DecodedInitializeTransferFeeConfigInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.InitializeTransferFeeConfig; - transferFeeConfigAuthority: PublicKey | null; - withdrawWithheldAuthority: PublicKey | null; - transferFeeBasisPoints: number; - maximumFee: bigint; - }; -} - -/** - * Decode an InitializeTransferFeeConfig instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializeTransferFeeConfigInstruction( - instruction: TransactionInstruction, - programId: PublicKey, -): DecodedInitializeTransferFeeConfigInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializeTransferFeeConfigInstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint }, - data, - } = decodeInitializeTransferFeeConfigInstructionUnchecked(instruction); - if ( - data.instruction !== TokenInstruction.TransferFeeExtension || - data.transferFeeInstruction !== TransferFeeInstruction.InitializeTransferFeeConfig - ) - throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - }, - data, - }; -} - -/** A decoded, non-validated InitializeTransferFeeConfig instruction */ -export interface DecodedInitializeTransferFeeConfigInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta | undefined; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.InitializeTransferFeeConfig; - transferFeeConfigAuthority: PublicKey | null; - withdrawWithheldAuthority: PublicKey | null; - transferFeeBasisPoints: number; - maximumFee: bigint; - }; -} - -/** - * Decode an InitializeTransferFeeConfig instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializeTransferFeeConfigInstructionUnchecked({ - programId, - keys: [mint], - data, -}: TransactionInstruction): DecodedInitializeTransferFeeConfigInstructionUnchecked { - const { - instruction, - transferFeeInstruction, - transferFeeConfigAuthority, - withdrawWithheldAuthority, - transferFeeBasisPoints, - maximumFee, - } = initializeTransferFeeConfigInstructionData.decode(data); - - return { - programId, - keys: { - mint, - }, - data: { - instruction, - transferFeeInstruction, - transferFeeConfigAuthority, - withdrawWithheldAuthority, - transferFeeBasisPoints, - maximumFee, - }, - }; -} - -// TransferCheckedWithFee -export interface TransferCheckedWithFeeInstructionData { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.TransferCheckedWithFee; - amount: bigint; - decimals: number; - fee: bigint; -} - -export const transferCheckedWithFeeInstructionData = struct([ - u8('instruction'), - u8('transferFeeInstruction'), - u64('amount'), - u8('decimals'), - u64('fee'), -]); - -/** - * Construct an TransferCheckedWithFee instruction - * - * @param source The source account - * @param mint The token mint - * @param destination The destination account - * @param authority The source account's owner/delegate - * @param signers The signer account(s) - * @param amount The amount of tokens to transfer - * @param decimals The expected number of base 10 digits to the right of the decimal place - * @param fee The expected fee assesed on this transfer, calculated off-chain based on the transferFeeBasisPoints and maximumFee of the mint. - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createTransferCheckedWithFeeInstruction( - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - authority: PublicKey, - amount: bigint, - decimals: number, - fee: bigint, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const data = Buffer.alloc(transferCheckedWithFeeInstructionData.span); - transferCheckedWithFeeInstructionData.encode( - { - instruction: TokenInstruction.TransferFeeExtension, - transferFeeInstruction: TransferFeeInstruction.TransferCheckedWithFee, - amount, - decimals, - fee, - }, - data, - ); - const keys = addSigners( - [ - { pubkey: source, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: destination, isSigner: false, isWritable: true }, - ], - authority, - multiSigners, - ); - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid TransferCheckedWithFee instruction */ -export interface DecodedTransferCheckedWithFeeInstruction { - programId: PublicKey; - keys: { - source: AccountMeta; - mint: AccountMeta; - destination: AccountMeta; - authority: AccountMeta; - signers: AccountMeta[] | null; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.TransferCheckedWithFee; - amount: bigint; - decimals: number; - fee: bigint; - }; -} - -/** - * Decode a TransferCheckedWithFee instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeTransferCheckedWithFeeInstruction( - instruction: TransactionInstruction, - programId: PublicKey, -): DecodedTransferCheckedWithFeeInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== transferCheckedWithFeeInstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { source, mint, destination, authority, signers }, - data, - } = decodeTransferCheckedWithFeeInstructionUnchecked(instruction); - if ( - data.instruction !== TokenInstruction.TransferFeeExtension || - data.transferFeeInstruction !== TransferFeeInstruction.TransferCheckedWithFee - ) - throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - source, - mint, - destination, - authority, - signers: signers ? signers : null, - }, - data, - }; -} - -/** A decoded, non-validated TransferCheckedWithFees instruction */ -export interface DecodedTransferCheckedWithFeeInstructionUnchecked { - programId: PublicKey; - keys: { - source: AccountMeta; - mint: AccountMeta; - destination: AccountMeta; - authority: AccountMeta; - signers: AccountMeta[] | undefined; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.TransferCheckedWithFee; - amount: bigint; - decimals: number; - fee: bigint; - }; -} - -/** - * Decode a TransferCheckedWithFees instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeTransferCheckedWithFeeInstructionUnchecked({ - programId, - keys: [source, mint, destination, authority, ...signers], - data, -}: TransactionInstruction): DecodedTransferCheckedWithFeeInstructionUnchecked { - const { instruction, transferFeeInstruction, amount, decimals, fee } = - transferCheckedWithFeeInstructionData.decode(data); - - return { - programId, - keys: { - source, - mint, - destination, - authority, - signers, - }, - data: { - instruction, - transferFeeInstruction, - amount, - decimals, - fee, - }, - }; -} - -// WithdrawWithheldTokensFromMint -export interface WithdrawWithheldTokensFromMintInstructionData { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.WithdrawWithheldTokensFromMint; -} - -export const withdrawWithheldTokensFromMintInstructionData = struct([ - u8('instruction'), - u8('transferFeeInstruction'), -]); - -/** - * Construct a WithdrawWithheldTokensFromMint instruction - * - * @param mint The token mint - * @param destination The destination account - * @param authority The source account's owner/delegate - * @param signers The signer account(s) - * @param programID SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createWithdrawWithheldTokensFromMintInstruction( - mint: PublicKey, - destination: PublicKey, - authority: PublicKey, - signers: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const data = Buffer.alloc(withdrawWithheldTokensFromMintInstructionData.span); - withdrawWithheldTokensFromMintInstructionData.encode( - { - instruction: TokenInstruction.TransferFeeExtension, - transferFeeInstruction: TransferFeeInstruction.WithdrawWithheldTokensFromMint, - }, - data, - ); - const keys = addSigners( - [ - { pubkey: mint, isSigner: false, isWritable: true }, - { pubkey: destination, isSigner: false, isWritable: true }, - ], - authority, - signers, - ); - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid WithdrawWithheldTokensFromMint instruction */ -export interface DecodedWithdrawWithheldTokensFromMintInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - destination: AccountMeta; - authority: AccountMeta; - signers: AccountMeta[] | null; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.WithdrawWithheldTokensFromMint; - }; -} - -/** - * Decode a WithdrawWithheldTokensFromMint instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeWithdrawWithheldTokensFromMintInstruction( - instruction: TransactionInstruction, - programId: PublicKey, -): DecodedWithdrawWithheldTokensFromMintInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== withdrawWithheldTokensFromMintInstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint, destination, authority, signers }, - data, - } = decodeWithdrawWithheldTokensFromMintInstructionUnchecked(instruction); - if ( - data.instruction !== TokenInstruction.TransferFeeExtension || - data.transferFeeInstruction !== TransferFeeInstruction.WithdrawWithheldTokensFromMint - ) - throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - destination, - authority, - signers: signers ? signers : null, - }, - data, - }; -} - -/** A decoded, valid WithdrawWithheldTokensFromMint instruction */ -export interface DecodedWithdrawWithheldTokensFromMintInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta; - destination: AccountMeta; - authority: AccountMeta; - signers: AccountMeta[] | null; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.WithdrawWithheldTokensFromMint; - }; -} - -/** - * Decode a WithdrawWithheldTokensFromMint instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeWithdrawWithheldTokensFromMintInstructionUnchecked({ - programId, - keys: [mint, destination, authority, ...signers], - data, -}: TransactionInstruction): DecodedWithdrawWithheldTokensFromMintInstructionUnchecked { - const { instruction, transferFeeInstruction } = withdrawWithheldTokensFromMintInstructionData.decode(data); - - return { - programId, - keys: { - mint, - destination, - authority, - signers, - }, - data: { - instruction, - transferFeeInstruction, - }, - }; -} - -// WithdrawWithheldTokensFromAccounts -export interface WithdrawWithheldTokensFromAccountsInstructionData { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.WithdrawWithheldTokensFromAccounts; - numTokenAccounts: number; -} - -export const withdrawWithheldTokensFromAccountsInstructionData = - struct([ - u8('instruction'), - u8('transferFeeInstruction'), - u8('numTokenAccounts'), - ]); - -/** - * Construct a WithdrawWithheldTokensFromAccounts instruction - * - * @param mint The token mint - * @param destination The destination account - * @param authority The source account's owner/delegate - * @param signers The signer account(s) - * @param sources The source accounts to withdraw from - * @param programID SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createWithdrawWithheldTokensFromAccountsInstruction( - mint: PublicKey, - destination: PublicKey, - authority: PublicKey, - signers: (Signer | PublicKey)[], - sources: PublicKey[], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const data = Buffer.alloc(withdrawWithheldTokensFromAccountsInstructionData.span); - withdrawWithheldTokensFromAccountsInstructionData.encode( - { - instruction: TokenInstruction.TransferFeeExtension, - transferFeeInstruction: TransferFeeInstruction.WithdrawWithheldTokensFromAccounts, - numTokenAccounts: sources.length, - }, - data, - ); - const keys = addSigners( - [ - { pubkey: mint, isSigner: false, isWritable: true }, - { pubkey: destination, isSigner: false, isWritable: true }, - ], - authority, - signers, - ); - for (const source of sources) { - keys.push({ pubkey: source, isSigner: false, isWritable: true }); - } - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid WithdrawWithheldTokensFromAccounts instruction */ -export interface DecodedWithdrawWithheldTokensFromAccountsInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - destination: AccountMeta; - authority: AccountMeta; - signers: AccountMeta[] | null; - sources: AccountMeta[] | null; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.WithdrawWithheldTokensFromAccounts; - numTokenAccounts: number; - }; -} - -/** - * Decode a WithdrawWithheldTokensFromAccounts instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeWithdrawWithheldTokensFromAccountsInstruction( - instruction: TransactionInstruction, - programId: PublicKey, -): DecodedWithdrawWithheldTokensFromAccountsInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== withdrawWithheldTokensFromAccountsInstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint, destination, authority, signers, sources }, - data, - } = decodeWithdrawWithheldTokensFromAccountsInstructionUnchecked(instruction); - if ( - data.instruction !== TokenInstruction.TransferFeeExtension || - data.transferFeeInstruction !== TransferFeeInstruction.WithdrawWithheldTokensFromAccounts - ) - throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - destination, - authority, - signers: signers ? signers : null, - sources: sources ? sources : null, - }, - data, - }; -} - -/** A decoded, valid WithdrawWithheldTokensFromAccounts instruction */ -export interface DecodedWithdrawWithheldTokensFromAccountsInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta; - destination: AccountMeta; - authority: AccountMeta; - signers: AccountMeta[] | null; - sources: AccountMeta[] | null; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.WithdrawWithheldTokensFromAccounts; - numTokenAccounts: number; - }; -} - -/** - * Decode a WithdrawWithheldTokensFromAccount instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeWithdrawWithheldTokensFromAccountsInstructionUnchecked({ - programId, - keys, - data, -}: TransactionInstruction): DecodedWithdrawWithheldTokensFromAccountsInstructionUnchecked { - const { instruction, transferFeeInstruction, numTokenAccounts } = - withdrawWithheldTokensFromAccountsInstructionData.decode(data); - const [mint, destination, authority, signers, sources] = [ - keys[0], - keys[1], - keys[2], - keys.slice(3, 3 + numTokenAccounts), - keys.slice(-1 * numTokenAccounts), - ]; - return { - programId, - keys: { - mint, - destination, - authority, - signers, - sources, - }, - data: { - instruction, - transferFeeInstruction, - numTokenAccounts, - }, - }; -} - -// HarvestWithheldTokensToMint - -export interface HarvestWithheldTokensToMintInstructionData { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.HarvestWithheldTokensToMint; -} - -export const harvestWithheldTokensToMintInstructionData = struct([ - u8('instruction'), - u8('transferFeeInstruction'), -]); - -/** - * Construct a HarvestWithheldTokensToMint instruction - * - * @param mint The token mint - * @param sources The source accounts to withdraw from - * @param programID SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createHarvestWithheldTokensToMintInstruction( - mint: PublicKey, - sources: PublicKey[], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const data = Buffer.alloc(harvestWithheldTokensToMintInstructionData.span); - harvestWithheldTokensToMintInstructionData.encode( - { - instruction: TokenInstruction.TransferFeeExtension, - transferFeeInstruction: TransferFeeInstruction.HarvestWithheldTokensToMint, - }, - data, - ); - const keys: AccountMeta[] = []; - keys.push({ pubkey: mint, isSigner: false, isWritable: true }); - for (const source of sources) { - keys.push({ pubkey: source, isSigner: false, isWritable: true }); - } - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid HarvestWithheldTokensToMint instruction */ -export interface DecodedHarvestWithheldTokensToMintInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - sources: AccountMeta[] | null; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.HarvestWithheldTokensToMint; - }; -} - -/** - * Decode a HarvestWithheldTokensToMint instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeHarvestWithheldTokensToMintInstruction( - instruction: TransactionInstruction, - programId: PublicKey, -): DecodedHarvestWithheldTokensToMintInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== harvestWithheldTokensToMintInstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint, sources }, - data, - } = decodeHarvestWithheldTokensToMintInstructionUnchecked(instruction); - if ( - data.instruction !== TokenInstruction.TransferFeeExtension || - data.transferFeeInstruction !== TransferFeeInstruction.HarvestWithheldTokensToMint - ) - throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - sources, - }, - data, - }; -} - -/** A decoded, valid HarvestWithheldTokensToMint instruction */ -export interface DecodedHarvestWithheldTokensToMintInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta; - sources: AccountMeta[] | null; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.HarvestWithheldTokensToMint; - }; -} - -/** - * Decode a HarvestWithheldTokensToMint instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeHarvestWithheldTokensToMintInstructionUnchecked({ - programId, - keys: [mint, ...sources], - data, -}: TransactionInstruction): DecodedHarvestWithheldTokensToMintInstructionUnchecked { - const { instruction, transferFeeInstruction } = harvestWithheldTokensToMintInstructionData.decode(data); - return { - programId, - keys: { - mint, - sources, - }, - data: { - instruction, - transferFeeInstruction, - }, - }; -} - -// SetTransferFee - -export interface SetTransferFeeInstructionData { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.SetTransferFee; - transferFeeBasisPoints: number; - maximumFee: bigint; -} - -export const setTransferFeeInstructionData = struct([ - u8('instruction'), - u8('transferFeeInstruction'), - u16('transferFeeBasisPoints'), - u64('maximumFee'), -]); - -/** - * Construct a SetTransferFeeInstruction instruction - * - * @param mint The token mint - * @param authority The authority of the transfer fee - * @param signers The signer account(s) - * @param transferFeeBasisPoints Amount of transfer collected as fees, expressed as basis points of the transfer amount - * @param maximumFee Maximum fee assessed on transfers - * @param programID SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createSetTransferFeeInstruction( - mint: PublicKey, - authority: PublicKey, - signers: (Signer | PublicKey)[], - transferFeeBasisPoints: number, - maximumFee: bigint, - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - - const data = Buffer.alloc(setTransferFeeInstructionData.span); - setTransferFeeInstructionData.encode( - { - instruction: TokenInstruction.TransferFeeExtension, - transferFeeInstruction: TransferFeeInstruction.SetTransferFee, - transferFeeBasisPoints: transferFeeBasisPoints, - maximumFee: maximumFee, - }, - data, - ); - const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, signers); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid SetTransferFee instruction */ -export interface DecodedSetTransferFeeInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - authority: AccountMeta; - signers: AccountMeta[] | null; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.SetTransferFee; - transferFeeBasisPoints: number; - maximumFee: bigint; - }; -} - -/** - * Decode an SetTransferFee instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeSetTransferFeeInstruction( - instruction: TransactionInstruction, - programId: PublicKey, -): DecodedSetTransferFeeInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== setTransferFeeInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint, authority, signers }, - data, - } = decodeSetTransferFeeInstructionUnchecked(instruction); - if ( - data.instruction !== TokenInstruction.TransferFeeExtension || - data.transferFeeInstruction !== TransferFeeInstruction.SetTransferFee - ) - throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - authority, - signers: signers ? signers : null, - }, - data, - }; -} - -/** A decoded, valid SetTransferFee instruction */ -export interface DecodedSetTransferFeeInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta; - authority: AccountMeta; - signers: AccountMeta[] | undefined; - }; - data: { - instruction: TokenInstruction.TransferFeeExtension; - transferFeeInstruction: TransferFeeInstruction.SetTransferFee; - transferFeeBasisPoints: number; - maximumFee: bigint; - }; -} - -/** - * Decode a SetTransferFee instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeSetTransferFeeInstructionUnchecked({ - programId, - keys: [mint, authority, ...signers], - data, -}: TransactionInstruction): DecodedSetTransferFeeInstructionUnchecked { - const { instruction, transferFeeInstruction, transferFeeBasisPoints, maximumFee } = - setTransferFeeInstructionData.decode(data); - - return { - programId, - keys: { - mint, - authority, - signers, - }, - data: { - instruction, - transferFeeInstruction, - transferFeeBasisPoints, - maximumFee, - }, - }; -} diff --git a/token/js/src/extensions/transferFee/state.ts b/token/js/src/extensions/transferFee/state.ts deleted file mode 100644 index bef4e9a7d89..00000000000 --- a/token/js/src/extensions/transferFee/state.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { Layout } from '@solana/buffer-layout'; -import { struct, u16 } from '@solana/buffer-layout'; -import { publicKey, u64 } from '@solana/buffer-layout-utils'; -import type { PublicKey } from '@solana/web3.js'; -import type { Account } from '../../state/account.js'; -import type { Mint } from '../../state/mint.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; - -export const MAX_FEE_BASIS_POINTS = 10000; -export const ONE_IN_BASIS_POINTS = BigInt(MAX_FEE_BASIS_POINTS); - -/** TransferFeeConfig as stored by the program */ -export interface TransferFee { - /** First epoch where the transfer fee takes effect */ - epoch: bigint; - /** Maximum fee assessed on transfers, expressed as an amount of tokens */ - maximumFee: bigint; - /** - * Amount of transfer collected as fees, expressed as basis points of the - * transfer amount, ie. increments of 0.01% - */ - transferFeeBasisPoints: number; -} - -/** Transfer fee extension data for mints. */ -export interface TransferFeeConfig { - /** Optional authority to set the fee */ - transferFeeConfigAuthority: PublicKey; - /** Withdraw from mint instructions must be signed by this key */ - withdrawWithheldAuthority: PublicKey; - /** Withheld transfer fee tokens that have been moved to the mint for withdrawal */ - withheldAmount: bigint; - /** Older transfer fee, used if the current epoch < newerTransferFee.epoch */ - olderTransferFee: TransferFee; - /** Newer transfer fee, used if the current epoch >= newerTransferFee.epoch */ - newerTransferFee: TransferFee; -} - -/** Buffer layout for de/serializing a transfer fee */ -export function transferFeeLayout(property?: string): Layout { - return struct([u64('epoch'), u64('maximumFee'), u16('transferFeeBasisPoints')], property); -} - -/** Calculate the transfer fee */ -export function calculateFee(transferFee: TransferFee, preFeeAmount: bigint): bigint { - const transferFeeBasisPoints = transferFee.transferFeeBasisPoints; - if (transferFeeBasisPoints === 0 || preFeeAmount === BigInt(0)) { - return BigInt(0); - } else { - const numerator = preFeeAmount * BigInt(transferFeeBasisPoints); - const rawFee = (numerator + ONE_IN_BASIS_POINTS - BigInt(1)) / ONE_IN_BASIS_POINTS; - const fee = rawFee > transferFee.maximumFee ? transferFee.maximumFee : rawFee; - return BigInt(fee); - } -} - -/** Buffer layout for de/serializing a transfer fee config extension */ -export const TransferFeeConfigLayout = struct([ - publicKey('transferFeeConfigAuthority'), - publicKey('withdrawWithheldAuthority'), - u64('withheldAmount'), - transferFeeLayout('olderTransferFee'), - transferFeeLayout('newerTransferFee'), -]); - -export const TRANSFER_FEE_CONFIG_SIZE = TransferFeeConfigLayout.span; - -/** Get the fee for given epoch */ -export function getEpochFee(transferFeeConfig: TransferFeeConfig, epoch: bigint): TransferFee { - if (epoch >= transferFeeConfig.newerTransferFee.epoch) { - return transferFeeConfig.newerTransferFee; - } else { - return transferFeeConfig.olderTransferFee; - } -} - -/** Calculate the fee for the given epoch and input amount */ -export function calculateEpochFee(transferFeeConfig: TransferFeeConfig, epoch: bigint, preFeeAmount: bigint): bigint { - const transferFee = getEpochFee(transferFeeConfig, epoch); - return calculateFee(transferFee, preFeeAmount); -} - -/** Transfer fee amount data for accounts. */ -export interface TransferFeeAmount { - /** Withheld transfer fee tokens that can be claimed by the fee authority */ - withheldAmount: bigint; -} -/** Buffer layout for de/serializing */ -export const TransferFeeAmountLayout = struct([u64('withheldAmount')]); -export const TRANSFER_FEE_AMOUNT_SIZE = TransferFeeAmountLayout.span; - -export function getTransferFeeConfig(mint: Mint): TransferFeeConfig | null { - const extensionData = getExtensionData(ExtensionType.TransferFeeConfig, mint.tlvData); - if (extensionData !== null) { - return TransferFeeConfigLayout.decode(extensionData); - } else { - return null; - } -} - -export function getTransferFeeAmount(account: Account): TransferFeeAmount | null { - const extensionData = getExtensionData(ExtensionType.TransferFeeAmount, account.tlvData); - if (extensionData !== null) { - return TransferFeeAmountLayout.decode(extensionData); - } else { - return null; - } -} diff --git a/token/js/src/extensions/transferHook/actions.ts b/token/js/src/extensions/transferHook/actions.ts deleted file mode 100644 index 591aa4ff301..00000000000 --- a/token/js/src/extensions/transferHook/actions.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { ConfirmOptions, Connection, Signer, TransactionSignature } from '@solana/web3.js'; -import type { PublicKey } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js'; -import { getSigners } from '../../actions/internal.js'; -import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../../constants.js'; -import { - createInitializeTransferHookInstruction, - createTransferCheckedWithFeeAndTransferHookInstruction, - createTransferCheckedWithTransferHookInstruction, - createUpdateTransferHookInstruction, -} from './instructions.js'; - -/** - * Initialize a transfer hook on a mint - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint to initialize with extension - * @param authority Transfer hook authority account - * @param transferHookProgramId The transfer hook program account - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function initializeTransferHook( - connection: Connection, - payer: Signer, - mint: PublicKey, - authority: PublicKey, - transferHookProgramId: PublicKey, - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const transaction = new Transaction().add( - createInitializeTransferHookInstruction(mint, authority, transferHookProgramId, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); -} - -/** - * Update the transfer hook program on a mint - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param mint Mint to modify - * @param transferHookProgramId New transfer hook program account - * @param authority Transfer hook update authority - * @param multiSigners Signing accounts if `freezeAuthority` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function updateTransferHook( - connection: Connection, - payer: Signer, - mint: PublicKey, - transferHookProgramId: PublicKey, - authority: Signer | PublicKey, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_2022_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - createUpdateTransferHookInstruction(mint, authorityPublicKey, transferHookProgramId, signers, programId), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Transfer tokens from one account to another, asserting the token mint, and decimals - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param source Source account - * @param mint Mint for the account - * @param destination Destination account - * @param authority Authority of the source account - * @param amount Number of tokens to transfer - * @param decimals Number of decimals in transfer amount - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function transferCheckedWithTransferHook( - connection: Connection, - payer: Signer, - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - authority: Signer | PublicKey, - amount: bigint, - decimals: number, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - await createTransferCheckedWithTransferHookInstruction( - connection, - source, - mint, - destination, - authorityPublicKey, - amount, - decimals, - signers, - confirmOptions?.commitment, - programId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} - -/** - * Transfer tokens from one account to another, asserting the transfer fee, token mint, and decimals - * - * @param connection Connection to use - * @param payer Payer of the transaction fees - * @param source Source account - * @param mint Mint for the account - * @param destination Destination account - * @param authority Authority of the source account - * @param amount Number of tokens to transfer - * @param decimals Number of decimals in transfer amount - * @param fee The calculated fee for the transfer fee extension - * @param multiSigners Signing accounts if `owner` is a multisig - * @param confirmOptions Options for confirming the transaction - * @param programId SPL Token program account - * - * @return Signature of the confirmed transaction - */ -export async function transferCheckedWithFeeAndTransferHook( - connection: Connection, - payer: Signer, - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - authority: Signer | PublicKey, - amount: bigint, - decimals: number, - fee: bigint, - multiSigners: Signer[] = [], - confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, -): Promise { - const [authorityPublicKey, signers] = getSigners(authority, multiSigners); - - const transaction = new Transaction().add( - await createTransferCheckedWithFeeAndTransferHookInstruction( - connection, - source, - mint, - destination, - authorityPublicKey, - amount, - decimals, - fee, - signers, - confirmOptions?.commitment, - programId, - ), - ); - - return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); -} diff --git a/token/js/src/extensions/transferHook/index.ts b/token/js/src/extensions/transferHook/index.ts deleted file mode 100644 index b788b0de4ad..00000000000 --- a/token/js/src/extensions/transferHook/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './actions.js'; -export * from './instructions.js'; -export * from './seeds.js'; -export * from './state.js'; diff --git a/token/js/src/extensions/transferHook/instructions.ts b/token/js/src/extensions/transferHook/instructions.ts deleted file mode 100644 index ab113386e26..00000000000 --- a/token/js/src/extensions/transferHook/instructions.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, Commitment, Connection, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../../constants.js'; -import { TokenUnsupportedInstructionError } from '../../errors.js'; -import { addSigners } from '../../instructions/internal.js'; -import { TokenInstruction } from '../../instructions/types.js'; -import { publicKey } from '@solana/buffer-layout-utils'; -import { createTransferCheckedInstruction } from '../../instructions/transferChecked.js'; -import { createTransferCheckedWithFeeInstruction } from '../transferFee/instructions.js'; -import { getMint } from '../../state/mint.js'; -import { getExtraAccountMetaAddress, getExtraAccountMetas, getTransferHook, resolveExtraAccountMeta } from './state.js'; - -export enum TransferHookInstruction { - Initialize = 0, - Update = 1, -} - -/** Deserialized instruction for the initiation of an transfer hook */ -export interface InitializeTransferHookInstructionData { - instruction: TokenInstruction.TransferHookExtension; - transferHookInstruction: TransferHookInstruction.Initialize; - authority: PublicKey; - transferHookProgramId: PublicKey; -} - -/** The struct that represents the instruction data as it is read by the program */ -export const initializeTransferHookInstructionData = struct([ - u8('instruction'), - u8('transferHookInstruction'), - publicKey('authority'), - publicKey('transferHookProgramId'), -]); - -/** - * Construct an InitializeTransferHook instruction - * - * @param mint Token mint account - * @param authority Transfer hook authority account - * @param transferHookProgramId Transfer hook program account - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeTransferHookInstruction( - mint: PublicKey, - authority: PublicKey, - transferHookProgramId: PublicKey, - programId: PublicKey, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializeTransferHookInstructionData.span); - initializeTransferHookInstructionData.encode( - { - instruction: TokenInstruction.TransferHookExtension, - transferHookInstruction: TransferHookInstruction.Initialize, - authority, - transferHookProgramId, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** Deserialized instruction for the initiation of an transfer hook */ -export interface UpdateTransferHookInstructionData { - instruction: TokenInstruction.TransferHookExtension; - transferHookInstruction: TransferHookInstruction.Update; - transferHookProgramId: PublicKey; -} - -/** The struct that represents the instruction data as it is read by the program */ -export const updateTransferHookInstructionData = struct([ - u8('instruction'), - u8('transferHookInstruction'), - publicKey('transferHookProgramId'), -]); - -/** - * Construct an UpdateTransferHook instruction - * - * @param mint Mint to update - * @param authority The mint's transfer hook authority - * @param transferHookProgramId The new transfer hook program account - * @param signers The signer account(s) for a multisig - * @param tokenProgramId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createUpdateTransferHookInstruction( - mint: PublicKey, - authority: PublicKey, - transferHookProgramId: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - - const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, multiSigners); - const data = Buffer.alloc(updateTransferHookInstructionData.span); - updateTransferHookInstructionData.encode( - { - instruction: TokenInstruction.TransferHookExtension, - transferHookInstruction: TransferHookInstruction.Update, - transferHookProgramId, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMeta[]): AccountMeta { - const maybeHighestPrivileges = accountMetas - .filter(x => x.pubkey.equals(accountMeta.pubkey)) - .reduce<{ isSigner: boolean; isWritable: boolean } | undefined>((acc, x) => { - if (!acc) return { isSigner: x.isSigner, isWritable: x.isWritable }; - return { isSigner: acc.isSigner || x.isSigner, isWritable: acc.isWritable || x.isWritable }; - }, undefined); - if (maybeHighestPrivileges) { - const { isSigner, isWritable } = maybeHighestPrivileges; - if (!isSigner && isSigner !== accountMeta.isSigner) { - accountMeta.isSigner = false; - } - if (!isWritable && isWritable !== accountMeta.isWritable) { - accountMeta.isWritable = false; - } - } - return accountMeta; -} - -/** - * Construct an `ExecuteInstruction` for a transfer hook program, without the - * additional accounts - * - * @param programId The program ID of the transfer hook program - * @param source The source account - * @param mint The mint account - * @param destination The destination account - * @param owner Owner of the source account - * @param validateStatePubkey The validate state pubkey - * @param amount The amount of tokens to transfer - * @returns Instruction to add to a transaction - */ -export function createExecuteInstruction( - programId: PublicKey, - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - owner: PublicKey, - validateStatePubkey: PublicKey, - amount: bigint, -): TransactionInstruction { - const keys = [source, mint, destination, owner, validateStatePubkey].map(pubkey => ({ - pubkey, - isSigner: false, - isWritable: false, - })); - - const data = Buffer.alloc(16); - data.set(Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]), 0); // `ExecuteInstruction` discriminator - data.writeBigUInt64LE(BigInt(amount), 8); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** - * Adds all the extra accounts needed for a transfer hook to an instruction. - * - * Note this will modify the instruction passed in. - * - * @param connection Connection to use - * @param instruction The instruction to add accounts to - * @param programId Transfer hook program ID - * @param source The source account - * @param mint The mint account - * @param destination The destination account - * @param owner Owner of the source account - * @param amount The amount of tokens to transfer - * @param commitment Commitment to use - */ -export async function addExtraAccountMetasForExecute( - connection: Connection, - instruction: TransactionInstruction, - programId: PublicKey, - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - owner: PublicKey, - amount: number | bigint, - commitment?: Commitment, -) { - const validateStatePubkey = getExtraAccountMetaAddress(mint, programId); - const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment); - if (validateStateAccount == null) { - return instruction; - } - const validateStateData = getExtraAccountMetas(validateStateAccount); - - // Check to make sure the provided keys are in the instruction - if (![source, mint, destination, owner].every(key => instruction.keys.some(meta => meta.pubkey.equals(key)))) { - throw new Error('Missing required account in instruction'); - } - - const executeInstruction = createExecuteInstruction( - programId, - source, - mint, - destination, - owner, - validateStatePubkey, - BigInt(amount), - ); - - for (const extraAccountMeta of validateStateData) { - executeInstruction.keys.push( - deEscalateAccountMeta( - await resolveExtraAccountMeta( - connection, - extraAccountMeta, - executeInstruction.keys, - executeInstruction.data, - executeInstruction.programId, - ), - executeInstruction.keys, - ), - ); - } - - // Add only the extra accounts resolved from the validation state - instruction.keys.push(...executeInstruction.keys.slice(5)); - - // Add the transfer hook program ID and the validation state account - instruction.keys.push({ pubkey: programId, isSigner: false, isWritable: false }); - instruction.keys.push({ pubkey: validateStatePubkey, isSigner: false, isWritable: false }); -} - -/** - * Construct an transferChecked instruction with extra accounts for transfer hook - * - * @param connection Connection to use - * @param source Source account - * @param mint Mint to update - * @param destination Destination account - * @param owner Owner of the source account - * @param amount The amount of tokens to transfer - * @param decimals Number of decimals in transfer amount - * @param multiSigners The signer account(s) for a multisig - * @param commitment Commitment to use - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export async function createTransferCheckedWithTransferHookInstruction( - connection: Connection, - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - owner: PublicKey, - amount: bigint, - decimals: number, - multiSigners: (Signer | PublicKey)[] = [], - commitment?: Commitment, - programId = TOKEN_PROGRAM_ID, -) { - const instruction = createTransferCheckedInstruction( - source, - mint, - destination, - owner, - amount, - decimals, - multiSigners, - programId, - ); - - const mintInfo = await getMint(connection, mint, commitment, programId); - const transferHook = getTransferHook(mintInfo); - - if (transferHook) { - await addExtraAccountMetasForExecute( - connection, - instruction, - transferHook.programId, - source, - mint, - destination, - owner, - amount, - commitment, - ); - } - - return instruction; -} - -/** - * Construct an transferChecked instruction with extra accounts for transfer hook - * - * @param connection Connection to use - * @param source Source account - * @param mint Mint to update - * @param destination Destination account - * @param owner Owner of the source account - * @param amount The amount of tokens to transfer - * @param decimals Number of decimals in transfer amount - * @param fee The calculated fee for the transfer fee extension - * @param multiSigners The signer account(s) for a multisig - * @param commitment Commitment to use - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export async function createTransferCheckedWithFeeAndTransferHookInstruction( - connection: Connection, - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - owner: PublicKey, - amount: bigint, - decimals: number, - fee: bigint, - multiSigners: (Signer | PublicKey)[] = [], - commitment?: Commitment, - programId = TOKEN_PROGRAM_ID, -) { - const instruction = createTransferCheckedWithFeeInstruction( - source, - mint, - destination, - owner, - amount, - decimals, - fee, - multiSigners, - programId, - ); - - const mintInfo = await getMint(connection, mint, commitment, programId); - const transferHook = getTransferHook(mintInfo); - - if (transferHook) { - await addExtraAccountMetasForExecute( - connection, - instruction, - transferHook.programId, - source, - mint, - destination, - owner, - amount, - commitment, - ); - } - - return instruction; -} diff --git a/token/js/src/extensions/transferHook/seeds.ts b/token/js/src/extensions/transferHook/seeds.ts deleted file mode 100644 index 230af7208a5..00000000000 --- a/token/js/src/extensions/transferHook/seeds.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { AccountMeta, Connection } from '@solana/web3.js'; -import { TokenTransferHookAccountDataNotFound, TokenTransferHookInvalidSeed } from '../../errors.js'; - -interface Seed { - data: Buffer; - packedLength: number; -} - -const DISCRIMINATOR_SPAN = 1; -const LITERAL_LENGTH_SPAN = 1; -const INSTRUCTION_ARG_OFFSET_SPAN = 1; -const INSTRUCTION_ARG_LENGTH_SPAN = 1; -const ACCOUNT_KEY_INDEX_SPAN = 1; -const ACCOUNT_DATA_ACCOUNT_INDEX_SPAN = 1; -const ACCOUNT_DATA_OFFSET_SPAN = 1; -const ACCOUNT_DATA_LENGTH_SPAN = 1; - -function unpackSeedLiteral(seeds: Uint8Array): Seed { - if (seeds.length < 1) { - throw new TokenTransferHookInvalidSeed(); - } - const [length, ...rest] = seeds; - if (rest.length < length) { - throw new TokenTransferHookInvalidSeed(); - } - return { - data: Buffer.from(rest.slice(0, length)), - packedLength: DISCRIMINATOR_SPAN + LITERAL_LENGTH_SPAN + length, - }; -} - -function unpackSeedInstructionArg(seeds: Uint8Array, instructionData: Buffer): Seed { - if (seeds.length < 2) { - throw new TokenTransferHookInvalidSeed(); - } - const [index, length] = seeds; - if (instructionData.length < length + index) { - throw new TokenTransferHookInvalidSeed(); - } - return { - data: instructionData.subarray(index, index + length), - packedLength: DISCRIMINATOR_SPAN + INSTRUCTION_ARG_OFFSET_SPAN + INSTRUCTION_ARG_LENGTH_SPAN, - }; -} - -function unpackSeedAccountKey(seeds: Uint8Array, previousMetas: AccountMeta[]): Seed { - if (seeds.length < 1) { - throw new TokenTransferHookInvalidSeed(); - } - const [index] = seeds; - if (previousMetas.length <= index) { - throw new TokenTransferHookInvalidSeed(); - } - return { - data: previousMetas[index].pubkey.toBuffer(), - packedLength: DISCRIMINATOR_SPAN + ACCOUNT_KEY_INDEX_SPAN, - }; -} - -async function unpackSeedAccountData( - seeds: Uint8Array, - previousMetas: AccountMeta[], - connection: Connection, -): Promise { - if (seeds.length < 3) { - throw new TokenTransferHookInvalidSeed(); - } - const [accountIndex, dataIndex, length] = seeds; - if (previousMetas.length <= accountIndex) { - throw new TokenTransferHookInvalidSeed(); - } - const accountInfo = await connection.getAccountInfo(previousMetas[accountIndex].pubkey); - if (accountInfo == null) { - throw new TokenTransferHookAccountDataNotFound(); - } - if (accountInfo.data.length < dataIndex + length) { - throw new TokenTransferHookInvalidSeed(); - } - return { - data: accountInfo.data.subarray(dataIndex, dataIndex + length), - packedLength: - DISCRIMINATOR_SPAN + ACCOUNT_DATA_ACCOUNT_INDEX_SPAN + ACCOUNT_DATA_OFFSET_SPAN + ACCOUNT_DATA_LENGTH_SPAN, - }; -} - -async function unpackFirstSeed( - seeds: Uint8Array, - previousMetas: AccountMeta[], - instructionData: Buffer, - connection: Connection, -): Promise { - const [discriminator, ...rest] = seeds; - const remaining = new Uint8Array(rest); - switch (discriminator) { - case 0: - return null; - case 1: - return unpackSeedLiteral(remaining); - case 2: - return unpackSeedInstructionArg(remaining, instructionData); - case 3: - return unpackSeedAccountKey(remaining, previousMetas); - case 4: - return unpackSeedAccountData(remaining, previousMetas, connection); - default: - throw new TokenTransferHookInvalidSeed(); - } -} - -export async function unpackSeeds( - seeds: Uint8Array, - previousMetas: AccountMeta[], - instructionData: Buffer, - connection: Connection, -): Promise { - const unpackedSeeds: Buffer[] = []; - let i = 0; - while (i < 32) { - const seed = await unpackFirstSeed(seeds.slice(i), previousMetas, instructionData, connection); - if (seed == null) { - break; - } - unpackedSeeds.push(seed.data); - i += seed.packedLength; - } - return unpackedSeeds; -} diff --git a/token/js/src/extensions/transferHook/state.ts b/token/js/src/extensions/transferHook/state.ts deleted file mode 100644 index d79604a9b13..00000000000 --- a/token/js/src/extensions/transferHook/state.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { blob, greedy, seq, struct, u32, u8 } from '@solana/buffer-layout'; -import type { Mint } from '../../state/mint.js'; -import { ExtensionType, getExtensionData } from '../extensionType.js'; -import type { AccountInfo, AccountMeta, Connection } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { bool, publicKey, u64 } from '@solana/buffer-layout-utils'; -import type { Account } from '../../state/account.js'; -import { TokenTransferHookAccountNotFound } from '../../errors.js'; -import { unpackSeeds } from './seeds.js'; - -/** TransferHook as stored by the program */ -export interface TransferHook { - /** The transfer hook update authority */ - authority: PublicKey; - /** The transfer hook program account */ - programId: PublicKey; -} - -/** Buffer layout for de/serializing a transfer hook extension */ -export const TransferHookLayout = struct([publicKey('authority'), publicKey('programId')]); - -export const TRANSFER_HOOK_SIZE = TransferHookLayout.span; - -export function getTransferHook(mint: Mint): TransferHook | null { - const extensionData = getExtensionData(ExtensionType.TransferHook, mint.tlvData); - if (extensionData !== null) { - return TransferHookLayout.decode(extensionData); - } else { - return null; - } -} - -/** TransferHookAccount as stored by the program */ -export interface TransferHookAccount { - /** - * Whether or not this account is currently transferring tokens - * True during the transfer hook cpi, otherwise false - */ - transferring: boolean; -} - -/** Buffer layout for de/serializing a transfer hook account extension */ -export const TransferHookAccountLayout = struct([bool('transferring')]); - -export const TRANSFER_HOOK_ACCOUNT_SIZE = TransferHookAccountLayout.span; - -export function getTransferHookAccount(account: Account): TransferHookAccount | null { - const extensionData = getExtensionData(ExtensionType.TransferHookAccount, account.tlvData); - if (extensionData !== null) { - return TransferHookAccountLayout.decode(extensionData); - } else { - return null; - } -} - -export function getExtraAccountMetaAddress(mint: PublicKey, programId: PublicKey): PublicKey { - const seeds = [Buffer.from('extra-account-metas'), mint.toBuffer()]; - return PublicKey.findProgramAddressSync(seeds, programId)[0]; -} - -/** ExtraAccountMeta as stored by the transfer hook program */ -export interface ExtraAccountMeta { - discriminator: number; - addressConfig: Uint8Array; - isSigner: boolean; - isWritable: boolean; -} - -/** Buffer layout for de/serializing an ExtraAccountMeta */ -export const ExtraAccountMetaLayout = struct([ - u8('discriminator'), - blob(32, 'addressConfig'), - bool('isSigner'), - bool('isWritable'), -]); - -export interface ExtraAccountMetaList { - count: number; - extraAccounts: ExtraAccountMeta[]; -} - -/** Buffer layout for de/serializing a list of ExtraAccountMeta prefixed by a u32 length */ -export const ExtraAccountMetaListLayout = struct([ - u32('count'), - seq(ExtraAccountMetaLayout, greedy(ExtraAccountMetaLayout.span), 'extraAccounts'), -]); - -/** Buffer layout for de/serializing a list of ExtraAccountMetaAccountData prefixed by a u32 length */ -export interface ExtraAccountMetaAccountData { - instructionDiscriminator: bigint; - length: number; - extraAccountsList: ExtraAccountMetaList; -} - -/** Buffer layout for de/serializing an ExtraAccountMetaAccountData */ -export const ExtraAccountMetaAccountDataLayout = struct([ - u64('instructionDiscriminator'), - u32('length'), - ExtraAccountMetaListLayout.replicate('extraAccountsList'), -]); - -/** Unpack an extra account metas account and parse the data into a list of ExtraAccountMetas */ -export function getExtraAccountMetas(account: AccountInfo): ExtraAccountMeta[] { - const extraAccountsList = ExtraAccountMetaAccountDataLayout.decode(account.data).extraAccountsList; - return extraAccountsList.extraAccounts.slice(0, extraAccountsList.count); -} - -/** Take an ExtraAccountMeta and construct that into an actual AccountMeta */ -export async function resolveExtraAccountMeta( - connection: Connection, - extraMeta: ExtraAccountMeta, - previousMetas: AccountMeta[], - instructionData: Buffer, - transferHookProgramId: PublicKey, -): Promise { - if (extraMeta.discriminator === 0) { - return { - pubkey: new PublicKey(extraMeta.addressConfig), - isSigner: extraMeta.isSigner, - isWritable: extraMeta.isWritable, - }; - } - - let programId = PublicKey.default; - - if (extraMeta.discriminator === 1) { - programId = transferHookProgramId; - } else { - const accountIndex = extraMeta.discriminator - (1 << 7); - if (previousMetas.length <= accountIndex) { - throw new TokenTransferHookAccountNotFound(); - } - programId = previousMetas[accountIndex].pubkey; - } - - const seeds = await unpackSeeds(extraMeta.addressConfig, previousMetas, instructionData, connection); - const pubkey = PublicKey.findProgramAddressSync(seeds, programId)[0]; - - return { pubkey, isSigner: extraMeta.isSigner, isWritable: extraMeta.isWritable }; -} diff --git a/token/js/src/index.ts b/token/js/src/index.ts deleted file mode 100644 index b63e805e44e..00000000000 --- a/token/js/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './actions/index.js'; -export * from './constants.js'; -export * from './errors.js'; -export * from './extensions/index.js'; -export * from './instructions/index.js'; -export * from './state/index.js'; diff --git a/token/js/src/instructions/amountToUiAmount.ts b/token/js/src/instructions/amountToUiAmount.ts deleted file mode 100644 index 57230e99d43..00000000000 --- a/token/js/src/instructions/amountToUiAmount.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface AmountToUiAmountInstructionData { - instruction: TokenInstruction.AmountToUiAmount; - amount: bigint; -} - -/** TODO: docs */ -export const amountToUiAmountInstructionData = struct([ - u8('instruction'), - u64('amount'), -]); - -/** - * Construct a AmountToUiAmount instruction - * - * @param mint Public key of the mint - * @param amount Amount of tokens to be converted to UiAmount - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createAmountToUiAmountInstruction( - mint: PublicKey, - amount: number | bigint, - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [{ pubkey: mint, isSigner: false, isWritable: false }]; - - const data = Buffer.alloc(amountToUiAmountInstructionData.span); - amountToUiAmountInstructionData.encode( - { - instruction: TokenInstruction.AmountToUiAmount, - amount: BigInt(amount), - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid AmountToUiAmount instruction */ -export interface DecodedAmountToUiAmountInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - }; - data: { - instruction: TokenInstruction.AmountToUiAmount; - amount: bigint; - }; -} - -/** - * Decode a AmountToUiAmount instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeAmountToUiAmountInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedAmountToUiAmountInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== amountToUiAmountInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint }, - data, - } = decodeAmountToUiAmountInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.AmountToUiAmount) throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - }, - data, - }; -} - -/** A decoded, non-validated AmountToUiAmount instruction */ -export interface DecodedAmountToUiAmountInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta | undefined; - }; - data: { - instruction: number; - amount: bigint; - }; -} - -/** - * Decode a AmountToUiAmount instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeAmountToUiAmountInstructionUnchecked({ - programId, - keys: [mint], - data, -}: TransactionInstruction): DecodedAmountToUiAmountInstructionUnchecked { - return { - programId, - keys: { - mint, - }, - data: amountToUiAmountInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/approve.ts b/token/js/src/instructions/approve.ts deleted file mode 100644 index 9ef38c8e391..00000000000 --- a/token/js/src/instructions/approve.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface ApproveInstructionData { - instruction: TokenInstruction.Approve; - amount: bigint; -} - -/** TODO: docs */ -export const approveInstructionData = struct([u8('instruction'), u64('amount')]); - -/** - * Construct an Approve instruction - * - * @param account Account to set the delegate for - * @param delegate Account authorized to transfer tokens from the account - * @param owner Owner of the account - * @param amount Maximum number of tokens the delegate may transfer - * @param multiSigners Signing accounts if `owner` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createApproveInstruction( - account: PublicKey, - delegate: PublicKey, - owner: PublicKey, - amount: number | bigint, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: delegate, isSigner: false, isWritable: false }, - ], - owner, - multiSigners, - ); - - const data = Buffer.alloc(approveInstructionData.span); - approveInstructionData.encode( - { - instruction: TokenInstruction.Approve, - amount: BigInt(amount), - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid Approve instruction */ -export interface DecodedApproveInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - delegate: AccountMeta; - owner: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.Approve; - amount: bigint; - }; -} - -/** - * Decode an Approve instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeApproveInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedApproveInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== approveInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, delegate, owner, multiSigners }, - data, - } = decodeApproveInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.Approve) throw new TokenInvalidInstructionTypeError(); - if (!account || !delegate || !owner) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - delegate, - owner, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated Approve instruction */ -export interface DecodedApproveInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - delegate: AccountMeta | undefined; - owner: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - amount: bigint; - }; -} - -/** - * Decode an Approve instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeApproveInstructionUnchecked({ - programId, - keys: [account, delegate, owner, ...multiSigners], - data, -}: TransactionInstruction): DecodedApproveInstructionUnchecked { - return { - programId, - keys: { - account, - delegate, - owner, - multiSigners, - }, - data: approveInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/approveChecked.ts b/token/js/src/instructions/approveChecked.ts deleted file mode 100644 index ccb52eb4eee..00000000000 --- a/token/js/src/instructions/approveChecked.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface ApproveCheckedInstructionData { - instruction: TokenInstruction.ApproveChecked; - amount: bigint; - decimals: number; -} - -/** TODO: docs */ -export const approveCheckedInstructionData = struct([ - u8('instruction'), - u64('amount'), - u8('decimals'), -]); - -/** - * Construct an ApproveChecked instruction - * - * @param account Account to set the delegate for - * @param mint Mint account - * @param delegate Account authorized to transfer of tokens from the account - * @param owner Owner of the account - * @param amount Maximum number of tokens the delegate may transfer - * @param decimals Number of decimals in approve amount - * @param multiSigners Signing accounts if `owner` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createApproveCheckedInstruction( - account: PublicKey, - mint: PublicKey, - delegate: PublicKey, - owner: PublicKey, - amount: number | bigint, - decimals: number, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: delegate, isSigner: false, isWritable: false }, - ], - owner, - multiSigners, - ); - - const data = Buffer.alloc(approveCheckedInstructionData.span); - approveCheckedInstructionData.encode( - { - instruction: TokenInstruction.ApproveChecked, - amount: BigInt(amount), - decimals, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid ApproveChecked instruction */ -export interface DecodedApproveCheckedInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - mint: AccountMeta; - delegate: AccountMeta; - owner: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.ApproveChecked; - amount: bigint; - decimals: number; - }; -} - -/** - * Decode an ApproveChecked instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeApproveCheckedInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedApproveCheckedInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== approveCheckedInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, mint, delegate, owner, multiSigners }, - data, - } = decodeApproveCheckedInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.ApproveChecked) throw new TokenInvalidInstructionTypeError(); - if (!account || !mint || !delegate || !owner) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - mint, - delegate, - owner, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated ApproveChecked instruction */ -export interface DecodedApproveCheckedInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - mint: AccountMeta | undefined; - delegate: AccountMeta | undefined; - owner: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - amount: bigint; - decimals: number; - }; -} - -/** - * Decode an ApproveChecked instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeApproveCheckedInstructionUnchecked({ - programId, - keys: [account, mint, delegate, owner, ...multiSigners], - data, -}: TransactionInstruction): DecodedApproveCheckedInstructionUnchecked { - return { - programId, - keys: { - account, - mint, - delegate, - owner, - multiSigners, - }, - data: approveCheckedInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/associatedTokenAccount.ts b/token/js/src/instructions/associatedTokenAccount.ts deleted file mode 100644 index d8d47c7bac9..00000000000 --- a/token/js/src/instructions/associatedTokenAccount.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { PublicKey } from '@solana/web3.js'; -import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; -import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'; -import { getAssociatedTokenAddressSync } from '../state/mint.js'; - -/** - * Construct a CreateAssociatedTokenAccount instruction - * - * @param payer Payer of the initialization fees - * @param associatedToken New associated token account - * @param owner Owner of the new account - * @param mint Token mint account - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * - * @return Instruction to add to a transaction - */ -export function createAssociatedTokenAccountInstruction( - payer: PublicKey, - associatedToken: PublicKey, - owner: PublicKey, - mint: PublicKey, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -): TransactionInstruction { - return buildAssociatedTokenAccountInstruction( - payer, - associatedToken, - owner, - mint, - Buffer.alloc(0), - programId, - associatedTokenProgramId, - ); -} - -/** - * Construct a CreateAssociatedTokenAccountIdempotent instruction - * - * @param payer Payer of the initialization fees - * @param associatedToken New associated token account - * @param owner Owner of the new account - * @param mint Token mint account - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * - * @return Instruction to add to a transaction - */ -export function createAssociatedTokenAccountIdempotentInstruction( - payer: PublicKey, - associatedToken: PublicKey, - owner: PublicKey, - mint: PublicKey, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -): TransactionInstruction { - return buildAssociatedTokenAccountInstruction( - payer, - associatedToken, - owner, - mint, - Buffer.from([1]), - programId, - associatedTokenProgramId, - ); -} - -/** - * Derive the associated token account and construct a CreateAssociatedTokenAccountIdempotent instruction - * - * @param payer Payer of the initialization fees - * @param owner Owner of the new account - * @param mint Token mint account - * @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address) - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * - * @return Instruction to add to a transaction - */ -export function createAssociatedTokenAccountIdempotentInstructionWithDerivation( - payer: PublicKey, - owner: PublicKey, - mint: PublicKey, - allowOwnerOffCurve = true, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -) { - const associatedToken = getAssociatedTokenAddressSync(mint, owner, allowOwnerOffCurve); - - return createAssociatedTokenAccountIdempotentInstruction( - payer, - associatedToken, - owner, - mint, - programId, - associatedTokenProgramId, - ); -} - -function buildAssociatedTokenAccountInstruction( - payer: PublicKey, - associatedToken: PublicKey, - owner: PublicKey, - mint: PublicKey, - instructionData: Buffer, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [ - { pubkey: payer, isSigner: true, isWritable: true }, - { pubkey: associatedToken, isSigner: false, isWritable: true }, - { pubkey: owner, isSigner: false, isWritable: false }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { pubkey: programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - keys, - programId: associatedTokenProgramId, - data: instructionData, - }); -} - -/** - * Construct a RecoverNested instruction - * - * @param nestedAssociatedToken Nested associated token account (must be owned by `ownerAssociatedToken`) - * @param nestedMint Token mint for the nested associated token account - * @param destinationAssociatedToken Wallet's associated token account - * @param ownerAssociatedToken Owner associated token account address (must be owned by `owner`) - * @param ownerMint Token mint for the owner associated token account - * @param owner Wallet address for the owner associated token account - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * - * @return Instruction to add to a transaction - */ -export function createRecoverNestedInstruction( - nestedAssociatedToken: PublicKey, - nestedMint: PublicKey, - destinationAssociatedToken: PublicKey, - ownerAssociatedToken: PublicKey, - ownerMint: PublicKey, - owner: PublicKey, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [ - { pubkey: nestedAssociatedToken, isSigner: false, isWritable: true }, - { pubkey: nestedMint, isSigner: false, isWritable: false }, - { pubkey: destinationAssociatedToken, isSigner: false, isWritable: true }, - { pubkey: ownerAssociatedToken, isSigner: false, isWritable: true }, - { pubkey: ownerMint, isSigner: false, isWritable: false }, - { pubkey: owner, isSigner: true, isWritable: true }, - { pubkey: programId, isSigner: false, isWritable: false }, - ]; - - return new TransactionInstruction({ - keys, - programId: associatedTokenProgramId, - data: Buffer.from([2]), - }); -} diff --git a/token/js/src/instructions/burn.ts b/token/js/src/instructions/burn.ts deleted file mode 100644 index f6f9708b896..00000000000 --- a/token/js/src/instructions/burn.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface BurnInstructionData { - instruction: TokenInstruction.Burn; - amount: bigint; -} - -/** TODO: docs */ -export const burnInstructionData = struct([u8('instruction'), u64('amount')]); - -/** - * Construct a Burn instruction - * - * @param account Account to burn tokens from - * @param mint Mint for the account - * @param owner Owner of the account - * @param amount Number of tokens to burn - * @param multiSigners Signing accounts if `owner` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createBurnInstruction( - account: PublicKey, - mint: PublicKey, - owner: PublicKey, - amount: number | bigint, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: true }, - ], - owner, - multiSigners, - ); - - const data = Buffer.alloc(burnInstructionData.span); - burnInstructionData.encode( - { - instruction: TokenInstruction.Burn, - amount: BigInt(amount), - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid Burn instruction */ -export interface DecodedBurnInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - mint: AccountMeta; - owner: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.Burn; - amount: bigint; - }; -} - -/** - * Decode a Burn instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeBurnInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedBurnInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== burnInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, mint, owner, multiSigners }, - data, - } = decodeBurnInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.Burn) throw new TokenInvalidInstructionTypeError(); - if (!account || !mint || !owner) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - mint, - owner, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated Burn instruction */ -export interface DecodedBurnInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - mint: AccountMeta | undefined; - owner: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - amount: bigint; - }; -} - -/** - * Decode a Burn instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeBurnInstructionUnchecked({ - programId, - keys: [account, mint, owner, ...multiSigners], - data, -}: TransactionInstruction): DecodedBurnInstructionUnchecked { - return { - programId, - keys: { - account, - mint, - owner, - multiSigners, - }, - data: burnInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/burnChecked.ts b/token/js/src/instructions/burnChecked.ts deleted file mode 100644 index 9f8926c4874..00000000000 --- a/token/js/src/instructions/burnChecked.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface BurnCheckedInstructionData { - instruction: TokenInstruction.BurnChecked; - amount: bigint; - decimals: number; -} - -/** TODO: docs */ -export const burnCheckedInstructionData = struct([ - u8('instruction'), - u64('amount'), - u8('decimals'), -]); - -/** - * Construct a BurnChecked instruction - * - * @param mint Mint for the account - * @param account Account to burn tokens from - * @param owner Owner of the account - * @param amount Number of tokens to burn - * @param decimals Number of decimals in burn amount - * @param multiSigners Signing accounts if `owner` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createBurnCheckedInstruction( - account: PublicKey, - mint: PublicKey, - owner: PublicKey, - amount: number | bigint, - decimals: number, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: true }, - ], - owner, - multiSigners, - ); - - const data = Buffer.alloc(burnCheckedInstructionData.span); - burnCheckedInstructionData.encode( - { - instruction: TokenInstruction.BurnChecked, - amount: BigInt(amount), - decimals, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid BurnChecked instruction */ -export interface DecodedBurnCheckedInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - mint: AccountMeta; - owner: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.BurnChecked; - amount: bigint; - decimals: number; - }; -} - -/** - * Decode a BurnChecked instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeBurnCheckedInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedBurnCheckedInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== burnCheckedInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, mint, owner, multiSigners }, - data, - } = decodeBurnCheckedInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.BurnChecked) throw new TokenInvalidInstructionTypeError(); - if (!account || !mint || !owner) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - mint, - owner, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated BurnChecked instruction */ -export interface DecodedBurnCheckedInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - mint: AccountMeta | undefined; - owner: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - amount: bigint; - decimals: number; - }; -} - -/** - * Decode a BurnChecked instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeBurnCheckedInstructionUnchecked({ - programId, - keys: [account, mint, owner, ...multiSigners], - data, -}: TransactionInstruction): DecodedBurnCheckedInstructionUnchecked { - return { - programId, - keys: { - account, - mint, - owner, - multiSigners, - }, - data: burnCheckedInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/closeAccount.ts b/token/js/src/instructions/closeAccount.ts deleted file mode 100644 index 9ed3a106265..00000000000 --- a/token/js/src/instructions/closeAccount.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface CloseAccountInstructionData { - instruction: TokenInstruction.CloseAccount; -} - -/** TODO: docs */ -export const closeAccountInstructionData = struct([u8('instruction')]); - -/** - * Construct a CloseAccount instruction - * - * @param account Account to close - * @param destination Account to receive the remaining balance of the closed account - * @param authority Account close authority - * @param multiSigners Signing accounts if `authority` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createCloseAccountInstruction( - account: PublicKey, - destination: PublicKey, - authority: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: destination, isSigner: false, isWritable: true }, - ], - authority, - multiSigners, - ); - - const data = Buffer.alloc(closeAccountInstructionData.span); - closeAccountInstructionData.encode({ instruction: TokenInstruction.CloseAccount }, data); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid CloseAccount instruction */ -export interface DecodedCloseAccountInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - destination: AccountMeta; - authority: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.CloseAccount; - }; -} - -/** - * Decode a CloseAccount instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeCloseAccountInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedCloseAccountInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== closeAccountInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, destination, authority, multiSigners }, - data, - } = decodeCloseAccountInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.CloseAccount) throw new TokenInvalidInstructionTypeError(); - if (!account || !destination || !authority) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - destination, - authority, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated CloseAccount instruction */ -export interface DecodedCloseAccountInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - destination: AccountMeta | undefined; - authority: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - }; -} - -/** - * Decode a CloseAccount instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeCloseAccountInstructionUnchecked({ - programId, - keys: [account, destination, authority, ...multiSigners], - data, -}: TransactionInstruction): DecodedCloseAccountInstructionUnchecked { - return { - programId, - keys: { - account, - destination, - authority, - multiSigners, - }, - data: closeAccountInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/createNativeMint.ts b/token/js/src/instructions/createNativeMint.ts deleted file mode 100644 index 4ae9dff8bd9..00000000000 --- a/token/js/src/instructions/createNativeMint.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { PublicKey } from '@solana/web3.js'; -import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; -import { NATIVE_MINT_2022, programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../constants.js'; -import { TokenUnsupportedInstructionError } from '../errors.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface CreateNativeMintInstructionData { - instruction: TokenInstruction.CreateNativeMint; -} - -/** TODO: docs */ -export const createNativeMintInstructionData = struct([u8('instruction')]); - -/** - * Construct a CreateNativeMint instruction - * - * @param account New token account - * @param mint Mint account - * @param owner Owner of the new account - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createCreateNativeMintInstruction( - payer: PublicKey, - nativeMintId = NATIVE_MINT_2022, - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [ - { pubkey: payer, isSigner: true, isWritable: true }, - { pubkey: nativeMintId, isSigner: false, isWritable: true }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - ]; - - const data = Buffer.alloc(createNativeMintInstructionData.span); - createNativeMintInstructionData.encode({ instruction: TokenInstruction.CreateNativeMint }, data); - - return new TransactionInstruction({ keys, programId, data }); -} diff --git a/token/js/src/instructions/decode.ts b/token/js/src/instructions/decode.ts deleted file mode 100644 index 31ef2828b03..00000000000 --- a/token/js/src/instructions/decode.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { u8 } from '@solana/buffer-layout'; -import type { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { TokenInvalidInstructionDataError, TokenInvalidInstructionTypeError } from '../errors.js'; -import type { DecodedAmountToUiAmountInstruction } from './amountToUiAmount.js'; -import { decodeAmountToUiAmountInstruction } from './amountToUiAmount.js'; -import type { DecodedApproveInstruction } from './approve.js'; -import { decodeApproveInstruction } from './approve.js'; -import type { DecodedApproveCheckedInstruction } from './approveChecked.js'; -import { decodeApproveCheckedInstruction } from './approveChecked.js'; -import type { DecodedBurnInstruction } from './burn.js'; -import { decodeBurnInstruction } from './burn.js'; -import type { DecodedBurnCheckedInstruction } from './burnChecked.js'; -import { decodeBurnCheckedInstruction } from './burnChecked.js'; -import type { DecodedCloseAccountInstruction } from './closeAccount.js'; -import { decodeCloseAccountInstruction } from './closeAccount.js'; -import type { DecodedFreezeAccountInstruction } from './freezeAccount.js'; -import { decodeFreezeAccountInstruction } from './freezeAccount.js'; -import type { DecodedInitializeAccountInstruction } from './initializeAccount.js'; -import { decodeInitializeAccountInstruction } from './initializeAccount.js'; -import type { DecodedInitializeAccount2Instruction } from './initializeAccount2.js'; -import { decodeInitializeAccount2Instruction } from './initializeAccount2.js'; -import type { DecodedInitializeAccount3Instruction } from './initializeAccount3.js'; -import { decodeInitializeAccount3Instruction } from './initializeAccount3.js'; -import type { DecodedInitializeMintInstruction } from './initializeMint.js'; -import { decodeInitializeMintInstruction } from './initializeMint.js'; -import type { DecodedInitializeMint2Instruction } from './initializeMint2.js'; -import { decodeInitializeMint2Instruction } from './initializeMint2.js'; -import type { DecodedInitializeMultisigInstruction } from './initializeMultisig.js'; -import { decodeInitializeMultisigInstruction } from './initializeMultisig.js'; -import type { DecodedMintToInstruction } from './mintTo.js'; -import { decodeMintToInstruction } from './mintTo.js'; -import type { DecodedMintToCheckedInstruction } from './mintToChecked.js'; -import { decodeMintToCheckedInstruction } from './mintToChecked.js'; -import type { DecodedRevokeInstruction } from './revoke.js'; -import { decodeRevokeInstruction } from './revoke.js'; -import type { DecodedSetAuthorityInstruction } from './setAuthority.js'; -import { decodeSetAuthorityInstruction } from './setAuthority.js'; -import type { DecodedSyncNativeInstruction } from './syncNative.js'; -import { decodeSyncNativeInstruction } from './syncNative.js'; -import type { DecodedThawAccountInstruction } from './thawAccount.js'; -import { decodeThawAccountInstruction } from './thawAccount.js'; -import type { DecodedTransferInstruction } from './transfer.js'; -import { decodeTransferInstruction } from './transfer.js'; -import type { DecodedTransferCheckedInstruction } from './transferChecked.js'; -import { decodeTransferCheckedInstruction } from './transferChecked.js'; -import { TokenInstruction } from './types.js'; -import type { DecodedUiAmountToAmountInstruction } from './uiAmountToAmount.js'; -import { decodeUiAmountToAmountInstruction } from './uiAmountToAmount.js'; - -/** TODO: docs */ -export type DecodedInstruction = - | DecodedInitializeMintInstruction - | DecodedInitializeAccountInstruction - | DecodedInitializeMultisigInstruction - | DecodedTransferInstruction - | DecodedApproveInstruction - | DecodedRevokeInstruction - | DecodedSetAuthorityInstruction - | DecodedMintToInstruction - | DecodedBurnInstruction - | DecodedCloseAccountInstruction - | DecodedFreezeAccountInstruction - | DecodedThawAccountInstruction - | DecodedTransferCheckedInstruction - | DecodedApproveCheckedInstruction - | DecodedMintToCheckedInstruction - | DecodedBurnCheckedInstruction - | DecodedInitializeAccount2Instruction - | DecodedSyncNativeInstruction - | DecodedInitializeAccount3Instruction - | DecodedInitializeMint2Instruction - | DecodedAmountToUiAmountInstruction - | DecodedUiAmountToAmountInstruction - // | DecodedInitializeMultisig2Instruction - // TODO: implement ^ and remove `never` - | never; - -/** TODO: docs */ -export function decodeInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedInstruction { - if (!instruction.data.length) throw new TokenInvalidInstructionDataError(); - - const type = u8().decode(instruction.data); - if (type === TokenInstruction.InitializeMint) return decodeInitializeMintInstruction(instruction, programId); - if (type === TokenInstruction.InitializeAccount) return decodeInitializeAccountInstruction(instruction, programId); - if (type === TokenInstruction.InitializeMultisig) - return decodeInitializeMultisigInstruction(instruction, programId); - if (type === TokenInstruction.Transfer) return decodeTransferInstruction(instruction, programId); - if (type === TokenInstruction.Approve) return decodeApproveInstruction(instruction, programId); - if (type === TokenInstruction.Revoke) return decodeRevokeInstruction(instruction, programId); - if (type === TokenInstruction.SetAuthority) return decodeSetAuthorityInstruction(instruction, programId); - if (type === TokenInstruction.MintTo) return decodeMintToInstruction(instruction, programId); - if (type === TokenInstruction.Burn) return decodeBurnInstruction(instruction, programId); - if (type === TokenInstruction.CloseAccount) return decodeCloseAccountInstruction(instruction, programId); - if (type === TokenInstruction.FreezeAccount) return decodeFreezeAccountInstruction(instruction, programId); - if (type === TokenInstruction.ThawAccount) return decodeThawAccountInstruction(instruction, programId); - if (type === TokenInstruction.TransferChecked) return decodeTransferCheckedInstruction(instruction, programId); - if (type === TokenInstruction.ApproveChecked) return decodeApproveCheckedInstruction(instruction, programId); - if (type === TokenInstruction.MintToChecked) return decodeMintToCheckedInstruction(instruction, programId); - if (type === TokenInstruction.BurnChecked) return decodeBurnCheckedInstruction(instruction, programId); - if (type === TokenInstruction.InitializeAccount2) - return decodeInitializeAccount2Instruction(instruction, programId); - if (type === TokenInstruction.SyncNative) return decodeSyncNativeInstruction(instruction, programId); - if (type === TokenInstruction.InitializeAccount3) - return decodeInitializeAccount3Instruction(instruction, programId); - if (type === TokenInstruction.InitializeMint2) return decodeInitializeMint2Instruction(instruction, programId); - if (type === TokenInstruction.AmountToUiAmount) return decodeAmountToUiAmountInstruction(instruction, programId); - if (type === TokenInstruction.UiAmountToAmount) return decodeUiAmountToAmountInstruction(instruction, programId); - // TODO: implement - if (type === TokenInstruction.InitializeMultisig2) throw new TokenInvalidInstructionTypeError(); - - throw new TokenInvalidInstructionTypeError(); -} - -/** TODO: docs */ -export function isInitializeMintInstruction(decoded: DecodedInstruction): decoded is DecodedInitializeMintInstruction { - return decoded.data.instruction === TokenInstruction.InitializeMint; -} - -/** TODO: docs */ -export function isInitializeAccountInstruction( - decoded: DecodedInstruction, -): decoded is DecodedInitializeAccountInstruction { - return decoded.data.instruction === TokenInstruction.InitializeAccount; -} - -/** TODO: docs */ -export function isInitializeMultisigInstruction( - decoded: DecodedInstruction, -): decoded is DecodedInitializeMultisigInstruction { - return decoded.data.instruction === TokenInstruction.InitializeMultisig; -} - -/** TODO: docs */ -export function isTransferInstruction(decoded: DecodedInstruction): decoded is DecodedTransferInstruction { - return decoded.data.instruction === TokenInstruction.Transfer; -} - -/** TODO: docs */ -export function isApproveInstruction(decoded: DecodedInstruction): decoded is DecodedApproveInstruction { - return decoded.data.instruction === TokenInstruction.Approve; -} - -/** TODO: docs */ -export function isRevokeInstruction(decoded: DecodedInstruction): decoded is DecodedRevokeInstruction { - return decoded.data.instruction === TokenInstruction.Revoke; -} - -/** TODO: docs */ -export function isSetAuthorityInstruction(decoded: DecodedInstruction): decoded is DecodedSetAuthorityInstruction { - return decoded.data.instruction === TokenInstruction.SetAuthority; -} - -/** TODO: docs */ -export function isMintToInstruction(decoded: DecodedInstruction): decoded is DecodedMintToInstruction { - return decoded.data.instruction === TokenInstruction.MintTo; -} - -/** TODO: docs */ -export function isBurnInstruction(decoded: DecodedInstruction): decoded is DecodedBurnInstruction { - return decoded.data.instruction === TokenInstruction.Burn; -} - -/** TODO: docs */ -export function isCloseAccountInstruction(decoded: DecodedInstruction): decoded is DecodedCloseAccountInstruction { - return decoded.data.instruction === TokenInstruction.CloseAccount; -} - -/** TODO: docs */ -export function isFreezeAccountInstruction(decoded: DecodedInstruction): decoded is DecodedFreezeAccountInstruction { - return decoded.data.instruction === TokenInstruction.FreezeAccount; -} - -/** TODO: docs */ -export function isThawAccountInstruction(decoded: DecodedInstruction): decoded is DecodedThawAccountInstruction { - return decoded.data.instruction === TokenInstruction.ThawAccount; -} - -/** TODO: docs */ -export function isTransferCheckedInstruction( - decoded: DecodedInstruction, -): decoded is DecodedTransferCheckedInstruction { - return decoded.data.instruction === TokenInstruction.TransferChecked; -} - -/** TODO: docs */ -export function isApproveCheckedInstruction(decoded: DecodedInstruction): decoded is DecodedApproveCheckedInstruction { - return decoded.data.instruction === TokenInstruction.ApproveChecked; -} - -/** TODO: docs */ -export function isMintToCheckedInstruction(decoded: DecodedInstruction): decoded is DecodedMintToCheckedInstruction { - return decoded.data.instruction === TokenInstruction.MintToChecked; -} - -/** TODO: docs */ -export function isBurnCheckedInstruction(decoded: DecodedInstruction): decoded is DecodedBurnCheckedInstruction { - return decoded.data.instruction === TokenInstruction.BurnChecked; -} - -/** TODO: docs */ -export function isInitializeAccount2Instruction( - decoded: DecodedInstruction, -): decoded is DecodedInitializeAccount2Instruction { - return decoded.data.instruction === TokenInstruction.InitializeAccount2; -} - -/** TODO: docs */ -export function isSyncNativeInstruction(decoded: DecodedInstruction): decoded is DecodedSyncNativeInstruction { - return decoded.data.instruction === TokenInstruction.SyncNative; -} - -/** TODO: docs */ -export function isInitializeAccount3Instruction( - decoded: DecodedInstruction, -): decoded is DecodedInitializeAccount3Instruction { - return decoded.data.instruction === TokenInstruction.InitializeAccount3; -} - -/** TODO: docs, implement */ -// export function isInitializeMultisig2Instruction( -// decoded: DecodedInstruction -// ): decoded is DecodedInitializeMultisig2Instruction { -// return decoded.data.instruction === TokenInstruction.InitializeMultisig2; -// } - -/** TODO: docs */ -export function isInitializeMint2Instruction( - decoded: DecodedInstruction, -): decoded is DecodedInitializeMint2Instruction { - return decoded.data.instruction === TokenInstruction.InitializeMint2; -} - -/** TODO: docs */ -export function isAmountToUiAmountInstruction( - decoded: DecodedInstruction, -): decoded is DecodedAmountToUiAmountInstruction { - return decoded.data.instruction === TokenInstruction.AmountToUiAmount; -} - -/** TODO: docs */ -export function isUiamountToAmountInstruction( - decoded: DecodedInstruction, -): decoded is DecodedUiAmountToAmountInstruction { - return decoded.data.instruction === TokenInstruction.UiAmountToAmount; -} diff --git a/token/js/src/instructions/freezeAccount.ts b/token/js/src/instructions/freezeAccount.ts deleted file mode 100644 index 8dbebcaa703..00000000000 --- a/token/js/src/instructions/freezeAccount.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface FreezeAccountInstructionData { - instruction: TokenInstruction.FreezeAccount; -} - -/** TODO: docs */ -export const freezeAccountInstructionData = struct([u8('instruction')]); - -/** - * Construct a FreezeAccount instruction - * - * @param account Account to freeze - * @param mint Mint account - * @param authority Mint freeze authority - * @param multiSigners Signing accounts if `authority` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createFreezeAccountInstruction( - account: PublicKey, - mint: PublicKey, - authority: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - ], - authority, - multiSigners, - ); - - const data = Buffer.alloc(freezeAccountInstructionData.span); - freezeAccountInstructionData.encode({ instruction: TokenInstruction.FreezeAccount }, data); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid FreezeAccount instruction */ -export interface DecodedFreezeAccountInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - mint: AccountMeta; - authority: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.FreezeAccount; - }; -} - -/** - * Decode a FreezeAccount instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeFreezeAccountInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedFreezeAccountInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== freezeAccountInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, mint, authority, multiSigners }, - data, - } = decodeFreezeAccountInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.FreezeAccount) throw new TokenInvalidInstructionTypeError(); - if (!account || !mint || !authority) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - mint, - authority, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated FreezeAccount instruction */ -export interface DecodedFreezeAccountInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - mint: AccountMeta | undefined; - authority: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - }; -} - -/** - * Decode a FreezeAccount instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeFreezeAccountInstructionUnchecked({ - programId, - keys: [account, mint, authority, ...multiSigners], - data, -}: TransactionInstruction): DecodedFreezeAccountInstructionUnchecked { - return { - programId, - keys: { - account, - mint, - authority, - multiSigners, - }, - data: freezeAccountInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/index.ts b/token/js/src/instructions/index.ts deleted file mode 100644 index 9ceabb71b64..00000000000 --- a/token/js/src/instructions/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -export { - createInitializeInstruction, - createUpdateFieldInstruction, - createRemoveKeyInstruction, - createUpdateAuthorityInstruction, - createEmitInstruction, -} from '@solana/spl-token-metadata'; -export { - createInitializeGroupInstruction, - createUpdateGroupMaxSizeInstruction, - createUpdateGroupAuthorityInstruction, - createInitializeMemberInstruction, -} from '@solana/spl-token-group'; - -export * from './associatedTokenAccount.js'; -export * from './decode.js'; -export * from './types.js'; - -export * from './initializeMint.js'; // 0 -export * from './initializeAccount.js'; // 1 -export * from './initializeMultisig.js'; // 2 -export * from './transfer.js'; // 3 -export * from './approve.js'; // 4 -export * from './revoke.js'; // 5 -export * from './setAuthority.js'; // 6 -export * from './mintTo.js'; // 7 -export * from './burn.js'; // 8 -export * from './closeAccount.js'; // 9 -export * from './freezeAccount.js'; // 10 -export * from './thawAccount.js'; // 11 -export * from './transferChecked.js'; // 12 -export * from './approveChecked.js'; // 13 -export * from './mintToChecked.js'; // 14 -export * from './burnChecked.js'; // 15 -export * from './initializeAccount2.js'; // 16 -export * from './syncNative.js'; // 17 -export * from './initializeAccount3.js'; // 18 -export * from './initializeMultisig2.js'; // 19 -export * from './initializeMint2.js'; // 20 -export * from './initializeImmutableOwner.js'; // 22 -export * from './amountToUiAmount.js'; // 23 -export * from './uiAmountToAmount.js'; // 24 -export * from './initializeMintCloseAuthority.js'; // 25 -export * from './reallocate.js'; // 29 -export * from './createNativeMint.js'; // 31 -export * from './initializeNonTransferableMint.js'; // 32 -export * from './initializePermanentDelegate.js'; // 35 diff --git a/token/js/src/instructions/initializeAccount.ts b/token/js/src/instructions/initializeAccount.ts deleted file mode 100644 index 7708b506313..00000000000 --- a/token/js/src/instructions/initializeAccount.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface InitializeAccountInstructionData { - instruction: TokenInstruction.InitializeAccount; -} - -/** TODO: docs */ -export const initializeAccountInstructionData = struct([u8('instruction')]); - -/** - * Construct an InitializeAccount instruction - * - * @param account New token account - * @param mint Mint account - * @param owner Owner of the new account - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeAccountInstruction( - account: PublicKey, - mint: PublicKey, - owner: PublicKey, - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: owner, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - ]; - - const data = Buffer.alloc(initializeAccountInstructionData.span); - initializeAccountInstructionData.encode({ instruction: TokenInstruction.InitializeAccount }, data); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializeAccount instruction */ -export interface DecodedInitializeAccountInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - mint: AccountMeta; - owner: AccountMeta; - rent: AccountMeta; - }; - data: { - instruction: TokenInstruction.InitializeAccount; - }; -} - -/** - * Decode an InitializeAccount instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializeAccountInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedInitializeAccountInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializeAccountInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, mint, owner, rent }, - data, - } = decodeInitializeAccountInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.InitializeAccount) throw new TokenInvalidInstructionTypeError(); - if (!account || !mint || !owner || !rent) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - mint, - owner, - rent, - }, - data, - }; -} - -/** A decoded, non-validated InitializeAccount instruction */ -export interface DecodedInitializeAccountInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - mint: AccountMeta | undefined; - owner: AccountMeta | undefined; - rent: AccountMeta | undefined; - }; - data: { - instruction: number; - }; -} - -/** - * Decode an InitializeAccount instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializeAccountInstructionUnchecked({ - programId, - keys: [account, mint, owner, rent], - data, -}: TransactionInstruction): DecodedInitializeAccountInstructionUnchecked { - return { - programId, - keys: { - account, - mint, - owner, - rent, - }, - data: initializeAccountInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/initializeAccount2.ts b/token/js/src/instructions/initializeAccount2.ts deleted file mode 100644 index f6c3c12928d..00000000000 --- a/token/js/src/instructions/initializeAccount2.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; - -export interface InitializeAccount2InstructionData { - instruction: TokenInstruction.InitializeAccount2; - owner: PublicKey; -} - -export const initializeAccount2InstructionData = struct([ - u8('instruction'), - publicKey('owner'), -]); - -/** - * Construct an InitializeAccount2 instruction - * - * @param account New token account - * @param mint Mint account - * @param owner New account's owner/multisignature - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeAccount2Instruction( - account: PublicKey, - mint: PublicKey, - owner: PublicKey, - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - ]; - const data = Buffer.alloc(initializeAccount2InstructionData.span); - initializeAccount2InstructionData.encode({ instruction: TokenInstruction.InitializeAccount2, owner }, data); - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializeAccount2 instruction */ -export interface DecodedInitializeAccount2Instruction { - programId: PublicKey; - keys: { - account: AccountMeta; - mint: AccountMeta; - rent: AccountMeta; - }; - data: { - instruction: TokenInstruction.InitializeAccount2; - owner: PublicKey; - }; -} - -/** - * Decode an InitializeAccount2 instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializeAccount2Instruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedInitializeAccount2Instruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializeAccount2InstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, mint, rent }, - data, - } = decodeInitializeAccount2InstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.InitializeAccount2) throw new TokenInvalidInstructionTypeError(); - if (!account || !mint || !rent) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - mint, - rent, - }, - data, - }; -} - -/** A decoded, non-validated InitializeAccount2 instruction */ -export interface DecodedInitializeAccount2InstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - mint: AccountMeta | undefined; - rent: AccountMeta | undefined; - }; - data: { - instruction: number; - owner: PublicKey; - }; -} - -/** - * Decode an InitializeAccount2 instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializeAccount2InstructionUnchecked({ - programId, - keys: [account, mint, rent], - data, -}: TransactionInstruction): DecodedInitializeAccount2InstructionUnchecked { - return { - programId, - keys: { - account, - mint, - rent, - }, - data: initializeAccount2InstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/initializeAccount3.ts b/token/js/src/instructions/initializeAccount3.ts deleted file mode 100644 index af167ea9c75..00000000000 --- a/token/js/src/instructions/initializeAccount3.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; - -export interface InitializeAccount3InstructionData { - instruction: TokenInstruction.InitializeAccount3; - owner: PublicKey; -} - -export const initializeAccount3InstructionData = struct([ - u8('instruction'), - publicKey('owner'), -]); - -/** - * Construct an InitializeAccount3 instruction - * - * @param account New token account - * @param mint Mint account - * @param owner New account's owner/multisignature - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeAccount3Instruction( - account: PublicKey, - mint: PublicKey, - owner: PublicKey, - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - ]; - const data = Buffer.alloc(initializeAccount3InstructionData.span); - initializeAccount3InstructionData.encode({ instruction: TokenInstruction.InitializeAccount3, owner }, data); - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializeAccount3 instruction */ -export interface DecodedInitializeAccount3Instruction { - programId: PublicKey; - keys: { - account: AccountMeta; - mint: AccountMeta; - }; - data: { - instruction: TokenInstruction.InitializeAccount3; - owner: PublicKey; - }; -} - -/** - * Decode an InitializeAccount3 instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializeAccount3Instruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedInitializeAccount3Instruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializeAccount3InstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, mint }, - data, - } = decodeInitializeAccount3InstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.InitializeAccount3) throw new TokenInvalidInstructionTypeError(); - if (!account || !mint) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - mint, - }, - data, - }; -} - -/** A decoded, non-validated InitializeAccount3 instruction */ -export interface DecodedInitializeAccount3InstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - mint: AccountMeta | undefined; - }; - data: { - instruction: number; - owner: PublicKey; - }; -} - -/** - * Decode an InitializeAccount3 instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializeAccount3InstructionUnchecked({ - programId, - keys: [account, mint], - data, -}: TransactionInstruction): DecodedInitializeAccount3InstructionUnchecked { - return { - programId, - keys: { - account, - mint, - }, - data: initializeAccount3InstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/initializeImmutableOwner.ts b/token/js/src/instructions/initializeImmutableOwner.ts deleted file mode 100644 index c1023d7810f..00000000000 --- a/token/js/src/instructions/initializeImmutableOwner.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; - -/** Deserialized instruction for the initiation of an immutable owner account */ -export interface InitializeImmutableOwnerInstructionData { - instruction: TokenInstruction.InitializeImmutableOwner; -} - -/** The struct that represents the instruction data as it is read by the program */ -export const initializeImmutableOwnerInstructionData = struct([ - u8('instruction'), -]); - -/** - * Construct an InitializeImmutableOwner instruction - * - * @param account Immutable Owner Account - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeImmutableOwnerInstruction( - account: PublicKey, - programId: PublicKey, -): TransactionInstruction { - const keys = [{ pubkey: account, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializeImmutableOwnerInstructionData.span); - initializeImmutableOwnerInstructionData.encode( - { - instruction: TokenInstruction.InitializeImmutableOwner, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializeImmutableOwner instruction */ -export interface DecodedInitializeImmutableOwnerInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - }; - data: { - instruction: TokenInstruction.InitializeImmutableOwner; - }; -} - -/** - * Decode an InitializeImmutableOwner instruction and validate it - * - * @param instruction InitializeImmutableOwner instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializeImmutableOwnerInstruction( - instruction: TransactionInstruction, - programId: PublicKey, -): DecodedInitializeImmutableOwnerInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializeImmutableOwnerInstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { account }, - data, - } = decodeInitializeImmutableOwnerInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.InitializeImmutableOwner) throw new TokenInvalidInstructionTypeError(); - if (!account) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - account, - }, - data, - }; -} - -/** A decoded, non-validated InitializeImmutableOwner instruction */ -export interface DecodedInitializeImmutableOwnerInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - }; - data: { - instruction: number; - }; -} - -/** - * Decode an InitializeImmutableOwner instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializeImmutableOwnerInstructionUnchecked({ - programId, - keys: [account], - data, -}: TransactionInstruction): DecodedInitializeImmutableOwnerInstructionUnchecked { - const { instruction } = initializeImmutableOwnerInstructionData.decode(data); - - return { - programId, - keys: { - account: account, - }, - data: { - instruction, - }, - }; -} diff --git a/token/js/src/instructions/initializeMint.ts b/token/js/src/instructions/initializeMint.ts deleted file mode 100644 index 99c67ab4223..00000000000 --- a/token/js/src/instructions/initializeMint.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; -import { COptionPublicKeyLayout } from '../serialization.js'; - -/** TODO: docs */ -export interface InitializeMintInstructionData { - instruction: TokenInstruction.InitializeMint; - decimals: number; - mintAuthority: PublicKey; - freezeAuthority: PublicKey | null; -} - -/** TODO: docs */ -export const initializeMintInstructionData = struct([ - u8('instruction'), - u8('decimals'), - publicKey('mintAuthority'), - new COptionPublicKeyLayout('freezeAuthority'), -]); - -/** - * Construct an InitializeMint instruction - * - * @param mint Token mint account - * @param decimals Number of decimals in token account amounts - * @param mintAuthority Minting authority - * @param freezeAuthority Optional authority that can freeze token accounts - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeMintInstruction( - mint: PublicKey, - decimals: number, - mintAuthority: PublicKey, - freezeAuthority: PublicKey | null, - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [ - { pubkey: mint, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - ]; - - const data = Buffer.alloc(initializeMintInstructionData.span); - initializeMintInstructionData.encode( - { - instruction: TokenInstruction.InitializeMint, - decimals, - mintAuthority, - freezeAuthority, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializeMint instruction */ -export interface DecodedInitializeMintInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - rent: AccountMeta; - }; - data: { - instruction: TokenInstruction.InitializeMint; - decimals: number; - mintAuthority: PublicKey; - freezeAuthority: PublicKey | null; - }; -} - -/** - * Decode an InitializeMint instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializeMintInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedInitializeMintInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializeMintInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint, rent }, - data, - } = decodeInitializeMintInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.InitializeMint) throw new TokenInvalidInstructionTypeError(); - if (!mint || !rent) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - mint, - rent, - }, - data, - }; -} - -/** A decoded, non-validated InitializeMint instruction */ -export interface DecodedInitializeMintInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta | undefined; - rent: AccountMeta | undefined; - }; - data: { - instruction: number; - decimals: number; - mintAuthority: PublicKey; - freezeAuthority: PublicKey | null; - }; -} - -/** - * Decode an InitializeMint instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializeMintInstructionUnchecked({ - programId, - keys: [mint, rent], - data, -}: TransactionInstruction): DecodedInitializeMintInstructionUnchecked { - const { instruction, decimals, mintAuthority, freezeAuthority } = initializeMintInstructionData.decode(data); - - return { - programId, - keys: { - mint, - rent, - }, - data: { - instruction, - decimals, - mintAuthority, - freezeAuthority, - }, - }; -} diff --git a/token/js/src/instructions/initializeMint2.ts b/token/js/src/instructions/initializeMint2.ts deleted file mode 100644 index d80778ffeb1..00000000000 --- a/token/js/src/instructions/initializeMint2.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; -import { COptionPublicKeyLayout } from '../serialization.js'; - -/** TODO: docs */ -export interface InitializeMint2InstructionData { - instruction: TokenInstruction.InitializeMint2; - decimals: number; - mintAuthority: PublicKey; - freezeAuthority: PublicKey | null; -} - -/** TODO: docs */ -export const initializeMint2InstructionData = struct([ - u8('instruction'), - u8('decimals'), - publicKey('mintAuthority'), - new COptionPublicKeyLayout('freezeAuthority'), -]); - -/** - * Construct an InitializeMint2 instruction - * - * @param mint Token mint account - * @param decimals Number of decimals in token account amounts - * @param mintAuthority Minting authority - * @param freezeAuthority Optional authority that can freeze token accounts - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeMint2Instruction( - mint: PublicKey, - decimals: number, - mintAuthority: PublicKey, - freezeAuthority: PublicKey | null, - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializeMint2InstructionData.span); - initializeMint2InstructionData.encode( - { - instruction: TokenInstruction.InitializeMint2, - decimals, - mintAuthority, - freezeAuthority, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializeMint2 instruction */ -export interface DecodedInitializeMint2Instruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - }; - data: { - instruction: TokenInstruction.InitializeMint2; - decimals: number; - mintAuthority: PublicKey; - freezeAuthority: PublicKey | null; - }; -} - -/** - * Decode an InitializeMint2 instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializeMint2Instruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedInitializeMint2Instruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializeMint2InstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint }, - data, - } = decodeInitializeMint2InstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.InitializeMint2) throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - }, - data, - }; -} - -/** A decoded, non-validated InitializeMint2 instruction */ -export interface DecodedInitializeMint2InstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta | undefined; - }; - data: { - instruction: number; - decimals: number; - mintAuthority: PublicKey; - freezeAuthority: PublicKey | null; - }; -} - -/** - * Decode an InitializeMint2 instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializeMint2InstructionUnchecked({ - programId, - keys: [mint], - data, -}: TransactionInstruction): DecodedInitializeMint2InstructionUnchecked { - const { instruction, decimals, mintAuthority, freezeAuthority } = initializeMint2InstructionData.decode(data); - - return { - programId, - keys: { - mint, - }, - data: { - instruction, - decimals, - mintAuthority, - freezeAuthority, - }, - }; -} diff --git a/token/js/src/instructions/initializeMintCloseAuthority.ts b/token/js/src/instructions/initializeMintCloseAuthority.ts deleted file mode 100644 index ccc65d81085..00000000000 --- a/token/js/src/instructions/initializeMintCloseAuthority.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { programSupportsExtensions } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, - TokenUnsupportedInstructionError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; -import { COptionPublicKeyLayout } from '../serialization.js'; - -/** TODO: docs */ -export interface InitializeMintCloseAuthorityInstructionData { - instruction: TokenInstruction.InitializeMintCloseAuthority; - closeAuthority: PublicKey | null; -} - -/** TODO: docs */ -export const initializeMintCloseAuthorityInstructionData = struct([ - u8('instruction'), - new COptionPublicKeyLayout('closeAuthority'), -]); - -/** - * Construct an InitializeMintCloseAuthority instruction - * - * @param mint Token mint account - * @param closeAuthority Optional authority that can close the mint - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeMintCloseAuthorityInstruction( - mint: PublicKey, - closeAuthority: PublicKey | null, - programId: PublicKey, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializeMintCloseAuthorityInstructionData.span); - initializeMintCloseAuthorityInstructionData.encode( - { - instruction: TokenInstruction.InitializeMintCloseAuthority, - closeAuthority, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializeMintCloseAuthority instruction */ -export interface DecodedInitializeMintCloseAuthorityInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - }; - data: { - instruction: TokenInstruction.InitializeMintCloseAuthority; - closeAuthority: PublicKey | null; - }; -} - -/** - * Decode an InitializeMintCloseAuthority instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializeMintCloseAuthorityInstruction( - instruction: TransactionInstruction, - programId: PublicKey, -): DecodedInitializeMintCloseAuthorityInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializeMintCloseAuthorityInstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint }, - data, - } = decodeInitializeMintCloseAuthorityInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.InitializeMintCloseAuthority) - throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - }, - data, - }; -} - -/** A decoded, non-validated InitializeMintCloseAuthority instruction */ -export interface DecodedInitializeMintCloseAuthorityInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta | undefined; - }; - data: { - instruction: number; - closeAuthority: PublicKey | null; - }; -} - -/** - * Decode an InitializeMintCloseAuthority instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializeMintCloseAuthorityInstructionUnchecked({ - programId, - keys: [mint], - data, -}: TransactionInstruction): DecodedInitializeMintCloseAuthorityInstructionUnchecked { - const { instruction, closeAuthority } = initializeMintCloseAuthorityInstructionData.decode(data); - - return { - programId, - keys: { - mint, - }, - data: { - instruction, - closeAuthority, - }, - }; -} diff --git a/token/js/src/instructions/initializeMultisig.ts b/token/js/src/instructions/initializeMultisig.ts deleted file mode 100644 index 4f21046b4fc..00000000000 --- a/token/js/src/instructions/initializeMultisig.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, Signer } from '@solana/web3.js'; -import { PublicKey, SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface InitializeMultisigInstructionData { - instruction: TokenInstruction.InitializeMultisig; - m: number; -} - -/** TODO: docs */ -export const initializeMultisigInstructionData = struct([ - u8('instruction'), - u8('m'), -]); - -/** - * Construct an InitializeMultisig instruction - * - * @param account Multisig account - * @param signers Full set of signers - * @param m Number of required signatures - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeMultisigInstruction( - account: PublicKey, - signers: (Signer | PublicKey)[], - m: number, - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, - ]; - for (const signer of signers) { - keys.push({ - pubkey: signer instanceof PublicKey ? signer : signer.publicKey, - isSigner: false, - isWritable: false, - }); - } - - const data = Buffer.alloc(initializeMultisigInstructionData.span); - initializeMultisigInstructionData.encode( - { - instruction: TokenInstruction.InitializeMultisig, - m, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializeMultisig instruction */ -export interface DecodedInitializeMultisigInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - rent: AccountMeta; - signers: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.InitializeMultisig; - m: number; - }; -} - -/** - * Decode an InitializeMultisig instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializeMultisigInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedInitializeMultisigInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializeMultisigInstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, rent, signers }, - data, - } = decodeInitializeMultisigInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.InitializeMultisig) throw new TokenInvalidInstructionTypeError(); - if (!account || !rent || !signers.length) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - rent, - signers, - }, - data, - }; -} - -/** A decoded, non-validated InitializeMultisig instruction */ -export interface DecodedInitializeMultisigInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - rent: AccountMeta | undefined; - signers: AccountMeta[]; - }; - data: { - instruction: number; - m: number; - }; -} - -/** - * Decode an InitializeMultisig instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializeMultisigInstructionUnchecked({ - programId, - keys: [account, rent, ...signers], - data, -}: TransactionInstruction): DecodedInitializeMultisigInstructionUnchecked { - return { - programId, - keys: { - account, - rent, - signers, - }, - data: initializeMultisigInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/initializeMultisig2.ts b/token/js/src/instructions/initializeMultisig2.ts deleted file mode 100644 index 434426c176f..00000000000 --- a/token/js/src/instructions/initializeMultisig2.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; // TODO: implement diff --git a/token/js/src/instructions/initializeNonTransferableMint.ts b/token/js/src/instructions/initializeNonTransferableMint.ts deleted file mode 100644 index c837178eb8f..00000000000 --- a/token/js/src/instructions/initializeNonTransferableMint.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { programSupportsExtensions } from '../constants.js'; -import { TokenUnsupportedInstructionError } from '../errors.js'; -import { TokenInstruction } from './types.js'; - -/** Deserialized instruction for the initiation of an immutable owner account */ -export interface InitializeNonTransferableMintInstructionData { - instruction: TokenInstruction.InitializeNonTransferableMint; -} - -/** The struct that represents the instruction data as it is read by the program */ -export const initializeNonTransferableMintInstructionData = struct([ - u8('instruction'), -]); - -/** - * Construct an InitializeNonTransferableMint instruction - * - * @param mint Mint Account to make non-transferable - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializeNonTransferableMintInstruction( - mint: PublicKey, - programId: PublicKey, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializeNonTransferableMintInstructionData.span); - initializeNonTransferableMintInstructionData.encode( - { - instruction: TokenInstruction.InitializeNonTransferableMint, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} diff --git a/token/js/src/instructions/initializePermanentDelegate.ts b/token/js/src/instructions/initializePermanentDelegate.ts deleted file mode 100644 index a4f28b4a913..00000000000 --- a/token/js/src/instructions/initializePermanentDelegate.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { AccountMeta } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { programSupportsExtensions } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, - TokenUnsupportedInstructionError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface InitializePermanentDelegateInstructionData { - instruction: TokenInstruction.InitializePermanentDelegate; - delegate: PublicKey; -} - -/** TODO: docs */ -export const initializePermanentDelegateInstructionData = struct([ - u8('instruction'), - publicKey('delegate'), -]); - -/** - * Construct an InitializePermanentDelegate instruction - * - * @param mint Token mint account - * @param permanentDelegate Authority that may sign for `Transfer`s and `Burn`s on any account - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createInitializePermanentDelegateInstruction( - mint: PublicKey, - permanentDelegate: PublicKey | null, - programId: PublicKey, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(initializePermanentDelegateInstructionData.span); - initializePermanentDelegateInstructionData.encode( - { - instruction: TokenInstruction.InitializePermanentDelegate, - delegate: permanentDelegate || new PublicKey(0), - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid InitializePermanentDelegate instruction */ -export interface DecodedInitializePermanentDelegateInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - }; - data: { - instruction: TokenInstruction.InitializePermanentDelegate; - delegate: PublicKey | null; - }; -} - -/** - * Decode an InitializePermanentDelegate instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeInitializePermanentDelegateInstruction( - instruction: TransactionInstruction, - programId: PublicKey, -): DecodedInitializePermanentDelegateInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== initializePermanentDelegateInstructionData.span) - throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint }, - data, - } = decodeInitializePermanentDelegateInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.InitializePermanentDelegate) throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - }, - data, - }; -} - -/** A decoded, non-validated InitializePermanentDelegate instruction */ -export interface DecodedInitializePermanentDelegateInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta | undefined; - }; - data: { - instruction: number; - delegate: PublicKey | null; - }; -} - -/** - * Decode an InitializePermanentDelegate instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeInitializePermanentDelegateInstructionUnchecked({ - programId, - keys: [mint], - data, -}: TransactionInstruction): DecodedInitializePermanentDelegateInstructionUnchecked { - const { instruction, delegate } = initializePermanentDelegateInstructionData.decode(data); - - return { - programId, - keys: { - mint, - }, - data: { - instruction, - delegate, - }, - }; -} diff --git a/token/js/src/instructions/internal.ts b/token/js/src/instructions/internal.ts deleted file mode 100644 index c2f4d6579d8..00000000000 --- a/token/js/src/instructions/internal.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { AccountMeta, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; - -/** @internal */ -export function addSigners( - keys: AccountMeta[], - ownerOrAuthority: PublicKey, - multiSigners: (Signer | PublicKey)[], -): AccountMeta[] { - if (multiSigners.length) { - keys.push({ pubkey: ownerOrAuthority, isSigner: false, isWritable: false }); - for (const signer of multiSigners) { - keys.push({ - pubkey: signer instanceof PublicKey ? signer : signer.publicKey, - isSigner: true, - isWritable: false, - }); - } - } else { - keys.push({ pubkey: ownerOrAuthority, isSigner: true, isWritable: false }); - } - return keys; -} diff --git a/token/js/src/instructions/mintTo.ts b/token/js/src/instructions/mintTo.ts deleted file mode 100644 index a1d93b89bb9..00000000000 --- a/token/js/src/instructions/mintTo.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface MintToInstructionData { - instruction: TokenInstruction.MintTo; - amount: bigint; -} - -/** TODO: docs */ -export const mintToInstructionData = struct([u8('instruction'), u64('amount')]); - -/** - * Construct a MintTo instruction - * - * @param mint Public key of the mint - * @param destination Address of the token account to mint to - * @param authority The mint authority - * @param amount Amount to mint - * @param multiSigners Signing accounts if `authority` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createMintToInstruction( - mint: PublicKey, - destination: PublicKey, - authority: PublicKey, - amount: number | bigint, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: mint, isSigner: false, isWritable: true }, - { pubkey: destination, isSigner: false, isWritable: true }, - ], - authority, - multiSigners, - ); - - const data = Buffer.alloc(mintToInstructionData.span); - mintToInstructionData.encode( - { - instruction: TokenInstruction.MintTo, - amount: BigInt(amount), - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid MintTo instruction */ -export interface DecodedMintToInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - destination: AccountMeta; - authority: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.MintTo; - amount: bigint; - }; -} - -/** - * Decode a MintTo instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeMintToInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedMintToInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== mintToInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint, destination, authority, multiSigners }, - data, - } = decodeMintToInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.MintTo) throw new TokenInvalidInstructionTypeError(); - if (!mint || !destination || !authority) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - mint, - destination, - authority, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated MintTo instruction */ -export interface DecodedMintToInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta | undefined; - destination: AccountMeta | undefined; - authority: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - amount: bigint; - }; -} - -/** - * Decode a MintTo instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeMintToInstructionUnchecked({ - programId, - keys: [mint, destination, authority, ...multiSigners], - data, -}: TransactionInstruction): DecodedMintToInstructionUnchecked { - return { - programId, - keys: { - mint, - destination, - authority, - multiSigners, - }, - data: mintToInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/mintToChecked.ts b/token/js/src/instructions/mintToChecked.ts deleted file mode 100644 index 85eb9a4fedb..00000000000 --- a/token/js/src/instructions/mintToChecked.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface MintToCheckedInstructionData { - instruction: TokenInstruction.MintToChecked; - amount: bigint; - decimals: number; -} - -/** TODO: docs */ -export const mintToCheckedInstructionData = struct([ - u8('instruction'), - u64('amount'), - u8('decimals'), -]); - -/** - * Construct a MintToChecked instruction - * - * @param mint Public key of the mint - * @param destination Address of the token account to mint to - * @param authority The mint authority - * @param amount Amount to mint - * @param decimals Number of decimals in amount to mint - * @param multiSigners Signing accounts if `authority` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createMintToCheckedInstruction( - mint: PublicKey, - destination: PublicKey, - authority: PublicKey, - amount: number | bigint, - decimals: number, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: mint, isSigner: false, isWritable: true }, - { pubkey: destination, isSigner: false, isWritable: true }, - ], - authority, - multiSigners, - ); - - const data = Buffer.alloc(mintToCheckedInstructionData.span); - mintToCheckedInstructionData.encode( - { - instruction: TokenInstruction.MintToChecked, - amount: BigInt(amount), - decimals, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid MintToChecked instruction */ -export interface DecodedMintToCheckedInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - destination: AccountMeta; - authority: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.MintToChecked; - amount: bigint; - decimals: number; - }; -} - -/** - * Decode a MintToChecked instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeMintToCheckedInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedMintToCheckedInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== mintToCheckedInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint, destination, authority, multiSigners }, - data, - } = decodeMintToCheckedInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.MintToChecked) throw new TokenInvalidInstructionTypeError(); - if (!mint || !destination || !authority) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - mint, - destination, - authority, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated MintToChecked instruction */ -export interface DecodedMintToCheckedInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta | undefined; - destination: AccountMeta | undefined; - authority: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - amount: bigint; - decimals: number; - }; -} - -/** - * Decode a MintToChecked instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeMintToCheckedInstructionUnchecked({ - programId, - keys: [mint, destination, authority, ...multiSigners], - data, -}: TransactionInstruction): DecodedMintToCheckedInstructionUnchecked { - return { - programId, - keys: { - mint, - destination, - authority, - multiSigners, - }, - data: mintToCheckedInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/reallocate.ts b/token/js/src/instructions/reallocate.ts deleted file mode 100644 index 6b90f35de92..00000000000 --- a/token/js/src/instructions/reallocate.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { seq, struct, u16, u8 } from '@solana/buffer-layout'; -import type { PublicKey, Signer } from '@solana/web3.js'; -import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; -import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../constants.js'; -import { TokenUnsupportedInstructionError } from '../errors.js'; -import type { ExtensionType } from '../extensions/extensionType.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface ReallocateInstructionData { - instruction: TokenInstruction.Reallocate; - extensionTypes: ExtensionType[]; -} - -/** - * Construct a Reallocate instruction - * - * @param account Address of the token account - * @param payer Address paying for the reallocation - * @param extensionTypes Extensions to reallocate for - * @param owner Owner of the account - * @param multiSigners Signing accounts if `owner` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createReallocateInstruction( - account: PublicKey, - payer: PublicKey, - extensionTypes: ExtensionType[], - owner: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_2022_PROGRAM_ID, -): TransactionInstruction { - if (!programSupportsExtensions(programId)) { - throw new TokenUnsupportedInstructionError(); - } - const baseKeys = [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: payer, isSigner: true, isWritable: true }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - ]; - const keys = addSigners(baseKeys, owner, multiSigners); - - const reallocateInstructionData = struct([ - u8('instruction'), - seq(u16(), extensionTypes.length, 'extensionTypes'), - ]); - const data = Buffer.alloc(reallocateInstructionData.span); - reallocateInstructionData.encode({ instruction: TokenInstruction.Reallocate, extensionTypes }, data); - - return new TransactionInstruction({ keys, programId, data }); -} diff --git a/token/js/src/instructions/revoke.ts b/token/js/src/instructions/revoke.ts deleted file mode 100644 index 5b723234291..00000000000 --- a/token/js/src/instructions/revoke.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface RevokeInstructionData { - instruction: TokenInstruction.Revoke; -} - -/** TODO: docs */ -export const revokeInstructionData = struct([u8('instruction')]); - -/** - * Construct a Revoke instruction - * - * @param account Address of the token account - * @param owner Owner of the account - * @param multiSigners Signing accounts if `owner` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createRevokeInstruction( - account: PublicKey, - owner: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners([{ pubkey: account, isSigner: false, isWritable: true }], owner, multiSigners); - - const data = Buffer.alloc(revokeInstructionData.span); - revokeInstructionData.encode({ instruction: TokenInstruction.Revoke }, data); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid Revoke instruction */ -export interface DecodedRevokeInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - owner: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.Revoke; - }; -} - -/** - * Decode a Revoke instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeRevokeInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedRevokeInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== revokeInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, owner, multiSigners }, - data, - } = decodeRevokeInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.Revoke) throw new TokenInvalidInstructionTypeError(); - if (!account || !owner) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - owner, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated Revoke instruction */ -export interface DecodedRevokeInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - owner: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - }; -} - -/** - * Decode a Revoke instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeRevokeInstructionUnchecked({ - programId, - keys: [account, owner, ...multiSigners], - data, -}: TransactionInstruction): DecodedRevokeInstructionUnchecked { - return { - programId, - keys: { - account, - owner, - multiSigners, - }, - data: revokeInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/setAuthority.ts b/token/js/src/instructions/setAuthority.ts deleted file mode 100644 index d09bd69b45e..00000000000 --- a/token/js/src/instructions/setAuthority.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { AccountMeta, Signer, PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; -import { COptionPublicKeyLayout } from '../serialization.js'; - -/** Authority types defined by the program */ -export enum AuthorityType { - MintTokens = 0, - FreezeAccount = 1, - AccountOwner = 2, - CloseAccount = 3, - TransferFeeConfig = 4, - WithheldWithdraw = 5, - CloseMint = 6, - InterestRate = 7, - PermanentDelegate = 8, - ConfidentialTransferMint = 9, - TransferHookProgramId = 10, - ConfidentialTransferFeeConfig = 11, - MetadataPointer = 12, - GroupPointer = 13, - GroupMemberPointer = 14, -} - -/** TODO: docs */ -export interface SetAuthorityInstructionData { - instruction: TokenInstruction.SetAuthority; - authorityType: AuthorityType; - newAuthority: PublicKey | null; -} - -/** TODO: docs */ -export const setAuthorityInstructionData = struct([ - u8('instruction'), - u8('authorityType'), - new COptionPublicKeyLayout('newAuthority'), -]); - -/** - * Construct a SetAuthority instruction - * - * @param account Address of the token account - * @param currentAuthority Current authority of the specified type - * @param authorityType Type of authority to set - * @param newAuthority New authority of the account - * @param multiSigners Signing accounts if `currentAuthority` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createSetAuthorityInstruction( - account: PublicKey, - currentAuthority: PublicKey, - authorityType: AuthorityType, - newAuthority: PublicKey | null, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners([{ pubkey: account, isSigner: false, isWritable: true }], currentAuthority, multiSigners); - - const data = Buffer.alloc(setAuthorityInstructionData.span); - setAuthorityInstructionData.encode( - { - instruction: TokenInstruction.SetAuthority, - authorityType, - newAuthority, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid SetAuthority instruction */ -export interface DecodedSetAuthorityInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - currentAuthority: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.SetAuthority; - authorityType: AuthorityType; - newAuthority: PublicKey | null; - }; -} - -/** - * Decode a SetAuthority instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeSetAuthorityInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedSetAuthorityInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== setAuthorityInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, currentAuthority, multiSigners }, - data, - } = decodeSetAuthorityInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.SetAuthority) throw new TokenInvalidInstructionTypeError(); - if (!account || !currentAuthority) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - currentAuthority, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated SetAuthority instruction */ -export interface DecodedSetAuthorityInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - currentAuthority: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - authorityType: AuthorityType; - newAuthority: PublicKey | null; - }; -} - -/** - * Decode a SetAuthority instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeSetAuthorityInstructionUnchecked({ - programId, - keys: [account, currentAuthority, ...multiSigners], - data, -}: TransactionInstruction): DecodedSetAuthorityInstructionUnchecked { - const { instruction, authorityType, newAuthority } = setAuthorityInstructionData.decode(data); - - return { - programId, - keys: { - account, - currentAuthority, - multiSigners, - }, - data: { - instruction, - authorityType, - newAuthority, - }, - }; -} diff --git a/token/js/src/instructions/syncNative.ts b/token/js/src/instructions/syncNative.ts deleted file mode 100644 index 8340a54603e..00000000000 --- a/token/js/src/instructions/syncNative.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface SyncNativeInstructionData { - instruction: TokenInstruction.SyncNative; -} - -/** TODO: docs */ -export const syncNativeInstructionData = struct([u8('instruction')]); - -/** - * Construct a SyncNative instruction - * - * @param account Native account to sync lamports from - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createSyncNativeInstruction(account: PublicKey, programId = TOKEN_PROGRAM_ID): TransactionInstruction { - const keys = [{ pubkey: account, isSigner: false, isWritable: true }]; - - const data = Buffer.alloc(syncNativeInstructionData.span); - syncNativeInstructionData.encode({ instruction: TokenInstruction.SyncNative }, data); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid SyncNative instruction */ -export interface DecodedSyncNativeInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - }; - data: { - instruction: TokenInstruction.SyncNative; - }; -} - -/** - * Decode a SyncNative instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeSyncNativeInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedSyncNativeInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== syncNativeInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account }, - data, - } = decodeSyncNativeInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.SyncNative) throw new TokenInvalidInstructionTypeError(); - if (!account) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - }, - data, - }; -} - -/** A decoded, non-validated SyncNative instruction */ -export interface DecodedSyncNativeInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - }; - data: { - instruction: number; - }; -} - -/** - * Decode a SyncNative instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeSyncNativeInstructionUnchecked({ - programId, - keys: [account], - data, -}: TransactionInstruction): DecodedSyncNativeInstructionUnchecked { - return { - programId, - keys: { - account, - }, - data: syncNativeInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/thawAccount.ts b/token/js/src/instructions/thawAccount.ts deleted file mode 100644 index 3423fb104e4..00000000000 --- a/token/js/src/instructions/thawAccount.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface ThawAccountInstructionData { - instruction: TokenInstruction.ThawAccount; -} - -/** TODO: docs */ -export const thawAccountInstructionData = struct([u8('instruction')]); - -/** - * Construct a ThawAccount instruction - * - * @param account Account to thaw - * @param mint Mint account - * @param authority Mint freeze authority - * @param multiSigners Signing accounts if `authority` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createThawAccountInstruction( - account: PublicKey, - mint: PublicKey, - authority: PublicKey, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: account, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - ], - authority, - multiSigners, - ); - - const data = Buffer.alloc(thawAccountInstructionData.span); - thawAccountInstructionData.encode({ instruction: TokenInstruction.ThawAccount }, data); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid ThawAccount instruction */ -export interface DecodedThawAccountInstruction { - programId: PublicKey; - keys: { - account: AccountMeta; - mint: AccountMeta; - authority: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.ThawAccount; - }; -} - -/** - * Decode a ThawAccount instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeThawAccountInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedThawAccountInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== thawAccountInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { account, mint, authority, multiSigners }, - data, - } = decodeThawAccountInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.ThawAccount) throw new TokenInvalidInstructionTypeError(); - if (!account || !mint || !authority) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - account, - mint, - authority, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated ThawAccount instruction */ -export interface DecodedThawAccountInstructionUnchecked { - programId: PublicKey; - keys: { - account: AccountMeta | undefined; - mint: AccountMeta | undefined; - authority: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - }; -} - -/** - * Decode a ThawAccount instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeThawAccountInstructionUnchecked({ - programId, - keys: [account, mint, authority, ...multiSigners], - data, -}: TransactionInstruction): DecodedThawAccountInstructionUnchecked { - return { - programId, - keys: { - account, - mint, - authority, - multiSigners, - }, - data: thawAccountInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/transfer.ts b/token/js/src/instructions/transfer.ts deleted file mode 100644 index 6cd415e3dd5..00000000000 --- a/token/js/src/instructions/transfer.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface TransferInstructionData { - instruction: TokenInstruction.Transfer; - amount: bigint; -} - -/** TODO: docs */ -export const transferInstructionData = struct([u8('instruction'), u64('amount')]); - -/** - * Construct a Transfer instruction - * - * @param source Source account - * @param destination Destination account - * @param owner Owner of the source account - * @param amount Number of tokens to transfer - * @param multiSigners Signing accounts if `owner` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createTransferInstruction( - source: PublicKey, - destination: PublicKey, - owner: PublicKey, - amount: number | bigint, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: source, isSigner: false, isWritable: true }, - { pubkey: destination, isSigner: false, isWritable: true }, - ], - owner, - multiSigners, - ); - - const data = Buffer.alloc(transferInstructionData.span); - transferInstructionData.encode( - { - instruction: TokenInstruction.Transfer, - amount: BigInt(amount), - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid Transfer instruction */ -export interface DecodedTransferInstruction { - programId: PublicKey; - keys: { - source: AccountMeta; - destination: AccountMeta; - owner: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.Transfer; - amount: bigint; - }; -} - -/** - * Decode a Transfer instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeTransferInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedTransferInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== transferInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { source, destination, owner, multiSigners }, - data, - } = decodeTransferInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.Transfer) throw new TokenInvalidInstructionTypeError(); - if (!source || !destination || !owner) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - source, - destination, - owner, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated Transfer instruction */ -export interface DecodedTransferInstructionUnchecked { - programId: PublicKey; - keys: { - source: AccountMeta | undefined; - destination: AccountMeta | undefined; - owner: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - amount: bigint; - }; -} - -/** - * Decode a Transfer instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeTransferInstructionUnchecked({ - programId, - keys: [source, destination, owner, ...multiSigners], - data, -}: TransactionInstruction): DecodedTransferInstructionUnchecked { - return { - programId, - keys: { - source, - destination, - owner, - multiSigners, - }, - data: transferInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/transferChecked.ts b/token/js/src/instructions/transferChecked.ts deleted file mode 100644 index b3f53dc94eb..00000000000 --- a/token/js/src/instructions/transferChecked.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { u64 } from '@solana/buffer-layout-utils'; -import type { AccountMeta, PublicKey, Signer } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { addSigners } from './internal.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface TransferCheckedInstructionData { - instruction: TokenInstruction.TransferChecked; - amount: bigint; - decimals: number; -} - -/** TODO: docs */ -export const transferCheckedInstructionData = struct([ - u8('instruction'), - u64('amount'), - u8('decimals'), -]); - -/** - * Construct a TransferChecked instruction - * - * @param source Source account - * @param mint Mint account - * @param destination Destination account - * @param owner Owner of the source account - * @param amount Number of tokens to transfer - * @param decimals Number of decimals in transfer amount - * @param multiSigners Signing accounts if `owner` is a multisig - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createTransferCheckedInstruction( - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - owner: PublicKey, - amount: number | bigint, - decimals: number, - multiSigners: (Signer | PublicKey)[] = [], - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = addSigners( - [ - { pubkey: source, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: destination, isSigner: false, isWritable: true }, - ], - owner, - multiSigners, - ); - - const data = Buffer.alloc(transferCheckedInstructionData.span); - transferCheckedInstructionData.encode( - { - instruction: TokenInstruction.TransferChecked, - amount: BigInt(amount), - decimals, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid TransferChecked instruction */ -export interface DecodedTransferCheckedInstruction { - programId: PublicKey; - keys: { - source: AccountMeta; - mint: AccountMeta; - destination: AccountMeta; - owner: AccountMeta; - multiSigners: AccountMeta[]; - }; - data: { - instruction: TokenInstruction.TransferChecked; - amount: bigint; - decimals: number; - }; -} - -/** - * Decode a TransferChecked instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeTransferCheckedInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedTransferCheckedInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - if (instruction.data.length !== transferCheckedInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { source, mint, destination, owner, multiSigners }, - data, - } = decodeTransferCheckedInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.TransferChecked) throw new TokenInvalidInstructionTypeError(); - if (!source || !mint || !destination || !owner) throw new TokenInvalidInstructionKeysError(); - - // TODO: key checks? - - return { - programId, - keys: { - source, - mint, - destination, - owner, - multiSigners, - }, - data, - }; -} - -/** A decoded, non-validated TransferChecked instruction */ -export interface DecodedTransferCheckedInstructionUnchecked { - programId: PublicKey; - keys: { - source: AccountMeta | undefined; - mint: AccountMeta | undefined; - destination: AccountMeta | undefined; - owner: AccountMeta | undefined; - multiSigners: AccountMeta[]; - }; - data: { - instruction: number; - amount: bigint; - decimals: number; - }; -} - -/** - * Decode a TransferChecked instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeTransferCheckedInstructionUnchecked({ - programId, - keys: [source, mint, destination, owner, ...multiSigners], - data, -}: TransactionInstruction): DecodedTransferCheckedInstructionUnchecked { - return { - programId, - keys: { - source, - mint, - destination, - owner, - multiSigners, - }, - data: transferCheckedInstructionData.decode(data), - }; -} diff --git a/token/js/src/instructions/types.ts b/token/js/src/instructions/types.ts deleted file mode 100644 index ca17645205c..00000000000 --- a/token/js/src/instructions/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** Instructions defined by the program */ -export enum TokenInstruction { - InitializeMint = 0, - InitializeAccount = 1, - InitializeMultisig = 2, - Transfer = 3, - Approve = 4, - Revoke = 5, - SetAuthority = 6, - MintTo = 7, - Burn = 8, - CloseAccount = 9, - FreezeAccount = 10, - ThawAccount = 11, - TransferChecked = 12, - ApproveChecked = 13, - MintToChecked = 14, - BurnChecked = 15, - InitializeAccount2 = 16, - SyncNative = 17, - InitializeAccount3 = 18, - InitializeMultisig2 = 19, - InitializeMint2 = 20, - GetAccountDataSize = 21, - InitializeImmutableOwner = 22, - AmountToUiAmount = 23, - UiAmountToAmount = 24, - InitializeMintCloseAuthority = 25, - TransferFeeExtension = 26, - ConfidentialTransferExtension = 27, - DefaultAccountStateExtension = 28, - Reallocate = 29, - MemoTransferExtension = 30, - CreateNativeMint = 31, - InitializeNonTransferableMint = 32, - InterestBearingMintExtension = 33, - CpiGuardExtension = 34, - InitializePermanentDelegate = 35, - TransferHookExtension = 36, - // ConfidentialTransferFeeExtension = 37, - // WithdrawalExcessLamports = 38, - MetadataPointerExtension = 39, - GroupPointerExtension = 40, - GroupMemberPointerExtension = 41, -} diff --git a/token/js/src/instructions/uiAmountToAmount.ts b/token/js/src/instructions/uiAmountToAmount.ts deleted file mode 100644 index 490ba175236..00000000000 --- a/token/js/src/instructions/uiAmountToAmount.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { blob, struct, u8 } from '@solana/buffer-layout'; -import type { AccountMeta, PublicKey } from '@solana/web3.js'; -import { TransactionInstruction } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenInvalidInstructionDataError, - TokenInvalidInstructionKeysError, - TokenInvalidInstructionProgramError, - TokenInvalidInstructionTypeError, -} from '../errors.js'; -import { TokenInstruction } from './types.js'; - -/** TODO: docs */ -export interface UiAmountToAmountInstructionData { - instruction: TokenInstruction.UiAmountToAmount; - amount: Uint8Array; -} - -/** TODO: docs */ - -/** - * Construct a UiAmountToAmount instruction - * - * @param mint Public key of the mint - * @param amount UiAmount of tokens to be converted to Amount - * @param programId SPL Token program account - * - * @return Instruction to add to a transaction - */ -export function createUiAmountToAmountInstruction( - mint: PublicKey, - amount: string, - programId = TOKEN_PROGRAM_ID, -): TransactionInstruction { - const keys = [{ pubkey: mint, isSigner: false, isWritable: false }]; - const buf = Buffer.from(amount, 'utf8'); - const uiAmountToAmountInstructionData = struct([ - u8('instruction'), - blob(buf.length, 'amount'), - ]); - - const data = Buffer.alloc(uiAmountToAmountInstructionData.span); - uiAmountToAmountInstructionData.encode( - { - instruction: TokenInstruction.UiAmountToAmount, - amount: buf, - }, - data, - ); - - return new TransactionInstruction({ keys, programId, data }); -} - -/** A decoded, valid UiAmountToAmount instruction */ -export interface DecodedUiAmountToAmountInstruction { - programId: PublicKey; - keys: { - mint: AccountMeta; - }; - data: { - instruction: TokenInstruction.UiAmountToAmount; - amount: Uint8Array; - }; -} - -/** - * Decode a UiAmountToAmount instruction and validate it - * - * @param instruction Transaction instruction to decode - * @param programId SPL Token program account - * - * @return Decoded, valid instruction - */ -export function decodeUiAmountToAmountInstruction( - instruction: TransactionInstruction, - programId = TOKEN_PROGRAM_ID, -): DecodedUiAmountToAmountInstruction { - if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); - const uiAmountToAmountInstructionData = struct([ - u8('instruction'), - blob(instruction.data.length - 1, 'amount'), - ]); - if (instruction.data.length !== uiAmountToAmountInstructionData.span) throw new TokenInvalidInstructionDataError(); - - const { - keys: { mint }, - data, - } = decodeUiAmountToAmountInstructionUnchecked(instruction); - if (data.instruction !== TokenInstruction.UiAmountToAmount) throw new TokenInvalidInstructionTypeError(); - if (!mint) throw new TokenInvalidInstructionKeysError(); - - return { - programId, - keys: { - mint, - }, - data, - }; -} - -/** A decoded, non-validated UiAmountToAmount instruction */ -export interface DecodedUiAmountToAmountInstructionUnchecked { - programId: PublicKey; - keys: { - mint: AccountMeta | undefined; - }; - data: { - instruction: number; - amount: Uint8Array; - }; -} - -/** - * Decode a UiAmountToAmount instruction without validating it - * - * @param instruction Transaction instruction to decode - * - * @return Decoded, non-validated instruction - */ -export function decodeUiAmountToAmountInstructionUnchecked({ - programId, - keys: [mint], - data, -}: TransactionInstruction): DecodedUiAmountToAmountInstructionUnchecked { - const uiAmountToAmountInstructionData = struct([ - u8('instruction'), - blob(data.length - 1, 'amount'), - ]); - return { - programId, - keys: { - mint, - }, - data: uiAmountToAmountInstructionData.decode(data), - }; -} diff --git a/token/js/src/serialization.ts b/token/js/src/serialization.ts deleted file mode 100644 index 57d426baa7f..00000000000 --- a/token/js/src/serialization.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Layout } from '@solana/buffer-layout'; -import { publicKey } from '@solana/buffer-layout-utils'; -import type { PublicKey } from '@solana/web3.js'; - -export class COptionPublicKeyLayout extends Layout { - private publicKeyLayout: Layout; - - constructor(property?: string | undefined) { - super(-1, property); - this.publicKeyLayout = publicKey(); - } - - decode(buffer: Uint8Array, offset: number = 0): PublicKey | null { - const option = buffer[offset]; - if (option === 0) { - return null; - } - return this.publicKeyLayout.decode(buffer, offset + 1); - } - - encode(src: PublicKey | null, buffer: Uint8Array, offset: number = 0): number { - if (src === null) { - buffer[offset] = 0; - return 1; - } else { - buffer[offset] = 1; - this.publicKeyLayout.encode(src, buffer, offset + 1); - return 33; - } - } - - getSpan(buffer?: Uint8Array, offset: number = 0): number { - if (buffer) { - const option = buffer[offset]; - return option === 0 ? 1 : 1 + this.publicKeyLayout.span; - } - return 1 + this.publicKeyLayout.span; - } -} diff --git a/token/js/src/state/account.ts b/token/js/src/state/account.ts deleted file mode 100644 index b068f472759..00000000000 --- a/token/js/src/state/account.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { struct, u32, u8 } from '@solana/buffer-layout'; -import { publicKey, u64 } from '@solana/buffer-layout-utils'; -import type { AccountInfo, Commitment, Connection, PublicKey } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenAccountNotFoundError, - TokenInvalidAccountError, - TokenInvalidAccountOwnerError, - TokenInvalidAccountSizeError, -} from '../errors.js'; -import { ACCOUNT_TYPE_SIZE, AccountType } from '../extensions/accountType.js'; -import type { ExtensionType } from '../extensions/extensionType.js'; -import { getAccountLen } from '../extensions/extensionType.js'; -import { MULTISIG_SIZE } from './multisig.js'; - -/** Information about a token account */ -export interface Account { - /** Address of the account */ - address: PublicKey; - /** Mint associated with the account */ - mint: PublicKey; - /** Owner of the account */ - owner: PublicKey; - /** Number of tokens the account holds */ - amount: bigint; - /** Authority that can transfer tokens from the account */ - delegate: PublicKey | null; - /** Number of tokens the delegate is authorized to transfer */ - delegatedAmount: bigint; - /** True if the account is initialized */ - isInitialized: boolean; - /** True if the account is frozen */ - isFrozen: boolean; - /** True if the account is a native token account */ - isNative: boolean; - /** - * If the account is a native token account, it must be rent-exempt. The rent-exempt reserve is the amount that must - * remain in the balance until the account is closed. - */ - rentExemptReserve: bigint | null; - /** Optional authority to close the account */ - closeAuthority: PublicKey | null; - tlvData: Buffer; -} - -/** Token account state as stored by the program */ -export enum AccountState { - Uninitialized = 0, - Initialized = 1, - Frozen = 2, -} - -/** Token account as stored by the program */ -export interface RawAccount { - mint: PublicKey; - owner: PublicKey; - amount: bigint; - delegateOption: 1 | 0; - delegate: PublicKey; - state: AccountState; - isNativeOption: 1 | 0; - isNative: bigint; - delegatedAmount: bigint; - closeAuthorityOption: 1 | 0; - closeAuthority: PublicKey; -} - -/** Buffer layout for de/serializing a token account */ -export const AccountLayout = struct([ - publicKey('mint'), - publicKey('owner'), - u64('amount'), - u32('delegateOption'), - publicKey('delegate'), - u8('state'), - u32('isNativeOption'), - u64('isNative'), - u64('delegatedAmount'), - u32('closeAuthorityOption'), - publicKey('closeAuthority'), -]); - -/** Byte length of a token account */ -export const ACCOUNT_SIZE = AccountLayout.span; - -/** - * Retrieve information about a token account - * - * @param connection Connection to use - * @param address Token account - * @param commitment Desired level of commitment for querying the state - * @param programId SPL Token program account - * - * @return Token account information - */ -export async function getAccount( - connection: Connection, - address: PublicKey, - commitment?: Commitment, - programId = TOKEN_PROGRAM_ID, -): Promise { - const info = await connection.getAccountInfo(address, commitment); - return unpackAccount(address, info, programId); -} - -/** - * Retrieve information about multiple token accounts in a single RPC call - * - * @param connection Connection to use - * @param addresses Token accounts - * @param commitment Desired level of commitment for querying the state - * @param programId SPL Token program account - * - * @return Token account information - */ -export async function getMultipleAccounts( - connection: Connection, - addresses: PublicKey[], - commitment?: Commitment, - programId = TOKEN_PROGRAM_ID, -): Promise { - const infos = await connection.getMultipleAccountsInfo(addresses, commitment); - return addresses.map((address, i) => unpackAccount(address, infos[i], programId)); -} - -/** Get the minimum lamport balance for a base token account to be rent exempt - * - * @param connection Connection to use - * @param commitment Desired level of commitment for querying the state - * - * @return Amount of lamports required - */ -export async function getMinimumBalanceForRentExemptAccount( - connection: Connection, - commitment?: Commitment, -): Promise { - return await getMinimumBalanceForRentExemptAccountWithExtensions(connection, [], commitment); -} - -/** Get the minimum lamport balance for a rent-exempt token account with extensions - * - * @param connection Connection to use - * @param commitment Desired level of commitment for querying the state - * - * @return Amount of lamports required - */ -export async function getMinimumBalanceForRentExemptAccountWithExtensions( - connection: Connection, - extensions: ExtensionType[], - commitment?: Commitment, -): Promise { - const accountLen = getAccountLen(extensions); - return await connection.getMinimumBalanceForRentExemption(accountLen, commitment); -} - -/** - * Unpack a token account - * - * @param address Token account - * @param info Token account data - * @param programId SPL Token program account - * - * @return Unpacked token account - */ -export function unpackAccount( - address: PublicKey, - info: AccountInfo | null, - programId = TOKEN_PROGRAM_ID, -): Account { - if (!info) throw new TokenAccountNotFoundError(); - if (!info.owner.equals(programId)) throw new TokenInvalidAccountOwnerError(); - if (info.data.length < ACCOUNT_SIZE) throw new TokenInvalidAccountSizeError(); - - const rawAccount = AccountLayout.decode(info.data.slice(0, ACCOUNT_SIZE)); - let tlvData = Buffer.alloc(0); - if (info.data.length > ACCOUNT_SIZE) { - if (info.data.length === MULTISIG_SIZE) throw new TokenInvalidAccountSizeError(); - if (info.data[ACCOUNT_SIZE] != AccountType.Account) throw new TokenInvalidAccountError(); - tlvData = info.data.slice(ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE); - } - - return { - address, - mint: rawAccount.mint, - owner: rawAccount.owner, - amount: rawAccount.amount, - delegate: rawAccount.delegateOption ? rawAccount.delegate : null, - delegatedAmount: rawAccount.delegatedAmount, - isInitialized: rawAccount.state !== AccountState.Uninitialized, - isFrozen: rawAccount.state === AccountState.Frozen, - isNative: !!rawAccount.isNativeOption, - rentExemptReserve: rawAccount.isNativeOption ? rawAccount.isNative : null, - closeAuthority: rawAccount.closeAuthorityOption ? rawAccount.closeAuthority : null, - tlvData, - }; -} diff --git a/token/js/src/state/index.ts b/token/js/src/state/index.ts deleted file mode 100644 index bcf6d756eef..00000000000 --- a/token/js/src/state/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './account.js'; -export * from './mint.js'; -export * from './multisig.js'; diff --git a/token/js/src/state/mint.ts b/token/js/src/state/mint.ts deleted file mode 100644 index cb09ec317d9..00000000000 --- a/token/js/src/state/mint.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { struct, u32, u8 } from '@solana/buffer-layout'; -import { bool, publicKey, u64 } from '@solana/buffer-layout-utils'; -import type { AccountInfo, Commitment, Connection } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'; -import { - TokenAccountNotFoundError, - TokenInvalidAccountOwnerError, - TokenInvalidAccountSizeError, - TokenInvalidMintError, - TokenOwnerOffCurveError, -} from '../errors.js'; -import { ACCOUNT_TYPE_SIZE, AccountType } from '../extensions/accountType.js'; -import type { ExtensionType } from '../extensions/extensionType.js'; -import { getMintLen } from '../extensions/extensionType.js'; -import { ACCOUNT_SIZE } from './account.js'; -import { MULTISIG_SIZE } from './multisig.js'; - -/** Information about a mint */ -export interface Mint { - /** Address of the mint */ - address: PublicKey; - /** - * Optional authority used to mint new tokens. The mint authority may only be provided during mint creation. - * If no mint authority is present then the mint has a fixed supply and no further tokens may be minted. - */ - mintAuthority: PublicKey | null; - /** Total supply of tokens */ - supply: bigint; - /** Number of base 10 digits to the right of the decimal place */ - decimals: number; - /** Is this mint initialized */ - isInitialized: boolean; - /** Optional authority to freeze token accounts */ - freezeAuthority: PublicKey | null; - /** Additional data for extension */ - tlvData: Buffer; -} - -/** Mint as stored by the program */ -export interface RawMint { - mintAuthorityOption: 1 | 0; - mintAuthority: PublicKey; - supply: bigint; - decimals: number; - isInitialized: boolean; - freezeAuthorityOption: 1 | 0; - freezeAuthority: PublicKey; -} - -/** Buffer layout for de/serializing a mint */ -export const MintLayout = struct([ - u32('mintAuthorityOption'), - publicKey('mintAuthority'), - u64('supply'), - u8('decimals'), - bool('isInitialized'), - u32('freezeAuthorityOption'), - publicKey('freezeAuthority'), -]); - -/** Byte length of a mint */ -export const MINT_SIZE = MintLayout.span; - -/** - * Retrieve information about a mint - * - * @param connection Connection to use - * @param address Mint account - * @param commitment Desired level of commitment for querying the state - * @param programId SPL Token program account - * - * @return Mint information - */ -export async function getMint( - connection: Connection, - address: PublicKey, - commitment?: Commitment, - programId = TOKEN_PROGRAM_ID, -): Promise { - const info = await connection.getAccountInfo(address, commitment); - return unpackMint(address, info, programId); -} - -/** - * Unpack a mint - * - * @param address Mint account - * @param info Mint account data - * @param programId SPL Token program account - * - * @return Unpacked mint - */ -export function unpackMint(address: PublicKey, info: AccountInfo | null, programId = TOKEN_PROGRAM_ID): Mint { - if (!info) throw new TokenAccountNotFoundError(); - if (!info.owner.equals(programId)) throw new TokenInvalidAccountOwnerError(); - if (info.data.length < MINT_SIZE) throw new TokenInvalidAccountSizeError(); - - const rawMint = MintLayout.decode(info.data.slice(0, MINT_SIZE)); - let tlvData = Buffer.alloc(0); - if (info.data.length > MINT_SIZE) { - if (info.data.length <= ACCOUNT_SIZE) throw new TokenInvalidAccountSizeError(); - if (info.data.length === MULTISIG_SIZE) throw new TokenInvalidAccountSizeError(); - if (info.data[ACCOUNT_SIZE] != AccountType.Mint) throw new TokenInvalidMintError(); - tlvData = info.data.slice(ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE); - } - - return { - address, - mintAuthority: rawMint.mintAuthorityOption ? rawMint.mintAuthority : null, - supply: rawMint.supply, - decimals: rawMint.decimals, - isInitialized: rawMint.isInitialized, - freezeAuthority: rawMint.freezeAuthorityOption ? rawMint.freezeAuthority : null, - tlvData, - }; -} - -/** Get the minimum lamport balance for a mint to be rent exempt - * - * @param connection Connection to use - * @param commitment Desired level of commitment for querying the state - * - * @return Amount of lamports required - */ -export async function getMinimumBalanceForRentExemptMint( - connection: Connection, - commitment?: Commitment, -): Promise { - return await getMinimumBalanceForRentExemptMintWithExtensions(connection, [], commitment); -} - -/** Get the minimum lamport balance for a rent-exempt mint with extensions - * - * @param connection Connection to use - * @param extensions Extension types included in the mint - * @param commitment Desired level of commitment for querying the state - * - * @return Amount of lamports required - */ -export async function getMinimumBalanceForRentExemptMintWithExtensions( - connection: Connection, - extensions: ExtensionType[], - commitment?: Commitment, -): Promise { - const mintLen = getMintLen(extensions); - return await connection.getMinimumBalanceForRentExemption(mintLen, commitment); -} - -/** - * Async version of getAssociatedTokenAddressSync - * For backwards compatibility - * - * @param mint Token mint account - * @param owner Owner of the new account - * @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address) - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * - * @return Promise containing the address of the associated token account - */ -export async function getAssociatedTokenAddress( - mint: PublicKey, - owner: PublicKey, - allowOwnerOffCurve = false, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -): Promise { - if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer())) throw new TokenOwnerOffCurveError(); - - const [address] = await PublicKey.findProgramAddress( - [owner.toBuffer(), programId.toBuffer(), mint.toBuffer()], - associatedTokenProgramId, - ); - - return address; -} - -/** - * Get the address of the associated token account for a given mint and owner - * - * @param mint Token mint account - * @param owner Owner of the new account - * @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address) - * @param programId SPL Token program account - * @param associatedTokenProgramId SPL Associated Token program account - * - * @return Address of the associated token account - */ -export function getAssociatedTokenAddressSync( - mint: PublicKey, - owner: PublicKey, - allowOwnerOffCurve = false, - programId = TOKEN_PROGRAM_ID, - associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, -): PublicKey { - if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer())) throw new TokenOwnerOffCurveError(); - - const [address] = PublicKey.findProgramAddressSync( - [owner.toBuffer(), programId.toBuffer(), mint.toBuffer()], - associatedTokenProgramId, - ); - - return address; -} diff --git a/token/js/src/state/multisig.ts b/token/js/src/state/multisig.ts deleted file mode 100644 index 04734837550..00000000000 --- a/token/js/src/state/multisig.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { struct, u8 } from '@solana/buffer-layout'; -import { bool, publicKey } from '@solana/buffer-layout-utils'; -import type { AccountInfo, Commitment, Connection, PublicKey } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../constants.js'; -import { TokenAccountNotFoundError, TokenInvalidAccountOwnerError, TokenInvalidAccountSizeError } from '../errors.js'; - -/** Information about a multisig */ -export interface Multisig { - /** Address of the multisig */ - address: PublicKey; - /** Number of signers required */ - m: number; - /** Number of possible signers, corresponds to the number of `signers` that are valid */ - n: number; - /** Is this mint initialized */ - isInitialized: boolean; - /** Full set of signers, of which `n` are valid */ - signer1: PublicKey; - signer2: PublicKey; - signer3: PublicKey; - signer4: PublicKey; - signer5: PublicKey; - signer6: PublicKey; - signer7: PublicKey; - signer8: PublicKey; - signer9: PublicKey; - signer10: PublicKey; - signer11: PublicKey; -} - -/** Multisig as stored by the program */ -export type RawMultisig = Omit; - -/** Buffer layout for de/serializing a multisig */ -export const MultisigLayout = struct([ - u8('m'), - u8('n'), - bool('isInitialized'), - publicKey('signer1'), - publicKey('signer2'), - publicKey('signer3'), - publicKey('signer4'), - publicKey('signer5'), - publicKey('signer6'), - publicKey('signer7'), - publicKey('signer8'), - publicKey('signer9'), - publicKey('signer10'), - publicKey('signer11'), -]); - -/** Byte length of a multisig */ -export const MULTISIG_SIZE = MultisigLayout.span; - -/** - * Retrieve information about a multisig - * - * @param connection Connection to use - * @param address Multisig account - * @param commitment Desired level of commitment for querying the state - * @param programId SPL Token program account - * - * @return Multisig information - */ -export async function getMultisig( - connection: Connection, - address: PublicKey, - commitment?: Commitment, - programId = TOKEN_PROGRAM_ID, -): Promise { - const info = await connection.getAccountInfo(address, commitment); - return unpackMultisig(address, info, programId); -} - -/** - * Unpack a multisig - * - * @param address Multisig account - * @param info Multisig account data - * @param programId SPL Token program account - * - * @return Unpacked multisig - */ -export function unpackMultisig( - address: PublicKey, - info: AccountInfo | null, - programId = TOKEN_PROGRAM_ID, -): Multisig { - if (!info) throw new TokenAccountNotFoundError(); - if (!info.owner.equals(programId)) throw new TokenInvalidAccountOwnerError(); - if (info.data.length != MULTISIG_SIZE) throw new TokenInvalidAccountSizeError(); - - const multisig = MultisigLayout.decode(info.data); - - return { address, ...multisig }; -} - -/** Get the minimum lamport balance for a multisig to be rent exempt - * - * @param connection Connection to use - * @param commitment Desired level of commitment for querying the state - * - * @return Amount of lamports required - */ -export async function getMinimumBalanceForRentExemptMultisig( - connection: Connection, - commitment?: Commitment, -): Promise { - return await connection.getMinimumBalanceForRentExemption(MULTISIG_SIZE, commitment); -} diff --git a/token/js/test/common.ts b/token/js/test/common.ts deleted file mode 100644 index 06ff1ef7ebf..00000000000 --- a/token/js/test/common.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signer } from '@solana/web3.js'; -import { PublicKey, Keypair, Connection } from '@solana/web3.js'; -import { TOKEN_PROGRAM_ID } from '../src'; - -export async function newAccountWithLamports(connection: Connection, lamports = 1000000): Promise { - const account = Keypair.generate(); - const signature = await connection.requestAirdrop(account.publicKey, lamports); - await connection.confirmTransaction(signature); - return account; -} - -export async function getConnection(): Promise { - const url = 'http://127.0.0.1:8899'; - const connection = new Connection(url, 'confirmed'); - return connection; -} - -export const TEST_PROGRAM_ID = process.env.TEST_PROGRAM_ID - ? new PublicKey(process.env.TEST_PROGRAM_ID) - : TOKEN_PROGRAM_ID; - -export const TRANSFER_HOOK_TEST_PROGRAM_ID = new PublicKey('TokenHookExampLe8smaVNrxTBezWTRbEwxwb1Zykrb'); diff --git a/token/js/test/e2e-2022/closeMint.test.ts b/token/js/test/e2e-2022/closeMint.test.ts deleted file mode 100644 index 0f144dc6aeb..00000000000 --- a/token/js/test/e2e-2022/closeMint.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; -import { - createAccount, - createInitializeMintInstruction, - createInitializeMintCloseAuthorityInstruction, - closeAccount, - mintTo, - getMintLen, - ExtensionType, - AuthorityType, - getMint, - setAuthority, - getMintCloseAuthority, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.MintCloseAuthority]; -describe('closeMint', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let closeAuthority: Keypair; - let account: PublicKey; - let destination: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - closeAuthority = Keypair.generate(); - }); - beforeEach(async () => { - const mintKeypair = Keypair.generate(); - mint = mintKeypair.publicKey; - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeMintCloseAuthorityInstruction(mint, closeAuthority.publicKey, TEST_PROGRAM_ID), - createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined); - }); - it('failsWithNonZeroAmount', async () => { - const owner = Keypair.generate(); - destination = Keypair.generate().publicKey; - account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID); - const amount = BigInt(1000); - await mintTo(connection, payer, mint, account, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID); - expect( - closeAccount(connection, payer, mint, destination, closeAuthority, [], undefined, TEST_PROGRAM_ID), - ).to.be.rejectedWith(Error); - }); - it('works', async () => { - destination = Keypair.generate().publicKey; - const accountInfo = await connection.getAccountInfo(mint); - let rentExemptAmount; - expect(accountInfo).to.not.equal(null); - if (accountInfo !== null) { - rentExemptAmount = accountInfo.lamports; - } - - await closeAccount(connection, payer, mint, destination, closeAuthority, [], undefined, TEST_PROGRAM_ID); - - const closedInfo = await connection.getAccountInfo(mint); - expect(closedInfo).to.equal(null); - - const destinationInfo = await connection.getAccountInfo(destination); - expect(destinationInfo).to.not.equal(null); - if (destinationInfo !== null) { - expect(destinationInfo.lamports).to.eql(rentExemptAmount); - } - }); - it('authority', async () => { - await setAuthority( - connection, - payer, - mint, - closeAuthority, - AuthorityType.CloseMint, - null, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const mintCloseAuthority = getMintCloseAuthority(mintInfo); - expect(mintCloseAuthority).to.not.equal(null); - if (mintCloseAuthority !== null) { - expect(mintCloseAuthority.closeAuthority).to.eql(PublicKey.default); - } - }); -}); diff --git a/token/js/test/e2e-2022/cpiGuard.test.ts b/token/js/test/e2e-2022/cpiGuard.test.ts deleted file mode 100644 index aee7aa0f24d..00000000000 --- a/token/js/test/e2e-2022/cpiGuard.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; -import { - createAccount, - createMint, - createEnableCpiGuardInstruction, - createDisableCpiGuardInstruction, - createInitializeAccountInstruction, - getAccount, - getCpiGuard, - enableCpiGuard, - disableCpiGuard, - getAccountLen, - ExtensionType, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const TRANSFER_AMOUNT = 1_000; -const EXTENSIONS = [ExtensionType.CpiGuard]; -describe('cpiGuard', () => { - let connection: Connection; - let payer: Signer; - let owner: Keypair; - let account: PublicKey; - - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - owner = Keypair.generate(); - }); - - beforeEach(async () => { - const mintKeypair = Keypair.generate(); - const mintAuthority = Keypair.generate(); - const accountKeypair = Keypair.generate(); - account = accountKeypair.publicKey; - const accountLen = getAccountLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(accountLen); - - const mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: account, - space: accountLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeAccountInstruction(account, mint, owner.publicKey, TEST_PROGRAM_ID), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, accountKeypair], undefined); - }); - - it('enable/disable via instruction', async () => { - let accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - let cpiGuard = getCpiGuard(accountInfo); - - expect(cpiGuard).to.equal(null); - - let transaction = new Transaction().add( - createEnableCpiGuardInstruction(account, owner.publicKey, [], TEST_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, owner], undefined); - - accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - cpiGuard = getCpiGuard(accountInfo); - - expect(cpiGuard).to.not.equal(null); - if (cpiGuard !== null) { - expect(cpiGuard.lockCpi).to.equal(true); - } - - transaction = new Transaction().add( - createDisableCpiGuardInstruction(account, owner.publicKey, [], TEST_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, owner], undefined); - - accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - cpiGuard = getCpiGuard(accountInfo); - - expect(cpiGuard).to.not.equal(null); - if (cpiGuard !== null) { - expect(cpiGuard.lockCpi).to.equal(false); - } - }); - - it('enable/disable via command', async () => { - let accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - let cpiGuard = getCpiGuard(accountInfo); - - expect(cpiGuard).to.equal(null); - - await enableCpiGuard(connection, payer, account, owner, [], undefined, TEST_PROGRAM_ID); - - accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - cpiGuard = getCpiGuard(accountInfo); - - expect(cpiGuard).to.not.equal(null); - if (cpiGuard !== null) { - expect(cpiGuard.lockCpi).to.equal(true); - } - - await disableCpiGuard(connection, payer, account, owner, [], undefined, TEST_PROGRAM_ID); - - accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - cpiGuard = getCpiGuard(accountInfo); - - expect(cpiGuard).to.not.equal(null); - if (cpiGuard !== null) { - expect(cpiGuard.lockCpi).to.equal(false); - } - }); -}); diff --git a/token/js/test/e2e-2022/defaultAccountState.test.ts b/token/js/test/e2e-2022/defaultAccountState.test.ts deleted file mode 100644 index 898bb2e6a7c..00000000000 --- a/token/js/test/e2e-2022/defaultAccountState.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; -import { - AccountState, - createAccount, - createInitializeMintInstruction, - createInitializeDefaultAccountStateInstruction, - getAccount, - getDefaultAccountState, - getMint, - getMintLen, - updateDefaultAccountState, - ExtensionType, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_STATE = AccountState.Frozen; -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.DefaultAccountState]; -describe('defaultAccountState', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let freezeAuthority: Keypair; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - freezeAuthority = Keypair.generate(); - }); - beforeEach(async () => { - const mintKeypair = Keypair.generate(); - mint = mintKeypair.publicKey; - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeDefaultAccountStateInstruction(mint, TEST_STATE, TEST_PROGRAM_ID), - createInitializeMintInstruction( - mint, - TEST_TOKEN_DECIMALS, - mintAuthority.publicKey, - freezeAuthority.publicKey, - TEST_PROGRAM_ID, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined); - }); - it('defaults to frozen', async () => { - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const defaultAccountState = getDefaultAccountState(mintInfo); - expect(defaultAccountState).to.not.equal(null); - if (defaultAccountState !== null) { - expect(defaultAccountState.state).to.eql(TEST_STATE); - } - const owner = Keypair.generate(); - const account = await createAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(accountInfo.isFrozen).to.equal(true); - expect(accountInfo.isInitialized).to.equal(true); - }); - it('defaults to initialized after update', async () => { - await updateDefaultAccountState( - connection, - payer, - mint, - AccountState.Initialized, - freezeAuthority, - [], - undefined, - TEST_PROGRAM_ID, - ); - const owner = Keypair.generate(); - const account = await createAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(accountInfo.isFrozen).to.equal(false); - expect(accountInfo.isInitialized).to.equal(true); - }); -}); diff --git a/token/js/test/e2e-2022/groupMemberPointer.test.ts b/token/js/test/e2e-2022/groupMemberPointer.test.ts deleted file mode 100644 index 8546a4869f7..00000000000 --- a/token/js/test/e2e-2022/groupMemberPointer.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; - -import { - AuthorityType, - ExtensionType, - createInitializeGroupMemberPointerInstruction, - createInitializeMintInstruction, - createSetAuthorityInstruction, - createUpdateGroupMemberPointerInstruction, - getGroupMemberPointerState, - getMint, - getMintLen, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.GroupMemberPointer]; - -describe('GroupMember pointer', () => { - let connection: Connection; - let payer: Signer; - let mint: Keypair; - let mintAuthority: Keypair; - let memberAddress: PublicKey; - - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - }); - - beforeEach(async () => { - mint = Keypair.generate(); - memberAddress = PublicKey.unique(); - - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint.publicKey, - space: mintLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeGroupMemberPointerInstruction( - mint.publicKey, - mintAuthority.publicKey, - memberAddress, - TEST_PROGRAM_ID, - ), - createInitializeMintInstruction( - mint.publicKey, - TEST_TOKEN_DECIMALS, - mintAuthority.publicKey, - null, - TEST_PROGRAM_ID, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mint], undefined); - }); - - it('can successfully initialize', async () => { - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const groupMemberPointer = getGroupMemberPointerState(mintInfo); - - expect(groupMemberPointer).to.deep.equal({ - authority: mintAuthority.publicKey, - memberAddress, - }); - }); - - it('can update to new address', async () => { - const newGroupMemberAddress = PublicKey.unique(); - const transaction = new Transaction().add( - createUpdateGroupMemberPointerInstruction( - mint.publicKey, - mintAuthority.publicKey, - newGroupMemberAddress, - undefined, - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const groupMemberPointer = getGroupMemberPointerState(mintInfo); - - expect(groupMemberPointer).to.deep.equal({ - authority: mintAuthority.publicKey, - memberAddress: newGroupMemberAddress, - }); - }); - - it('can update authority', async () => { - const newAuthority = PublicKey.unique(); - const transaction = new Transaction().add( - createSetAuthorityInstruction( - mint.publicKey, - mintAuthority.publicKey, - AuthorityType.GroupMemberPointer, - newAuthority, - [], - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const groupMemberPointer = getGroupMemberPointerState(mintInfo); - - expect(groupMemberPointer).to.deep.equal({ - authority: newAuthority, - memberAddress, - }); - }); - - it('can update authority to null', async () => { - const transaction = new Transaction().add( - createSetAuthorityInstruction( - mint.publicKey, - mintAuthority.publicKey, - AuthorityType.GroupMemberPointer, - null, - [], - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const groupMemberPointer = getGroupMemberPointerState(mintInfo); - - expect(groupMemberPointer).to.deep.equal({ - authority: null, - memberAddress, - }); - }); -}); diff --git a/token/js/test/e2e-2022/groupPointer.test.ts b/token/js/test/e2e-2022/groupPointer.test.ts deleted file mode 100644 index 1855c032ef8..00000000000 --- a/token/js/test/e2e-2022/groupPointer.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; - -import { - AuthorityType, - ExtensionType, - createInitializeGroupPointerInstruction, - createInitializeMintInstruction, - createSetAuthorityInstruction, - createUpdateGroupPointerInstruction, - getGroupPointerState, - getMint, - getMintLen, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.GroupPointer]; - -describe('Group pointer', () => { - let connection: Connection; - let payer: Signer; - let mint: Keypair; - let mintAuthority: Keypair; - let groupAddress: PublicKey; - - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - }); - - beforeEach(async () => { - mint = Keypair.generate(); - groupAddress = PublicKey.unique(); - - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint.publicKey, - space: mintLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeGroupPointerInstruction( - mint.publicKey, - mintAuthority.publicKey, - groupAddress, - TEST_PROGRAM_ID, - ), - createInitializeMintInstruction( - mint.publicKey, - TEST_TOKEN_DECIMALS, - mintAuthority.publicKey, - null, - TEST_PROGRAM_ID, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mint], undefined); - }); - - it('can successfully initialize', async () => { - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const groupPointer = getGroupPointerState(mintInfo); - - expect(groupPointer).to.deep.equal({ - authority: mintAuthority.publicKey, - groupAddress, - }); - }); - - it('can update to new address', async () => { - const newGroupAddress = PublicKey.unique(); - const transaction = new Transaction().add( - createUpdateGroupPointerInstruction( - mint.publicKey, - mintAuthority.publicKey, - newGroupAddress, - undefined, - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const groupPointer = getGroupPointerState(mintInfo); - - expect(groupPointer).to.deep.equal({ - authority: mintAuthority.publicKey, - groupAddress: newGroupAddress, - }); - }); - - it('can update authority', async () => { - const newAuthority = PublicKey.unique(); - const transaction = new Transaction().add( - createSetAuthorityInstruction( - mint.publicKey, - mintAuthority.publicKey, - AuthorityType.GroupPointer, - newAuthority, - [], - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const groupPointer = getGroupPointerState(mintInfo); - - expect(groupPointer).to.deep.equal({ - authority: newAuthority, - groupAddress, - }); - }); - - it('can update authority to null', async () => { - const transaction = new Transaction().add( - createSetAuthorityInstruction( - mint.publicKey, - mintAuthority.publicKey, - AuthorityType.GroupPointer, - null, - [], - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const groupPointer = getGroupPointerState(mintInfo); - - expect(groupPointer).to.deep.equal({ - authority: null, - groupAddress, - }); - }); -}); diff --git a/token/js/test/e2e-2022/immutableOwner.test.ts b/token/js/test/e2e-2022/immutableOwner.test.ts deleted file mode 100644 index 43088a2e144..00000000000 --- a/token/js/test/e2e-2022/immutableOwner.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js'; - -import { - AuthorityType, - setAuthority, - ExtensionType, - createInitializeImmutableOwnerInstruction, - createInitializeAccountInstruction, - createMint, - getAccountLen, -} from '../../src'; - -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.ImmutableOwner]; -describe('immutableOwner', () => { - let connection: Connection; - let payer: Signer; - let owner: Keypair; - let account: PublicKey; - let mint: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - }); - beforeEach(async () => { - const mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - owner = Keypair.generate(); - const accountLen = getAccountLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(accountLen); - const accountKeypair = Keypair.generate(); - account = accountKeypair.publicKey; - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: account, - space: accountLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeImmutableOwnerInstruction(account, TEST_PROGRAM_ID), - createInitializeAccountInstruction(account, mint, owner.publicKey, TEST_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, accountKeypair], undefined); - }); - it('AccountOwner', async () => { - const newOwner = Keypair.generate(); - expect( - setAuthority( - connection, - payer, - account, - newOwner, - AuthorityType.AccountOwner, - owner.publicKey, - [], - undefined, - TEST_PROGRAM_ID, - ), - ).to.be.rejectedWith(Error); - }); -}); diff --git a/token/js/test/e2e-2022/interestBearingMint.test.ts b/token/js/test/e2e-2022/interestBearingMint.test.ts deleted file mode 100644 index 3561abe42d4..00000000000 --- a/token/js/test/e2e-2022/interestBearingMint.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; -import { - AuthorityType, - createInterestBearingMint, - getInterestBearingMintConfigState, - getMint, - setAuthority, - updateRateInterestBearingMint, -} from '../../src'; -import { getConnection, newAccountWithLamports, TEST_PROGRAM_ID } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const TEST_RATE = 10; -const TEST_UPDATE_RATE = 50; - -describe('interestBearingMint', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let rateAuthority: Keypair; - let mintAuthority: Keypair; - let freezeAuthority: Keypair; - let mintKeypair: Keypair; - - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - rateAuthority = Keypair.generate(); - mintAuthority = Keypair.generate(); - freezeAuthority = Keypair.generate(); - }); - - it('initialize and update rate', async () => { - mintKeypair = Keypair.generate(); - mint = mintKeypair.publicKey; - await createInterestBearingMint( - connection, - payer, - mintAuthority.publicKey, - freezeAuthority.publicKey, - rateAuthority.publicKey, - TEST_RATE, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const interestBearingMintConfigState = getInterestBearingMintConfigState(mintInfo); - expect(interestBearingMintConfigState).to.not.equal(null); - if (interestBearingMintConfigState !== null) { - expect(interestBearingMintConfigState.rateAuthority).to.eql(rateAuthority.publicKey); - expect(interestBearingMintConfigState.preUpdateAverageRate).to.eql(TEST_RATE); - expect(interestBearingMintConfigState.currentRate).to.eql(TEST_RATE); - expect(interestBearingMintConfigState.lastUpdateTimestamp).to.be.greaterThan(0); - expect(interestBearingMintConfigState.initializationTimestamp).to.be.greaterThan(0); - } - - await updateRateInterestBearingMint( - connection, - payer, - mint, - rateAuthority, - TEST_UPDATE_RATE, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfoUpdatedRate = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const updatedRateConfigState = getInterestBearingMintConfigState(mintInfoUpdatedRate); - - expect(updatedRateConfigState).to.not.equal(null); - if (updatedRateConfigState !== null) { - expect(updatedRateConfigState.rateAuthority).to.eql(rateAuthority.publicKey); - expect(updatedRateConfigState.currentRate).to.eql(TEST_UPDATE_RATE); - expect(updatedRateConfigState.preUpdateAverageRate).to.eql(TEST_RATE); - expect(updatedRateConfigState.lastUpdateTimestamp).to.be.greaterThan(0); - expect(updatedRateConfigState.initializationTimestamp).to.be.greaterThan(0); - } - }); - it('authority', async () => { - await setAuthority( - connection, - payer, - mint, - rateAuthority, - AuthorityType.InterestRate, - null, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const rateConfigState = getInterestBearingMintConfigState(mintInfo); - expect(rateConfigState).to.not.equal(null); - if (rateConfigState !== null) { - expect(rateConfigState.rateAuthority).to.eql(PublicKey.default); - } - }); -}); diff --git a/token/js/test/e2e-2022/memoTransfer.test.ts b/token/js/test/e2e-2022/memoTransfer.test.ts deleted file mode 100644 index e7200ebe36a..00000000000 --- a/token/js/test/e2e-2022/memoTransfer.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; -import { createMemoInstruction } from '@solana/spl-memo'; -import { - createAccount, - createMint, - createEnableRequiredMemoTransfersInstruction, - createInitializeAccountInstruction, - createTransferInstruction, - getAccount, - getMemoTransfer, - disableRequiredMemoTransfers, - enableRequiredMemoTransfers, - mintTo, - transfer, - getAccountLen, - ExtensionType, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const TRANSFER_AMOUNT = 1_000; -const EXTENSIONS = [ExtensionType.MemoTransfer]; -describe('memoTransfer', () => { - let connection: Connection; - let payer: Signer; - let owner: Keypair; - let mint: PublicKey; - let mintAuthority: Keypair; - let source: PublicKey; - let destination: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - owner = Keypair.generate(); - }); - beforeEach(async () => { - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - - source = await createAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, // uses ATA by default - undefined, - TEST_PROGRAM_ID, - ); - - const destinationKeypair = Keypair.generate(); - destination = destinationKeypair.publicKey; - const accountLen = getAccountLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(accountLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: destination, - space: accountLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeAccountInstruction(destination, mint, owner.publicKey, TEST_PROGRAM_ID), - createEnableRequiredMemoTransfersInstruction(destination, owner.publicKey, [], TEST_PROGRAM_ID), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, owner, destinationKeypair], undefined); - await mintTo( - connection, - payer, - mint, - source, - mintAuthority, - TRANSFER_AMOUNT * 10, - [], - undefined, - TEST_PROGRAM_ID, - ); - }); - it('fails without memo when enabled', async () => { - const accountInfo = await getAccount(connection, destination, undefined, TEST_PROGRAM_ID); - const memoTransfer = getMemoTransfer(accountInfo); - expect(memoTransfer).to.not.equal(null); - if (memoTransfer !== null) { - expect(memoTransfer.requireIncomingTransferMemos).to.equal(true); - } - expect( - transfer(connection, payer, source, destination, owner, TRANSFER_AMOUNT, [], undefined, TEST_PROGRAM_ID), - ).to.be.rejectedWith(Error); - }); - it('works without memo when disabled', async () => { - await disableRequiredMemoTransfers(connection, payer, destination, owner, [], undefined, TEST_PROGRAM_ID); - await transfer(connection, payer, source, destination, owner, TRANSFER_AMOUNT, [], undefined, TEST_PROGRAM_ID); - await enableRequiredMemoTransfers(connection, payer, destination, owner, [], undefined, TEST_PROGRAM_ID); - expect( - transfer(connection, payer, source, destination, owner, TRANSFER_AMOUNT, [], undefined, TEST_PROGRAM_ID), - ).to.be.rejectedWith(Error); - }); - it('works with memo when enabled', async () => { - const transaction = new Transaction().add( - createMemoInstruction('transfer with a memo', [payer.publicKey, owner.publicKey]), - createTransferInstruction(source, destination, owner.publicKey, TRANSFER_AMOUNT, [], TEST_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, owner], { - preflightCommitment: 'confirmed', - }); - }); -}); diff --git a/token/js/test/e2e-2022/metadataPointer.test.ts b/token/js/test/e2e-2022/metadataPointer.test.ts deleted file mode 100644 index 4fbdb6de3cb..00000000000 --- a/token/js/test/e2e-2022/metadataPointer.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; - -import { - ExtensionType, - createInitializeMetadataPointerInstruction, - createInitializeMintInstruction, - createUpdateMetadataPointerInstruction, - getMetadataPointerState, - getMint, - getMintLen, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.MetadataPointer]; - -describe('Metadata pointer', () => { - let connection: Connection; - let payer: Signer; - let mint: Keypair; - let mintAuthority: Keypair; - let metadataAddress: PublicKey; - - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - }); - - beforeEach(async () => { - mint = Keypair.generate(); - metadataAddress = PublicKey.unique(); - - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint.publicKey, - space: mintLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeMetadataPointerInstruction( - mint.publicKey, - mintAuthority.publicKey, - metadataAddress, - TEST_PROGRAM_ID, - ), - createInitializeMintInstruction( - mint.publicKey, - TEST_TOKEN_DECIMALS, - mintAuthority.publicKey, - null, - TEST_PROGRAM_ID, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mint], undefined); - }); - - it('can successfully initialize', async () => { - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const metadataPointer = getMetadataPointerState(mintInfo); - - expect(metadataPointer).to.deep.equal({ - authority: mintAuthority.publicKey, - metadataAddress, - }); - }); - - it('can update to new address', async () => { - const newMetadataAddress = PublicKey.unique(); - const transaction = new Transaction().add( - createUpdateMetadataPointerInstruction( - mint.publicKey, - mintAuthority.publicKey, - newMetadataAddress, - undefined, - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintAuthority], undefined); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const metadataPointer = getMetadataPointerState(mintInfo); - - expect(metadataPointer).to.deep.equal({ - authority: mintAuthority.publicKey, - metadataAddress: newMetadataAddress, - }); - }); -}); diff --git a/token/js/test/e2e-2022/nonTransferableMint.test.ts b/token/js/test/e2e-2022/nonTransferableMint.test.ts deleted file mode 100644 index 1261b6cef29..00000000000 --- a/token/js/test/e2e-2022/nonTransferableMint.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; -import { - createInitializeMintInstruction, - createInitializeNonTransferableMintInstruction, - createInitializeImmutableOwnerInstruction, - createInitializeAccountInstruction, - mintTo, - getAccountLen, - getMint, - getMintLen, - getNonTransferable, - transfer, - ExtensionType, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.NonTransferable]; -describe('nonTransferable', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - }); - beforeEach(async () => { - const mintKeypair = Keypair.generate(); - mint = mintKeypair.publicKey; - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeNonTransferableMintInstruction(mint, TEST_PROGRAM_ID), - createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined); - }); - it('fails transfer', async () => { - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const nonTransferable = getNonTransferable(mintInfo); - expect(nonTransferable).to.not.equal(null); - - const owner = Keypair.generate(); - const accountLen = getAccountLen([ExtensionType.ImmutableOwner, ExtensionType.NonTransferableAccount]); - const lamports = await connection.getMinimumBalanceForRentExemption(accountLen); - - const sourceKeypair = Keypair.generate(); - const source = sourceKeypair.publicKey; - let transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: source, - space: accountLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeImmutableOwnerInstruction(source, TEST_PROGRAM_ID), - createInitializeAccountInstruction(source, mint, owner.publicKey, TEST_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, sourceKeypair], undefined); - - const destinationKeypair = Keypair.generate(); - const destination = destinationKeypair.publicKey; - transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: destination, - space: accountLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeImmutableOwnerInstruction(destination, TEST_PROGRAM_ID), - createInitializeAccountInstruction(destination, mint, owner.publicKey, TEST_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, destinationKeypair], undefined); - - const amount = BigInt(1000); - await mintTo(connection, payer, mint, source, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID); - - expect( - transfer(connection, payer, source, destination, owner, amount, [], undefined, TEST_PROGRAM_ID), - ).to.be.rejectedWith(Error); - }); -}); diff --git a/token/js/test/e2e-2022/permanentDelegate.test.ts b/token/js/test/e2e-2022/permanentDelegate.test.ts deleted file mode 100644 index 29e69cd3a55..00000000000 --- a/token/js/test/e2e-2022/permanentDelegate.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; -import { - createAccount, - createInitializeMintInstruction, - mintTo, - getMintLen, - ExtensionType, - createInitializePermanentDelegateInstruction, - burn, - transferChecked, - AuthorityType, - getMint, - setAuthority, - getPermanentDelegate, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 0; -const EXTENSIONS = [ExtensionType.PermanentDelegate]; -describe('permanentDelegate', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let permanentDelegate: Keypair; - let account: PublicKey; - let destination: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - permanentDelegate = Keypair.generate(); - }); - beforeEach(async () => { - const mintKeypair = Keypair.generate(); - mint = mintKeypair.publicKey; - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializePermanentDelegateInstruction(mint, permanentDelegate.publicKey, TEST_PROGRAM_ID), - createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined); - }); - it('burn tokens ', async () => { - const owner = Keypair.generate(); - account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID); - await mintTo(connection, payer, mint, account, mintAuthority, 5, [], undefined, TEST_PROGRAM_ID); - await burn(connection, payer, account, mint, permanentDelegate, 2, undefined, undefined, TEST_PROGRAM_ID); - const info = await connection.getTokenAccountBalance(account); - expect(info).to.not.equal(null); - if (info !== null) { - expect(info.value.uiAmount).to.eql(3); - } - }); - it('transfer tokens', async () => { - const owner1 = Keypair.generate(); - const owner2 = Keypair.generate(); - destination = await createAccount( - connection, - payer, - mint, - owner2.publicKey, - undefined, - undefined, - TEST_PROGRAM_ID, - ); - account = await createAccount(connection, payer, mint, owner1.publicKey, undefined, undefined, TEST_PROGRAM_ID); - await mintTo(connection, payer, mint, account, mintAuthority, 5, [], undefined, TEST_PROGRAM_ID); - await transferChecked( - connection, - payer, - account, - mint, - destination, - permanentDelegate, - 2, - 0, - undefined, - undefined, - TEST_PROGRAM_ID, - ); - const source_info = await connection.getTokenAccountBalance(account); - const destination_info = await connection.getTokenAccountBalance(destination); - expect(source_info).to.not.equal(null); - expect(destination_info).to.not.equal(null); - if (source_info !== null) { - expect(source_info.value.uiAmount).to.eql(3); - } - if (destination_info !== null) { - expect(destination_info.value.uiAmount).to.eql(2); - } - }); - it('authority', async () => { - await setAuthority( - connection, - payer, - mint, - permanentDelegate, - AuthorityType.PermanentDelegate, - null, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const permanentDelegateConfig = getPermanentDelegate(mintInfo); - expect(permanentDelegateConfig).to.not.equal(null); - if (permanentDelegateConfig !== null) { - expect(permanentDelegateConfig.delegate).to.eql(PublicKey.default); - } - }); -}); diff --git a/token/js/test/e2e-2022/reallocate.test.ts b/token/js/test/e2e-2022/reallocate.test.ts deleted file mode 100644 index 399b917f2ea..00000000000 --- a/token/js/test/e2e-2022/reallocate.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair, Transaction, sendAndConfirmTransaction } from '@solana/web3.js'; - -import { ExtensionType, createAccount, createMint, createReallocateInstruction, getAccountLen } from '../../src'; - -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.ImmutableOwner]; -describe('reallocate', () => { - let connection: Connection; - let payer: Signer; - let owner: Keypair; - let account: PublicKey; - let mint: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - }); - beforeEach(async () => { - const mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - owner = Keypair.generate(); - account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID); - }); - it('works', async () => { - const transaction = new Transaction().add( - createReallocateInstruction( - account, - payer.publicKey, - EXTENSIONS, - owner.publicKey, - undefined, - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, owner], undefined); - const info = await connection.getAccountInfo(account); - expect(info).to.not.equal(null); - if (info !== null) { - const expectedAccountLen = getAccountLen(EXTENSIONS); - expect(info.data.length).to.eql(expectedAccountLen); - } - }); -}); diff --git a/token/js/test/e2e-2022/tlv.test.ts b/token/js/test/e2e-2022/tlv.test.ts deleted file mode 100644 index b75c30ed821..00000000000 --- a/token/js/test/e2e-2022/tlv.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; - -import type { Account, Mint } from '../../src'; -import { - createInitializeAccountInstruction, - getAccount, - getAccountLen, - createMint, - ExtensionType, - getExtensionData, - isAccountExtension, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 9; -const ACCOUNT_EXTENSIONS = Object.values(ExtensionType) - .filter(Number.isInteger) - .filter((e: any): e is ExtensionType => isAccountExtension(e)); - -describe('tlv test', () => { - let connection: Connection; - let payer: Signer; - let owner: Keypair; - - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - owner = Keypair.generate(); - }); - - // test that the parser gracefully handles accounts with arbitrary extra bytes - it('parses account with extra bytes', async () => { - const initTestAccount = async (extraBytes: number) => { - const mintKeypair = Keypair.generate(); - const accountKeypair = Keypair.generate(); - const account = accountKeypair.publicKey; - const accountLen = getAccountLen([]) + extraBytes; - const lamports = await connection.getMinimumBalanceForRentExemption(accountLen); - - const mint = await createMint( - connection, - payer, - mintKeypair.publicKey, - mintKeypair.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: account, - space: accountLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeAccountInstruction(account, mint, owner.publicKey, TEST_PROGRAM_ID), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, accountKeypair], undefined); - - return account; - }; - - const promises: Promise<[number, Account] | undefined>[] = []; - for (let i = 0; i < 16; i++) { - // trying to alloc exactly one extra byte causes an unpack failure in the program when initializing - if (i == 1) continue; - - promises.push( - initTestAccount(i) - .then((account: PublicKey) => getAccount(connection, account, undefined, TEST_PROGRAM_ID)) - .then((accountInfo: Account) => { - for (const extension of ACCOUNT_EXTENSIONS) { - // realistically this will never fail with a non-null value, it will just throw - expect( - getExtensionData(extension, accountInfo.tlvData), - `account parse test failed: found ${ExtensionType[extension]}, but should not have. \ - test case: no extensions, ${i} extra bytes`, - ).to.equal(null); - } - return Promise.resolve(undefined); - }), - ); - } - - await Promise.all(promises); - }); -}); diff --git a/token/js/test/e2e-2022/tokenGroup.test.ts b/token/js/test/e2e-2022/tokenGroup.test.ts deleted file mode 100644 index b076f8ac55d..00000000000 --- a/token/js/test/e2e-2022/tokenGroup.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; -import { packTokenGroup } from '@solana/spl-token-group'; - -import { - ExtensionType, - createInitializeMintInstruction, - createInitializeGroupPointerInstruction, - tokenGroupInitializeGroup, - tokenGroupUpdateGroupMaxSize, - tokenGroupUpdateGroupAuthority, - getTokenGroupState, - getMint, - getMintLen, - tokenGroupInitializeGroupWithRentTransfer, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.GroupPointer]; - -describe('tokenGroup', async () => { - let connection: Connection; - let payer: Signer; - let mint: Keypair; - let mintAuthority: Keypair; - let updateAuthority: Keypair; - - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - updateAuthority = Keypair.generate(); - }); - - beforeEach(async () => { - mint = Keypair.generate(); - - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint.publicKey, - space: mintLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeGroupPointerInstruction( - mint.publicKey, - mintAuthority.publicKey, - mint.publicKey, - TEST_PROGRAM_ID, - ), - createInitializeMintInstruction( - mint.publicKey, - TEST_TOKEN_DECIMALS, - mintAuthority.publicKey, - null, - TEST_PROGRAM_ID, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mint], undefined); - }); - - it('can fetch un-initialized token group as null', async () => { - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(getTokenGroupState(mintInfo)).to.deep.equal(null); - }); - - it('can initialize', async () => { - const tokenGroup = { - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - size: BigInt(0), - maxSize: BigInt(10), - }; - - // Transfer the required amount for rent exemption - const lamports = await connection.getMinimumBalanceForRentExemption(packTokenGroup(tokenGroup).length); - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: mint.publicKey, - lamports, - }), - ); - await sendAndConfirmTransaction(connection, transaction, [payer], undefined); - - await tokenGroupInitializeGroup( - connection, - payer, - mint.publicKey, - mintAuthority.publicKey, - tokenGroup.updateAuthority, - tokenGroup.maxSize, - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const group = getTokenGroupState(mintInfo); - expect(group).to.deep.equal(tokenGroup); - }); - - it('can initialize with rent transfer', async () => { - const tokenGroup = { - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - size: BigInt(0), - maxSize: BigInt(10), - }; - - await tokenGroupInitializeGroupWithRentTransfer( - connection, - payer, - mint.publicKey, - mintAuthority.publicKey, - tokenGroup.updateAuthority, - tokenGroup.maxSize, - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const group = getTokenGroupState(mintInfo); - expect(group).to.deep.equal(tokenGroup); - }); - - it('can update max size', async () => { - const tokenGroup = { - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - size: BigInt(0), - maxSize: BigInt(10), - }; - - // Transfer the required amount for rent exemption - const lamports = await connection.getMinimumBalanceForRentExemption(packTokenGroup(tokenGroup).length); - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: mint.publicKey, - lamports, - }), - ); - await sendAndConfirmTransaction(connection, transaction, [payer], undefined); - - await tokenGroupInitializeGroup( - connection, - payer, - mint.publicKey, - mintAuthority.publicKey, - tokenGroup.updateAuthority, - tokenGroup.maxSize, - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - - await tokenGroupUpdateGroupMaxSize( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - BigInt(20), - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const group = getTokenGroupState(mintInfo); - expect(group).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - size: BigInt(0), - maxSize: BigInt(20), - }); - }); - - it('can update authority', async () => { - const tokenGroup = { - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - size: BigInt(0), - maxSize: BigInt(10), - }; - - // Transfer the required amount for rent exemption - const lamports = await connection.getMinimumBalanceForRentExemption(packTokenGroup(tokenGroup).length); - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: mint.publicKey, - lamports, - }), - ); - await sendAndConfirmTransaction(connection, transaction, [payer], undefined); - - await tokenGroupInitializeGroup( - connection, - payer, - mint.publicKey, - mintAuthority.publicKey, - tokenGroup.updateAuthority, - tokenGroup.maxSize, - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - - const newUpdateAuthority = Keypair.generate(); - await tokenGroupUpdateGroupAuthority( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - newUpdateAuthority.publicKey, - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - - const mintInfo = await getMint(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - const group = getTokenGroupState(mintInfo); - expect(group).to.deep.equal({ - updateAuthority: newUpdateAuthority.publicKey, - mint: mint.publicKey, - size: BigInt(0), - maxSize: BigInt(10), - }); - }); -}); diff --git a/token/js/test/e2e-2022/tokenGroupMember.test.ts b/token/js/test/e2e-2022/tokenGroupMember.test.ts deleted file mode 100644 index 07cee386132..00000000000 --- a/token/js/test/e2e-2022/tokenGroupMember.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; - -import { - ExtensionType, - createInitializeMintInstruction, - createInitializeGroupInstruction, - getTokenGroupState, - getMint, - getMintLen, - createInitializeGroupMemberPointerInstruction, - createInitializeGroupPointerInstruction, - getTokenGroupMemberState, - tokenGroupInitializeGroupWithRentTransfer, - tokenGroupMemberInitializeWithRentTransfer, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; - -describe('tokenGroupMember', async () => { - let connection: Connection; - let payer: Signer; - - let groupMint: Keypair; - let groupUpdateAuthority: Keypair; - - let memberMint: Keypair; - let memberMintAuthority: Keypair; - let memberUpdateAuthority: Keypair; - - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - - groupMint = Keypair.generate(); - const groupMintAuthority = Keypair.generate(); - groupUpdateAuthority = Keypair.generate(); - - memberMint = Keypair.generate(); - memberMintAuthority = Keypair.generate(); - memberUpdateAuthority = Keypair.generate(); - - const groupMintLen = getMintLen([ExtensionType.GroupPointer]); - const groupMintLamports = await connection.getMinimumBalanceForRentExemption(groupMintLen); - - const memberMintLen = getMintLen([ExtensionType.GroupMemberPointer]); - const memberMintLamports = await connection.getMinimumBalanceForRentExemption(memberMintLen); - - // Create the group mint and initialize the group. - await sendAndConfirmTransaction( - connection, - new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: groupMint.publicKey, - space: groupMintLen, - lamports: groupMintLamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeGroupPointerInstruction( - groupMint.publicKey, - groupUpdateAuthority.publicKey, - groupMint.publicKey, - TEST_PROGRAM_ID, - ), - createInitializeMintInstruction( - groupMint.publicKey, - TEST_TOKEN_DECIMALS, - groupMintAuthority.publicKey, - null, - TEST_PROGRAM_ID, - ), - ), - [payer, groupMint], - undefined, - ); - await tokenGroupInitializeGroupWithRentTransfer( - connection, - payer, - groupMint.publicKey, - groupMintAuthority.publicKey, - groupUpdateAuthority.publicKey, - BigInt(3), - [payer, groupMintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - - // Create the member mint. - await sendAndConfirmTransaction( - connection, - new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: memberMint.publicKey, - space: memberMintLen, - lamports: memberMintLamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeGroupMemberPointerInstruction( - memberMint.publicKey, - memberUpdateAuthority.publicKey, - memberMint.publicKey, - TEST_PROGRAM_ID, - ), - createInitializeMintInstruction( - memberMint.publicKey, - TEST_TOKEN_DECIMALS, - memberMintAuthority.publicKey, - null, - TEST_PROGRAM_ID, - ), - ), - [payer, memberMint], - undefined, - ); - }); - - it('can initialize a group member', async () => { - const tokenGroupMember = { - mint: memberMint.publicKey, - group: groupMint.publicKey, - memberNumber: BigInt(1), - }; - - await tokenGroupMemberInitializeWithRentTransfer( - connection, - payer, - memberMint.publicKey, - memberMintAuthority.publicKey, - groupMint.publicKey, - groupUpdateAuthority.publicKey, - [memberMintAuthority, groupUpdateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - - const mintInfo = await getMint(connection, memberMint.publicKey, undefined, TEST_PROGRAM_ID); - const member = getTokenGroupMemberState(mintInfo); - expect(member).to.deep.equal(tokenGroupMember); - }); -}); diff --git a/token/js/test/e2e-2022/tokenMetadata.test.ts b/token/js/test/e2e-2022/tokenMetadata.test.ts deleted file mode 100644 index 22ecfdd1783..00000000000 --- a/token/js/test/e2e-2022/tokenMetadata.test.ts +++ /dev/null @@ -1,524 +0,0 @@ -import { expect } from 'chai'; -import { getBase64Encoder } from '@solana/codecs-strings'; -import { createEmitInstruction, pack } from '@solana/spl-token-metadata'; -import { - type Connection, - sendAndConfirmTransaction, - Keypair, - type Signer, - SystemProgram, - Transaction, - VersionedTransaction, - TransactionMessage, -} from '@solana/web3.js'; - -import { - ExtensionType, - createInitializeMetadataPointerInstruction, - createInitializeMintInstruction, - getMintLen, - getTokenMetadata, - tokenMetadataInitialize, - tokenMetadataInitializeWithRentTransfer, - tokenMetadataRemoveKey, - tokenMetadataUpdateAuthority, - tokenMetadataUpdateField, - tokenMetadataUpdateFieldWithRentTransfer, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.MetadataPointer]; - -describe('tokenMetadata', async () => { - let connection: Connection; - let payer: Signer; - let mint: Keypair; - let mintAuthority: Keypair; - let updateAuthority: Keypair; - - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - updateAuthority = Keypair.generate(); - }); - - beforeEach(async () => { - mint = Keypair.generate(); - - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint.publicKey, - space: mintLen, - lamports: lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeMetadataPointerInstruction( - mint.publicKey, - updateAuthority.publicKey, - mint.publicKey, - TEST_PROGRAM_ID, - ), - createInitializeMintInstruction( - mint.publicKey, - TEST_TOKEN_DECIMALS, - mintAuthority.publicKey, - null, - TEST_PROGRAM_ID, - ), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mint], undefined); - }); - - it('can fetch un-initialized token metadata as null', async () => { - expect(await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID)).to.deep.equal(null); - }); - - it('can initialize', async () => { - const tokenMetadata = { - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }; - - // Transfer the required amount for rent exemption - const lamports = await connection.getMinimumBalanceForRentExemption(pack(tokenMetadata).length); - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: mint.publicKey, - lamports, - }), - ); - await sendAndConfirmTransaction(connection, transaction, [payer], undefined); - - await tokenMetadataInitialize( - connection, - payer, - mint.publicKey, - tokenMetadata.updateAuthority, - mintAuthority, - tokenMetadata.name, - tokenMetadata.symbol, - tokenMetadata.uri, - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }); - }); - - it('can initialize with rent transfer', async () => { - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - mintAuthority, - 'name', - 'symbol', - 'uri', - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }); - }); - - it('can update an existing default field', async () => { - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - mintAuthority, - 'name', - 'symbol', - 'uri', - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataUpdateField( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'name', - 'TEST', - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'TEST', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }); - }); - - it('can update an existing default field with rent transfer', async () => { - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - mintAuthority, - 'name', - 'symbol', - 'uri', - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataUpdateFieldWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'name', - 'My Shiny New Token Metadata', - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'My Shiny New Token Metadata', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }); - }); - - it('can create a custom field with rent transfer', async () => { - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - mintAuthority, - 'name', - 'symbol', - 'uri', - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataUpdateFieldWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'myCustomField', - 'CUSTOM', - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [['myCustomField', 'CUSTOM']], - }); - }); - - it('can update a custom field', async () => { - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - mintAuthority, - 'name', - 'symbol', - 'uri', - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataUpdateFieldWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'myCustomField', - 'CUSTOM', - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataUpdateField( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'myCustomField', - 'test', - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [['myCustomField', 'test']], - }); - }); - - it('can update a custom field with rent transfer', async () => { - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - mintAuthority, - 'name', - 'symbol', - 'uri', - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataUpdateFieldWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'myCustomField', - 'CUSTOM', - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataUpdateFieldWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'myCustomField', - 'My Shiny Custom Field', - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [['myCustomField', 'My Shiny Custom Field']], - }); - }); - - it('can remove a custom field', async () => { - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - mintAuthority, - 'name', - 'symbol', - 'uri', - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataUpdateFieldWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'myCustomField', - 'CUSTOM', - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataRemoveKey( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'myCustomField', - true, - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }); - }); - - it('can handle removal of a key that does not exist when idempotent is true', async () => { - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - mintAuthority, - 'name', - 'symbol', - 'uri', - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - await tokenMetadataRemoveKey( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - 'myCustomField', - true, - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }); - }); - - it('can update the authority', async () => { - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - mintAuthority, - 'name', - 'symbol', - 'uri', - [mintAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const newAuthority = Keypair.generate().publicKey; - await tokenMetadataUpdateAuthority( - connection, - payer, - mint.publicKey, - updateAuthority.publicKey, - newAuthority, - [updateAuthority], - undefined, - TEST_PROGRAM_ID, - ); - const meta = await getTokenMetadata(connection, mint.publicKey, undefined, TEST_PROGRAM_ID); - expect(meta).to.deep.equal({ - updateAuthority: newAuthority, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }); - }); - - it('can emit part of a token metadata', async () => { - const tokenMetadata = { - updateAuthority: updateAuthority.publicKey, - mint: mint.publicKey, - name: 'name', - symbol: 'symbol', - uri: 'uri', - additionalMetadata: [], - }; - - await tokenMetadataInitializeWithRentTransfer( - connection, - payer, - mint.publicKey, - tokenMetadata.updateAuthority, - mintAuthority, - tokenMetadata.name, - tokenMetadata.symbol, - tokenMetadata.uri, - undefined, - undefined, - TEST_PROGRAM_ID, - ); - - const payerKey = payer.publicKey; - const recentBlockhash = await connection.getLatestBlockhash().then(res => res.blockhash); - const instructions = [ - createEmitInstruction({ - programId: TEST_PROGRAM_ID, - metadata: mint.publicKey, - start: 0n, - end: 32n, - }), - ]; - const messageV0 = new TransactionMessage({ - payerKey, - recentBlockhash, - instructions, - }).compileToV0Message(); - const tx = new VersionedTransaction(messageV0); - tx.sign([payer]); - - const returnDataBase64 = (await connection - .simulateTransaction(tx) - .then(res => res.value.returnData?.data[0])) as string; - const returnData = getBase64Encoder().encode(returnDataBase64); - - expect(returnData).to.deep.equal(tokenMetadata.updateAuthority.toBuffer()); - }); -}); diff --git a/token/js/test/e2e-2022/transferFee.test.ts b/token/js/test/e2e-2022/transferFee.test.ts deleted file mode 100644 index edea4158385..00000000000 --- a/token/js/test/e2e-2022/transferFee.test.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { Keypair, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js'; - -import { - ExtensionType, - createInitializeMintInstruction, - getTransferFeeAmount, - getTransferFeeConfig, - mintTo, - transferChecked, - createAccount, - getAccount, - getMint, - getMintLen, - setAuthority, - AuthorityType, -} from '../../src'; - -import { - createInitializeTransferFeeConfigInstruction, - harvestWithheldTokensToMint, - transferCheckedWithFee, - withdrawWithheldTokensFromAccounts, - withdrawWithheldTokensFromMint, - setTransferFee, -} from '../../src/extensions/transferFee/index'; - -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; -const TEST_TOKEN_DECIMALS = 2; -const MINT_EXTENSIONS = [ExtensionType.TransferFeeConfig]; -const MINT_AMOUNT = BigInt(1_000_000_000); -const TRANSFER_AMOUNT = BigInt(1_000_000); -const FEE_BASIS_POINTS = 100; -const MAX_FEE = BigInt(100_000); -const FEE = (TRANSFER_AMOUNT * BigInt(FEE_BASIS_POINTS)) / BigInt(10_000); -describe('transferFee', () => { - let connection: Connection; - let payer: Signer; - let owner: Keypair; - let sourceAccount: PublicKey; - let destinationAccount: PublicKey; - let mint: PublicKey; - let mintAuthority: Keypair; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - }); - - async function setupTransferFeeMint( - transferFeeConfigAuthority: PublicKey | null, - withdrawWithheldAuthority: PublicKey | null, - ) { - const mintKeypair = Keypair.generate(); - mint = mintKeypair.publicKey; - mintAuthority = Keypair.generate(); - const mintLen = getMintLen(MINT_EXTENSIONS); - const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen); - const mintTransaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports: mintLamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeTransferFeeConfigInstruction( - mint, - transferFeeConfigAuthority, - withdrawWithheldAuthority, - FEE_BASIS_POINTS, - MAX_FEE, - TEST_PROGRAM_ID, - ), - createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID), - ); - await sendAndConfirmTransaction(connection, mintTransaction, [payer, mintKeypair], undefined); - } - - describe('with authorities set', () => { - let transferFeeConfigAuthority: Keypair; - let withdrawWithheldAuthority: Keypair; - beforeEach(async () => { - transferFeeConfigAuthority = Keypair.generate(); - withdrawWithheldAuthority = Keypair.generate(); - - await setupTransferFeeMint(transferFeeConfigAuthority.publicKey, withdrawWithheldAuthority.publicKey); - - owner = Keypair.generate(); - sourceAccount = await createAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, - undefined, - TEST_PROGRAM_ID, - ); - await mintTo( - connection, - payer, - mint, - sourceAccount, - mintAuthority, - MINT_AMOUNT, - [], - undefined, - TEST_PROGRAM_ID, - ); - - const accountKeypair = Keypair.generate(); - destinationAccount = await createAccount( - connection, - payer, - mint, - owner.publicKey, - accountKeypair, - undefined, - TEST_PROGRAM_ID, - ); - - await transferChecked( - connection, - payer, - sourceAccount, - mint, - destinationAccount, - owner, - TRANSFER_AMOUNT, - TEST_TOKEN_DECIMALS, - [], - undefined, - TEST_PROGRAM_ID, - ); - }); - it('initializes', async () => { - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferFeeConfig = getTransferFeeConfig(mintInfo); - expect(transferFeeConfig).to.not.equal(null); - if (transferFeeConfig !== null) { - expect(transferFeeConfig.transferFeeConfigAuthority).to.eql(transferFeeConfigAuthority.publicKey); - expect(transferFeeConfig.withdrawWithheldAuthority).to.eql(withdrawWithheldAuthority.publicKey); - expect(transferFeeConfig.olderTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); - expect(transferFeeConfig.olderTransferFee.maximumFee).to.eql(MAX_FEE); - expect(transferFeeConfig.newerTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); - expect(transferFeeConfig.newerTransferFee.maximumFee).to.eql(MAX_FEE); - expect(transferFeeConfig.withheldAmount).to.eql(BigInt(0)); - } - - const accountInfo = await getAccount(connection, destinationAccount, undefined, TEST_PROGRAM_ID); - const transferFeeAmount = getTransferFeeAmount(accountInfo); - expect(transferFeeAmount).to.not.equal(null); - if (transferFeeAmount !== null) { - expect(transferFeeAmount.withheldAmount).to.eql(FEE); - } - }); - it('transferCheckedWithFee', async () => { - await transferCheckedWithFee( - connection, - payer, - sourceAccount, - mint, - destinationAccount, - owner, - TRANSFER_AMOUNT, - TEST_TOKEN_DECIMALS, - FEE, - [], - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, destinationAccount, undefined, TEST_PROGRAM_ID); - const transferFeeAmount = getTransferFeeAmount(accountInfo); - expect(transferFeeAmount).to.not.equal(null); - if (transferFeeAmount !== null) { - expect(transferFeeAmount.withheldAmount).to.eql(FEE * BigInt(2)); - } - }); - it('withdrawWithheldTokensFromAccounts', async () => { - await withdrawWithheldTokensFromAccounts( - connection, - payer, - mint, - destinationAccount, - withdrawWithheldAuthority, - [], - [destinationAccount], - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, destinationAccount, undefined, TEST_PROGRAM_ID); - expect(accountInfo.amount).to.eql(TRANSFER_AMOUNT); - const transferFeeAmount = getTransferFeeAmount(accountInfo); - expect(transferFeeAmount).to.not.equal(null); - if (transferFeeAmount !== null) { - expect(transferFeeAmount.withheldAmount).to.eql(BigInt(0)); - } - }); - it('harvestWithheldTokensToMint', async () => { - await harvestWithheldTokensToMint( - connection, - payer, - mint, - [destinationAccount], - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, destinationAccount, undefined, TEST_PROGRAM_ID); - const transferFeeAmount = getTransferFeeAmount(accountInfo); - expect(transferFeeAmount).to.not.equal(null); - if (transferFeeAmount !== null) { - expect(transferFeeAmount.withheldAmount).to.eql(BigInt(0)); - } - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferFeeConfig = getTransferFeeConfig(mintInfo); - expect(transferFeeConfig).to.not.equal(null); - if (transferFeeConfig !== null) { - expect(transferFeeConfig.withheldAmount).to.eql(FEE); - } - }); - it('withdrawWithheldTokensFromMint', async () => { - await harvestWithheldTokensToMint( - connection, - payer, - mint, - [destinationAccount], - undefined, - TEST_PROGRAM_ID, - ); - await withdrawWithheldTokensFromMint( - connection, - payer, - mint, - destinationAccount, - withdrawWithheldAuthority, - [], - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, destinationAccount, undefined, TEST_PROGRAM_ID); - expect(accountInfo.amount).to.eql(TRANSFER_AMOUNT); - const transferFeeAmount = getTransferFeeAmount(accountInfo); - expect(transferFeeAmount).to.not.equal(null); - if (transferFeeAmount !== null) { - expect(transferFeeAmount.withheldAmount).to.eql(BigInt(0)); - } - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferFeeConfig = getTransferFeeConfig(mintInfo); - expect(transferFeeConfig).to.not.equal(null); - if (transferFeeConfig !== null) { - expect(transferFeeConfig.withheldAmount).to.eql(BigInt(0)); - } - }); - it('transferFeeConfigAuthority', async () => { - await setAuthority( - connection, - payer, - mint, - transferFeeConfigAuthority, - AuthorityType.TransferFeeConfig, - null, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferFeeConfig = getTransferFeeConfig(mintInfo); - expect(transferFeeConfig).to.not.equal(null); - if (transferFeeConfig !== null) { - expect(transferFeeConfig.transferFeeConfigAuthority).to.eql(PublicKey.default); - } - }); - it('withdrawWithheldAuthority', async () => { - await setAuthority( - connection, - payer, - mint, - withdrawWithheldAuthority, - AuthorityType.WithheldWithdraw, - null, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferFeeConfig = getTransferFeeConfig(mintInfo); - expect(transferFeeConfig).to.not.equal(null); - if (transferFeeConfig !== null) { - expect(transferFeeConfig.withdrawWithheldAuthority).to.eql(PublicKey.default); - } - }); - it('setTransferFee', async () => { - const UPDATED_FEE_BASIS_POINTS = 150; - const UPDATED_MAX_FEE = BigInt(150_000); - - await setTransferFee( - connection, - payer, - mint, - transferFeeConfigAuthority, - [], - UPDATED_FEE_BASIS_POINTS, - UPDATED_MAX_FEE, - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferFeeConfig = getTransferFeeConfig(mintInfo); - expect(transferFeeConfig).to.not.equal(null); - if (transferFeeConfig !== null) { - expect(transferFeeConfig.transferFeeConfigAuthority).to.eql(transferFeeConfigAuthority.publicKey); - expect(transferFeeConfig.olderTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); - expect(transferFeeConfig.olderTransferFee.maximumFee).to.eql(MAX_FEE); - expect(transferFeeConfig.newerTransferFee.transferFeeBasisPoints).to.eql(UPDATED_FEE_BASIS_POINTS); - expect(transferFeeConfig.newerTransferFee.maximumFee).to.eql(UPDATED_MAX_FEE); - } - }); - }); - - describe('with null authorities', () => { - it('initializes with null transfer fee config authority', async () => { - const withdrawWithheldAuthority = Keypair.generate(); - await setupTransferFeeMint(null, withdrawWithheldAuthority.publicKey); - - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferFeeConfig = getTransferFeeConfig(mintInfo); - expect(transferFeeConfig).to.not.equal(null); - if (transferFeeConfig !== null) { - expect(transferFeeConfig.transferFeeConfigAuthority).to.eql(PublicKey.default); - expect(transferFeeConfig.withdrawWithheldAuthority).to.eql(withdrawWithheldAuthority.publicKey); - expect(transferFeeConfig.olderTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); - expect(transferFeeConfig.olderTransferFee.maximumFee).to.eql(MAX_FEE); - expect(transferFeeConfig.newerTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); - expect(transferFeeConfig.newerTransferFee.maximumFee).to.eql(MAX_FEE); - expect(transferFeeConfig.withheldAmount).to.eql(BigInt(0)); - } - }); - it('initializes with null withdraw withheld authority', async () => { - const transferFeeConfigAuthority = Keypair.generate(); - await setupTransferFeeMint(transferFeeConfigAuthority.publicKey, null); - - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferFeeConfig = getTransferFeeConfig(mintInfo); - expect(transferFeeConfig).to.not.equal(null); - if (transferFeeConfig !== null) { - expect(transferFeeConfig.transferFeeConfigAuthority).to.eql(transferFeeConfigAuthority.publicKey); - expect(transferFeeConfig.withdrawWithheldAuthority).to.eql(PublicKey.default); - expect(transferFeeConfig.olderTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); - expect(transferFeeConfig.olderTransferFee.maximumFee).to.eql(MAX_FEE); - expect(transferFeeConfig.newerTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); - expect(transferFeeConfig.newerTransferFee.maximumFee).to.eql(MAX_FEE); - expect(transferFeeConfig.withheldAmount).to.eql(BigInt(0)); - } - }); - it('initializes with both authorities null', async () => { - await setupTransferFeeMint(null, null); - - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferFeeConfig = getTransferFeeConfig(mintInfo); - expect(transferFeeConfig).to.not.equal(null); - if (transferFeeConfig !== null) { - expect(transferFeeConfig.transferFeeConfigAuthority).to.eql(PublicKey.default); - expect(transferFeeConfig.withdrawWithheldAuthority).to.eql(PublicKey.default); - expect(transferFeeConfig.olderTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); - expect(transferFeeConfig.olderTransferFee.maximumFee).to.eql(MAX_FEE); - expect(transferFeeConfig.newerTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); - expect(transferFeeConfig.newerTransferFee.maximumFee).to.eql(MAX_FEE); - expect(transferFeeConfig.withheldAmount).to.eql(BigInt(0)); - } - }); - }); -}); diff --git a/token/js/test/e2e-2022/transferHook.test.ts b/token/js/test/e2e-2022/transferHook.test.ts deleted file mode 100644 index 12256286163..00000000000 --- a/token/js/test/e2e-2022/transferHook.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { expect } from 'chai'; -import type { AccountMeta, Connection, Signer } from '@solana/web3.js'; -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; -import { - createInitializeMintInstruction, - getMint, - getMintLen, - ExtensionType, - createInitializeTransferHookInstruction, - getTransferHook, - updateTransferHook, - AuthorityType, - setAuthority, - createAssociatedTokenAccountInstruction, - getAssociatedTokenAddressSync, - ASSOCIATED_TOKEN_PROGRAM_ID, - createMintToCheckedInstruction, - getExtraAccountMetaAddress, - ExtraAccountMetaListLayout, - ExtraAccountMetaLayout, - transferCheckedWithTransferHook, - createAssociatedTokenAccountIdempotent, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection, TRANSFER_HOOK_TEST_PROGRAM_ID } from '../common'; -import { createHash } from 'crypto'; - -const TEST_TOKEN_DECIMALS = 2; -const EXTENSIONS = [ExtensionType.TransferHook]; -describe('transferHook', () => { - let connection: Connection; - let payer: Signer; - let payerAta: PublicKey; - let destinationAuthority: PublicKey; - let destinationAta: PublicKey; - let transferHookAuthority: Keypair; - let pdaExtraAccountMeta: PublicKey; - let mint: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - destinationAuthority = Keypair.generate().publicKey; - transferHookAuthority = Keypair.generate(); - }); - beforeEach(async () => { - const mintKeypair = Keypair.generate(); - mint = mintKeypair.publicKey; - pdaExtraAccountMeta = getExtraAccountMetaAddress(mint, TRANSFER_HOOK_TEST_PROGRAM_ID); - payerAta = getAssociatedTokenAddressSync( - mint, - payer.publicKey, - false, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - destinationAta = getAssociatedTokenAddressSync( - mint, - destinationAuthority, - false, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - const mintLen = getMintLen(EXTENSIONS); - const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint, - space: mintLen, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeTransferHookInstruction( - mint, - transferHookAuthority.publicKey, - TRANSFER_HOOK_TEST_PROGRAM_ID, - TEST_PROGRAM_ID, - ), - createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, payer.publicKey, null, TEST_PROGRAM_ID), - ); - - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair], undefined); - }); - it('is initialized', async () => { - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferHook = getTransferHook(mintInfo); - expect(transferHook).to.not.equal(null); - if (transferHook !== null) { - expect(transferHook.authority).to.eql(transferHookAuthority.publicKey); - expect(transferHook.programId).to.eql(TRANSFER_HOOK_TEST_PROGRAM_ID); - } - }); - it('can be updated', async () => { - const newTransferHookProgramId = Keypair.generate().publicKey; - await updateTransferHook( - connection, - payer, - mint, - newTransferHookProgramId, - transferHookAuthority, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferHook = getTransferHook(mintInfo); - expect(transferHook).to.not.equal(null); - if (transferHook !== null) { - expect(transferHook.authority).to.eql(transferHookAuthority.publicKey); - expect(transferHook.programId).to.eql(newTransferHookProgramId); - } - }); - it('authority', async () => { - await setAuthority( - connection, - payer, - mint, - transferHookAuthority, - AuthorityType.TransferHookProgramId, - null, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - const transferHook = getTransferHook(mintInfo); - expect(transferHook).to.not.equal(null); - if (transferHook !== null) { - expect(transferHook.authority).to.eql(PublicKey.default); - } - }); - it('transferChecked', async () => { - const extraAccount = Keypair.generate().publicKey; - const keys: AccountMeta[] = [ - { pubkey: pdaExtraAccountMeta, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: payer.publicKey, isSigner: true, isWritable: false }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - ]; - - const data = Buffer.alloc(8 + 4 + ExtraAccountMetaLayout.span); - const discriminator = createHash('sha256') - .update('spl-transfer-hook-interface:initialize-extra-account-metas') - .digest() - .subarray(0, 8); - discriminator.copy(data); - ExtraAccountMetaListLayout.encode( - { - count: 1, - extraAccounts: [ - { - discriminator: 0, - addressConfig: extraAccount.toBuffer(), - isSigner: false, - isWritable: false, - }, - ], - }, - data, - 8, - ); - - const initExtraAccountMetaInstruction = new TransactionInstruction({ - keys, - data, - programId: TRANSFER_HOOK_TEST_PROGRAM_ID, - }); - - const setupTransaction = new Transaction().add( - initExtraAccountMetaInstruction, - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: pdaExtraAccountMeta, - lamports: 10000000, - }), - createAssociatedTokenAccountInstruction( - payer.publicKey, - payerAta, - payer.publicKey, - mint, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ), - createMintToCheckedInstruction( - mint, - payerAta, - payer.publicKey, - 5 * 10 ** TEST_TOKEN_DECIMALS, - TEST_TOKEN_DECIMALS, - [], - TEST_PROGRAM_ID, - ), - ); - - await sendAndConfirmTransaction(connection, setupTransaction, [payer]); - - await createAssociatedTokenAccountIdempotent( - connection, - payer, - mint, - destinationAuthority, - undefined, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - - await transferCheckedWithTransferHook( - connection, - payer, - payerAta, - mint, - destinationAta, - payer, - BigInt(10 ** TEST_TOKEN_DECIMALS), - TEST_TOKEN_DECIMALS, - [], - undefined, - TEST_PROGRAM_ID, - ); - }); -}); diff --git a/token/js/test/e2e/amount.test.ts b/token/js/test/e2e/amount.test.ts deleted file mode 100644 index 0db64133f37..00000000000 --- a/token/js/test/e2e/amount.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; -import { createMint, amountToUiAmount, uiAmountToAmount } from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -describe('Amount', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - }); - it('amountToUiAmount', async () => { - const amount = BigInt(5245); - const uiAmount = await amountToUiAmount(connection, payer, mint, amount, TEST_PROGRAM_ID); - expect(uiAmount).to.eql('52.45'); - }); - it('uiAmountToAmount', async () => { - const uiAmount = await uiAmountToAmount(connection, payer, mint, '52.45', TEST_PROGRAM_ID); - expect(uiAmount).to.eql(BigInt(5245)); - }); -}); diff --git a/token/js/test/e2e/burn.test.ts b/token/js/test/e2e/burn.test.ts deleted file mode 100644 index fd04c3a1bf5..00000000000 --- a/token/js/test/e2e/burn.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; -import { createMint, createAccount, getAccount, mintTo, burn, burnChecked } from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -describe('burn', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let owner: Keypair; - let account: PublicKey; - let amount: bigint; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - }); - beforeEach(async () => { - owner = Keypair.generate(); - account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID); - amount = BigInt(1000); - await mintTo(connection, payer, mint, account, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID); - }); - it('burn', async () => { - const burnAmount = BigInt(1); - await burn(connection, payer, account, mint, owner, burnAmount, [], undefined, TEST_PROGRAM_ID); - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(accountInfo.amount).to.eql(amount - burnAmount); - }); - it('burnChecked', async () => { - const burnAmount = BigInt(1); - await burnChecked( - connection, - payer, - account, - mint, - owner, - burnAmount, - TEST_TOKEN_DECIMALS, - [], - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(accountInfo.amount).to.eql(amount - burnAmount); - }); -}); diff --git a/token/js/test/e2e/close.test.ts b/token/js/test/e2e/close.test.ts deleted file mode 100644 index deb2dde7667..00000000000 --- a/token/js/test/e2e/close.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { expect, use } from 'chai'; -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; -import { createMint, createAccount, closeAccount, mintTo } from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -const TEST_TOKEN_DECIMALS = 2; -describe('close', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let freezeAuthority: Keypair; - let owner: Keypair; - let account: PublicKey; - let destination: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - freezeAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - freezeAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - }); - beforeEach(async () => { - owner = Keypair.generate(); - destination = Keypair.generate().publicKey; - account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID); - }); - it('failsWithNonZeroAmount', async () => { - const amount = BigInt(1000); - await mintTo(connection, payer, mint, account, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID); - expect( - closeAccount(connection, payer, account, destination, owner, [], undefined, TEST_PROGRAM_ID), - ).to.be.rejectedWith(Error); - }); - it('works', async () => { - const accountInfo = await connection.getAccountInfo(account); - let tokenRentExemptAmount; - expect(accountInfo).to.not.equal(null); - if (accountInfo !== null) { - tokenRentExemptAmount = accountInfo.lamports; - } - - await closeAccount(connection, payer, account, destination, owner, [], undefined, TEST_PROGRAM_ID); - - const closedInfo = await connection.getAccountInfo(account); - expect(closedInfo).to.equal(null); - - const destinationInfo = await connection.getAccountInfo(destination); - expect(destinationInfo).to.not.equal(null); - if (destinationInfo !== null) { - expect(destinationInfo.lamports).to.eql(tokenRentExemptAmount); - } - }); -}); diff --git a/token/js/test/e2e/create.test.ts b/token/js/test/e2e/create.test.ts deleted file mode 100644 index 570f38a51f3..00000000000 --- a/token/js/test/e2e/create.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; - -import { - ASSOCIATED_TOKEN_PROGRAM_ID, - createMint, - getMint, - createAccount, - createAssociatedTokenAccountIdempotent, - getAccount, - getAssociatedTokenAddress, - getOrCreateAssociatedTokenAccount, -} from '../../src'; - -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -const TEST_TOKEN_DECIMALS = 2; -describe('createMint', () => { - it('works', async () => { - const connection = await getConnection(); - const payer = await newAccountWithLamports(connection, 1000000000); - const testMintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - const mint = await createMint( - connection, - payer, - testMintAuthority.publicKey, - testMintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - - expect(mintInfo.mintAuthority).to.eql(testMintAuthority.publicKey); - expect(mintInfo.supply).to.eql(BigInt(0)); - expect(mintInfo.decimals).to.eql(TEST_TOKEN_DECIMALS); - expect(mintInfo.isInitialized).to.equal(true); - expect(mintInfo.freezeAuthority).to.eql(testMintAuthority.publicKey); - }); -}); - -describe('createAccount', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - const mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - }); - it('auxiliary token account', async () => { - const owner = Keypair.generate(); - const account = await createAccount( - connection, - payer, - mint, - owner.publicKey, - Keypair.generate(), - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(accountInfo.mint).to.eql(mint); - expect(accountInfo.owner).to.eql(owner.publicKey); - expect(accountInfo.amount).to.eql(BigInt(0)); - expect(accountInfo.delegate).to.equal(null); - expect(accountInfo.delegatedAmount).to.eql(BigInt(0)); - expect(accountInfo.isInitialized).to.equal(true); - expect(accountInfo.isFrozen).to.equal(false); - expect(accountInfo.isNative).to.equal(false); - expect(accountInfo.rentExemptReserve).to.equal(null); - expect(accountInfo.closeAuthority).to.equal(null); - - // you can create as many accounts as with same owner - const account2 = await createAccount( - connection, - payer, - mint, - owner.publicKey, - Keypair.generate(), - undefined, - TEST_PROGRAM_ID, - ); - expect(account2).to.not.eql(account); - }); - it('creates associated token account if it does not exist', async () => { - const owner = Keypair.generate(); - const associatedAddress = await getAssociatedTokenAddress( - mint, - owner.publicKey, - false, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - - // associated account shouldn't exist - const info = await connection.getAccountInfo(associatedAddress); - expect(info).to.equal(null); - - const createdAccountInfo = await getOrCreateAssociatedTokenAccount( - connection, - payer, - mint, - owner.publicKey, - false, - undefined, - undefined, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - expect(createdAccountInfo.mint).to.eql(mint); - expect(createdAccountInfo.owner).to.eql(owner.publicKey); - expect(createdAccountInfo.amount).to.eql(BigInt(0)); - expect(createdAccountInfo.delegate).to.equal(null); - expect(createdAccountInfo.delegatedAmount).to.eql(BigInt(0)); - expect(createdAccountInfo.isInitialized).to.equal(true); - expect(createdAccountInfo.isFrozen).to.equal(false); - expect(createdAccountInfo.isNative).to.equal(false); - expect(createdAccountInfo.rentExemptReserve).to.equal(null); - expect(createdAccountInfo.closeAuthority).to.equal(null); - - // do it again, just gives the account info - const accountInfo = await getOrCreateAssociatedTokenAccount( - connection, - payer, - mint, - owner.publicKey, - false, - undefined, - undefined, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - - expect(createdAccountInfo).to.eql(accountInfo); - }); - it('associated token account', async () => { - const owner = Keypair.generate(); - const associatedAddress = await getAssociatedTokenAddress( - mint, - owner.publicKey, - false, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - - // associated account shouldn't exist - const info = await connection.getAccountInfo(associatedAddress); - expect(info).to.equal(null); - - const createdAddress = await createAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, // uses ATA by default - undefined, - TEST_PROGRAM_ID, - ); - expect(createdAddress).to.eql(associatedAddress); - - const accountInfo = await getAccount(connection, associatedAddress, undefined, TEST_PROGRAM_ID); - expect(accountInfo).to.not.equal(null); - expect(accountInfo.mint).to.eql(mint); - expect(accountInfo.owner).to.eql(owner.publicKey); - expect(accountInfo.amount).to.eql(BigInt(0)); - - // creating again should cause TX error for the associated token account - expect( - createAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, // uses ATA by default - undefined, - TEST_PROGRAM_ID, - ), - ).to.be.rejectedWith(Error); - - // when creating again but with idempotent mode, TX should not throw error - return expect( - createAssociatedTokenAccountIdempotent( - connection, - payer, - mint, - owner.publicKey, - undefined, - TEST_PROGRAM_ID, - ), - ).to.be.fulfilled; - }); -}); diff --git a/token/js/test/e2e/freeze.test.ts b/token/js/test/e2e/freeze.test.ts deleted file mode 100644 index b013edf1642..00000000000 --- a/token/js/test/e2e/freeze.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; - -import { burn, createMint, createAccount, getAccount, freezeAccount, thawAccount, mintTo } from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -const TEST_TOKEN_DECIMALS = 2; -describe('freezeThaw', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let freezeAuthority: Keypair; - let owner: Keypair; - let account: PublicKey; - let amount: bigint; - const burnAmount = BigInt(1); - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - freezeAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - freezeAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - }); - beforeEach(async () => { - owner = Keypair.generate(); - account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID); - amount = BigInt(1000); - await mintTo(connection, payer, mint, account, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID); - }); - it('freezes', async () => { - await freezeAccount(connection, payer, account, mint, freezeAuthority, [], undefined, TEST_PROGRAM_ID); - - expect( - burn(connection, payer, account, mint, owner, burnAmount, [], undefined, TEST_PROGRAM_ID), - ).to.be.rejectedWith(Error); - }); - it('thaws', async () => { - await freezeAccount(connection, payer, account, mint, freezeAuthority, [], undefined, TEST_PROGRAM_ID); - - await thawAccount(connection, payer, account, mint, freezeAuthority, [], undefined, TEST_PROGRAM_ID); - - await burn(connection, payer, account, mint, owner, burnAmount, [], undefined, TEST_PROGRAM_ID); - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - - expect(accountInfo.amount).to.eql(amount - burnAmount); - }); -}); diff --git a/token/js/test/e2e/initialize.test.ts b/token/js/test/e2e/initialize.test.ts deleted file mode 100644 index ae635120adf..00000000000 --- a/token/js/test/e2e/initialize.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { Connection, Signer } from '@solana/web3.js'; -import { Transaction, SystemProgram, Keypair, sendAndConfirmTransaction } from '@solana/web3.js'; -import { expect } from 'chai'; -import { - getMinimumBalanceForRentExemptMint, - MINT_SIZE, - createInitializeMint2Instruction, - getMint, - createInitializeMintInstruction, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; - -describe('initialize mint', () => { - let connection: Connection; - let payer: Signer; - let mintKeypair: Keypair; - let lamports: number; - beforeEach(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintKeypair = Keypair.generate(); - lamports = await getMinimumBalanceForRentExemptMint(connection); - }); - it('works', async () => { - const mintAuthority = Keypair.generate().publicKey; - const freezeAuthority = Keypair.generate().publicKey; - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mintKeypair.publicKey, - space: MINT_SIZE, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeMintInstruction( - mintKeypair.publicKey, - TEST_TOKEN_DECIMALS, - mintAuthority, - freezeAuthority, - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair]); - const mintInfo = await getMint(connection, mintKeypair.publicKey, undefined, TEST_PROGRAM_ID); - expect(mintInfo.mintAuthority).to.eql(mintAuthority); - expect(mintInfo.supply).to.eql(BigInt(0)); - expect(mintInfo.decimals).to.eql(TEST_TOKEN_DECIMALS); - expect(mintInfo.isInitialized).to.equal(true); - expect(mintInfo.freezeAuthority).to.eql(freezeAuthority); - }); - it('works with null freeze authority', async () => { - const mintAuthority = Keypair.generate().publicKey; - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mintKeypair.publicKey, - space: MINT_SIZE, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeMintInstruction( - mintKeypair.publicKey, - TEST_TOKEN_DECIMALS, - mintAuthority, - null, - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair]); - const mintInfo = await getMint(connection, mintKeypair.publicKey, undefined, TEST_PROGRAM_ID); - expect(mintInfo.mintAuthority).to.eql(mintAuthority); - expect(mintInfo.supply).to.eql(BigInt(0)); - expect(mintInfo.decimals).to.eql(TEST_TOKEN_DECIMALS); - expect(mintInfo.isInitialized).to.equal(true); - expect(mintInfo.freezeAuthority).to.equal(null); - }); -}); -describe('initialize mint 2', () => { - let connection: Connection; - let payer: Signer; - let mintKeypair: Keypair; - let lamports: number; - beforeEach(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintKeypair = Keypair.generate(); - lamports = await getMinimumBalanceForRentExemptMint(connection); - }); - it('works', async () => { - const mintAuthority = Keypair.generate().publicKey; - const freezeAuthority = Keypair.generate().publicKey; - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mintKeypair.publicKey, - space: MINT_SIZE, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeMint2Instruction( - mintKeypair.publicKey, - TEST_TOKEN_DECIMALS, - mintAuthority, - freezeAuthority, - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair]); - const mintInfo = await getMint(connection, mintKeypair.publicKey, undefined, TEST_PROGRAM_ID); - expect(mintInfo.mintAuthority).to.eql(mintAuthority); - expect(mintInfo.supply).to.eql(BigInt(0)); - expect(mintInfo.decimals).to.eql(TEST_TOKEN_DECIMALS); - expect(mintInfo.isInitialized).to.equal(true); - expect(mintInfo.freezeAuthority).to.eql(freezeAuthority); - }); - it('works with null freeze authority', async () => { - const mintAuthority = Keypair.generate().publicKey; - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mintKeypair.publicKey, - space: MINT_SIZE, - lamports, - programId: TEST_PROGRAM_ID, - }), - createInitializeMint2Instruction( - mintKeypair.publicKey, - TEST_TOKEN_DECIMALS, - mintAuthority, - null, - TEST_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair]); - const mintInfo = await getMint(connection, mintKeypair.publicKey, undefined, TEST_PROGRAM_ID); - expect(mintInfo.mintAuthority).to.eql(mintAuthority); - expect(mintInfo.supply).to.eql(BigInt(0)); - expect(mintInfo.decimals).to.eql(TEST_TOKEN_DECIMALS); - expect(mintInfo.isInitialized).to.equal(true); - expect(mintInfo.freezeAuthority).to.equal(null); - }); -}); diff --git a/token/js/test/e2e/mint.test.ts b/token/js/test/e2e/mint.test.ts deleted file mode 100644 index 2dfe0cb0623..00000000000 --- a/token/js/test/e2e/mint.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; - -import { createMint, getMint, createAccount, getAccount, mintTo, mintToChecked } from '../../src'; - -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -const TEST_TOKEN_DECIMALS = 2; -describe('mint', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let owner: Keypair; - let account: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - owner = Keypair.generate(); - account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID); - }); - it('mintTo', async () => { - const amount = BigInt(1000); - await mintTo(connection, payer, mint, account, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID); - - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - expect(mintInfo.supply).to.eql(amount); - - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(accountInfo.amount).to.eql(amount); - }); - it('mintToChecked', async () => { - const amount = BigInt(1000); - await mintToChecked( - connection, - payer, - mint, - account, - mintAuthority, - amount, - TEST_TOKEN_DECIMALS, - [], - undefined, - TEST_PROGRAM_ID, - ); - - expect( - mintToChecked(connection, payer, mint, account, mintAuthority, amount, 1, [], undefined, TEST_PROGRAM_ID), - ).to.be.rejectedWith(Error); - }); -}); diff --git a/token/js/test/e2e/multisig.test.ts b/token/js/test/e2e/multisig.test.ts deleted file mode 100644 index c5941901344..00000000000 --- a/token/js/test/e2e/multisig.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; - -import { - AuthorityType, - createMint, - createAccount, - getAccount, - mintTo, - transfer, - approve, - getMultisig, - createMultisig, - setAuthority, -} from '../../src'; - -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -const TEST_TOKEN_DECIMALS = 2; -const M = 2; -const N = 5; -describe('multisig', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let account1: PublicKey; - let account2: PublicKey; - let amount: bigint; - let multisig: PublicKey; - let signers: Keypair[]; - let signerPublicKeys: PublicKey[]; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - signers = []; - signerPublicKeys = []; - for (let i = 0; i < N; ++i) { - const signer = Keypair.generate(); - signers.push(signer); - signerPublicKeys.push(signer.publicKey); - } - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - }); - beforeEach(async () => { - multisig = await createMultisig(connection, payer, signerPublicKeys, M, undefined, undefined, TEST_PROGRAM_ID); - account1 = await createAccount( - connection, - payer, - mint, - multisig, - Keypair.generate(), - undefined, - TEST_PROGRAM_ID, - ); - account2 = await createAccount( - connection, - payer, - mint, - multisig, - Keypair.generate(), - undefined, - TEST_PROGRAM_ID, - ); - amount = BigInt(1000); - await mintTo(connection, payer, mint, account1, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID); - }); - it('create', async () => { - const multisigInfo = await getMultisig(connection, multisig, undefined, TEST_PROGRAM_ID); - expect(multisigInfo.m).to.eql(M); - expect(multisigInfo.n).to.eql(N); - expect(multisigInfo.signer1).to.eql(signerPublicKeys[0]); - expect(multisigInfo.signer2).to.eql(signerPublicKeys[1]); - expect(multisigInfo.signer3).to.eql(signerPublicKeys[2]); - expect(multisigInfo.signer4).to.eql(signerPublicKeys[3]); - expect(multisigInfo.signer5).to.eql(signerPublicKeys[4]); - }); - it('transfer', async () => { - await transfer(connection, payer, account1, account2, multisig, amount, signers, undefined, TEST_PROGRAM_ID); - const accountInfo = await getAccount(connection, account2, undefined, TEST_PROGRAM_ID); - expect(accountInfo.amount).to.eql(amount); - }); - it('approve', async () => { - const delegate = Keypair.generate().publicKey; - await approve(connection, payer, account1, delegate, multisig, amount, signers, undefined, TEST_PROGRAM_ID); - const approvedAccountInfo = await getAccount(connection, account1, undefined, TEST_PROGRAM_ID); - expect(approvedAccountInfo.delegatedAmount).to.eql(amount); - expect(approvedAccountInfo.delegate).to.eql(delegate); - }); - it('setAuthority', async () => { - const newOwner = Keypair.generate().publicKey; - await setAuthority( - connection, - payer, - account1, - multisig, - AuthorityType.AccountOwner, - newOwner, - signers, - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, account1, undefined, TEST_PROGRAM_ID); - expect(accountInfo.owner).to.eql(newOwner); - }); -}); diff --git a/token/js/test/e2e/native.test.ts b/token/js/test/e2e/native.test.ts deleted file mode 100644 index 6eb32350974..00000000000 --- a/token/js/test/e2e/native.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { expect } from 'chai'; -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair, Transaction, SystemProgram, sendAndConfirmTransaction } from '@solana/web3.js'; -import { - NATIVE_MINT, - NATIVE_MINT_2022, - TOKEN_PROGRAM_ID, - closeAccount, - getAccount, - createNativeMint, - createWrappedNativeAccount, - syncNative, -} from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -describe('native', () => { - let connection: Connection; - let payer: Signer; - let owner: Keypair; - let account: PublicKey; - let amount: number; - let nativeMint: PublicKey; - before(async () => { - amount = 1_000_000_000; - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 100_000_000_000); - if (TEST_PROGRAM_ID == TOKEN_PROGRAM_ID) { - nativeMint = NATIVE_MINT; - } else { - nativeMint = NATIVE_MINT_2022; - await createNativeMint(connection, payer, undefined, nativeMint, TEST_PROGRAM_ID); - } - }); - beforeEach(async () => { - owner = Keypair.generate(); - account = await createWrappedNativeAccount( - connection, - payer, - owner.publicKey, - amount, - undefined, - undefined, - TEST_PROGRAM_ID, - nativeMint, - ); - }); - it('works', async () => { - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(accountInfo.isNative).to.equal(true); - expect(accountInfo.amount).to.eql(BigInt(amount)); - }); - it('syncNative', async () => { - let balance = 0; - const preInfo = await connection.getAccountInfo(account); - expect(preInfo).to.not.equal(null); - if (preInfo != null) { - balance = preInfo.lamports; - } - - // transfer lamports into the native account - const additionalLamports = 100; - await sendAndConfirmTransaction( - connection, - new Transaction().add( - SystemProgram.transfer({ - fromPubkey: payer.publicKey, - toPubkey: account, - lamports: additionalLamports, - }), - ), - [payer], - ); - - // no change in the amount - const preAccountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(preAccountInfo.isNative).to.equal(true); - expect(preAccountInfo.amount).to.eql(BigInt(amount)); - - // but change in lamports - const postInfo = await connection.getAccountInfo(account); - expect(postInfo).to.not.equal(null); - if (postInfo !== null) { - expect(postInfo.lamports).to.eql(balance + additionalLamports); - } - - // sync, amount changes - await syncNative(connection, payer, account, undefined, TEST_PROGRAM_ID); - const postAccountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(postAccountInfo.isNative).to.equal(true); - expect(postAccountInfo.amount).to.eql(BigInt(amount + additionalLamports)); - }); - it('closeAccount', async () => { - let balance = 0; - const preInfo = await connection.getAccountInfo(account); - expect(preInfo).to.not.equal(null); - if (preInfo != null) { - balance = preInfo.lamports; - } - const destination = Keypair.generate().publicKey; - await closeAccount(connection, payer, account, destination, owner, [], undefined, TEST_PROGRAM_ID); - const nullInfo = await connection.getAccountInfo(account); - expect(nullInfo).to.equal(null); - const destinationInfo = await connection.getAccountInfo(destination); - expect(destinationInfo).to.not.equal(null); - if (destinationInfo != null) { - expect(destinationInfo.lamports).to.eql(balance); - } - }); -}); diff --git a/token/js/test/e2e/recoverNested.test.ts b/token/js/test/e2e/recoverNested.test.ts deleted file mode 100644 index fc11e8dcc99..00000000000 --- a/token/js/test/e2e/recoverNested.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair, Transaction, sendAndConfirmTransaction } from '@solana/web3.js'; -import { expect } from 'chai'; - -import { - ASSOCIATED_TOKEN_PROGRAM_ID, - createMint, - getAccount, - createAssociatedTokenAccount, - createAssociatedTokenAccountInstruction, - getAssociatedTokenAddressSync, - mintTo, - recoverNested, -} from '../../src'; - -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; - -describe('recoverNested', () => { - let connection: Connection; - let payer: Signer; - let owner: Signer; - let mint: PublicKey; - let associatedToken: PublicKey; - let nestedMint: PublicKey; - const nestedMintAmount = 1; - let nestedAssociatedToken: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 10000000000); - owner = Keypair.generate(); - - // mint - const mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - 0, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - - associatedToken = await createAssociatedTokenAccount( - connection, - payer, - mint, - owner.publicKey, - undefined, - TEST_PROGRAM_ID, - ); - - // nested mint - const nestedMintAuthority = Keypair.generate(); - const nestedMintKeypair = Keypair.generate(); - nestedMint = await createMint( - connection, - payer, - nestedMintAuthority.publicKey, - nestedMintAuthority.publicKey, - 0, - nestedMintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - - nestedAssociatedToken = getAssociatedTokenAddressSync( - nestedMint, - associatedToken, - true, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ); - const transaction = new Transaction().add( - createAssociatedTokenAccountInstruction( - payer.publicKey, - nestedAssociatedToken, - associatedToken, - nestedMint, - TEST_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, - ), - ); - await sendAndConfirmTransaction(connection, transaction, [payer], undefined); - - // use mintTo to make nestedAssociatedToken have some tokens - await mintTo( - connection, - payer, - nestedMint, - nestedAssociatedToken, - nestedMintAuthority, - nestedMintAmount, - undefined, - undefined, - TEST_PROGRAM_ID, - ); - }); - it('success', async () => { - // create destination associated token - const destinationAssociatedToken = await createAssociatedTokenAccount( - connection, - payer, - nestedMint, - owner.publicKey, - undefined, - TEST_PROGRAM_ID, - ); - - await recoverNested(connection, payer, owner, mint, nestedMint, undefined, TEST_PROGRAM_ID); - - expect(await connection.getAccountInfo(nestedAssociatedToken)).to.equal(null); - - const accountInfo = await getAccount(connection, destinationAssociatedToken, undefined, TEST_PROGRAM_ID); - expect(accountInfo).to.not.equal(null); - expect(accountInfo.mint).to.eql(nestedMint); - expect(accountInfo.owner).to.eql(owner.publicKey); - expect(accountInfo.amount).to.eql(BigInt(nestedMintAmount)); - }); -}); diff --git a/token/js/test/e2e/setAuthority.test.ts b/token/js/test/e2e/setAuthority.test.ts deleted file mode 100644 index cc17bcfd9e7..00000000000 --- a/token/js/test/e2e/setAuthority.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; - -import { AuthorityType, createMint, createAccount, getAccount, getMint, setAuthority } from '../../src'; - -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -const TEST_TOKEN_DECIMALS = 2; -describe('setAuthority', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let freezeAuthority: Keypair; - let owner: Keypair; - let account: PublicKey; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - freezeAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - freezeAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - }); - beforeEach(async () => { - owner = Keypair.generate(); - account = await createAccount( - connection, - payer, - mint, - owner.publicKey, - Keypair.generate(), - undefined, - TEST_PROGRAM_ID, - ); - }); - it('AccountOwner', async () => { - const newOwner = Keypair.generate(); - await setAuthority( - connection, - payer, - account, - owner, - AuthorityType.AccountOwner, - newOwner.publicKey, - [], - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(accountInfo.owner).to.eql(newOwner.publicKey); - await setAuthority( - connection, - payer, - account, - newOwner, - AuthorityType.AccountOwner, - owner.publicKey, - [], - undefined, - TEST_PROGRAM_ID, - ); - expect( - setAuthority( - connection, - payer, - account, - newOwner, - AuthorityType.AccountOwner, - owner.publicKey, - [], - undefined, - TEST_PROGRAM_ID, - ), - ).to.be.rejectedWith(Error); - }); - it('MintAuthority', async () => { - await setAuthority( - connection, - payer, - mint, - mintAuthority, - AuthorityType.MintTokens, - null, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - expect(mintInfo.mintAuthority).to.equal(null); - }); - it('CloseAuthority', async () => { - const closeAuthority = Keypair.generate(); - await setAuthority( - connection, - payer, - account, - owner, - AuthorityType.CloseAccount, - closeAuthority.publicKey, - [], - undefined, - TEST_PROGRAM_ID, - ); - const accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID); - expect(accountInfo.closeAuthority).to.eql(closeAuthority.publicKey); - }); - it('FreezeAuthority', async () => { - await setAuthority( - connection, - payer, - mint, - freezeAuthority, - AuthorityType.FreezeAccount, - null, - [], - undefined, - TEST_PROGRAM_ID, - ); - const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); - expect(mintInfo.freezeAuthority).to.equal(null); - }); -}); diff --git a/token/js/test/e2e/transfer.test.ts b/token/js/test/e2e/transfer.test.ts deleted file mode 100644 index 019b0ff9fd8..00000000000 --- a/token/js/test/e2e/transfer.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { Connection, PublicKey, Signer } from '@solana/web3.js'; -import { Keypair } from '@solana/web3.js'; - -import { - createMint, - createAccount, - getAccount, - mintTo, - transfer, - transferChecked, - approve, - approveChecked, - revoke, -} from '../../src'; - -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -const TEST_TOKEN_DECIMALS = 2; -describe('transfer', () => { - let connection: Connection; - let payer: Signer; - let mint: PublicKey; - let mintAuthority: Keypair; - let owner1: Keypair; - let account1: PublicKey; - let owner2: Keypair; - let account2: PublicKey; - let amount: bigint; - before(async () => { - connection = await getConnection(); - payer = await newAccountWithLamports(connection, 1000000000); - mintAuthority = Keypair.generate(); - const mintKeypair = Keypair.generate(); - mint = await createMint( - connection, - payer, - mintAuthority.publicKey, - mintAuthority.publicKey, - TEST_TOKEN_DECIMALS, - mintKeypair, - undefined, - TEST_PROGRAM_ID, - ); - }); - beforeEach(async () => { - owner1 = Keypair.generate(); - account1 = await createAccount( - connection, - payer, - mint, - owner1.publicKey, - undefined, - undefined, - TEST_PROGRAM_ID, - ); - owner2 = Keypair.generate(); - account2 = await createAccount( - connection, - payer, - mint, - owner2.publicKey, - undefined, - undefined, - TEST_PROGRAM_ID, - ); - amount = BigInt(1000); - await mintTo(connection, payer, mint, account1, mintAuthority, amount, [], undefined, TEST_PROGRAM_ID); - }); - it('transfer', async () => { - await transfer(connection, payer, account1, account2, owner1, amount, [], undefined, TEST_PROGRAM_ID); - - const destAccountInfo = await getAccount(connection, account2, undefined, TEST_PROGRAM_ID); - expect(destAccountInfo.amount).to.eql(amount); - - const sourceAccountInfo = await getAccount(connection, account1, undefined, TEST_PROGRAM_ID); - expect(sourceAccountInfo.amount).to.eql(BigInt(0)); - }); - it('transferChecked', async () => { - const transferAmount = amount / BigInt(2); - await transferChecked( - connection, - payer, - account1, - mint, - account2, - owner1, - transferAmount, - TEST_TOKEN_DECIMALS, - [], - undefined, - TEST_PROGRAM_ID, - ); - - const destAccountInfo = await getAccount(connection, account2, undefined, TEST_PROGRAM_ID); - expect(destAccountInfo.amount).to.eql(transferAmount); - - const sourceAccountInfo = await getAccount(connection, account1, undefined, TEST_PROGRAM_ID); - expect(sourceAccountInfo.amount).to.eql(transferAmount); - expect( - transferChecked( - connection, - payer, - account1, - mint, - account2, - owner1, - transferAmount, - TEST_TOKEN_DECIMALS - 1, - [], - undefined, - TEST_PROGRAM_ID, - ), - ).to.be.rejectedWith(Error); - }); - it('approveRevoke', async () => { - const delegate = Keypair.generate(); - const delegatedAmount = amount / BigInt(2); - await approve( - connection, - payer, - account1, - delegate.publicKey, - owner1, - delegatedAmount, - [], - undefined, - TEST_PROGRAM_ID, - ); - const approvedAccountInfo = await getAccount(connection, account1, undefined, TEST_PROGRAM_ID); - expect(approvedAccountInfo.delegatedAmount).to.eql(delegatedAmount); - expect(approvedAccountInfo.delegate).to.eql(delegate.publicKey); - await revoke(connection, payer, account1, owner1, [], undefined, TEST_PROGRAM_ID); - const revokedAccountInfo = await getAccount(connection, account1, undefined, TEST_PROGRAM_ID); - expect(revokedAccountInfo.delegatedAmount).to.eql(BigInt(0)); - expect(revokedAccountInfo.delegate).to.equal(null); - }); - it('delegateTransfer', async () => { - const delegate = Keypair.generate(); - const delegatedAmount = amount / BigInt(2); - await approveChecked( - connection, - payer, - mint, - account1, - delegate.publicKey, - owner1, - delegatedAmount, - TEST_TOKEN_DECIMALS, - [], - undefined, - TEST_PROGRAM_ID, - ); - const transferAmount = delegatedAmount - BigInt(1); - await transfer(connection, payer, account1, account2, delegate, transferAmount, [], undefined, TEST_PROGRAM_ID); - const accountInfo = await getAccount(connection, account1, undefined, TEST_PROGRAM_ID); - expect(accountInfo.delegatedAmount).to.eql(delegatedAmount - transferAmount); - expect(accountInfo.delegate).to.eql(delegate.publicKey); - expect( - transfer(connection, payer, account1, account2, delegate, BigInt(2), [], undefined, TEST_PROGRAM_ID), - ).to.be.rejectedWith(Error); - }); -}); diff --git a/token/js/test/unit/decode.test.ts b/token/js/test/unit/decode.test.ts deleted file mode 100644 index 1dfe99ab925..00000000000 --- a/token/js/test/unit/decode.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Keypair } from '@solana/web3.js'; -import { expect } from 'chai'; -import { - createInitializeMintCloseAuthorityInstruction, - createInitializePermanentDelegateInstruction, - TOKEN_2022_PROGRAM_ID, -} from '../../src'; - -describe('spl-token-2022 instructions', () => { - it('InitializeMintCloseAuthority', () => { - const ix = createInitializeMintCloseAuthorityInstruction( - Keypair.generate().publicKey, - Keypair.generate().publicKey, - TOKEN_2022_PROGRAM_ID, - ); - expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID); - expect(ix.keys).to.have.length(1); - }); - it('InitializePermanentDelegate', () => { - const ix = createInitializePermanentDelegateInstruction( - Keypair.generate().publicKey, - Keypair.generate().publicKey, - TOKEN_2022_PROGRAM_ID, - ); - expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID); - expect(ix.keys).to.have.length(1); - }); -}); diff --git a/token/js/test/unit/groupMemberPointer.test.ts b/token/js/test/unit/groupMemberPointer.test.ts deleted file mode 100644 index eebfec22841..00000000000 --- a/token/js/test/unit/groupMemberPointer.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { expect } from 'chai'; -import type { Mint } from '../../src'; -import { - TOKEN_2022_PROGRAM_ID, - createInitializeGroupMemberPointerInstruction, - createUpdateGroupMemberPointerInstruction, - getGroupMemberPointerState, -} from '../../src'; - -const AUTHORITY_ADDRESS_BYTES = Buffer.alloc(32).fill(8); -const GROUP_MEMBER_ADDRESS_BYTES = Buffer.alloc(32).fill(5); -const NULL_OPTIONAL_NONZERO_PUBKEY_BYTES = Buffer.alloc(32).fill(0); - -describe('SPL Token 2022 GroupMemberPointer Extension', () => { - it('can create InitializeGroupMemberPointerInstruction', () => { - const mint = PublicKey.unique(); - const authority = new PublicKey(AUTHORITY_ADDRESS_BYTES); - const memberAddress = new PublicKey(GROUP_MEMBER_ADDRESS_BYTES); - const instruction = createInitializeGroupMemberPointerInstruction( - mint, - authority, - memberAddress, - TOKEN_2022_PROGRAM_ID, - ); - expect(instruction).to.deep.equal( - new TransactionInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - keys: [{ isSigner: false, isWritable: true, pubkey: mint }], - data: Buffer.concat([ - Buffer.from([ - 41, // Token instruction discriminator - 0, // GroupMemberPointer instruction discriminator - ]), - AUTHORITY_ADDRESS_BYTES, - GROUP_MEMBER_ADDRESS_BYTES, - ]), - }), - ); - }); - it('can create UpdateGroupMemberPointerInstruction', () => { - const mint = PublicKey.unique(); - const authority = PublicKey.unique(); - const memberAddress = new PublicKey(GROUP_MEMBER_ADDRESS_BYTES); - const instruction = createUpdateGroupMemberPointerInstruction(mint, authority, memberAddress); - expect(instruction).to.deep.equal( - new TransactionInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - keys: [ - { isSigner: false, isWritable: true, pubkey: mint }, - { isSigner: true, isWritable: false, pubkey: authority }, - ], - data: Buffer.concat([ - Buffer.from([ - 41, // Token instruction discriminator - 1, // GroupMemberPointer instruction discriminator - ]), - GROUP_MEMBER_ADDRESS_BYTES, - ]), - }), - ); - }); - it('can create UpdateGroupMemberPointerInstruction to none', () => { - const mint = PublicKey.unique(); - const authority = PublicKey.unique(); - const memberAddress = null; - const instruction = createUpdateGroupMemberPointerInstruction(mint, authority, memberAddress); - expect(instruction).to.deep.equal( - new TransactionInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - keys: [ - { isSigner: false, isWritable: true, pubkey: mint }, - { isSigner: true, isWritable: false, pubkey: authority }, - ], - data: Buffer.concat([ - Buffer.from([ - 41, // Token instruction discriminator - 1, // GroupMemberPointer instruction discriminator - ]), - NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, - ]), - }), - ); - }); - it('can get state with authority and group address', async () => { - const mintInfo = { - tlvData: Buffer.concat([ - Buffer.from([ - // Extension discriminator - 22, 0, - // Extension length - 64, 0, - ]), - AUTHORITY_ADDRESS_BYTES, - GROUP_MEMBER_ADDRESS_BYTES, - ]), - } as Mint; - const groupPointer = getGroupMemberPointerState(mintInfo); - expect(groupPointer).to.deep.equal({ - authority: new PublicKey(AUTHORITY_ADDRESS_BYTES), - memberAddress: new PublicKey(GROUP_MEMBER_ADDRESS_BYTES), - }); - }); - it('can get state with only group address', async () => { - const mintInfo = { - tlvData: Buffer.concat([ - Buffer.from([ - // Extension discriminator - 22, 0, - // Extension length - 64, 0, - ]), - NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, - GROUP_MEMBER_ADDRESS_BYTES, - ]), - } as Mint; - const groupPointer = getGroupMemberPointerState(mintInfo); - expect(groupPointer).to.deep.equal({ - authority: null, - memberAddress: new PublicKey(GROUP_MEMBER_ADDRESS_BYTES), - }); - }); - it('can get state with only authority address', async () => { - const mintInfo = { - tlvData: Buffer.concat([ - Buffer.from([ - // Extension discriminator - 22, 0, - // Extension length - 64, 0, - ]), - AUTHORITY_ADDRESS_BYTES, - NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, - ]), - } as Mint; - const groupPointer = getGroupMemberPointerState(mintInfo); - expect(groupPointer).to.deep.equal({ - authority: new PublicKey(AUTHORITY_ADDRESS_BYTES), - memberAddress: null, - }); - }); -}); diff --git a/token/js/test/unit/groupPointer.test.ts b/token/js/test/unit/groupPointer.test.ts deleted file mode 100644 index ddd93e3fea0..00000000000 --- a/token/js/test/unit/groupPointer.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { expect } from 'chai'; -import type { Mint } from '../../src'; -import { - TOKEN_2022_PROGRAM_ID, - createInitializeGroupPointerInstruction, - createUpdateGroupPointerInstruction, - getGroupPointerState, -} from '../../src'; - -const AUTHORITY_ADDRESS_BYTES = Buffer.alloc(32).fill(8); -const GROUP_ADDRESS_BYTES = Buffer.alloc(32).fill(5); -const NULL_OPTIONAL_NONZERO_PUBKEY_BYTES = Buffer.alloc(32).fill(0); - -describe('SPL Token 2022 GroupPointer Extension', () => { - it('can create InitializeGroupPointerInstruction', () => { - const mint = PublicKey.unique(); - const authority = new PublicKey(AUTHORITY_ADDRESS_BYTES); - const groupAddress = new PublicKey(GROUP_ADDRESS_BYTES); - const instruction = createInitializeGroupPointerInstruction( - mint, - authority, - groupAddress, - TOKEN_2022_PROGRAM_ID, - ); - expect(instruction).to.deep.equal( - new TransactionInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - keys: [{ isSigner: false, isWritable: true, pubkey: mint }], - data: Buffer.concat([ - Buffer.from([ - 40, // Token instruction discriminator - 0, // GroupPointer instruction discriminator - ]), - AUTHORITY_ADDRESS_BYTES, - GROUP_ADDRESS_BYTES, - ]), - }), - ); - }); - it('can create UpdateGroupPointerInstruction', () => { - const mint = PublicKey.unique(); - const authority = PublicKey.unique(); - const groupAddress = new PublicKey(GROUP_ADDRESS_BYTES); - const instruction = createUpdateGroupPointerInstruction(mint, authority, groupAddress); - expect(instruction).to.deep.equal( - new TransactionInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - keys: [ - { isSigner: false, isWritable: true, pubkey: mint }, - { isSigner: true, isWritable: false, pubkey: authority }, - ], - data: Buffer.concat([ - Buffer.from([ - 40, // Token instruction discriminator - 1, // GroupPointer instruction discriminator - ]), - GROUP_ADDRESS_BYTES, - ]), - }), - ); - }); - it('can create UpdateGroupPointerInstruction to none', () => { - const mint = PublicKey.unique(); - const authority = PublicKey.unique(); - const groupAddress = null; - const instruction = createUpdateGroupPointerInstruction(mint, authority, groupAddress); - expect(instruction).to.deep.equal( - new TransactionInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - keys: [ - { isSigner: false, isWritable: true, pubkey: mint }, - { isSigner: true, isWritable: false, pubkey: authority }, - ], - data: Buffer.concat([ - Buffer.from([ - 40, // Token instruction discriminator - 1, // GroupPointer instruction discriminator - ]), - NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, - ]), - }), - ); - }); - it('can get state with authority and group address', async () => { - const mintInfo = { - tlvData: Buffer.concat([ - Buffer.from([ - // Extension discriminator - 20, 0, - // Extension length - 64, 0, - ]), - AUTHORITY_ADDRESS_BYTES, - GROUP_ADDRESS_BYTES, - ]), - } as Mint; - const groupPointer = getGroupPointerState(mintInfo); - expect(groupPointer).to.deep.equal({ - authority: new PublicKey(AUTHORITY_ADDRESS_BYTES), - groupAddress: new PublicKey(GROUP_ADDRESS_BYTES), - }); - }); - it('can get state with only group address', async () => { - const mintInfo = { - tlvData: Buffer.concat([ - Buffer.from([ - // Extension discriminator - 20, 0, - // Extension length - 64, 0, - ]), - NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, - GROUP_ADDRESS_BYTES, - ]), - } as Mint; - const groupPointer = getGroupPointerState(mintInfo); - expect(groupPointer).to.deep.equal({ - authority: null, - groupAddress: new PublicKey(GROUP_ADDRESS_BYTES), - }); - }); - it('can get state with only authority address', async () => { - const mintInfo = { - tlvData: Buffer.concat([ - Buffer.from([ - // Extension discriminator - 20, 0, - // Extension length - 64, 0, - ]), - AUTHORITY_ADDRESS_BYTES, - NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, - ]), - } as Mint; - const groupPointer = getGroupPointerState(mintInfo); - expect(groupPointer).to.deep.equal({ - authority: new PublicKey(AUTHORITY_ADDRESS_BYTES), - groupAddress: null, - }); - }); -}); diff --git a/token/js/test/unit/index.test.ts b/token/js/test/unit/index.test.ts deleted file mode 100644 index c974bd0f292..00000000000 --- a/token/js/test/unit/index.test.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { Keypair, PublicKey } from '@solana/web3.js'; -import { expect, use } from 'chai'; -import { - ASSOCIATED_TOKEN_PROGRAM_ID, - createAssociatedTokenAccountInstruction, - createAssociatedTokenAccountIdempotentInstruction, - createAssociatedTokenAccountIdempotentInstructionWithDerivation, - createReallocateInstruction, - createInitializeMintInstruction, - createInitializeMint2Instruction, - createSyncNativeInstruction, - createTransferCheckedInstruction, - getAssociatedTokenAddress, - TOKEN_PROGRAM_ID, - TOKEN_2022_PROGRAM_ID, - TokenInstruction, - TokenOwnerOffCurveError, - getAccountLen, - ExtensionType, - isMintExtension, - isAccountExtension, - getAssociatedTokenAddressSync, - createInitializeAccount2Instruction, - createInitializeAccount3Instruction, - createAmountToUiAmountInstruction, - createUiAmountToAmountInstruction, - getMintLen, -} from '../../src'; -import chaiAsPromised from 'chai-as-promised'; -use(chaiAsPromised); - -describe('spl-token instructions', () => { - it('TransferChecked', () => { - const ix = createTransferCheckedInstruction( - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - 1, - 9, - ); - expect(ix.programId).to.eql(TOKEN_PROGRAM_ID); - expect(ix.keys).to.have.length(4); - }); - - it('InitializeMint', () => { - const ix = createInitializeMintInstruction(Keypair.generate().publicKey, 9, Keypair.generate().publicKey, null); - expect(ix.programId).to.eql(TOKEN_PROGRAM_ID); - expect(ix.keys).to.have.length(2); - }); - - it('InitializeMint2', () => { - const ix = createInitializeMint2Instruction( - Keypair.generate().publicKey, - 9, - Keypair.generate().publicKey, - null, - ); - expect(ix.programId).to.eql(TOKEN_PROGRAM_ID); - expect(ix.keys).to.have.length(1); - }); - - it('SyncNative', () => { - const ix = createSyncNativeInstruction(Keypair.generate().publicKey); - expect(ix.programId).to.eql(TOKEN_PROGRAM_ID); - expect(ix.keys).to.have.length(1); - }); - - it('InitializeAccount2', () => { - const ix = createInitializeAccount2Instruction( - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - ); - expect(ix.programId).to.eql(TOKEN_PROGRAM_ID); - expect(ix.keys).to.have.length(3); - }); - - it('InitializeAccount3', () => { - const ix = createInitializeAccount3Instruction( - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - ); - expect(ix.programId).to.eql(TOKEN_PROGRAM_ID); - expect(ix.keys).to.have.length(2); - }); -}); - -describe('spl-token-2022 instructions', () => { - it('TransferChecked', () => { - const ix = createTransferCheckedInstruction( - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - 1, - 9, - [], - TOKEN_2022_PROGRAM_ID, - ); - expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID); - expect(ix.keys).to.have.length(4); - }); - - it('InitializeMint', () => { - const ix = createInitializeMintInstruction( - Keypair.generate().publicKey, - 9, - Keypair.generate().publicKey, - null, - TOKEN_2022_PROGRAM_ID, - ); - expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID); - expect(ix.keys).to.have.length(2); - }); - - it('InitializeMint2', () => { - const ix = createInitializeMint2Instruction( - Keypair.generate().publicKey, - 9, - Keypair.generate().publicKey, - null, - TOKEN_2022_PROGRAM_ID, - ); - expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID); - expect(ix.keys).to.have.length(1); - }); - - it('SyncNative', () => { - const ix = createSyncNativeInstruction(Keypair.generate().publicKey, TOKEN_2022_PROGRAM_ID); - expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID); - expect(ix.keys).to.have.length(1); - }); - - it('Reallocate', () => { - const publicKey = Keypair.generate().publicKey; - const extensionTypes = [ExtensionType.MintCloseAuthority, ExtensionType.TransferFeeConfig]; - const ix = createReallocateInstruction(publicKey, publicKey, extensionTypes, publicKey); - expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID); - expect(ix.keys).to.have.length(4); - console.error(ix.data); - expect(ix.data[0]).to.eql(TokenInstruction.Reallocate); - expect(ix.data[1]).to.eql(extensionTypes[0]); - expect(ix.data[3]).to.eql(extensionTypes[1]); - }); - - it('AmountToUiAmount', () => { - const ix = createAmountToUiAmountInstruction(Keypair.generate().publicKey, 22, TOKEN_2022_PROGRAM_ID); - expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID); - expect(ix.keys).to.have.length(1); - }); - - it('UiAmountToAmount', () => { - const ix = createUiAmountToAmountInstruction(Keypair.generate().publicKey, '22', TOKEN_2022_PROGRAM_ID); - expect(ix.programId).to.eql(TOKEN_2022_PROGRAM_ID); - expect(ix.keys).to.have.length(1); - }); -}); - -describe('spl-associated-token-account instructions', () => { - it('create', () => { - const ix = createAssociatedTokenAccountInstruction( - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - ); - expect(ix.programId).to.eql(ASSOCIATED_TOKEN_PROGRAM_ID); - expect(ix.keys).to.have.length(6); - }); - - it('create idempotent', () => { - const ix = createAssociatedTokenAccountIdempotentInstruction( - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - ); - expect(ix.programId).to.eql(ASSOCIATED_TOKEN_PROGRAM_ID); - expect(ix.keys).to.have.length(6); - }); - - it('create idempotent with derivation', () => { - const ix = createAssociatedTokenAccountIdempotentInstructionWithDerivation( - Keypair.generate().publicKey, - Keypair.generate().publicKey, - Keypair.generate().publicKey, - ); - expect(ix.programId).to.eql(ASSOCIATED_TOKEN_PROGRAM_ID); - expect(ix.keys).to.have.length(6); - }); - - it('create idempotent with derivation same without', () => { - const payer = Keypair.generate().publicKey; - const owner = Keypair.generate().publicKey; - const mint = Keypair.generate().publicKey; - const associatedToken = getAssociatedTokenAddressSync(mint, owner, true); - const ix = createAssociatedTokenAccountIdempotentInstruction(payer, associatedToken, owner, mint); - const ixDerivation = createAssociatedTokenAccountIdempotentInstructionWithDerivation(payer, owner, mint); - expect(ix).to.deep.eq(ixDerivation); - }); -}); - -describe('state', () => { - it('getAssociatedTokenAddress', async () => { - const associatedPublicKey = await getAssociatedTokenAddress( - new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'), - new PublicKey('B8UwBUUnKwCyKuGMbFKWaG7exYdDk2ozZrPg72NyVbfj'), - ); - expect(associatedPublicKey.toString()).to.eql( - new PublicKey('DShWnroshVbeUp28oopA3Pu7oFPDBtC1DBmPECXXAQ9n').toString(), - ); - await expect( - getAssociatedTokenAddress( - new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'), - associatedPublicKey, - ), - ).to.be.rejectedWith(TokenOwnerOffCurveError); - - const associatedPublicKey2 = await getAssociatedTokenAddress( - new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'), - associatedPublicKey, - true, - ); - expect(associatedPublicKey2.toString()).to.eql( - new PublicKey('F3DmXZFqkfEWFA7MN2vDPs813GeEWPaT6nLk4PSGuWJd').toString(), - ); - }); - - it('getAssociatedTokenAddressSync matches getAssociatedTokenAddress', async () => { - const asyncAssociatedPublicKey = await getAssociatedTokenAddress( - new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'), - new PublicKey('B8UwBUUnKwCyKuGMbFKWaG7exYdDk2ozZrPg72NyVbfj'), - ); - const associatedPublicKey = getAssociatedTokenAddressSync( - new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'), - new PublicKey('B8UwBUUnKwCyKuGMbFKWaG7exYdDk2ozZrPg72NyVbfj'), - ); - expect(associatedPublicKey.toString()).to.eql( - new PublicKey('DShWnroshVbeUp28oopA3Pu7oFPDBtC1DBmPECXXAQ9n').toString(), - ); - expect(asyncAssociatedPublicKey.toString()).to.eql(associatedPublicKey.toString()); - - expect(function () { - getAssociatedTokenAddressSync( - new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'), - associatedPublicKey, - ); - }).to.throw(TokenOwnerOffCurveError); - - const asyncAssociatedPublicKey2 = await getAssociatedTokenAddress( - new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'), - asyncAssociatedPublicKey, - true, - ); - const associatedPublicKey2 = getAssociatedTokenAddressSync( - new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'), - associatedPublicKey, - true, - ); - expect(associatedPublicKey2.toString()).to.eql( - new PublicKey('F3DmXZFqkfEWFA7MN2vDPs813GeEWPaT6nLk4PSGuWJd').toString(), - ); - expect(asyncAssociatedPublicKey2.toString()).to.eql(associatedPublicKey2.toString()); - }); -}); - -describe('extensionType', () => { - it('calculates size for accounts', () => { - expect(getAccountLen([ExtensionType.MintCloseAuthority, ExtensionType.TransferFeeConfig])).to.eql(314); - expect(getAccountLen([])).to.eql(165); - expect(getAccountLen([ExtensionType.ImmutableOwner])).to.eql(170); - expect(getAccountLen([ExtensionType.PermanentDelegate])).to.eql(202); - }); - - it('calculates size for mints', () => { - expect(getMintLen([ExtensionType.TransferFeeConfig, ExtensionType.NonTransferable])).to.eql(282); - expect(getMintLen([])).to.eql(82); - expect(getMintLen([ExtensionType.TransferHook])).to.eql(234); - expect(getMintLen([ExtensionType.MetadataPointer])).to.eql(234); - expect( - getMintLen([ExtensionType.TransferFeeConfig, ExtensionType.NonTransferable], { - [ExtensionType.TokenMetadata]: 200, - }), - ).to.eql(486); - expect( - getMintLen([], { - [ExtensionType.TokenMetadata]: 200, - }), - ).to.eql(370); - // Should error on an extension that isn't variable-length - expect(() => - getMintLen([ExtensionType.TransferFeeConfig, ExtensionType.NonTransferable], { - [ExtensionType.TransferHook]: 200, - }), - ).to.throw('Extension 14 is not variable length'); - }); - - it('exclusive and exhaustive predicates', () => { - const exts = Object.values(ExtensionType).filter(Number.isInteger); - const mintExts = exts.filter((e: any): e is ExtensionType => isMintExtension(e)); - const accountExts = exts.filter((e: any): e is ExtensionType => isAccountExtension(e)); - const collectedExts = [ExtensionType.Uninitialized].concat(mintExts, accountExts); - - expect(collectedExts.sort()).to.eql(exts.sort()); - }); -}); diff --git a/token/js/test/unit/interestBearing.test.ts b/token/js/test/unit/interestBearing.test.ts deleted file mode 100644 index c3bce68970d..00000000000 --- a/token/js/test/unit/interestBearing.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { expect } from 'chai'; -import type { Connection } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; -import { - amountToUiAmountForMintWithoutSimulation, - uiAmountToAmountForMintWithoutSimulation, -} from '../../src/actions/amountToUiAmount'; -import { AccountLayout, InterestBearingMintConfigStateLayout, TOKEN_2022_PROGRAM_ID } from '../../src'; -import { MintLayout } from '../../src/state/mint'; -import { ExtensionType } from '../../src/extensions/extensionType'; -import { AccountType } from '../../src/extensions/accountType'; - -const ONE_YEAR_IN_SECONDS = 31556736; - -// Mock connection class -class MockConnection { - private mockAccountInfo: any; - private mockClock: { - epoch: number; - epochStartTimestamp: number; - leaderScheduleEpoch: number; - slot: number; - unixTimestamp: number; - }; - - constructor() { - this.mockAccountInfo = null; - this.mockClock = { - epoch: 0, - epochStartTimestamp: 0, - leaderScheduleEpoch: 0, - slot: 0, - unixTimestamp: ONE_YEAR_IN_SECONDS, - }; - } - - getAccountInfo = async (address: PublicKey) => { - return this.getParsedAccountInfo(address); - }; - - // used to get the clock timestamp - getParsedAccountInfo = async (address: PublicKey) => { - if (address.toString() === 'SysvarC1ock11111111111111111111111111111111') { - return { - value: { - data: { - parsed: { - info: this.mockClock, - }, - }, - }, - }; - } - return this.mockAccountInfo; - }; - - setClockTimestamp(timestamp: number) { - this.mockClock = { - ...this.mockClock, - unixTimestamp: timestamp, - }; - } - - resetClock() { - this.mockClock = { - ...this.mockClock, - unixTimestamp: ONE_YEAR_IN_SECONDS, - }; - } - - setAccountInfo(info: any) { - this.mockAccountInfo = info; - } -} - -function createMockMintData( - decimals = 2, - hasInterestBearingConfig = false, - config: { preUpdateAverageRate?: number; currentRate?: number } = {}, -) { - const mintData = Buffer.alloc(MintLayout.span); - MintLayout.encode( - { - mintAuthorityOption: 1, - mintAuthority: new PublicKey(new Uint8Array(32).fill(1)), - supply: BigInt(1000000), - decimals: decimals, - isInitialized: true, - freezeAuthorityOption: 1, - freezeAuthority: new PublicKey(new Uint8Array(32).fill(1)), - }, - mintData, - ); - - const baseData = Buffer.alloc(AccountLayout.span + 1); - mintData.copy(baseData, 0); - baseData[AccountLayout.span] = AccountType.Mint; - - if (!hasInterestBearingConfig) { - return baseData; - } - - // write extension data using the InterestBearingMintConfigStateLayout - const extensionData = Buffer.alloc(InterestBearingMintConfigStateLayout.span); - const rateAuthority = new Uint8Array(32).fill(1); // rate authority - Buffer.from(rateAuthority).copy(extensionData, 0); - extensionData.writeBigUInt64LE(BigInt(0), 32); // initialization timestamp - extensionData.writeInt16LE(config.preUpdateAverageRate || 500, 40); // pre-update average rate - extensionData.writeBigUInt64LE(BigInt(ONE_YEAR_IN_SECONDS), 42); // last update timestamp - extensionData.writeInt16LE(config.currentRate || 500, 50); // current rate - - const TYPE_SIZE = 2; - const LENGTH_SIZE = 2; - const tlvBuffer = Buffer.alloc(TYPE_SIZE + LENGTH_SIZE + extensionData.length); - tlvBuffer.writeUInt16LE(ExtensionType.InterestBearingConfig, 0); - tlvBuffer.writeUInt16LE(extensionData.length, TYPE_SIZE); - extensionData.copy(tlvBuffer, TYPE_SIZE + LENGTH_SIZE); - - const fullData = Buffer.alloc(baseData.length + tlvBuffer.length); - baseData.copy(fullData, 0); - tlvBuffer.copy(fullData, baseData.length); - - return fullData; -} - -describe('amountToUiAmountForMintWithoutSimulation', () => { - let connection: MockConnection; - const mint = new PublicKey('So11111111111111111111111111111111111111112'); - - beforeEach(() => { - connection = new MockConnection() as unknown as MockConnection; - }); - - afterEach(() => { - connection.resetClock(); - }); - - it('should return the correct UiAmount when interest bearing config is not present', async () => { - const testCases = [ - { decimals: 0, amount: BigInt(100), expected: '100' }, - { decimals: 2, amount: BigInt(100), expected: '1' }, - { decimals: 9, amount: BigInt(1000000000), expected: '1' }, - { decimals: 10, amount: BigInt(1), expected: '1e-10' }, - { decimals: 10, amount: BigInt(1000000000), expected: '0.1' }, - ]; - - for (const { decimals, amount, expected } of testCases) { - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(decimals, false), - }); - - const result = await amountToUiAmountForMintWithoutSimulation( - connection as unknown as Connection, - mint, - amount, - ); - expect(result).to.equal(expected); - } - }); - - // continuous compounding interest of 5% for 1 year for 1 token = 1.0512710963760240397 - it('should return the correct UiAmount for constant 5% rate', async () => { - const testCases = [ - { decimals: 0, amount: BigInt(1), expected: '1' }, - { decimals: 1, amount: BigInt(1), expected: '0.1' }, - { decimals: 10, amount: BigInt(1), expected: '1e-10' }, - { decimals: 10, amount: BigInt(10000000000), expected: '1.0512710963' }, - ]; - - for (const { decimals, amount, expected } of testCases) { - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(decimals, true), - }); - - const result = await amountToUiAmountForMintWithoutSimulation( - connection as unknown as Connection, - mint, - amount, - ); - expect(result).to.equal(expected); - } - }); - - it('should return the correct UiAmount for constant -5% rate', async () => { - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(10, true, { preUpdateAverageRate: -500, currentRate: -500 }), - }); - - const result = await amountToUiAmountForMintWithoutSimulation( - connection as unknown as Connection, - mint, - BigInt(10000000000), - ); - expect(result).to.equal('0.9512294245'); - }); - - it('should return the correct UiAmount for netting out rates', async () => { - connection.setClockTimestamp(ONE_YEAR_IN_SECONDS * 2); - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(10, true, { preUpdateAverageRate: -500, currentRate: 500 }), - }); - - const result = await amountToUiAmountForMintWithoutSimulation( - connection as unknown as Connection, - mint, - BigInt(10000000000), - ); - expect(result).to.equal('1'); - }); - - it('should handle huge values correctly', async () => { - connection.setClockTimestamp(ONE_YEAR_IN_SECONDS * 2); - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(6, true), - }); - - const result = await amountToUiAmountForMintWithoutSimulation( - connection as unknown as Connection, - mint, - BigInt('18446744073709551615'), - ); - expect(result).to.equal('20386805083448.098'); - }); -}); - -describe('amountToUiAmountForMintWithoutSimulation', () => { - let connection: MockConnection; - const mint = new PublicKey('So11111111111111111111111111111111111111112'); - - beforeEach(() => { - connection = new MockConnection() as unknown as MockConnection; - }); - - afterEach(() => { - connection.resetClock(); - }); - it('should return the correct amount for constant 5% rate', async () => { - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(0, true), - }); - - const result = await uiAmountToAmountForMintWithoutSimulation( - connection as unknown as Connection, - mint, - '1.0512710963760241', - ); - expect(result).to.equal(1n); - }); - - it('should handle decimal places correctly', async () => { - const testCases = [ - { decimals: 1, uiAmount: '0.10512710963760241', expected: 1n }, - { decimals: 10, uiAmount: '0.00000000010512710963760242', expected: 1n }, - { decimals: 10, uiAmount: '1.0512710963760241', expected: 10000000000n }, - ]; - - for (const { decimals, uiAmount, expected } of testCases) { - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(decimals, true), - }); - - const result = await uiAmountToAmountForMintWithoutSimulation( - connection as unknown as Connection, - mint, - uiAmount, - ); - expect(result).to.equal(expected); - } - }); - - it('should return the correct amount for constant -5% rate', async () => { - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(10, true, { preUpdateAverageRate: -500, currentRate: -500 }), - }); - - const result = await uiAmountToAmountForMintWithoutSimulation( - connection as unknown as Connection, - mint, - '0.951229424500714', - ); - expect(result).to.equal(9999999999n); // calculation truncates to avoid floating point precision issues in transfers - }); - - it('should return the correct amount for netting out rates', async () => { - connection.setClockTimestamp(ONE_YEAR_IN_SECONDS * 2); - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(10, true, { preUpdateAverageRate: -500, currentRate: 500 }), - }); - - const result = await uiAmountToAmountForMintWithoutSimulation(connection as unknown as Connection, mint, '1'); - expect(result).to.equal(10000000000n); - }); - - it('should handle huge values correctly', async () => { - connection.setClockTimestamp(ONE_YEAR_IN_SECONDS * 2); - connection.setAccountInfo({ - owner: TOKEN_2022_PROGRAM_ID, - lamports: 1000000, - data: createMockMintData(0, true), - }); - - const result = await uiAmountToAmountForMintWithoutSimulation( - connection as unknown as Connection, - mint, - '20386805083448100000', - ); - expect(result).to.equal(18446744073709551616n); - }); -}); diff --git a/token/js/test/unit/metadataPointer.test.ts b/token/js/test/unit/metadataPointer.test.ts deleted file mode 100644 index 1b614cfccdb..00000000000 --- a/token/js/test/unit/metadataPointer.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { expect } from 'chai'; -import type { Mint } from '../../src'; -import { - TOKEN_2022_PROGRAM_ID, - createInitializeMetadataPointerInstruction, - createUpdateMetadataPointerInstruction, - getMetadataPointerState, -} from '../../src'; - -const AUTHORITY_ADDRESS_BYTES = Buffer.alloc(32).fill(8); -const METADATA_ADDRESS_BYTES = Buffer.alloc(32).fill(5); -const NULL_OPTIONAL_NONZERO_PUBKEY_BYTES = Buffer.alloc(32).fill(0); - -describe('SPL Token 2022 MetadataPointer Extension', () => { - it('can create InitializeMetadataPointerInstruction', () => { - const mint = PublicKey.unique(); - const authority = new PublicKey(AUTHORITY_ADDRESS_BYTES); - const metadataAddress = new PublicKey(METADATA_ADDRESS_BYTES); - const instruction = createInitializeMetadataPointerInstruction( - mint, - authority, - metadataAddress, - TOKEN_2022_PROGRAM_ID, - ); - expect(instruction).to.deep.equal( - new TransactionInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - keys: [{ isSigner: false, isWritable: true, pubkey: mint }], - data: Buffer.concat([ - Buffer.from([ - 39, // Token instruction discriminator - 0, // MetadataPointer instruction discriminator - ]), - AUTHORITY_ADDRESS_BYTES, - METADATA_ADDRESS_BYTES, - ]), - }), - ); - }); - it('can create UpdateMetadataPointerInstruction', () => { - const mint = PublicKey.unique(); - const authority = PublicKey.unique(); - const metadataAddress = new PublicKey(METADATA_ADDRESS_BYTES); - const instruction = createUpdateMetadataPointerInstruction(mint, authority, metadataAddress); - expect(instruction).to.deep.equal( - new TransactionInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - keys: [ - { isSigner: false, isWritable: true, pubkey: mint }, - { isSigner: true, isWritable: false, pubkey: authority }, - ], - data: Buffer.concat([ - Buffer.from([ - 39, // Token instruction discriminator - 1, // MetadataPointer instruction discriminator - ]), - METADATA_ADDRESS_BYTES, - ]), - }), - ); - }); - it('can create UpdateMetadataPointerInstruction to none', () => { - const mint = PublicKey.unique(); - const authority = PublicKey.unique(); - const metadataAddress = null; - const instruction = createUpdateMetadataPointerInstruction(mint, authority, metadataAddress); - expect(instruction).to.deep.equal( - new TransactionInstruction({ - programId: TOKEN_2022_PROGRAM_ID, - keys: [ - { isSigner: false, isWritable: true, pubkey: mint }, - { isSigner: true, isWritable: false, pubkey: authority }, - ], - data: Buffer.concat([ - Buffer.from([ - 39, // Token instruction discriminator - 1, // MetadataPointer instruction discriminator - ]), - NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, - ]), - }), - ); - }); - it('can get state with authority and metadata address', async () => { - const mintInfo = { - tlvData: Buffer.concat([ - Buffer.from([ - // Extension discriminator - 18, 0, - // Extension length - 64, 0, - ]), - AUTHORITY_ADDRESS_BYTES, - METADATA_ADDRESS_BYTES, - ]), - } as Mint; - const metadataPointer = getMetadataPointerState(mintInfo); - expect(metadataPointer).to.deep.equal({ - authority: new PublicKey(AUTHORITY_ADDRESS_BYTES), - metadataAddress: new PublicKey(METADATA_ADDRESS_BYTES), - }); - }); - it('can get state with only metadata address', async () => { - const mintInfo = { - tlvData: Buffer.concat([ - Buffer.from([ - // Extension discriminator - 18, 0, - // Extension length - 64, 0, - ]), - NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, - METADATA_ADDRESS_BYTES, - ]), - } as Mint; - const metadataPointer = getMetadataPointerState(mintInfo); - expect(metadataPointer).to.deep.equal({ - authority: null, - metadataAddress: new PublicKey(METADATA_ADDRESS_BYTES), - }); - }); - it('can get state with only authority address', async () => { - const mintInfo = { - tlvData: Buffer.concat([ - Buffer.from([ - // Extension discriminator - 18, 0, - // Extension length - 64, 0, - ]), - AUTHORITY_ADDRESS_BYTES, - NULL_OPTIONAL_NONZERO_PUBKEY_BYTES, - ]), - } as Mint; - const metadataPointer = getMetadataPointerState(mintInfo); - expect(metadataPointer).to.deep.equal({ - authority: new PublicKey(AUTHORITY_ADDRESS_BYTES), - metadataAddress: null, - }); - }); -}); diff --git a/token/js/test/unit/programId.test.ts b/token/js/test/unit/programId.test.ts deleted file mode 100644 index 3ea894cce0d..00000000000 --- a/token/js/test/unit/programId.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { expect } from 'chai'; -import { PublicKey } from '@solana/web3.js'; -import { - AccountState, - createCreateNativeMintInstruction, - createEnableRequiredMemoTransfersInstruction, - createInitializeNonTransferableMintInstruction, - createInitializeTransferFeeConfigInstruction, - createInitializeMintCloseAuthorityInstruction, - createInitializeDefaultAccountStateInstruction, - NATIVE_MINT, - NATIVE_MINT_2022, - TOKEN_PROGRAM_ID, - TOKEN_2022_PROGRAM_ID, - TokenUnsupportedInstructionError, - createInitializePermanentDelegateInstruction, - createEnableCpiGuardInstruction, - createInitializeTransferHookInstruction, -} from '../../src'; - -describe('unsupported extensions in spl-token', () => { - const mint = new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'); - const account = new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'); - const authority = new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'); - const payer = new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'); - const transferHookProgramId = new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'); - it('initializeMintCloseAuthority', () => { - expect(function () { - createInitializeMintCloseAuthorityInstruction(mint, null, TOKEN_PROGRAM_ID); - }).to.throw(TokenUnsupportedInstructionError); - expect(function () { - createInitializeMintCloseAuthorityInstruction(mint, null, TOKEN_2022_PROGRAM_ID); - }).to.not.throw(TokenUnsupportedInstructionError); - }); - it('defaultAccountState', () => { - expect(function () { - createInitializeDefaultAccountStateInstruction(mint, AccountState.Frozen, TOKEN_PROGRAM_ID); - }).to.throw(TokenUnsupportedInstructionError); - expect(function () { - createInitializeDefaultAccountStateInstruction(mint, AccountState.Frozen, TOKEN_2022_PROGRAM_ID); - }).to.not.throw(TokenUnsupportedInstructionError); - }); - it('memoTransfer', () => { - expect(function () { - createEnableRequiredMemoTransfersInstruction(account, authority, [], TOKEN_PROGRAM_ID); - }).to.throw(TokenUnsupportedInstructionError); - expect(function () { - createEnableRequiredMemoTransfersInstruction(account, authority, [], TOKEN_2022_PROGRAM_ID); - }).to.not.throw(TokenUnsupportedInstructionError); - }); - it('transferFee', () => { - expect(function () { - createInitializeTransferFeeConfigInstruction(mint, null, null, 0, BigInt(0), TOKEN_PROGRAM_ID); - }).to.throw(TokenUnsupportedInstructionError); - expect(function () { - createInitializeTransferFeeConfigInstruction(mint, null, null, 0, BigInt(0), TOKEN_2022_PROGRAM_ID); - }).to.not.throw(TokenUnsupportedInstructionError); - }); - it('nativeMint', () => { - expect(function () { - createCreateNativeMintInstruction(payer, NATIVE_MINT, TOKEN_PROGRAM_ID); - }).to.throw(TokenUnsupportedInstructionError); - expect(function () { - createCreateNativeMintInstruction(payer, NATIVE_MINT_2022, TOKEN_2022_PROGRAM_ID); - }).to.not.throw(TokenUnsupportedInstructionError); - }); - it('transferHook', () => { - expect(function () { - createInitializeTransferHookInstruction(mint, authority, transferHookProgramId, TOKEN_PROGRAM_ID); - }).to.throw(TokenUnsupportedInstructionError); - expect(function () { - createInitializeTransferHookInstruction(mint, authority, transferHookProgramId, TOKEN_2022_PROGRAM_ID); - }).to.not.throw(TokenUnsupportedInstructionError); - }); - it('nonTransferableMint', () => { - expect(function () { - createInitializeNonTransferableMintInstruction(mint, TOKEN_PROGRAM_ID); - }).to.throw(TokenUnsupportedInstructionError); - expect(function () { - createInitializeNonTransferableMintInstruction(mint, TOKEN_2022_PROGRAM_ID); - }).to.not.throw(TokenUnsupportedInstructionError); - }); - it('initializePermanentDelegate', () => { - expect(function () { - createInitializePermanentDelegateInstruction(mint, null, TOKEN_PROGRAM_ID); - }).to.throw(TokenUnsupportedInstructionError); - expect(function () { - createInitializePermanentDelegateInstruction(mint, null, TOKEN_2022_PROGRAM_ID); - }).to.not.throw(TokenUnsupportedInstructionError); - }); - it('cpiGuard', () => { - expect(function () { - createEnableCpiGuardInstruction(account, authority, [], TOKEN_PROGRAM_ID); - }).to.throw(TokenUnsupportedInstructionError); - expect(function () { - createEnableCpiGuardInstruction(account, authority, [], TOKEN_2022_PROGRAM_ID); - }).to.not.throw(TokenUnsupportedInstructionError); - }); -}); diff --git a/token/js/test/unit/tokenMetadata.test.ts b/token/js/test/unit/tokenMetadata.test.ts deleted file mode 100644 index 9d1f14d9f99..00000000000 --- a/token/js/test/unit/tokenMetadata.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import { expect } from 'chai'; - -import type { TokenMetadata } from '@solana/spl-token-metadata'; -import { Field } from '@solana/spl-token-metadata'; -import { updateTokenMetadata } from '../../src'; - -describe('SPL Token 2022 Metadata Extension', () => { - describe('Update token metadata', () => { - it('guards against updates on mint or updateAuthority', async () => { - const input = Object.freeze({ - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - } as TokenMetadata); - - expect(() => updateTokenMetadata(input, 'mint', 'string')).to.throw( - 'Cannot update mint via this instruction', - ); - expect(() => updateTokenMetadata(input, 'updateAuthority', 'string')).to.throw( - 'Cannot update updateAuthority via this instruction', - ); - }); - it('can update name', async () => { - const input = Object.freeze({ - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - } as TokenMetadata); - - const expected: TokenMetadata = { - mint: PublicKey.default, - name: 'updated_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - }; - - expect(updateTokenMetadata(input, 'name', 'updated_name')).to.deep.equal(expected); - expect(updateTokenMetadata(input, 'Name', 'updated_name')).to.deep.equal(expected); - expect(updateTokenMetadata(input, Field.Name, 'updated_name')).to.deep.equal(expected); - }); - - it('can update symbol', async () => { - const input = Object.freeze({ - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - } as TokenMetadata); - - const expected: TokenMetadata = { - mint: PublicKey.default, - name: 'new_name', - symbol: 'updated_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - }; - - expect(updateTokenMetadata(input, 'symbol', 'updated_symbol')).to.deep.equal(expected); - expect(updateTokenMetadata(input, 'Symbol', 'updated_symbol')).to.deep.equal(expected); - expect(updateTokenMetadata(input, Field.Symbol, 'updated_symbol')).to.deep.equal(expected); - }); - - it('can update uri', async () => { - const input = Object.freeze({ - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - } as TokenMetadata); - - const expected: TokenMetadata = { - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'updated_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - }; - - expect(updateTokenMetadata(input, 'uri', 'updated_uri')).to.deep.equal(expected); - expect(updateTokenMetadata(input, 'Uri', 'updated_uri')).to.deep.equal(expected); - expect(updateTokenMetadata(input, Field.Uri, 'updated_uri')).to.deep.equal(expected); - }); - - it('can update additional Metadata', async () => { - const input = Object.freeze({ - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - } as TokenMetadata); - - const expected: TokenMetadata = { - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'update1'], - ['key2', 'value2'], - ], - }; - - expect(updateTokenMetadata(input, 'key1', 'update1')).to.deep.equal(expected); - }); - - it('can add additional Metadata', async () => { - const input = Object.freeze({ - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - } as TokenMetadata); - - const expected: TokenMetadata = { - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ['key3', 'value3'], - ], - }; - - expect(updateTokenMetadata(input, 'key3', 'value3')).to.deep.equal(expected); - }); - - it('can update `additionalMetadata` key to additional metadata', async () => { - const input = Object.freeze({ - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ['additionalMetadata', 'value3'], - ], - } as TokenMetadata); - - const expected: TokenMetadata = { - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ['additionalMetadata', 'update3'], - ], - }; - - expect(updateTokenMetadata(input, 'additionalMetadata', 'update3')).to.deep.equal(expected); - }); - - it('can add `additionalMetadata` key to additional metadata', async () => { - const input = Object.freeze({ - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ], - } as TokenMetadata); - - const expected: TokenMetadata = { - mint: PublicKey.default, - name: 'new_name', - symbol: 'new_symbol', - uri: 'new_uri', - additionalMetadata: [ - ['key1', 'value1'], - ['key2', 'value2'], - ['additionalMetadata', 'value3'], - ], - }; - - expect(updateTokenMetadata(input, 'additionalMetadata', 'value3')).to.deep.equal(expected); - }); - }); -}); diff --git a/token/js/test/unit/transferHook.test.ts b/token/js/test/unit/transferHook.test.ts deleted file mode 100644 index 56ed5e96821..00000000000 --- a/token/js/test/unit/transferHook.test.ts +++ /dev/null @@ -1,540 +0,0 @@ -import type { ExtraAccountMeta, ExtraAccountMetaList } from '../../src'; -import { - ACCOUNT_SIZE, - ACCOUNT_TYPE_SIZE, - ExtensionType, - ExtraAccountMetaAccountDataLayout, - ExtraAccountMetaLayout, - LENGTH_SIZE, - MintLayout, - TOKEN_2022_PROGRAM_ID, - TRANSFER_HOOK_SIZE, - TYPE_SIZE, - TransferHookLayout, - addExtraAccountMetasForExecute, - createTransferCheckedWithTransferHookInstruction, - getExtraAccountMetaAddress, - getExtraAccountMetas, - resolveExtraAccountMeta, -} from '../../src'; -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import type { Connection } from '@solana/web3.js'; -import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { getConnection } from '../common'; -use(chaiAsPromised); - -describe('transferHook', () => { - describe('validation data', () => { - let connection: Connection; - const testProgramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj'); - const instructionData = Buffer.from(Array.from(Array(32).keys())); - const plainAccount = new PublicKey('6c5q79ccBTWvZTEx3JkdHThtMa2eALba5bfvHGf8kA2c'); - const seeds = [ - Buffer.from('seed'), - Buffer.from([4, 5, 6, 7]), - plainAccount.toBuffer(), - Buffer.from([2, 2, 2, 2]), - ]; - const pdaPublicKey = PublicKey.findProgramAddressSync(seeds, testProgramId)[0]; - const pdaPublicKeyWithProgramId = PublicKey.findProgramAddressSync(seeds, plainAccount)[0]; - - const plainSeed = Buffer.concat([ - Buffer.from([1]), // u8 discriminator - Buffer.from([4]), // u8 length - Buffer.from('seed'), // 4 bytes seed - ]); - - const instructionDataSeed = Buffer.concat([ - Buffer.from([2]), // u8 discriminator - Buffer.from([4]), // u8 offset - Buffer.from([4]), // u8 length - ]); - - const accountKeySeed = Buffer.concat([ - Buffer.from([3]), // u8 discriminator - Buffer.from([0]), // u8 index - ]); - - const accountDataSeed = Buffer.concat([ - Buffer.from([4]), // u8 discriminator - Buffer.from([0]), // u8 account index - Buffer.from([2]), // u8 account data offset - Buffer.from([4]), // u8 account data length - ]); - - const addressConfig = Buffer.concat([plainSeed, instructionDataSeed, accountKeySeed, accountDataSeed], 32); - - const plainExtraAccountMeta = { - discriminator: 0, - addressConfig: plainAccount.toBuffer(), - isSigner: false, - isWritable: false, - }; - const plainExtraAccount = Buffer.concat([ - Buffer.from([0]), // u8 discriminator - plainAccount.toBuffer(), // 32 bytes address - Buffer.from([0]), // bool isSigner - Buffer.from([0]), // bool isWritable - ]); - - const pdaExtraAccountMeta = { - discriminator: 1, - addressConfig, - isSigner: true, - isWritable: false, - }; - const pdaExtraAccount = Buffer.concat([ - Buffer.from([1]), // u8 discriminator - addressConfig, // 32 bytes address config - Buffer.from([1]), // bool isSigner - Buffer.from([0]), // bool isWritable - ]); - - const pdaExtraAccountMetaWithProgramId = { - discriminator: 128, - addressConfig, - isSigner: false, - isWritable: true, - }; - const pdaExtraAccountWithProgramId = Buffer.concat([ - Buffer.from([128]), // u8 discriminator - addressConfig, // 32 bytes address config - Buffer.from([0]), // bool isSigner - Buffer.from([1]), // bool isWritable - ]); - - const extraAccountList = Buffer.concat([ - Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), // u64 accountDiscriminator - Buffer.from([0, 0, 0, 0]), // u32 length - Buffer.from([3, 0, 0, 0]), // u32 count - plainExtraAccount, - pdaExtraAccount, - pdaExtraAccountWithProgramId, - ]); - - before(async () => { - connection = await getConnection(); - connection.getAccountInfo = async ( - _publicKey: PublicKey, - _commitmentOrConfig?: Parameters<(typeof connection)['getAccountInfo']>[1], - ): ReturnType<(typeof connection)['getAccountInfo']> => ({ - data: Buffer.from([0, 0, 2, 2, 2, 2]), - owner: PublicKey.default, - executable: false, - lamports: 0, - }); - }); - - it('can parse extra metas', () => { - const accountInfo = { - data: extraAccountList, - owner: PublicKey.default, - executable: false, - lamports: 0, - }; - const parsedExtraAccounts = getExtraAccountMetas(accountInfo); - expect(parsedExtraAccounts).to.not.equal(null); - if (parsedExtraAccounts == null) { - return; - } - - expect(parsedExtraAccounts).to.have.length(3); - if (parsedExtraAccounts.length !== 3) { - return; - } - - expect(parsedExtraAccounts[0].discriminator).to.eql(0); - expect(parsedExtraAccounts[0].addressConfig).to.eql(plainAccount.toBuffer()); - expect(parsedExtraAccounts[0].isSigner).to.equal(false); - expect(parsedExtraAccounts[0].isWritable).to.equal(false); - - expect(parsedExtraAccounts[1].discriminator).to.eql(1); - expect(parsedExtraAccounts[1].addressConfig).to.eql(addressConfig); - expect(parsedExtraAccounts[1].isSigner).to.equal(true); - expect(parsedExtraAccounts[1].isWritable).to.equal(false); - - expect(parsedExtraAccounts[2].discriminator).to.eql(128); - expect(parsedExtraAccounts[2].addressConfig).to.eql(addressConfig); - expect(parsedExtraAccounts[2].isSigner).to.equal(false); - expect(parsedExtraAccounts[2].isWritable).to.equal(true); - }); - - it('can resolve extra metas', async () => { - const resolvedPlainAccount = await resolveExtraAccountMeta( - connection, - plainExtraAccountMeta, - [], - instructionData, - testProgramId, - ); - - expect(resolvedPlainAccount.pubkey).to.eql(plainAccount); - expect(resolvedPlainAccount.isSigner).to.equal(false); - expect(resolvedPlainAccount.isWritable).to.equal(false); - - const resolvedPdaAccount = await resolveExtraAccountMeta( - connection, - pdaExtraAccountMeta, - [resolvedPlainAccount], - instructionData, - testProgramId, - ); - - expect(resolvedPdaAccount.pubkey).to.eql(pdaPublicKey); - expect(resolvedPdaAccount.isSigner).to.equal(true); - expect(resolvedPdaAccount.isWritable).to.equal(false); - - const resolvedPdaAccountWithProgramId = await resolveExtraAccountMeta( - connection, - pdaExtraAccountMetaWithProgramId, - [resolvedPlainAccount], - instructionData, - testProgramId, - ); - - expect(resolvedPdaAccountWithProgramId.pubkey).to.eql(pdaPublicKeyWithProgramId); - expect(resolvedPdaAccountWithProgramId.isSigner).to.equal(false); - expect(resolvedPdaAccountWithProgramId.isWritable).to.equal(true); - }); - }); - - describe('adding extra metas to instructions', () => { - let connection: Connection; - - let transferHookProgramId: PublicKey; - - let sourcePubkey: PublicKey; - let mintPubkey: PublicKey; - let destinationPubkey: PublicKey; - let authorityPubkey: PublicKey; - let validateStatePubkey: PublicKey; - - const amount = 100n; - const amountInLeBytes = Buffer.alloc(8); - amountInLeBytes.writeBigUInt64LE(amount); - const decimals = 0; - - // Arbitrary program ID included to test external PDAs - let arbitraryProgramId: PublicKey; - - beforeEach(async () => { - connection = await getConnection(); - - transferHookProgramId = Keypair.generate().publicKey; - - sourcePubkey = Keypair.generate().publicKey; - mintPubkey = Keypair.generate().publicKey; - destinationPubkey = Keypair.generate().publicKey; - authorityPubkey = Keypair.generate().publicKey; - validateStatePubkey = getExtraAccountMetaAddress(mintPubkey, transferHookProgramId); - - arbitraryProgramId = Keypair.generate().publicKey; - }); - - function createMockFetchAccountDataFn(extraAccounts: ExtraAccountMeta[]) { - return async function mockFetchAccountDataFn( - publicKey: PublicKey, - _commitmentOrConfig?: Parameters[1], - ): ReturnType { - // Mocked mint state - if (publicKey.equals(mintPubkey)) { - const data = Buffer.alloc( - ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE + LENGTH_SIZE + TRANSFER_HOOK_SIZE, - ); - MintLayout.encode( - { - mintAuthorityOption: 0, - mintAuthority: PublicKey.default, - supply: 10000n, - decimals, - isInitialized: true, - freezeAuthorityOption: 0, - freezeAuthority: PublicKey.default, - }, - data, - 0, - ); - data.writeUint8(1, ACCOUNT_SIZE); // Account type (1): Mint = 1 - data.writeUint16LE(ExtensionType.TransferHook, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE); - data.writeUint16LE(TRANSFER_HOOK_SIZE, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE); - TransferHookLayout.encode( - { - authority: Keypair.generate().publicKey, - programId: transferHookProgramId, - }, - data, - ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE + LENGTH_SIZE, - ); - return { - data, - owner: TOKEN_2022_PROGRAM_ID, - executable: false, - lamports: 0, - }; - } - - // Mocked validate state - if (publicKey.equals(validateStatePubkey)) { - const extraAccountsList: ExtraAccountMetaList = { - count: extraAccounts.length, - extraAccounts, - }; - const instructionDiscriminator = Buffer.from([ - 105, 37, 101, 197, 75, 251, 102, 26, - ]).readBigUInt64LE(); - const data = Buffer.alloc(8 + 4 + 4 + ExtraAccountMetaLayout.span * extraAccounts.length); - ExtraAccountMetaAccountDataLayout.encode( - { - instructionDiscriminator, - length: 4 + ExtraAccountMetaLayout.span * extraAccounts.length, - extraAccountsList, - }, - data, - ); - return { - data, - owner: transferHookProgramId, - executable: false, - lamports: 0, - }; - } - - return { - data: Buffer.from([]), - owner: PublicKey.default, - executable: false, - lamports: 0, - }; - }; - } - - const addressConfig = (data: Uint8Array) => { - const addressConfig = Buffer.alloc(32); - addressConfig.set(data, 0); - return addressConfig; - }; - - const fixedAddress = (address: PublicKey, isSigner: boolean, isWritable: boolean) => ({ - discriminator: 0, - addressConfig: address.toBuffer(), - isSigner, - isWritable, - }); - - const pda = (seeds: number[], isSigner: boolean, isWritable: boolean) => ({ - discriminator: 1, - addressConfig: addressConfig(new Uint8Array(seeds)), - isSigner, - isWritable, - }); - - const externalPda = (programKeyIndex: number, seeds: number[], isSigner: boolean, isWritable: boolean) => ({ - discriminator: (1 << 7) + programKeyIndex, - addressConfig: addressConfig(new Uint8Array(seeds)), - isSigner, - isWritable, - }); - - it('can add extra account metas for execute', async () => { - const extraMeta1Pubkey = Keypair.generate().publicKey; - const extraMeta2Pubkey = Keypair.generate().publicKey; - const extraMeta3Pubkey = Keypair.generate().publicKey; - - // prettier-ignore - connection.getAccountInfo = createMockFetchAccountDataFn([ - fixedAddress(extraMeta1Pubkey, false, false), - fixedAddress(extraMeta2Pubkey, false, false), - fixedAddress(extraMeta3Pubkey, false, false), - pda([ - 3, 0, // First seed: Account key at index 0 (2) - 3, 4, // Second seed: Account key at index 4 (2) - ], false, false), - pda([ - 3, 5, // First seed: Account key at index 5 (2) - 3, 6, // Second seed: Account key at index 6 (2) - ], false, false), - pda([ - 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) - 2, 8, 8, // Second seed: Instruction data 8..16 (3) - ], false, false), - ]); - - const extraMeta4Pubkey = PublicKey.findProgramAddressSync( - [sourcePubkey.toBuffer(), validateStatePubkey.toBuffer()], - transferHookProgramId, - )[0]; - const extraMeta5Pubkey = PublicKey.findProgramAddressSync( - [extraMeta1Pubkey.toBuffer(), extraMeta2Pubkey.toBuffer()], - transferHookProgramId, - )[0]; - const extraMeta6Pubkey = PublicKey.findProgramAddressSync( - [ - Buffer.from('prefix'), - amountInLeBytes, // Instruction data 8..16 - ], - transferHookProgramId, - )[0]; - - // Fail missing key - const rawInstructionMissingKey = new TransactionInstruction({ - keys: [ - // source missing - { pubkey: mintPubkey, isSigner: false, isWritable: false }, - { pubkey: destinationPubkey, isSigner: false, isWritable: true }, - { pubkey: authorityPubkey, isSigner: true, isWritable: false }, - ], - programId: transferHookProgramId, - }); - await expect( - addExtraAccountMetasForExecute( - connection, - rawInstructionMissingKey, - transferHookProgramId, - sourcePubkey, - mintPubkey, - destinationPubkey, - authorityPubkey, - amount, - ), - ).to.be.rejectedWith('Missing required account in instruction'); - - const instruction = new TransactionInstruction({ - keys: [ - { pubkey: sourcePubkey, isSigner: false, isWritable: true }, - { pubkey: mintPubkey, isSigner: false, isWritable: false }, - { pubkey: destinationPubkey, isSigner: false, isWritable: true }, - { pubkey: authorityPubkey, isSigner: true, isWritable: false }, - ], - programId: transferHookProgramId, - }); - - await addExtraAccountMetasForExecute( - connection, - instruction, - transferHookProgramId, - sourcePubkey, - mintPubkey, - destinationPubkey, - authorityPubkey, - amount, - ); - - const checkMetas = [ - { pubkey: sourcePubkey, isSigner: false, isWritable: true }, - { pubkey: mintPubkey, isSigner: false, isWritable: false }, - { pubkey: destinationPubkey, isSigner: false, isWritable: true }, - { pubkey: authorityPubkey, isSigner: true, isWritable: false }, - { pubkey: extraMeta1Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta2Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta3Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta4Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta5Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta6Pubkey, isSigner: false, isWritable: false }, - { pubkey: transferHookProgramId, isSigner: false, isWritable: false }, - { pubkey: validateStatePubkey, isSigner: false, isWritable: false }, - ]; - - expect(instruction.keys).to.eql(checkMetas); - }); - - it('can create a transfer instruction with extra metas', async () => { - // prettier-ignore - connection.getAccountInfo = createMockFetchAccountDataFn([ - pda([ - 3, 0, // First seed: Account key at index 0 (2) - 3, 1, // Second seed: Account key at index 1 (2) - ], false, false), - pda([ - 3, 4, // First seed: Account key at index 4 (2) - ], false, false), - pda([ - 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) - 2, 8, 8, // Second seed: Instruction data 8..16 (3) - ], false, false), - fixedAddress(arbitraryProgramId, false, false), - externalPda(8, [ - 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) - 2, 8, 8, // Second seed: Instruction data 8..16 (3) - 3, 6, // Third seed: Account key at index 6 (2) - ], false, false), - externalPda(8, [ - 1, 14, 97, 110, 111, 116, 104, 101, 114, 95, 112, 114, 101, 102, 105, - 120, // First seed: Literal "another_prefix" (16) - 2, 8, 8, // Second seed: Instruction data 8..16 (3) - 3, 6, // Third seed: Account key at index 6 (2) - 3, 9, // Fourth seed: Account key at index 9 (2) - ], false, false), - ]); - - const extraMeta1Pubkey = PublicKey.findProgramAddressSync( - [ - sourcePubkey.toBuffer(), // Account key at index 0 - mintPubkey.toBuffer(), // Account key at index 1 - ], - transferHookProgramId, - )[0]; - const extraMeta2Pubkey = PublicKey.findProgramAddressSync( - [ - validateStatePubkey.toBuffer(), // Account key at index 4 - ], - transferHookProgramId, - )[0]; - const extraMeta3Pubkey = PublicKey.findProgramAddressSync( - [ - Buffer.from('prefix'), - amountInLeBytes, // Instruction data 8..16 - ], - transferHookProgramId, - )[0]; - const extraMeta4Pubkey = arbitraryProgramId; - const extraMeta5Pubkey = PublicKey.findProgramAddressSync( - [ - Buffer.from('prefix'), - amountInLeBytes, // Instruction data 8..16 - extraMeta2Pubkey.toBuffer(), - ], - extraMeta4Pubkey, // PDA off of the arbitrary program ID - )[0]; - const extraMeta6Pubkey = PublicKey.findProgramAddressSync( - [ - Buffer.from('another_prefix'), - amountInLeBytes, // Instruction data 8..16 - extraMeta2Pubkey.toBuffer(), - extraMeta5Pubkey.toBuffer(), - ], - extraMeta4Pubkey, // PDA off of the arbitrary program ID - )[0]; - - const instruction = await createTransferCheckedWithTransferHookInstruction( - connection, - sourcePubkey, - mintPubkey, - destinationPubkey, - authorityPubkey, - amount, - decimals, - [], - undefined, - TOKEN_2022_PROGRAM_ID, - ); - - const checkMetas = [ - { pubkey: sourcePubkey, isSigner: false, isWritable: true }, - { pubkey: mintPubkey, isSigner: false, isWritable: false }, - { pubkey: destinationPubkey, isSigner: false, isWritable: true }, - { pubkey: authorityPubkey, isSigner: true, isWritable: false }, - { pubkey: extraMeta1Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta2Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta3Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta4Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta5Pubkey, isSigner: false, isWritable: false }, - { pubkey: extraMeta6Pubkey, isSigner: false, isWritable: false }, - { pubkey: transferHookProgramId, isSigner: false, isWritable: false }, - { pubkey: validateStatePubkey, isSigner: false, isWritable: false }, - ]; - - expect(instruction.keys).to.eql(checkMetas); - }); - }); -}); diff --git a/token/js/test/unit/transferfee.test.ts b/token/js/test/unit/transferfee.test.ts deleted file mode 100644 index 0e9c9111ff8..00000000000 --- a/token/js/test/unit/transferfee.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - calculateFee, - calculateEpochFee, - ONE_IN_BASIS_POINTS, - createInitializeTransferFeeConfigInstruction, - decodeInitializeTransferFeeConfigInstructionUnchecked, -} from '../../src'; -import { expect } from 'chai'; -import { Keypair, PublicKey } from '@solana/web3.js'; - -describe('transferFee', () => { - describe('encoding/decoding `InitializeTransferFeeConfig` instructions', () => { - it('should encode and decode with both authorities', () => { - const mint = Keypair.generate().publicKey; - const transferFeeConfigAuthority = Keypair.generate().publicKey; - const withdrawWithheldAuthority = Keypair.generate().publicKey; - const instruction = createInitializeTransferFeeConfigInstruction( - mint, - transferFeeConfigAuthority, - withdrawWithheldAuthority, - 100, - 100n, - ); - const decoded = decodeInitializeTransferFeeConfigInstructionUnchecked(instruction); - expect(decoded.data.transferFeeConfigAuthority).to.eql(transferFeeConfigAuthority); - expect(decoded.data.withdrawWithheldAuthority).to.eql(withdrawWithheldAuthority); - expect(decoded.data.transferFeeBasisPoints).to.eql(100); - expect(decoded.data.maximumFee).to.eql(100n); - }); - it('should encode and decode with no transfer fee config authority', () => { - const mint = Keypair.generate().publicKey; - const withdrawWithheldAuthority = Keypair.generate().publicKey; - const instruction = createInitializeTransferFeeConfigInstruction( - mint, - null, - withdrawWithheldAuthority, - 100, - 100n, - ); - const decoded = decodeInitializeTransferFeeConfigInstructionUnchecked(instruction); - expect(decoded.data.transferFeeConfigAuthority).to.eql(null); - expect(decoded.data.withdrawWithheldAuthority).to.eql(withdrawWithheldAuthority); - expect(decoded.data.transferFeeBasisPoints).to.eql(100); - expect(decoded.data.maximumFee).to.eql(100n); - }); - it('should encode and decode with no withdraw withheld authority', () => { - const mint = Keypair.generate().publicKey; - const transferFeeConfigAuthority = Keypair.generate().publicKey; - const instruction = createInitializeTransferFeeConfigInstruction( - mint, - transferFeeConfigAuthority, - null, - 100, - 100n, - ); - const decoded = decodeInitializeTransferFeeConfigInstructionUnchecked(instruction); - expect(decoded.data.transferFeeConfigAuthority).to.eql(transferFeeConfigAuthority); - expect(decoded.data.withdrawWithheldAuthority).to.eql(null); - expect(decoded.data.transferFeeBasisPoints).to.eql(100); - expect(decoded.data.maximumFee).to.eql(100n); - }); - it('should encode and decode with no authorities', () => { - const mint = Keypair.generate().publicKey; - const instruction = createInitializeTransferFeeConfigInstruction(mint, null, null, 100, 100n); - const decoded = decodeInitializeTransferFeeConfigInstructionUnchecked(instruction); - expect(decoded.data.transferFeeConfigAuthority).to.eql(null); - expect(decoded.data.withdrawWithheldAuthority).to.eql(null); - expect(decoded.data.transferFeeBasisPoints).to.eql(100); - expect(decoded.data.maximumFee).to.eql(100n); - }); - }); - - describe('calculateFee', () => { - it('should return 0 fee when transferFeeBasisPoints is 0', () => { - const transferFee = { - epoch: 1n, - maximumFee: 100n, - transferFeeBasisPoints: 0, - }; - const preFeeAmount = 100n; - const fee = calculateFee(transferFee, preFeeAmount); - expect(fee).to.eql(0n); - }); - - it('should return 0 fee when preFeeAmount is 0', () => { - const transferFee = { - epoch: 1n, - maximumFee: 100n, - transferFeeBasisPoints: 100, - }; - const preFeeAmount = 0n; - const fee = calculateFee(transferFee, preFeeAmount); - expect(fee).to.eql(0n); - }); - - it('should calculate the fee correctly when preFeeAmount is non-zero', () => { - const transferFee = { - epoch: 1n, - maximumFee: 100n, - transferFeeBasisPoints: 50, - }; - const preFeeAmount = 500n; - const fee = calculateFee(transferFee, preFeeAmount); - expect(fee).to.eql(3n); - }); - - it('fee should be equal to maximum fee', () => { - const transferFee = { - epoch: 1n, - maximumFee: 5000n, - transferFeeBasisPoints: 50, - }; - const preFeeAmount = transferFee.maximumFee; - const fee = calculateFee(transferFee, preFeeAmount * ONE_IN_BASIS_POINTS); - expect(fee).to.eql(transferFee.maximumFee); - }); - it('fee should be equal to maximum fee when added 1 to preFeeAmount', () => { - const transferFee = { - epoch: 1n, - maximumFee: 5000n, - transferFeeBasisPoints: 50, - }; - const preFeeAmount = transferFee.maximumFee; - const fee = calculateFee(transferFee, preFeeAmount * ONE_IN_BASIS_POINTS + 1n); - expect(fee).to.eql(transferFee.maximumFee); - }); - }); - - describe('calculateEpochFee', () => { - const transferFeeConfig = { - transferFeeConfigAuthority: PublicKey.default, - withdrawWithheldAuthority: PublicKey.default, - withheldAmount: 500n, - olderTransferFee: { - epoch: 1n, - maximumFee: 100n, - transferFeeBasisPoints: 50, - }, - newerTransferFee: { - epoch: 2n, - maximumFee: 200n, - transferFeeBasisPoints: 75, - }, - }; - - it('should return olderTransferFee when epoch is less than newerTransferFee.epoch', () => { - const preFeeAmount = 200n; - const epoch = 1n; - const fee = calculateEpochFee(transferFeeConfig, epoch, preFeeAmount); - expect(fee).to.eql(1n); - }); - - it('should return newerTransferFee when epoch is greater than or equal to newerTransferFee.epoch', () => { - const preFeeAmount = 200n; - const epoch = 2n; - const fee = calculateEpochFee(transferFeeConfig, epoch, preFeeAmount); - expect(fee).to.eql(2n); - }); - - it('should cap the fee to the maximumFee when calculated fee exceeds maximumFee', () => { - const preFeeAmount = 500n; - const epoch = 2n; - const fee = calculateEpochFee(transferFeeConfig, epoch, preFeeAmount); - expect(fee).to.eql(4n); - }); - }); -}); diff --git a/token/js/tsconfig.all.json b/token/js/tsconfig.all.json deleted file mode 100644 index 985513259e2..00000000000 --- a/token/js/tsconfig.all.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "./tsconfig.root.json", - "references": [ - { - "path": "./tsconfig.cjs.json" - }, - { - "path": "./tsconfig.esm.json" - } - ] -} diff --git a/token/js/tsconfig.base.json b/token/js/tsconfig.base.json deleted file mode 100644 index 90620c4e485..00000000000 --- a/token/js/tsconfig.base.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "include": [], - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Node", - "esModuleInterop": true, - "isolatedModules": true, - "noEmitOnError": true, - "resolveJsonModule": true, - "strict": true, - "stripInternal": true - } -} diff --git a/token/js/tsconfig.cjs.json b/token/js/tsconfig.cjs.json deleted file mode 100644 index 2db9b71569e..00000000000 --- a/token/js/tsconfig.cjs.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "include": ["src"], - "compilerOptions": { - "outDir": "lib/cjs", - "target": "ES2016", - "module": "CommonJS", - "sourceMap": true - } -} diff --git a/token/js/tsconfig.esm.json b/token/js/tsconfig.esm.json deleted file mode 100644 index 25e7e25e751..00000000000 --- a/token/js/tsconfig.esm.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "include": ["src"], - "compilerOptions": { - "outDir": "lib/esm", - "declarationDir": "lib/types", - "target": "ES2020", - "module": "ES2020", - "sourceMap": true, - "declaration": true, - "declarationMap": true - } -} diff --git a/token/js/tsconfig.json b/token/js/tsconfig.json deleted file mode 100644 index 2f9b239bfca..00000000000 --- a/token/js/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.all.json", - "include": ["src", "test"], - "compilerOptions": { - "noEmit": true, - "skipLibCheck": true - } -} diff --git a/token/js/tsconfig.root.json b/token/js/tsconfig.root.json deleted file mode 100644 index fadf294ab43..00000000000 --- a/token/js/tsconfig.root.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "composite": true - } -} diff --git a/token/js/typedoc.json b/token/js/typedoc.json deleted file mode 100644 index c39fc53aee1..00000000000 --- a/token/js/typedoc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entryPoints": ["src/index.ts"], - "out": "docs", - "readme": "README.md" -} diff --git a/token/program-2022-test/Cargo.toml b/token/program-2022-test/Cargo.toml deleted file mode 100644 index 2e280ee764a..00000000000 --- a/token/program-2022-test/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -authors = ["Solana Labs Maintainers "] -description = "SPL-Token 2022 Integration Tests" -edition = "2021" -license = "Apache-2.0" -name = "spl-token-2022-test" -repository = "https://github.com/solana-labs/solana-program-library" -version = "0.0.1" - -[features] -test-sbf = ["zk-ops"] -default = ["zk-ops"] -zk-ops = [] - -[build-dependencies] -walkdir = "2" - -[dev-dependencies] -async-trait = "0.1" -borsh = "1.5.3" -bytemuck = "1.21.0" -futures-util = "0.3" -solana-program = "2.1.0" -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" -spl-associated-token-account = { version = "6.0.0", path = "../../associated-token-account/program" } -spl-elgamal-registry = { version = "0.1.0", path = "../confidential-transfer/elgamal-registry" } -spl-memo = { version = "6.0.0", features = ["no-entrypoint"] } -spl-pod = { version = "0.5.0", path = "../../libraries/pod" } -spl-record = { version = "0.3.0", path = "../../record/program", features = [ - "no-entrypoint", -]} -spl-token-2022 = { version = "6.0.0", path = "../program-2022", features = [ - "no-entrypoint", -] } -spl-token-confidential-transfer-proof-extraction = { version = "0.2.0", path = "../confidential-transfer/proof-extraction" } -spl-token-confidential-transfer-proof-generation = { version = "0.2.0", path = "../confidential-transfer/proof-generation" } -spl-instruction-padding = { version = "0.3.0", path = "../../instruction-padding/program", features = [ - "no-entrypoint", -] } -spl-tlv-account-resolution = { version = "0.9.0", path = "../../libraries/tlv-account-resolution" } -spl-token-client = { version = "0.13.0", path = "../client" } -spl-token-group-interface = { version = "0.5.0", path = "../../token-group/interface" } -spl-token-metadata-interface = { version = "0.6.0", path = "../../token-metadata/interface" } -spl-transfer-hook-example = { version = "0.6", path = "../transfer-hook/example", features = [ - "no-entrypoint", -] } -spl-transfer-hook-interface = { version = "0.9.0", path = "../transfer-hook/interface" } -test-case = "3.3" diff --git a/token/program-2022-test/README.md b/token/program-2022-test/README.md deleted file mode 100644 index 11d5768709b..00000000000 --- a/token/program-2022-test/README.md +++ /dev/null @@ -1,26 +0,0 @@ -## SPL Token 2022 Program Tests - -All of the end-to-end tests for spl-token-2022 exist in this directory. - -### Requirements - -These tests require other built on-chain programs, including: - -* spl-instruction-padding -* spl-transfer-hook-example -* spl-transfer-hook-example-downgrade -* spl-transfer-hook-example-fail -* spl-transfer-hook-example-success -* spl-transfer-hook-example-swap -* spl-transfer-hook-example-swap-with-fee - -Built versions of these programs exist in `tests/fixtures`, and may be -regenerated from the following places in this repo: - -* instruction-padding/program -* token/transfer-hook/example -* token/program-2022-test/transfer-hook-test-programs/downgrade -* token/program-2022-test/transfer-hook-test-programs/fail -* token/program-2022-test/transfer-hook-test-programs/success -* token/program-2022-test/transfer-hook-test-programs/swap -* token/program-2022-test/transfer-hook-test-programs/swap-with-fee diff --git a/token/program-2022-test/build.rs b/token/program-2022-test/build.rs deleted file mode 100644 index 2cde9ec6201..00000000000 --- a/token/program-2022-test/build.rs +++ /dev/null @@ -1,67 +0,0 @@ -extern crate walkdir; - -use { - std::{env, path::Path, process::Command}, - walkdir::WalkDir, -}; - -fn rerun_if_changed(directory: &Path) { - let src = directory.join("src"); - let files_in_src: Vec<_> = WalkDir::new(src) - .into_iter() - .map(|entry| entry.unwrap()) - .filter(|entry| { - if !entry.file_type().is_file() { - return false; - } - true - }) - .map(|f| f.path().to_str().unwrap().to_owned()) - .collect(); - - for file in files_in_src { - if !Path::new(&file).is_file() { - panic!("{} is not a file", file); - } - println!("cargo:rerun-if-changed={}", file); - } - let toml = directory.join("Cargo.toml").to_str().unwrap().to_owned(); - println!("cargo:rerun-if-changed={}", toml); -} - -fn main() { - let cwd = env::current_dir().expect("Unable to get current working directory"); - - let spl_token_2022_dir = cwd - .parent() - .expect("Unable to get parent directory of current working dir") - .join("program-2022"); - rerun_if_changed(&spl_token_2022_dir); - - let instruction_padding_dir = cwd - .parent() - .expect("Unable to get parent directory of current working dir") - .parent() - .expect("Unable to get grandparent directory of current working dir") - .join("instruction-padding") - .join("program"); - rerun_if_changed(&instruction_padding_dir); - - println!("cargo:rerun-if-changed=build.rs"); - - for program_dir in [spl_token_2022_dir, instruction_padding_dir] { - let program_toml = program_dir.join("Cargo.toml"); - let program_toml = format!("{}", program_toml.display()); - let args = vec!["build-sbf", "--manifest-path", &program_toml]; - let output = Command::new("cargo") - .args(&args) - .output() - .expect("Error running cargo build-sbf"); - if let Ok(output_str) = std::str::from_utf8(&output.stdout) { - let subs = output_str.split('\n'); - for sub in subs { - println!("cargo:warning=(not a warning) {}", sub); - } - } - } -} diff --git a/token/program-2022-test/tests/burn.rs b/token/program-2022-test/tests/burn.rs deleted file mode 100644 index df4ab148b2b..00000000000 --- a/token/program-2022-test/tests/burn.rs +++ /dev/null @@ -1,300 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::error::TokenError, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, -}; - -async fn run_basic(context: TestContext) { - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - bob, - .. - } = context.token_context.unwrap(); - - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - - // mint a token - let amount = 10; - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - // unchecked is ok - token_unchecked - .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); - - // checked is ok - token - .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); - - // burn too much is not ok - let error = token - .burn(&alice_account, &alice.pubkey(), amount, &[&alice]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InsufficientFunds as u32) - ) - ))) - ); - - // wrong signer - let error = token - .burn(&alice_account, &bob.pubkey(), 1, &[&bob]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn basic() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_basic(context).await; -} - -#[tokio::test] -async fn basic_with_extension() { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(Pubkey::new_unique()), - transfer_fee_basis_points: 100u16, - maximum_fee: 1_000u64, - }]) - .await - .unwrap(); - run_basic(context).await; -} - -async fn run_self_owned(context: TestContext) { - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - .. - } = context.token_context.unwrap(); - - token - .create_auxiliary_token_account(&alice, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice.pubkey(); - - // mint a token - let amount = 10; - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - // unchecked is ok - token_unchecked - .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); - - // checked is ok - token - .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); -} - -#[tokio::test] -async fn self_owned() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_self_owned(context).await; -} - -#[tokio::test] -async fn self_owned_with_extension() { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(Pubkey::new_unique()), - transfer_fee_basis_points: 100u16, - maximum_fee: 1_000u64, - }]) - .await - .unwrap(); - run_self_owned(context).await; -} - -async fn run_burn_and_close_system_or_incinerator(context: TestContext, non_owner: &Pubkey) { - let TokenContext { - mint_authority, - token, - alice, - .. - } = context.token_context.unwrap(); - - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - - // mint a token - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - 1, - &[&mint_authority], - ) - .await - .unwrap(); - - // transfer token to incinerator/system - let non_owner_account = Keypair::new(); - token - .create_auxiliary_token_account(&non_owner_account, non_owner) - .await - .unwrap(); - let non_owner_account = non_owner_account.pubkey(); - token - .transfer( - &alice_account, - &non_owner_account, - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); - - // can't close when holding tokens - let carlos = Keypair::new(); - let error = token - .close_account( - &non_owner_account, - &solana_program::incinerator::id(), - &carlos.pubkey(), - &[&carlos], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonNativeHasBalance as u32) - ) - ))) - ); - - // but anyone can burn it - token - .burn(&non_owner_account, &carlos.pubkey(), 1, &[&carlos]) - .await - .unwrap(); - - // closing fails if destination is not the incinerator - let error = token - .close_account( - &non_owner_account, - &carlos.pubkey(), - &carlos.pubkey(), - &[&carlos], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidAccountData) - ))) - ); - - let error = token - .close_account( - &non_owner_account, - &solana_program::system_program::id(), - &carlos.pubkey(), - &[&carlos], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidAccountData) - ))) - ); - - // ... and then close it - token.get_new_latest_blockhash().await.unwrap(); - token - .close_account( - &non_owner_account, - &solana_program::incinerator::id(), - &carlos.pubkey(), - &[&carlos], - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn burn_and_close_incinerator_tokens() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_burn_and_close_system_or_incinerator(context, &solana_program::incinerator::id()).await; -} - -#[tokio::test] -async fn burn_and_close_system_tokens() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_burn_and_close_system_or_incinerator(context, &solana_program::system_program::id()).await; -} diff --git a/token/program-2022-test/tests/close_account.rs b/token/program-2022-test/tests/close_account.rs deleted file mode 100644 index d070b9d1134..00000000000 --- a/token/program-2022-test/tests/close_account.rs +++ /dev/null @@ -1,170 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, program_pack::Pack, pubkey::Pubkey, signature::Signer, - signer::keypair::Keypair, system_instruction, transaction::TransactionError, - transport::TransportError, - }, - spl_token_2022::{instruction, state::Account}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, -}; - -#[tokio::test] -async fn success_init_after_close_account() { - let mut context = TestContext::new().await; - let payer = Keypair::from_bytes(&context.context.lock().await.payer.to_bytes()).unwrap(); - context.init_token_with_mint(vec![]).await.unwrap(); - let token = context.token_context.take().unwrap().token; - let token_program_id = spl_token_2022::id(); - let owner = Keypair::new(); - let token_account_keypair = Keypair::new(); - token - .create_auxiliary_token_account(&token_account_keypair, &owner.pubkey()) - .await - .unwrap(); - let token_account = token_account_keypair.pubkey(); - - let destination = Pubkey::new_unique(); - token - .process_ixs( - &[ - instruction::close_account( - &token_program_id, - &token_account, - &destination, - &owner.pubkey(), - &[], - ) - .unwrap(), - system_instruction::create_account( - &payer.pubkey(), - &token_account, - 1_000_000_000, - Account::LEN as u64, - &token_program_id, - ), - instruction::initialize_account( - &token_program_id, - &token_account, - token.get_address(), - &owner.pubkey(), - ) - .unwrap(), - ], - &[&owner, &payer, &token_account_keypair], - ) - .await - .unwrap(); - let destination = token.get_account(destination).await.unwrap(); - assert!(destination.lamports > 0); -} - -#[tokio::test] -async fn fail_init_after_close_account() { - let mut context = TestContext::new().await; - let payer = Keypair::from_bytes(&context.context.lock().await.payer.to_bytes()).unwrap(); - context.init_token_with_mint(vec![]).await.unwrap(); - let token = context.token_context.take().unwrap().token; - let token_program_id = spl_token_2022::id(); - let owner = Keypair::new(); - let token_account_keypair = Keypair::new(); - token - .create_auxiliary_token_account(&token_account_keypair, &owner.pubkey()) - .await - .unwrap(); - let token_account = token_account_keypair.pubkey(); - - let destination = Pubkey::new_unique(); - let error = token - .process_ixs( - &[ - instruction::close_account( - &token_program_id, - &token_account, - &destination, - &owner.pubkey(), - &[], - ) - .unwrap(), - system_instruction::transfer(&payer.pubkey(), &token_account, 1_000_000_000), - instruction::initialize_account( - &token_program_id, - &token_account, - token.get_address(), - &owner.pubkey(), - ) - .unwrap(), - ], - &[&owner, &payer], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(2, InstructionError::InvalidAccountData,) - ))) - ); - let error = token.get_account(destination).await.unwrap_err(); - assert_eq!(error, TokenClientError::AccountNotFound); -} - -#[tokio::test] -async fn fail_init_after_close_mint() { - let close_authority = Keypair::new(); - let mut context = TestContext::new().await; - let payer = Keypair::from_bytes(&context.context.lock().await.payer.to_bytes()).unwrap(); - context - .init_token_with_mint(vec![ExtensionInitializationParams::MintCloseAuthority { - close_authority: Some(close_authority.pubkey()), - }]) - .await - .unwrap(); - let token = context.token_context.take().unwrap().token; - let token_program_id = spl_token_2022::id(); - - let destination = Pubkey::new_unique(); - let error = token - .process_ixs( - &[ - instruction::close_account( - &token_program_id, - token.get_address(), - &destination, - &close_authority.pubkey(), - &[], - ) - .unwrap(), - system_instruction::transfer(&payer.pubkey(), token.get_address(), 1_000_000_000), - instruction::initialize_mint_close_authority( - &token_program_id, - token.get_address(), - None, - ) - .unwrap(), - instruction::initialize_mint( - &token_program_id, - token.get_address(), - &close_authority.pubkey(), - None, - 0, - ) - .unwrap(), - ], - &[&close_authority, &payer], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(2, InstructionError::InvalidAccountData,) - ))) - ); - let error = token.get_account(destination).await.unwrap_err(); - assert_eq!(error, TokenClientError::AccountNotFound); -} diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs deleted file mode 100644 index f0640c54b80..00000000000 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ /dev/null @@ -1,3331 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - bytemuck::Zeroable, - program_test::{ - ConfidentialTokenAccountBalances, ConfidentialTokenAccountMeta, ConfidentialTransferOption, - TestContext, TokenContext, - }, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - signer::{keypair::Keypair, signers::Signers}, - system_instruction, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_elgamal_registry::state::ELGAMAL_REGISTRY_ACCOUNT_LEN, - spl_record::state::RecordData, - spl_token_2022::{ - error::TokenError, - extension::{ - confidential_transfer::{ - self, - account_info::{EmptyAccountAccountInfo, TransferAccountInfo, WithdrawAccountInfo}, - ConfidentialTransferAccount, MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, - }, - BaseStateWithExtensions, ExtensionType, - }, - solana_zk_sdk::{ - encryption::{auth_encryption::*, elgamal::*, pod::elgamal::PodElGamalCiphertext}, - zk_elgamal_proof_program::proof_data::*, - }, - }, - spl_token_client::{ - client::ProgramBanksClientProcessTransaction, - token::{ - ExtensionInitializationParams, ProofAccount, ProofAccountWithCiphertext, Token, - TokenError as TokenClientError, TokenResult, - }, - }, - spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation}, - spl_token_confidential_transfer_proof_generation::{ - transfer::TransferProofData, transfer_with_fee::TransferWithFeeProofData, - withdraw::WithdrawProofData, - }, - std::convert::TryInto, -}; - -#[cfg(feature = "zk-ops")] -const TEST_MAXIMUM_FEE: u64 = 100; -#[cfg(feature = "zk-ops")] -const TEST_FEE_BASIS_POINTS: u16 = 250; - -async fn configure_account_with_option( - token: &Token, - account: &Pubkey, - authority: &Pubkey, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - signing_keypairs: &S, - option: ConfidentialTransferOption, -) -> TokenResult<()> { - match option { - ConfidentialTransferOption::InstructionData => { - token - .confidential_transfer_configure_token_account( - account, - authority, - None, - None, - elgamal_keypair, - aes_key, - signing_keypairs, - ) - .await - } - ConfidentialTransferOption::RecordAccount => { - let pubkey_validity_proof_data = PubkeyValidityProofData::new(elgamal_keypair).unwrap(); - - let pubkey_validity_proof_record_account = Keypair::new(); - let record_account_authority = Keypair::new(); - - token - .confidential_transfer_create_record_account( - &pubkey_validity_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &pubkey_validity_proof_data, - &pubkey_validity_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let pubkey_validity_proof_account = ProofAccount::RecordAccount( - pubkey_validity_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - let result = token - .confidential_transfer_configure_token_account( - account, - authority, - Some(&pubkey_validity_proof_account), - None, - elgamal_keypair, - aes_key, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_record_account( - &pubkey_validity_proof_record_account.pubkey(), - account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - result - } - ConfidentialTransferOption::ContextStateAccount => { - let pubkey_validity_proof_data = PubkeyValidityProofData::new(elgamal_keypair).unwrap(); - - let pubkey_validity_proof_context_account = Keypair::new(); - let context_account_authority = Keypair::new(); - - token - .confidential_transfer_create_context_state_account( - &pubkey_validity_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &pubkey_validity_proof_data, - false, - &[&pubkey_validity_proof_context_account], - ) - .await - .unwrap(); - - let pubkey_validity_proof_account = - ProofAccount::ContextAccount(pubkey_validity_proof_context_account.pubkey()); - - let result = token - .confidential_transfer_configure_token_account( - account, - authority, - Some(&pubkey_validity_proof_account), - None, - elgamal_keypair, - aes_key, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_context_state_account( - &pubkey_validity_proof_context_account.pubkey(), - account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - result - } - } -} - -#[tokio::test] -async fn confidential_transfer_configure_token_account() { - confidential_transfer_configure_token_account_with_option( - ConfidentialTransferOption::InstructionData, - ) - .await; - confidential_transfer_configure_token_account_with_option( - ConfidentialTransferOption::RecordAccount, - ) - .await; - confidential_transfer_configure_token_account_with_option( - ConfidentialTransferOption::ContextStateAccount, - ) - .await; -} - -async fn confidential_transfer_configure_token_account_with_option( - option: ConfidentialTransferOption, -) { - let authority = Keypair::new(); - let auto_approve_new_accounts = false; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { token, alice, .. } = context.token_context.unwrap(); - let alice_account_keypair = Keypair::new(); - token - .create_auxiliary_token_account_with_extension_space( - &alice_account_keypair, - &alice.pubkey(), - vec![ExtensionType::ConfidentialTransferAccount], - ) - .await - .unwrap(); - let elgamal_keypair = - ElGamalKeypair::new_from_signer(&alice, &alice_account_keypair.pubkey().to_bytes()) - .unwrap(); - let aes_key = - AeKey::new_from_signer(&alice, &alice_account_keypair.pubkey().to_bytes()).unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta { - token_account: alice_account_keypair.pubkey(), - elgamal_keypair, - aes_key, - }; - - configure_account_with_option( - &token, - &alice_meta.token_account, - &alice.pubkey(), - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], - option, - ) - .await - .unwrap(); - - let alice_elgamal_pubkey = (*alice_meta.elgamal_keypair.pubkey()).into(); - - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert!(!bool::from(&extension.approved)); - assert!(bool::from(&extension.allow_confidential_credits)); - assert_eq!(extension.elgamal_pubkey, alice_elgamal_pubkey); - assert_eq!( - alice_meta - .aes_key - .decrypt(&(extension.decryptable_available_balance.try_into().unwrap())) - .unwrap(), - 0 - ); - - token - .confidential_transfer_approve_account( - &alice_meta.token_account, - &authority.pubkey(), - &[&authority], - ) - .await - .unwrap(); - - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert!(bool::from(&extension.approved)); - - // Configuring an already initialized account should produce an error - let err = configure_account_with_option( - &token, - &alice_meta.token_account, - &alice.pubkey(), - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], - option, - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::ExtensionAlreadyInitialized as u32), - ) - ))) - ); -} - -#[tokio::test] -async fn confidential_transfer_fail_approving_account_on_wrong_mint() { - let authority = Keypair::new(); - let auto_approve_new_accounts = false; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context_a = TestContext::new().await; - context_a - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let token_a_context = context_a.token_context.unwrap(); - - let mut context_b = TestContext { - context: context_a.context.clone(), - token_context: None, - }; - context_b - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - let TokenContext { token, alice, .. } = context_b.token_context.unwrap(); - let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; - - let err = token_a_context - .token - .confidential_transfer_approve_account( - &alice_meta.token_account, - &authority.pubkey(), - &[&authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn confidential_transfer_enable_disable_confidential_credits() { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; - - token - .confidential_transfer_disable_confidential_credits( - &alice_meta.token_account, - &alice.pubkey(), - &[&alice], - ) - .await - .unwrap(); - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert!(!bool::from(&extension.allow_confidential_credits)); - - token - .mint_to( - &alice_meta.token_account, - &mint_authority.pubkey(), - 10, - &[&mint_authority], - ) - .await - .unwrap(); - - let err = token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - 10, - decimals, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom( - TokenError::ConfidentialTransferDepositsAndTransfersDisabled as u32 - ) - ) - ))) - ); - - token - .confidential_transfer_enable_confidential_credits( - &alice_meta.token_account, - &alice.pubkey(), - &[&alice], - ) - .await - .unwrap(); - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert!(bool::from(&extension.allow_confidential_credits)); - - // Refresh the blockhash since we're doing the same thing twice in a row - token.get_new_latest_blockhash().await.unwrap(); - token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - 10, - decimals, - &[&alice], - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn confidential_transfer_enable_disable_non_confidential_credits() { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - .. - } = context.token_context.unwrap(); - let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; - let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, None, false, false).await; - - token - .mint_to( - &alice_meta.token_account, - &mint_authority.pubkey(), - 10, - &[&mint_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_disable_non_confidential_credits( - &bob_meta.token_account, - &bob.pubkey(), - &[&bob], - ) - .await - .unwrap(); - let state = token - .get_account_info(&bob_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert!(!bool::from(&extension.allow_non_confidential_credits)); - - let err = token - .transfer( - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 10, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonConfidentialTransfersDisabled as u32) - ) - ))) - ); - - token - .confidential_transfer_enable_non_confidential_credits( - &bob_meta.token_account, - &bob.pubkey(), - &[&bob], - ) - .await - .unwrap(); - let state = token - .get_account_info(&bob_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert!(bool::from(&extension.allow_non_confidential_credits)); - - // transfer a different number to change the signature - token - .transfer( - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 9, - &[&alice], - ) - .await - .unwrap(); -} - -#[cfg(feature = "zk-ops")] -async fn empty_account_with_option( - token: &Token, - account: &Pubkey, - authority: &Pubkey, - elgamal_keypair: &ElGamalKeypair, - signing_keypairs: &S, - option: ConfidentialTransferOption, -) -> TokenResult<()> { - match option { - ConfidentialTransferOption::InstructionData => { - token - .confidential_transfer_empty_account( - account, - authority, - None, - None, - elgamal_keypair, - signing_keypairs, - ) - .await - } - ConfidentialTransferOption::RecordAccount => { - let state = token.get_account_info(account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let account_info = EmptyAccountAccountInfo::new(extension); - - let zero_ciphertext_proof_data = - account_info.generate_proof_data(elgamal_keypair).unwrap(); - - let zero_ciphertext_proof_record_account = Keypair::new(); - let record_account_authority = Keypair::new(); - - token - .confidential_transfer_create_record_account( - &zero_ciphertext_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &zero_ciphertext_proof_data, - &zero_ciphertext_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let zero_ciphertext_account = ProofAccount::RecordAccount( - zero_ciphertext_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - let result = token - .confidential_transfer_empty_account( - account, - authority, - Some(&zero_ciphertext_account), - None, - elgamal_keypair, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_record_account( - &zero_ciphertext_proof_record_account.pubkey(), - account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - result - } - ConfidentialTransferOption::ContextStateAccount => { - let state = token.get_account_info(account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let account_info = EmptyAccountAccountInfo::new(extension); - - let zero_ciphertext_proof_data = - account_info.generate_proof_data(elgamal_keypair).unwrap(); - - let zero_ciphertext_proof_context_account = Keypair::new(); - let context_account_authority = Keypair::new(); - - token - .confidential_transfer_create_context_state_account( - &zero_ciphertext_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &zero_ciphertext_proof_data, - false, - &[&zero_ciphertext_proof_context_account], - ) - .await - .unwrap(); - - let zero_ciphertext_account = - ProofAccount::ContextAccount(zero_ciphertext_proof_context_account.pubkey()); - - let result = token - .confidential_transfer_empty_account( - account, - authority, - Some(&zero_ciphertext_account), - None, - elgamal_keypair, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_context_state_account( - &zero_ciphertext_proof_context_account.pubkey(), - account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - result - } - } -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_empty_account() { - confidential_transfer_empty_account_with_option(ConfidentialTransferOption::InstructionData) - .await; - confidential_transfer_empty_account_with_option(ConfidentialTransferOption::RecordAccount) - .await; - confidential_transfer_empty_account_with_option( - ConfidentialTransferOption::ContextStateAccount, - ) - .await; -} - -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_empty_account_with_option(option: ConfidentialTransferOption) { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - - // newly created confidential transfer account should hold no balance and - // therefore, immediately closable - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { token, alice, .. } = context.token_context.unwrap(); - let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; - - empty_account_with_option( - &token, - &alice_meta.token_account, - &alice.pubkey(), - &alice_meta.elgamal_keypair, - &[&alice], - option, - ) - .await - .unwrap(); -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_deposit() { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, Some(2), false, false).await; - - token - .mint_to( - &alice_meta.token_account, - &mint_authority.pubkey(), - 65537, - &[&mint_authority], - ) - .await - .unwrap(); - - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - assert_eq!(state.base.amount, 65537); - let extension = state - .get_extension::() - .unwrap(); - assert_eq!(extension.pending_balance_credit_counter, 0.into()); - assert_eq!(extension.expected_pending_balance_credit_counter, 0.into()); - assert_eq!(extension.actual_pending_balance_credit_counter, 0.into()); - assert_eq!(extension.pending_balance_lo, PodElGamalCiphertext::zeroed()); - assert_eq!(extension.pending_balance_hi, PodElGamalCiphertext::zeroed()); - assert_eq!(extension.available_balance, PodElGamalCiphertext::zeroed()); - - token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - 65537, - decimals, - &[&alice], - ) - .await - .unwrap(); - - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - assert_eq!(state.base.amount, 0); - let extension = state - .get_extension::() - .unwrap(); - assert_eq!(extension.pending_balance_credit_counter, 1.into()); - assert_eq!(extension.expected_pending_balance_credit_counter, 0.into()); - assert_eq!(extension.actual_pending_balance_credit_counter, 0.into()); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 1, - pending_balance_hi: 1, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - // deposit zero amount - token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - 0, - decimals, - &[&alice], - ) - .await - .unwrap(); - - token - .confidential_transfer_apply_pending_balance( - &alice_meta.token_account, - &alice.pubkey(), - None, - alice_meta.elgamal_keypair.secret(), - &alice_meta.aes_key, - &[&alice], - ) - .await - .unwrap(); - - // try to deposit over maximum allowed value - let illegal_amount = MAXIMUM_DEPOSIT_TRANSFER_AMOUNT.checked_add(1).unwrap(); - - token - .mint_to( - &alice_meta.token_account, - &mint_authority.pubkey(), - illegal_amount, - &[&mint_authority], - ) - .await - .unwrap(); - - let err = token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - illegal_amount, - decimals, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MaximumDepositAmountExceeded as u32), - ) - ))) - ); - - // deposit maximum allowed value - token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, - decimals, - &[&alice], - ) - .await - .unwrap(); - - // maximum pending balance credits exceeded - token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - 0, - decimals, - &[&alice], - ) - .await - .unwrap(); - - let err = token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - 1, - decimals, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom( - TokenError::MaximumPendingBalanceCreditCounterExceeded as u32 - ), - ) - ))) - ); - - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - assert_eq!(state.base.amount, 1); - let extension = state - .get_extension::() - .unwrap(); - assert_eq!(extension.pending_balance_credit_counter, 2.into()); - assert_eq!(extension.expected_pending_balance_credit_counter, 2.into()); - assert_eq!(extension.actual_pending_balance_credit_counter, 2.into()); -} - -#[allow(clippy::too_many_arguments)] -#[cfg(feature = "zk-ops")] -async fn withdraw_with_option( - token: &Token, - source_account: &Pubkey, - source_authority: &Pubkey, - withdraw_amount: u64, - decimals: u8, - source_elgamal_keypair: &ElGamalKeypair, - source_aes_key: &AeKey, - signing_keypairs: &S, - option: ConfidentialTransferOption, -) -> TokenResult<()> { - match option { - ConfidentialTransferOption::InstructionData => { - token - .confidential_transfer_withdraw( - source_account, - source_authority, - None, - None, - withdraw_amount, - decimals, - None, - source_elgamal_keypair, - source_aes_key, - signing_keypairs, - ) - .await - } - ConfidentialTransferOption::RecordAccount => { - let state = token.get_account_info(source_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let withdraw_account_info = WithdrawAccountInfo::new(extension); - - let WithdrawProofData { - equality_proof_data, - range_proof_data, - } = withdraw_account_info - .generate_proof_data(withdraw_amount, source_elgamal_keypair, source_aes_key) - .unwrap(); - - let equality_proof_record_account = Keypair::new(); - let range_proof_record_account = Keypair::new(); - let record_account_authority = Keypair::new(); - - token - .confidential_transfer_create_record_account( - &equality_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &equality_proof_data, - &equality_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let equality_proof_account = ProofAccount::RecordAccount( - equality_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - token - .confidential_transfer_create_record_account( - &range_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &range_proof_data, - &range_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let range_proof_account = ProofAccount::RecordAccount( - range_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - let result = token - .confidential_transfer_withdraw( - source_account, - source_authority, - Some(&equality_proof_account), - Some(&range_proof_account), - withdraw_amount, - decimals, - None, - source_elgamal_keypair, - source_aes_key, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_record_account( - &equality_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_record_account( - &range_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - result - } - ConfidentialTransferOption::ContextStateAccount => { - let state = token.get_account_info(source_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let withdraw_account_info = WithdrawAccountInfo::new(extension); - - let WithdrawProofData { - equality_proof_data, - range_proof_data, - } = withdraw_account_info - .generate_proof_data(withdraw_amount, source_elgamal_keypair, source_aes_key) - .unwrap(); - - let equality_proof_context_account = Keypair::new(); - let range_proof_context_account = Keypair::new(); - let context_account_authority = Keypair::new(); - - token - .confidential_transfer_create_context_state_account( - &equality_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &equality_proof_data, - false, - &[&equality_proof_context_account], - ) - .await - .unwrap(); - - let equality_proof_account = - ProofAccount::ContextAccount(equality_proof_context_account.pubkey()); - - token - .confidential_transfer_create_context_state_account( - &range_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &range_proof_data, - false, - &[&range_proof_context_account], - ) - .await - .unwrap(); - - let range_proof_account = - ProofAccount::ContextAccount(range_proof_context_account.pubkey()); - - let result = token - .confidential_transfer_withdraw( - source_account, - source_authority, - Some(&equality_proof_account), - Some(&range_proof_account), - withdraw_amount, - decimals, - None, - source_elgamal_keypair, - source_aes_key, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_context_state_account( - &equality_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_context_state_account( - &range_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - result - } - } -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_withdraw() { - confidential_transfer_withdraw_with_option(ConfidentialTransferOption::InstructionData).await; - confidential_transfer_withdraw_with_option(ConfidentialTransferOption::RecordAccount).await; - confidential_transfer_withdraw_with_option(ConfidentialTransferOption::ContextStateAccount) - .await; -} - -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_withdraw_with_option(option: ConfidentialTransferOption) { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - false, - &mint_authority, - 42, - decimals, - ) - .await; - - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - assert_eq!(state.base.amount, 0); - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 42, - decryptable_available_balance: 42, - }, - ) - .await; - - // withdraw zero amount - withdraw_with_option( - &token, - &alice_meta.token_account, - &alice.pubkey(), - 0, - decimals, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 42, - decryptable_available_balance: 42, - }, - ) - .await; - - // withdraw entire balance - withdraw_with_option( - &token, - &alice_meta.token_account, - &alice.pubkey(), - 42, - decimals, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], - option, - ) - .await - .unwrap(); - - let state = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - assert_eq!(state.base.amount, 42); - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; -} - -#[allow(clippy::too_many_arguments)] -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_with_option( - token: &Token, - source_account: &Pubkey, - destination_account: &Pubkey, - source_authority: &Pubkey, - transfer_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - source_aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - memo: Option<(&str, Vec)>, - signing_keypairs: &S, - option: ConfidentialTransferOption, -) -> TokenResult<()> { - match option { - ConfidentialTransferOption::InstructionData => { - let transfer_token = if let Some((memo, signing_pubkey)) = memo { - token.with_memo(memo, signing_pubkey) - } else { - token - }; - - transfer_token - .confidential_transfer_transfer( - source_account, - destination_account, - source_authority, - None, - None, - None, - transfer_amount, - None, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - signing_keypairs, - ) - .await - } - ConfidentialTransferOption::RecordAccount => { - let state = token.get_account_info(source_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let transfer_account_info = TransferAccountInfo::new(extension); - - let TransferProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - } = transfer_account_info - .generate_split_transfer_proof_data( - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ) - .unwrap(); - - let transfer_amount_auditor_ciphertext_lo = - ciphertext_validity_proof_data_with_ciphertext.ciphertext_lo; - let transfer_amount_auditor_ciphertext_hi = - ciphertext_validity_proof_data_with_ciphertext.ciphertext_hi; - - let equality_proof_record_account = Keypair::new(); - let ciphertext_validity_proof_record_account = Keypair::new(); - let range_proof_record_account = Keypair::new(); - let record_account_authority = Keypair::new(); - - token - .confidential_transfer_create_record_account( - &equality_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &equality_proof_data, - &equality_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let equality_proof_account = ProofAccount::RecordAccount( - equality_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - token - .confidential_transfer_create_record_account( - &ciphertext_validity_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &ciphertext_validity_proof_data_with_ciphertext.proof_data, - &ciphertext_validity_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let ciphertext_validity_proof_account = ProofAccount::RecordAccount( - ciphertext_validity_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - token - .confidential_transfer_create_record_account( - &range_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &range_proof_data, - &range_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let range_proof_account = ProofAccount::RecordAccount( - range_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - let transfer_token = if let Some((memo, signing_pubkey)) = memo { - token.with_memo(memo, signing_pubkey) - } else { - token - }; - - let ciphertext_validity_proof_account_with_ciphertext = ProofAccountWithCiphertext { - proof_account: ciphertext_validity_proof_account, - ciphertext_lo: transfer_amount_auditor_ciphertext_lo, - ciphertext_hi: transfer_amount_auditor_ciphertext_hi, - }; - - let result = transfer_token - .confidential_transfer_transfer( - source_account, - destination_account, - source_authority, - Some(&equality_proof_account), - Some(&ciphertext_validity_proof_account_with_ciphertext), - Some(&range_proof_account), - transfer_amount, - None, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_record_account( - &equality_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_record_account( - &ciphertext_validity_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_record_account( - &range_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - result - } - ConfidentialTransferOption::ContextStateAccount => { - let state = token.get_account_info(source_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let transfer_account_info = TransferAccountInfo::new(extension); - - let TransferProofData { - equality_proof_data, - ciphertext_validity_proof_data_with_ciphertext, - range_proof_data, - } = transfer_account_info - .generate_split_transfer_proof_data( - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ) - .unwrap(); - - let transfer_amount_auditor_ciphertext_lo = - ciphertext_validity_proof_data_with_ciphertext.ciphertext_lo; - let transfer_amount_auditor_ciphertext_hi = - ciphertext_validity_proof_data_with_ciphertext.ciphertext_hi; - - let equality_proof_context_account = Keypair::new(); - let ciphertext_validity_proof_context_account = Keypair::new(); - let range_proof_context_account = Keypair::new(); - let context_account_authority = Keypair::new(); - - token - .confidential_transfer_create_context_state_account( - &equality_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &equality_proof_data, - false, - &[&equality_proof_context_account], - ) - .await - .unwrap(); - - let equality_proof_context_proof_account = - ProofAccount::ContextAccount(equality_proof_context_account.pubkey()); - - token - .confidential_transfer_create_context_state_account( - &ciphertext_validity_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &ciphertext_validity_proof_data_with_ciphertext.proof_data, - false, - &[&ciphertext_validity_proof_context_account], - ) - .await - .unwrap(); - - let ciphertext_validity_proof_context_proof_account = - ProofAccount::ContextAccount(ciphertext_validity_proof_context_account.pubkey()); - - token - .confidential_transfer_create_context_state_account( - &range_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &range_proof_data, - false, - &[&range_proof_context_account], - ) - .await - .unwrap(); - - let range_proof_context_proof_account = - ProofAccount::ContextAccount(range_proof_context_account.pubkey()); - - let transfer_token = if let Some((memo, signing_pubkey)) = memo { - token.with_memo(memo, signing_pubkey) - } else { - token - }; - - let ciphertext_validity_proof_account_with_ciphertext = ProofAccountWithCiphertext { - proof_account: ciphertext_validity_proof_context_proof_account, - ciphertext_lo: transfer_amount_auditor_ciphertext_lo, - ciphertext_hi: transfer_amount_auditor_ciphertext_hi, - }; - - let result = transfer_token - .confidential_transfer_transfer( - source_account, - destination_account, - source_authority, - Some(&equality_proof_context_proof_account), - Some(&ciphertext_validity_proof_account_with_ciphertext), - Some(&range_proof_context_proof_account), - transfer_amount, - None, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_context_state_account( - &equality_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_context_state_account( - &ciphertext_validity_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_context_state_account( - &range_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - result - } - } -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_transfer() { - confidential_transfer_transfer_with_option(ConfidentialTransferOption::InstructionData).await; - confidential_transfer_transfer_with_option(ConfidentialTransferOption::RecordAccount).await; - confidential_transfer_transfer_with_option(ConfidentialTransferOption::ContextStateAccount) - .await; -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn pause_confidential_deposit() { - let authority = Keypair::new(); - let pausable_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::PausableConfig { - authority: pausable_authority.pubkey(), - }, - ]) - .await - .unwrap(); - let TokenContext { - token, - alice, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; - - token - .mint_to( - &alice_meta.token_account, - &mint_authority.pubkey(), - 42, - &[mint_authority], - ) - .await - .unwrap(); - - token - .pause(&pausable_authority.pubkey(), &[&pausable_authority]) - .await - .unwrap(); - - let error = token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - 42, - decimals, - &[alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn pause_confidential_withdraw() { - let authority = Keypair::new(); - let pausable_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::PausableConfig { - authority: pausable_authority.pubkey(), - }, - ]) - .await - .unwrap(); - let TokenContext { - token, - alice, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice, None, false, false).await; - - token - .mint_to( - &alice_meta.token_account, - &mint_authority.pubkey(), - 42, - &[mint_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_deposit( - &alice_meta.token_account, - &alice.pubkey(), - 42, - decimals, - &[&alice], - ) - .await - .unwrap(); - - token - .confidential_transfer_apply_pending_balance( - &alice_meta.token_account, - &alice.pubkey(), - None, - alice_meta.elgamal_keypair.secret(), - &alice_meta.aes_key, - &[&alice], - ) - .await - .unwrap(); - - token - .pause(&pausable_authority.pubkey(), &[&pausable_authority]) - .await - .unwrap(); - - let error = withdraw_with_option( - &token, - &alice_meta.token_account, - &alice.pubkey(), - 42, - decimals, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - &[&alice], - ConfidentialTransferOption::InstructionData, - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn pause_confidential_transfer() { - let authority = Keypair::new(); - let pausable_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::PausableConfig { - authority: pausable_authority.pubkey(), - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - false, - &mint_authority, - 42, - decimals, - ) - .await; - - let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, Some(2), false, false).await; - - // pause it - token - .pause(&pausable_authority.pubkey(), &[&pausable_authority]) - .await - .unwrap(); - let error = confidential_transfer_with_option( - &token, - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 10, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - None, - &[&alice], - ConfidentialTransferOption::InstructionData, - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); -} - -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_transfer_with_option(option: ConfidentialTransferOption) { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - false, - &mint_authority, - 42, - decimals, - ) - .await; - - let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, Some(2), false, false).await; - - // Self-transfer of 0 tokens - confidential_transfer_with_option( - &token, - &alice_meta.token_account, - &alice_meta.token_account, - &alice.pubkey(), - 0, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - alice_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - None, - &[&alice], - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 42, - decryptable_available_balance: 42, - }, - ) - .await; - - // Self-transfer of N tokens - confidential_transfer_with_option( - &token, - &alice_meta.token_account, - &alice_meta.token_account, - &alice.pubkey(), - 42, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - alice_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - None, - &[&alice], - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 42, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - token - .confidential_transfer_apply_pending_balance( - &alice_meta.token_account, - &alice.pubkey(), - None, - alice_meta.elgamal_keypair.secret(), - &alice_meta.aes_key, - &[&alice], - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 42, - decryptable_available_balance: 42, - }, - ) - .await; - - confidential_transfer_with_option( - &token, - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 42, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - None, - &[&alice], - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - bob_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 42, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - confidential_transfer_with_option( - &token, - &bob_meta.token_account, - &bob_meta.token_account, - &bob.pubkey(), - 0, - &bob_meta.elgamal_keypair, - &bob_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - None, - &[&bob], - option, - ) - .await - .unwrap(); - - let err = confidential_transfer_with_option( - &token, - &bob_meta.token_account, - &bob_meta.token_account, - &bob.pubkey(), - 0, - &bob_meta.elgamal_keypair, - &bob_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - None, - &[&bob], - option, - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom( - TokenError::MaximumPendingBalanceCreditCounterExceeded as u32 - ), - ) - ))) - ); - - token - .confidential_transfer_apply_pending_balance( - &bob_meta.token_account, - &bob.pubkey(), - None, - bob_meta.elgamal_keypair.secret(), - &bob_meta.aes_key, - &[&bob], - ) - .await - .unwrap(); - - bob_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 42, - decryptable_available_balance: 42, - }, - ) - .await; -} - -#[allow(clippy::too_many_arguments)] -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_with_fee_with_option( - token: &Token, - source_account: &Pubkey, - destination_account: &Pubkey, - source_authority: &Pubkey, - transfer_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - source_aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey, - fee_rate_basis_points: u16, - maximum_fee: u64, - memo: Option<(&str, Vec)>, - signing_keypairs: &S, - option: ConfidentialTransferOption, -) -> TokenResult<()> { - match option { - ConfidentialTransferOption::InstructionData => { - let transfer_token = if let Some((memo, signing_pubkey)) = memo { - token.with_memo(memo, signing_pubkey) - } else { - token - }; - - transfer_token - .confidential_transfer_transfer_with_fee( - source_account, - destination_account, - source_authority, - None, - None, - None, - None, - None, - transfer_amount, - None, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - fee_rate_basis_points, - maximum_fee, - signing_keypairs, - ) - .await - } - ConfidentialTransferOption::RecordAccount => { - let state = token.get_account_info(source_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let transfer_account_info = TransferAccountInfo::new(extension); - - let TransferWithFeeProofData { - equality_proof_data, - transfer_amount_ciphertext_validity_proof_data_with_ciphertext, - percentage_with_cap_proof_data, - fee_ciphertext_validity_proof_data, - range_proof_data, - } = transfer_account_info - .generate_split_transfer_with_fee_proof_data( - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - fee_rate_basis_points, - maximum_fee, - ) - .unwrap(); - - let transfer_amount_auditor_ciphertext_lo = - transfer_amount_ciphertext_validity_proof_data_with_ciphertext.ciphertext_lo; - let transfer_amount_auditor_ciphertext_hi = - transfer_amount_ciphertext_validity_proof_data_with_ciphertext.ciphertext_hi; - - let equality_proof_record_account = Keypair::new(); - let transfer_amount_ciphertext_validity_proof_record_account = Keypair::new(); - let fee_sigma_proof_record_account = Keypair::new(); - let fee_ciphertext_validity_proof_record_account = Keypair::new(); - let range_proof_record_account = Keypair::new(); - let record_account_authority = Keypair::new(); - - token - .confidential_transfer_create_record_account( - &equality_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &equality_proof_data, - &equality_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let equality_proof_account = ProofAccount::RecordAccount( - equality_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - token - .confidential_transfer_create_record_account( - &transfer_amount_ciphertext_validity_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &transfer_amount_ciphertext_validity_proof_data_with_ciphertext.proof_data, - &transfer_amount_ciphertext_validity_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let transfer_amount_ciphertext_validity_proof_account = ProofAccount::RecordAccount( - transfer_amount_ciphertext_validity_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - token - .confidential_transfer_create_record_account( - &fee_sigma_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &percentage_with_cap_proof_data, - &fee_sigma_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let fee_sigma_proof_account = ProofAccount::RecordAccount( - fee_sigma_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - token - .confidential_transfer_create_record_account( - &fee_ciphertext_validity_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &fee_ciphertext_validity_proof_data, - &fee_ciphertext_validity_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let fee_ciphertext_validity_proof_account = ProofAccount::RecordAccount( - fee_ciphertext_validity_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - token - .confidential_transfer_create_record_account( - &range_proof_record_account.pubkey(), - &record_account_authority.pubkey(), - &range_proof_data, - &range_proof_record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let range_proof_account = ProofAccount::RecordAccount( - range_proof_record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - let transfer_token = if let Some((memo, signing_pubkey)) = memo { - token.with_memo(memo, signing_pubkey) - } else { - token - }; - - let transfer_amount_ciphertext_validity_proof_account_with_ciphertext = - ProofAccountWithCiphertext { - proof_account: transfer_amount_ciphertext_validity_proof_account, - ciphertext_lo: transfer_amount_auditor_ciphertext_lo, - ciphertext_hi: transfer_amount_auditor_ciphertext_hi, - }; - - let result = transfer_token - .confidential_transfer_transfer_with_fee( - source_account, - destination_account, - source_authority, - Some(&equality_proof_account), - Some(&transfer_amount_ciphertext_validity_proof_account_with_ciphertext), - Some(&fee_sigma_proof_account), - Some(&fee_ciphertext_validity_proof_account), - Some(&range_proof_account), - transfer_amount, - None, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - fee_rate_basis_points, - maximum_fee, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_record_account( - &equality_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_record_account( - &transfer_amount_ciphertext_validity_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_record_account( - &fee_sigma_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_record_account( - &fee_ciphertext_validity_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_record_account( - &range_proof_record_account.pubkey(), - source_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - result - } - ConfidentialTransferOption::ContextStateAccount => { - let state = token.get_account_info(source_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let transfer_account_info = TransferAccountInfo::new(extension); - - let TransferWithFeeProofData { - equality_proof_data, - transfer_amount_ciphertext_validity_proof_data_with_ciphertext, - percentage_with_cap_proof_data, - fee_ciphertext_validity_proof_data, - range_proof_data, - } = transfer_account_info - .generate_split_transfer_with_fee_proof_data( - transfer_amount, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - fee_rate_basis_points, - maximum_fee, - ) - .unwrap(); - - let transfer_amount_auditor_ciphertext_lo = - transfer_amount_ciphertext_validity_proof_data_with_ciphertext.ciphertext_lo; - let transfer_amount_auditor_ciphertext_hi = - transfer_amount_ciphertext_validity_proof_data_with_ciphertext.ciphertext_hi; - - let equality_proof_context_account = Keypair::new(); - let transfer_amount_ciphertext_validity_proof_context_account = Keypair::new(); - let percentage_with_cap_proof_context_account = Keypair::new(); - let fee_ciphertext_validity_proof_context_account = Keypair::new(); - let range_proof_context_account = Keypair::new(); - let context_account_authority = Keypair::new(); - - token - .confidential_transfer_create_context_state_account( - &equality_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &equality_proof_data, - false, - &[&equality_proof_context_account], - ) - .await - .unwrap(); - - let equality_proof_context_proof_account = - ProofAccount::ContextAccount(equality_proof_context_account.pubkey()); - - token - .confidential_transfer_create_context_state_account( - &transfer_amount_ciphertext_validity_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &transfer_amount_ciphertext_validity_proof_data_with_ciphertext.proof_data, - false, - &[&transfer_amount_ciphertext_validity_proof_context_account], - ) - .await - .unwrap(); - - let transfer_amount_ciphertext_validity_proof_context_proof_account = - ProofAccount::ContextAccount( - transfer_amount_ciphertext_validity_proof_context_account.pubkey(), - ); - - token - .confidential_transfer_create_context_state_account( - &percentage_with_cap_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &percentage_with_cap_proof_data, - false, - &[&percentage_with_cap_proof_context_account], - ) - .await - .unwrap(); - - let fee_sigma_proof_context_proof_account = - ProofAccount::ContextAccount(percentage_with_cap_proof_context_account.pubkey()); - - token - .confidential_transfer_create_context_state_account( - &fee_ciphertext_validity_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &fee_ciphertext_validity_proof_data, - false, - &[&fee_ciphertext_validity_proof_context_account], - ) - .await - .unwrap(); - - let fee_ciphertext_validity_proof_context_proof_account = ProofAccount::ContextAccount( - fee_ciphertext_validity_proof_context_account.pubkey(), - ); - - token - .confidential_transfer_create_context_state_account( - &range_proof_context_account.pubkey(), - &context_account_authority.pubkey(), - &range_proof_data, - false, - &[&range_proof_context_account], - ) - .await - .unwrap(); - - let range_proof_context_proof_account = - ProofAccount::ContextAccount(range_proof_context_account.pubkey()); - - let transfer_token = if let Some((memo, signing_pubkey)) = memo { - token.with_memo(memo, signing_pubkey) - } else { - token - }; - - let transfer_amount_ciphertext_validity_proof_account_with_ciphertext = - ProofAccountWithCiphertext { - proof_account: transfer_amount_ciphertext_validity_proof_context_proof_account, - ciphertext_lo: transfer_amount_auditor_ciphertext_lo, - ciphertext_hi: transfer_amount_auditor_ciphertext_hi, - }; - - let result = transfer_token - .confidential_transfer_transfer_with_fee( - source_account, - destination_account, - source_authority, - Some(&equality_proof_context_proof_account), - Some(&transfer_amount_ciphertext_validity_proof_account_with_ciphertext), - Some(&fee_sigma_proof_context_proof_account), - Some(&fee_ciphertext_validity_proof_context_proof_account), - Some(&range_proof_context_proof_account), - transfer_amount, - None, - source_elgamal_keypair, - source_aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - fee_rate_basis_points, - maximum_fee, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_context_state_account( - &equality_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_context_state_account( - &transfer_amount_ciphertext_validity_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_context_state_account( - &percentage_with_cap_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_context_state_account( - &fee_ciphertext_validity_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_close_context_state_account( - &range_proof_context_account.pubkey(), - source_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - result - } - } -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_transfer_with_fee() { - confidential_transfer_transfer_with_fee_with_option( - ConfidentialTransferOption::InstructionData, - ) - .await; - confidential_transfer_transfer_with_fee_with_option(ConfidentialTransferOption::RecordAccount) - .await; - confidential_transfer_transfer_with_fee_with_option( - ConfidentialTransferOption::ContextStateAccount, - ) - .await; -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn pause_confidential_transfer_with_fee() { - let transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - - let pausable_authority = Keypair::new(); - let confidential_transfer_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let confidential_transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); - let withdraw_withheld_authority_elgamal_pubkey = - (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }, - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(confidential_transfer_authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(confidential_transfer_fee_authority.pubkey()), - withdraw_withheld_authority_elgamal_pubkey, - }, - ExtensionInitializationParams::PausableConfig { - authority: pausable_authority.pubkey(), - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - true, - &mint_authority, - 100, - decimals, - ) - .await; - - let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, None, false, true).await; - - token - .pause(&pausable_authority.pubkey(), &[&pausable_authority]) - .await - .unwrap(); - - let error = confidential_transfer_with_fee_with_option( - &token, - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 10, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - withdraw_withheld_authority_elgamal_keypair.pubkey(), - TEST_FEE_BASIS_POINTS, - TEST_MAXIMUM_FEE, - None, - &[&alice], - ConfidentialTransferOption::InstructionData, - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); -} - -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_transfer_with_fee_with_option(option: ConfidentialTransferOption) { - let transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - - let confidential_transfer_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let confidential_transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); - let withdraw_withheld_authority_elgamal_pubkey = - (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }, - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(confidential_transfer_authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(confidential_transfer_fee_authority.pubkey()), - withdraw_withheld_authority_elgamal_pubkey, - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - true, - &mint_authority, - 100, - decimals, - ) - .await; - - let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, None, false, true).await; - - // Self-transfer of 0 tokens - confidential_transfer_with_fee_with_option( - &token, - &alice_meta.token_account, - &alice_meta.token_account, - &alice.pubkey(), - 0, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - alice_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - withdraw_withheld_authority_elgamal_keypair.pubkey(), - TEST_FEE_BASIS_POINTS, - TEST_MAXIMUM_FEE, - None, - &[&alice], - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 100, - decryptable_available_balance: 100, - }, - ) - .await; - - // Self-transfers does not incur a fee - confidential_transfer_with_fee_with_option( - &token, - &alice_meta.token_account, - &alice_meta.token_account, - &alice.pubkey(), - 100, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - alice_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - withdraw_withheld_authority_elgamal_keypair.pubkey(), - TEST_FEE_BASIS_POINTS, - TEST_MAXIMUM_FEE, - None, - &[&alice], - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 100, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - token - .confidential_transfer_apply_pending_balance( - &alice_meta.token_account, - &alice.pubkey(), - None, - alice_meta.elgamal_keypair.secret(), - &alice_meta.aes_key, - &[&alice], - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 100, - decryptable_available_balance: 100, - }, - ) - .await; - - confidential_transfer_with_fee_with_option( - &token, - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 100, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - withdraw_withheld_authority_elgamal_keypair.pubkey(), - TEST_FEE_BASIS_POINTS, - TEST_MAXIMUM_FEE, - None, - &[&alice], - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - token - .confidential_transfer_empty_account( - &alice_meta.token_account, - &alice.pubkey(), - None, - None, - &alice_meta.elgamal_keypair, - &[&alice], - ) - .await - .unwrap(); - - let err = token - .confidential_transfer_empty_account( - &bob_meta.token_account, - &bob.pubkey(), - None, - None, - &bob_meta.elgamal_keypair, - &[&bob], - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::ConfidentialTransferAccountHasBalance as u32) - ) - ))) - ); - - bob_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 97, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - token - .confidential_transfer_apply_pending_balance( - &bob_meta.token_account, - &bob.pubkey(), - None, - bob_meta.elgamal_keypair.secret(), - &bob_meta.aes_key, - &[&bob], - ) - .await - .unwrap(); - - bob_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 97, - decryptable_available_balance: 97, - }, - ) - .await; -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_transfer_memo() { - confidential_transfer_transfer_memo_with_option(ConfidentialTransferOption::InstructionData) - .await; - confidential_transfer_transfer_memo_with_option(ConfidentialTransferOption::RecordAccount) - .await; - confidential_transfer_transfer_memo_with_option( - ConfidentialTransferOption::ContextStateAccount, - ) - .await; -} - -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_transfer_memo_with_option(option: ConfidentialTransferOption) { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - false, - &mint_authority, - 42, - decimals, - ) - .await; - - let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, None, true, false).await; - - // transfer without memo - let err = confidential_transfer_with_option( - &token, - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 42, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - None, - &[&alice], - option, - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoMemo as u32) - ) - ))) - ); - - // transfer with memo - confidential_transfer_with_option( - &token, - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 42, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - Some(("🦖", vec![alice.pubkey()])), - &[&alice], - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - bob_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 42, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_transfer_with_fee_and_memo() { - confidential_transfer_transfer_with_fee_and_memo_option( - ConfidentialTransferOption::InstructionData, - ) - .await; - confidential_transfer_transfer_with_fee_and_memo_option( - ConfidentialTransferOption::RecordAccount, - ) - .await; - confidential_transfer_transfer_with_fee_and_memo_option( - ConfidentialTransferOption::ContextStateAccount, - ) - .await; -} - -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_transfer_with_fee_and_memo_option( - option: ConfidentialTransferOption, -) { - let transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - - let confidential_transfer_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let confidential_transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); - let withdraw_withheld_authority_elgamal_pubkey = - (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }, - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(confidential_transfer_authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(confidential_transfer_fee_authority.pubkey()), - withdraw_withheld_authority_elgamal_pubkey, - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - true, - &mint_authority, - 100, - decimals, - ) - .await; - - let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, None, true, true).await; - - let err = confidential_transfer_with_fee_with_option( - &token, - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 100, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - withdraw_withheld_authority_elgamal_keypair.pubkey(), - TEST_FEE_BASIS_POINTS, - TEST_MAXIMUM_FEE, - None, - &[&alice], - option, - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoMemo as u32) - ) - ))) - ); - - confidential_transfer_with_fee_with_option( - &token, - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - 100, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - withdraw_withheld_authority_elgamal_keypair.pubkey(), - TEST_FEE_BASIS_POINTS, - TEST_MAXIMUM_FEE, - Some(("🦖", vec![alice.pubkey()])), - &[&alice], - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - bob_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 97, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; -} - -#[tokio::test] -async fn confidential_transfer_configure_token_account_with_registry() { - let authority = Keypair::new(); - let auto_approve_new_accounts = false; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { token, alice, .. } = context.token_context.unwrap(); - let alice_account_keypair = Keypair::new(); - token - .create_auxiliary_token_account_with_extension_space( - &alice_account_keypair, - &alice.pubkey(), - vec![ExtensionType::ConfidentialTransferAccount], - ) - .await - .unwrap(); - let elgamal_keypair = ElGamalKeypair::new_rand(); - - // create ElGamal registry - let ctx = context.context.lock().await; - let proof_data = - confidential_transfer::instruction::PubkeyValidityProofData::new(&elgamal_keypair).unwrap(); - let proof_location = ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::InstructionData(&proof_data), - ); - - let elgamal_registry_address = spl_elgamal_registry::get_elgamal_registry_address( - &alice.pubkey(), - &spl_elgamal_registry::id(), - ); - - let rent = ctx.banks_client.get_rent().await.unwrap(); - let space = ELGAMAL_REGISTRY_ACCOUNT_LEN; - let system_instruction = system_instruction::transfer( - &ctx.payer.pubkey(), - &elgamal_registry_address, - rent.minimum_balance(space), - ); - let create_registry_instructions = - spl_elgamal_registry::instruction::create_registry(&alice.pubkey(), proof_location) - .unwrap(); - - let instructions = [&[system_instruction], &create_registry_instructions[..]].concat(); - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &alice], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - - // update ElGamal registry - let new_elgamal_keypair = - ElGamalKeypair::new_from_signer(&alice, &alice_account_keypair.pubkey().to_bytes()) - .unwrap(); - let proof_data = - confidential_transfer::instruction::PubkeyValidityProofData::new(&new_elgamal_keypair) - .unwrap(); - let proof_location = ProofLocation::InstructionOffset( - 1.try_into().unwrap(), - ProofData::InstructionData(&proof_data), - ); - - let payer_pubkey = ctx.payer.pubkey(); - let instructions = - spl_elgamal_registry::instruction::update_registry(&alice.pubkey(), proof_location) - .unwrap(); - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &alice], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - drop(ctx); - - // configure account using ElGamal registry - let alice_account_keypair = Keypair::new(); - let alice_token_account = alice_account_keypair.pubkey(); - token - .create_auxiliary_token_account_with_extension_space( - &alice_account_keypair, - &alice.pubkey(), - vec![], // do not allocate space for confidential transfers - ) - .await - .unwrap(); - - token - .confidential_transfer_configure_token_account_with_registry( - &alice_account_keypair.pubkey(), - &elgamal_registry_address, - Some(&payer_pubkey), // test account allocation - ) - .await - .unwrap(); - - let state = token.get_account_info(&alice_token_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert!(!bool::from(&extension.approved)); - assert!(bool::from(&extension.allow_confidential_credits)); - assert_eq!( - extension.elgamal_pubkey, - (*new_elgamal_keypair.pubkey()).into() - ); -} diff --git a/token/program-2022-test/tests/confidential_transfer_fee.rs b/token/program-2022-test/tests/confidential_transfer_fee.rs deleted file mode 100644 index c2664525c5f..00000000000 --- a/token/program-2022-test/tests/confidential_transfer_fee.rs +++ /dev/null @@ -1,1224 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - bytemuck::Zeroable, - program_test::{ConfidentialTransferOption, TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - signer::{keypair::Keypair, signers::Signers}, - transaction::TransactionError, - transport::TransportError, - }, - spl_record::state::RecordData, - spl_token_2022::{ - error::TokenError, - extension::{ - confidential_transfer::{ - ConfidentialTransferAccount, ConfidentialTransferMint, DecryptableBalance, - }, - confidential_transfer_fee::{ - account_info::WithheldTokensInfo, ConfidentialTransferFeeAmount, - ConfidentialTransferFeeConfig, - }, - transfer_fee::TransferFee, - BaseStateWithExtensions, ExtensionType, - }, - instruction, - solana_zk_sdk::encryption::{ - auth_encryption::*, elgamal::*, pod::elgamal::PodElGamalCiphertext, - }, - }, - spl_token_client::{ - client::{ProgramBanksClientProcessTransaction, SendTransaction, SimulateTransaction}, - token::{ - ExtensionInitializationParams, ProofAccount, Token, TokenError as TokenClientError, - TokenResult, - }, - }, - std::convert::TryInto, -}; - -#[cfg(feature = "zk-ops")] -const TEST_MAXIMUM_FEE: u64 = 100; -#[cfg(feature = "zk-ops")] -const TEST_FEE_BASIS_POINTS: u16 = 250; - -struct ConfidentialTokenAccountMeta { - token_account: Pubkey, - elgamal_keypair: ElGamalKeypair, - aes_key: AeKey, -} - -impl ConfidentialTokenAccountMeta { - async fn new( - token: &Token, - owner: &Keypair, - mint_authority: &Keypair, - amount: u64, - decimals: u8, - ) -> Self - where - T: SendTransaction + SimulateTransaction, - { - let token_account_keypair = Keypair::new(); - let extensions = vec![ - ExtensionType::ConfidentialTransferAccount, - ExtensionType::ConfidentialTransferFeeAmount, - ]; - - token - .create_auxiliary_token_account_with_extension_space( - &token_account_keypair, - &owner.pubkey(), - extensions, - ) - .await - .unwrap(); - let token_account = token_account_keypair.pubkey(); - - let elgamal_keypair = - ElGamalKeypair::new_from_signer(owner, &token_account.to_bytes()).unwrap(); - let aes_key = AeKey::new_from_signer(owner, &token_account.to_bytes()).unwrap(); - - token - .confidential_transfer_configure_token_account( - &token_account, - &owner.pubkey(), - None, - None, - &elgamal_keypair, - &aes_key, - &[owner], - ) - .await - .unwrap(); - - token - .mint_to( - &token_account, - &mint_authority.pubkey(), - amount, - &[mint_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_deposit( - &token_account, - &owner.pubkey(), - amount, - decimals, - &[owner], - ) - .await - .unwrap(); - - token - .confidential_transfer_apply_pending_balance( - &token_account, - &owner.pubkey(), - None, - elgamal_keypair.secret(), - &aes_key, - &[owner], - ) - .await - .unwrap(); - - Self { - token_account, - elgamal_keypair, - aes_key, - } - } - - #[cfg(feature = "zk-ops")] - async fn check_balances(&self, token: &Token, expected: ConfidentialTokenAccountBalances) - where - T: SendTransaction + SimulateTransaction, - { - let state = token.get_account_info(&self.token_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - - assert_eq!( - self.elgamal_keypair - .secret() - .decrypt_u32(&extension.pending_balance_lo.try_into().unwrap()) - .unwrap(), - expected.pending_balance_lo, - ); - assert_eq!( - self.elgamal_keypair - .secret() - .decrypt_u32(&extension.pending_balance_hi.try_into().unwrap()) - .unwrap(), - expected.pending_balance_hi, - ); - assert_eq!( - self.elgamal_keypair - .secret() - .decrypt_u32(&extension.available_balance.try_into().unwrap()) - .unwrap(), - expected.available_balance, - ); - assert_eq!( - self.aes_key - .decrypt(&extension.decryptable_available_balance.try_into().unwrap()) - .unwrap(), - expected.decryptable_available_balance, - ); - } -} - -#[cfg(feature = "zk-ops")] -struct ConfidentialTokenAccountBalances { - pending_balance_lo: u64, - pending_balance_hi: u64, - available_balance: u64, - decryptable_available_balance: u64, -} - -#[cfg(feature = "zk-ops")] -async fn check_withheld_amount_in_mint( - token: &Token, - withdraw_withheld_authority_elgamal_keypair: &ElGamalKeypair, - expected: u64, -) where - T: SendTransaction + SimulateTransaction, -{ - let state = token.get_mint_info().await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - - let decrypted_amount = withdraw_withheld_authority_elgamal_keypair - .secret() - .decrypt_u32(&extension.withheld_amount.try_into().unwrap()) - .unwrap(); - - assert_eq!(decrypted_amount, expected); -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_fee_config() { - let transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - - let confidential_transfer_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let confidential_transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); - let withdraw_withheld_authority_elgamal_pubkey = - (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - - // Try invalid combinations of extensions - let err = context - .init_token_with_mint(vec![ - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }, - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(confidential_transfer_authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 3, - InstructionError::Custom(TokenError::InvalidExtensionCombination as u32), - ) - ))) - ); - - let err = context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(confidential_transfer_authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(confidential_transfer_fee_authority.pubkey()), - withdraw_withheld_authority_elgamal_pubkey, - }, - ]) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 3, - InstructionError::Custom(TokenError::InvalidExtensionCombination as u32), - ) - ))) - ); - - let err = context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(confidential_transfer_fee_authority.pubkey()), - withdraw_withheld_authority_elgamal_pubkey, - }, - ]) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 2, - InstructionError::Custom(TokenError::InvalidExtensionCombination as u32), - ) - ))) - ); - - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }, - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(confidential_transfer_authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(confidential_transfer_fee_authority.pubkey()), - withdraw_withheld_authority_elgamal_pubkey, - }, - ]) - .await - .unwrap(); -} - -#[tokio::test] -async fn confidential_transfer_initialize_and_update_mint() { - let authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ]) - .await - .unwrap(); - - let TokenContext { token, .. } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - - assert_eq!( - extension.authority, - Some(authority.pubkey()).try_into().unwrap() - ); - assert_eq!( - extension.auto_approve_new_accounts, - auto_approve_new_accounts.into() - ); - assert_eq!( - extension.auditor_elgamal_pubkey, - Some(auditor_elgamal_pubkey).try_into().unwrap() - ); - - // Change the authority - let new_authority = Keypair::new(); - let wrong_keypair = Keypair::new(); - - let err = token - .set_authority( - token.get_address(), - &wrong_keypair.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::ConfidentialTransferMint, - &[&wrong_keypair], - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - token - .set_authority( - token.get_address(), - &authority.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::ConfidentialTransferMint, - &[&authority], - ) - .await - .unwrap(); - - // New authority can change mint parameters while the old cannot - let new_auto_approve_new_accounts = false; - let new_auditor_elgamal_pubkey = None; - - let err = token - .confidential_transfer_update_mint( - &authority.pubkey(), - new_auto_approve_new_accounts, - new_auditor_elgamal_pubkey, - &[&authority], - ) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - token - .confidential_transfer_update_mint( - &new_authority.pubkey(), - new_auto_approve_new_accounts, - new_auditor_elgamal_pubkey, - &[&new_authority], - ) - .await - .unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.authority, - Some(new_authority.pubkey()).try_into().unwrap() - ); - assert_eq!( - extension.auto_approve_new_accounts, - new_auto_approve_new_accounts.into(), - ); - assert_eq!( - extension.auditor_elgamal_pubkey, - new_auditor_elgamal_pubkey.try_into().unwrap(), - ); - - // Set new authority to None - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - instruction::AuthorityType::ConfidentialTransferMint, - &[&new_authority], - ) - .await - .unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, None.try_into().unwrap()); -} - -#[allow(clippy::too_many_arguments)] -#[cfg(feature = "zk-ops")] -async fn withdraw_withheld_tokens_from_mint_with_option( - token: &Token, - destination_account: &Pubkey, - withdraw_withheld_authority: &Pubkey, - withdraw_withheld_authority_elgamal_keypair: &ElGamalKeypair, - destination_elgamal_pubkey: &ElGamalPubkey, - new_decryptable_available_balance: &DecryptableBalance, - signing_keypairs: &S, - option: ConfidentialTransferOption, -) -> TokenResult<()> { - match option { - ConfidentialTransferOption::InstructionData => { - token - .confidential_transfer_withdraw_withheld_tokens_from_mint( - destination_account, - withdraw_withheld_authority, - None, - None, - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - new_decryptable_available_balance, - signing_keypairs, - ) - .await - } - ConfidentialTransferOption::RecordAccount => { - let state = token.get_mint_info().await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let account_info = WithheldTokensInfo::new(&extension.withheld_amount); - - let equality_proof = account_info - .generate_proof_data( - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - ) - .unwrap(); - - let record_account = Keypair::new(); - let record_account_authority = Keypair::new(); - - token - .confidential_transfer_create_record_account( - &record_account.pubkey(), - &record_account_authority.pubkey(), - &equality_proof, - &record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let proof_account = ProofAccount::RecordAccount( - record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - let result = token - .confidential_transfer_withdraw_withheld_tokens_from_mint( - destination_account, - withdraw_withheld_authority, - Some(&proof_account), - None, - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - new_decryptable_available_balance, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_record_account( - &record_account.pubkey(), - destination_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - result - } - ConfidentialTransferOption::ContextStateAccount => { - let state = token.get_mint_info().await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let account_info = WithheldTokensInfo::new(&extension.withheld_amount); - - let equality_proof = account_info - .generate_proof_data( - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - ) - .unwrap(); - - let context_account = Keypair::new(); - let context_account_authority = Keypair::new(); - - token - .confidential_transfer_create_context_state_account( - &context_account.pubkey(), - &context_account_authority.pubkey(), - &equality_proof, - false, - &[&context_account], - ) - .await - .unwrap(); - - let proof_account = ProofAccount::ContextAccount(context_account.pubkey()); - - let result = token - .confidential_transfer_withdraw_withheld_tokens_from_mint( - destination_account, - withdraw_withheld_authority, - Some(&proof_account), - None, - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - new_decryptable_available_balance, - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_context_state_account( - &context_account.pubkey(), - destination_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - result - } - } -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_withdraw_withheld_tokens_from_mint() { - confidential_transfer_withdraw_withheld_tokens_from_mint_with_option( - ConfidentialTransferOption::InstructionData, - ) - .await; - confidential_transfer_withdraw_withheld_tokens_from_mint_with_option( - ConfidentialTransferOption::RecordAccount, - ) - .await; - confidential_transfer_withdraw_withheld_tokens_from_mint_with_option( - ConfidentialTransferOption::ContextStateAccount, - ) - .await; -} - -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_withdraw_withheld_tokens_from_mint_with_option( - option: ConfidentialTransferOption, -) { - let transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - - let confidential_transfer_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let confidential_transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); - let withdraw_withheld_authority_elgamal_pubkey = - (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }, - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(confidential_transfer_authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(confidential_transfer_fee_authority.pubkey()), - withdraw_withheld_authority_elgamal_pubkey, - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = - ConfidentialTokenAccountMeta::new(&token, &alice, &mint_authority, 100, decimals).await; - let bob_meta = - ConfidentialTokenAccountMeta::new(&token, &bob, &mint_authority, 0, decimals).await; - - let transfer_fee_parameters = TransferFee { - epoch: 0.into(), - maximum_fee: TEST_MAXIMUM_FEE.into(), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), - }; - - // Test fee is 2.5% so the withheld fees should be 3 - token - .confidential_transfer_transfer_with_fee( - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - None, - None, - None, - None, - None, - 100, - None, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - withdraw_withheld_authority_elgamal_keypair.pubkey(), - transfer_fee_parameters.transfer_fee_basis_points.into(), - transfer_fee_parameters.maximum_fee.into(), - &[&alice], - ) - .await - .unwrap(); - - let new_decryptable_available_balance = alice_meta.aes_key.encrypt(0); - token - .confidential_transfer_withdraw_withheld_tokens_from_mint( - &alice_meta.token_account, - &withdraw_withheld_authority.pubkey(), - None, - None, - &withdraw_withheld_authority_elgamal_keypair, - alice_meta.elgamal_keypair.pubkey(), - &new_decryptable_available_balance.into(), - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - - // withheld fees are not harvested to mint yet - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - token - .confidential_transfer_harvest_withheld_tokens_to_mint(&[&bob_meta.token_account]) - .await - .unwrap(); - - let state = token - .get_account_info(&bob_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert_eq!(extension.withheld_amount, PodElGamalCiphertext::zeroed()); - - // calculate and encrypt fee to attach to the `WithdrawWithheldTokensFromMint` - // instruction data - let fee = transfer_fee_parameters.calculate_fee(100).unwrap(); - let new_decryptable_available_balance = alice_meta.aes_key.encrypt(fee); - - check_withheld_amount_in_mint(&token, &withdraw_withheld_authority_elgamal_keypair, fee).await; - - withdraw_withheld_tokens_from_mint_with_option( - &token, - &alice_meta.token_account, - &withdraw_withheld_authority.pubkey(), - &withdraw_withheld_authority_elgamal_keypair, - alice_meta.elgamal_keypair.pubkey(), - &new_decryptable_available_balance.into(), - &[&withdraw_withheld_authority], - option, - ) - .await - .unwrap(); - - // withheld fees are withdrawn back to alice's account - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 3, - decryptable_available_balance: 3, - }, - ) - .await; - - check_withheld_amount_in_mint(&token, &withdraw_withheld_authority_elgamal_keypair, 0).await; -} - -#[allow(clippy::too_many_arguments)] -#[cfg(feature = "zk-ops")] -async fn withdraw_withheld_tokens_from_accounts_with_option( - token: &Token, - destination_account: &Pubkey, - withdraw_withheld_authority: &Pubkey, - withdraw_withheld_authority_elgamal_keypair: &ElGamalKeypair, - destination_elgamal_pubkey: &ElGamalPubkey, - new_decryptable_available_balance: &DecryptableBalance, - signing_keypairs: &S, - source: &Pubkey, - option: ConfidentialTransferOption, -) -> TokenResult<()> { - match option { - ConfidentialTransferOption::InstructionData => { - token - .confidential_transfer_withdraw_withheld_tokens_from_accounts( - destination_account, - withdraw_withheld_authority, - None, - None, - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - new_decryptable_available_balance, - &[source], - signing_keypairs, - ) - .await - } - ConfidentialTransferOption::RecordAccount => { - let state = token.get_account_info(source).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let account_info = WithheldTokensInfo::new(&extension.withheld_amount); - - let equality_proof = account_info - .generate_proof_data( - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - ) - .unwrap(); - - let record_account = Keypair::new(); - let record_account_authority = Keypair::new(); - - token - .confidential_transfer_create_record_account( - &record_account.pubkey(), - &record_account_authority.pubkey(), - &equality_proof, - &record_account, - &record_account_authority, - ) - .await - .unwrap(); - - let proof_account = ProofAccount::RecordAccount( - record_account.pubkey(), - RecordData::WRITABLE_START_INDEX as u32, - ); - - let result = token - .confidential_transfer_withdraw_withheld_tokens_from_accounts( - destination_account, - withdraw_withheld_authority, - Some(&proof_account), - None, - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - new_decryptable_available_balance, - &[source], - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_record_account( - &record_account.pubkey(), - destination_account, - &record_account_authority.pubkey(), - &[&record_account_authority], - ) - .await - .unwrap(); - - result - } - ConfidentialTransferOption::ContextStateAccount => { - let state = token.get_account_info(source).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - let account_info = WithheldTokensInfo::new(&extension.withheld_amount); - - let equality_proof = account_info - .generate_proof_data( - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - ) - .unwrap(); - - let context_account = Keypair::new(); - let context_account_authority = Keypair::new(); - - token - .confidential_transfer_create_context_state_account( - &context_account.pubkey(), - &context_account_authority.pubkey(), - &equality_proof, - false, - &[&context_account], - ) - .await - .unwrap(); - - let proof_account = ProofAccount::ContextAccount(context_account.pubkey()); - - let result = token - .confidential_transfer_withdraw_withheld_tokens_from_accounts( - destination_account, - withdraw_withheld_authority, - Some(&proof_account), - None, - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - new_decryptable_available_balance, - &[source], - signing_keypairs, - ) - .await; - - token - .confidential_transfer_close_context_state_account( - &context_account.pubkey(), - destination_account, - &context_account_authority.pubkey(), - &[&context_account_authority], - ) - .await - .unwrap(); - - result - } - } -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_withdraw_withheld_tokens_from_accounts() { - confidential_transfer_withdraw_withheld_tokens_from_accounts_with_option( - ConfidentialTransferOption::InstructionData, - ) - .await; - confidential_transfer_withdraw_withheld_tokens_from_accounts_with_option( - ConfidentialTransferOption::RecordAccount, - ) - .await; - confidential_transfer_withdraw_withheld_tokens_from_accounts_with_option( - ConfidentialTransferOption::ContextStateAccount, - ) - .await; -} - -#[cfg(feature = "zk-ops")] -async fn confidential_transfer_withdraw_withheld_tokens_from_accounts_with_option( - option: ConfidentialTransferOption, -) { - let transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - - let confidential_transfer_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let confidential_transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); - let withdraw_withheld_authority_elgamal_pubkey = - (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }, - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(confidential_transfer_authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(confidential_transfer_fee_authority.pubkey()), - withdraw_withheld_authority_elgamal_pubkey, - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = - ConfidentialTokenAccountMeta::new(&token, &alice, &mint_authority, 100, decimals).await; - let bob_meta = - ConfidentialTokenAccountMeta::new(&token, &bob, &mint_authority, 0, decimals).await; - - let transfer_fee_parameters = TransferFee { - epoch: 0.into(), - maximum_fee: TEST_MAXIMUM_FEE.into(), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), - }; - - // Test fee is 2.5% so the withheld fees should be 3 - token - .confidential_transfer_transfer_with_fee( - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - None, - None, - None, - None, - None, - 100, - None, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - withdraw_withheld_authority_elgamal_keypair.pubkey(), - transfer_fee_parameters.transfer_fee_basis_points.into(), - transfer_fee_parameters.maximum_fee.into(), - &[&alice], - ) - .await - .unwrap(); - - let fee = transfer_fee_parameters.calculate_fee(100).unwrap(); - let new_decryptable_available_balance = alice_meta.aes_key.encrypt(fee); - withdraw_withheld_tokens_from_accounts_with_option( - &token, - &alice_meta.token_account, - &withdraw_withheld_authority.pubkey(), - &withdraw_withheld_authority_elgamal_keypair, - alice_meta.elgamal_keypair.pubkey(), - &new_decryptable_available_balance.into(), - &[&withdraw_withheld_authority], - &bob_meta.token_account, - option, - ) - .await - .unwrap(); - - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: fee, - decryptable_available_balance: fee, - }, - ) - .await; - - bob_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 97, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - let state = token - .get_account_info(&bob_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert_eq!(extension.withheld_amount, PodElGamalCiphertext::zeroed()); -} - -#[cfg(feature = "zk-ops")] -#[tokio::test] -async fn confidential_transfer_harvest_withheld_tokens_to_mint() { - let transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - - let confidential_transfer_authority = Keypair::new(); - let auto_approve_new_accounts = true; - let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); - let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); - - let confidential_transfer_fee_authority = Keypair::new(); - let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); - let withdraw_withheld_authority_elgamal_pubkey = - (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }, - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(confidential_transfer_authority.pubkey()), - auto_approve_new_accounts, - auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), - }, - ExtensionInitializationParams::ConfidentialTransferFeeConfig { - authority: Some(confidential_transfer_fee_authority.pubkey()), - withdraw_withheld_authority_elgamal_pubkey, - }, - ]) - .await - .unwrap(); - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = context.token_context.unwrap(); - - let alice_meta = - ConfidentialTokenAccountMeta::new(&token, &alice, &mint_authority, 100, decimals).await; - let bob_meta = - ConfidentialTokenAccountMeta::new(&token, &bob, &mint_authority, 0, decimals).await; - - let transfer_fee_parameters = TransferFee { - epoch: 0.into(), - maximum_fee: TEST_MAXIMUM_FEE.into(), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), - }; - - // there are no withheld fees in bob's account yet, but try harvesting - token - .confidential_transfer_harvest_withheld_tokens_to_mint(&[&bob_meta.token_account]) - .await - .unwrap(); - - // Test fee is 2.5% so the withheld fees should be 3 - token - .confidential_transfer_transfer_with_fee( - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - None, - None, - None, - None, - None, - 100, - None, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - Some(auditor_elgamal_keypair.pubkey()), - withdraw_withheld_authority_elgamal_keypair.pubkey(), - transfer_fee_parameters.transfer_fee_basis_points.into(), - transfer_fee_parameters.maximum_fee.into(), - &[&alice], - ) - .await - .unwrap(); - - // disable harvest withheld tokens to mint - token - .confidential_transfer_disable_harvest_to_mint( - &confidential_transfer_fee_authority.pubkey(), - &[&confidential_transfer_fee_authority], - ) - .await - .unwrap(); - - let err = token - .confidential_transfer_harvest_withheld_tokens_to_mint(&[&bob_meta.token_account]) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::HarvestToMintDisabled as u32), - ) - ))) - ); - - // enable harvest withheld tokens to mint - token - .confidential_transfer_enable_harvest_to_mint( - &confidential_transfer_fee_authority.pubkey(), - &[&confidential_transfer_fee_authority], - ) - .await - .unwrap(); - - // Refresh the blockhash since we're doing the same thing twice in a row - token.get_new_latest_blockhash().await.unwrap(); - token - .confidential_transfer_harvest_withheld_tokens_to_mint(&[&bob_meta.token_account]) - .await - .unwrap(); - - let state = token - .get_account_info(&bob_meta.token_account) - .await - .unwrap(); - let extension = state - .get_extension::() - .unwrap(); - assert_eq!(extension.withheld_amount, PodElGamalCiphertext::zeroed()); - - // calculate and encrypt fee to attach to the `WithdrawWithheldTokensFromMint` - // instruction data - let fee = transfer_fee_parameters.calculate_fee(100).unwrap(); - - check_withheld_amount_in_mint(&token, &withdraw_withheld_authority_elgamal_keypair, fee).await; -} diff --git a/token/program-2022-test/tests/cpi_guard.rs b/token/program-2022-test/tests/cpi_guard.rs deleted file mode 100644 index 16b3ab7541f..00000000000 --- a/token/program-2022-test/tests/cpi_guard.rs +++ /dev/null @@ -1,814 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{keypair_clone, TestContext, TokenContext}, - solana_program_test::{ - processor, - tokio::{self, sync::Mutex}, - ProgramTest, - }, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_instruction_padding::instruction::wrap_instruction, - spl_token_2022::{ - error::TokenError, - extension::{ - cpi_guard::{self, CpiGuard}, - BaseStateWithExtensions, ExtensionType, - }, - instruction::{self, AuthorityType}, - processor::Processor as SplToken2022Processor, - }, - spl_token_client::{ - client::ProgramBanksClientProcessTransaction, - token::{Token, TokenError as TokenClientError}, - }, - std::sync::Arc, -}; - -// set up a bank and bank client with spl token 2022 and the instruction padder -// also creates a token with no extensions and inits two token accounts -async fn make_context() -> TestContext { - // TODO this may be removed when we upgrade to a solana version with a fixed - // `get_stack_height()` stub - if std::env::var("BPF_OUT_DIR").is_err() && std::env::var("SBF_OUT_DIR").is_err() { - panic!("CpiGuard tests MUST be invoked with `cargo test-sbf`, NOT `cargo test --feature test-sbf`. \ - In a non-BPF context, `get_stack_height()` always returns 0, and all tests WILL fail."); - } - - let mut program_test = ProgramTest::new( - "spl_token_2022", - spl_token_2022::id(), - processor!(SplToken2022Processor::process), - ); - - program_test.add_program( - "spl_instruction_padding", - spl_instruction_padding::id(), - processor!(spl_instruction_padding::processor::process), - ); - - let program_context = program_test.start_with_context().await; - let program_context = Arc::new(Mutex::new(program_context)); - - let mut test_context = TestContext { - context: program_context, - token_context: None, - }; - - test_context.init_token_with_mint(vec![]).await.unwrap(); - let token_context = test_context.token_context.as_ref().unwrap(); - - token_context - .token - .create_auxiliary_token_account_with_extension_space( - &token_context.alice, - &token_context.alice.pubkey(), - vec![ExtensionType::CpiGuard], - ) - .await - .unwrap(); - - token_context - .token - .create_auxiliary_token_account(&token_context.bob, &token_context.bob.pubkey()) - .await - .unwrap(); - - test_context -} - -fn client_error(token_error: TokenError) -> TokenClientError { - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::Custom(token_error as u32)), - ))) -} - -#[tokio::test] -async fn test_cpi_guard_enable_disable() { - let context = make_context().await; - let TokenContext { - token, alice, bob, .. - } = context.token_context.unwrap(); - - // enable guard properly - token - .enable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - // guard is enabled - let alice_state = token.get_account_info(&alice.pubkey()).await.unwrap(); - let extension = alice_state.get_extension::().unwrap(); - assert!(bool::from(extension.lock_cpi)); - - // attempt to disable through cpi. this fails - let error = token - .process_ixs( - &[wrap_instruction( - spl_instruction_padding::id(), - cpi_guard::instruction::disable_cpi_guard( - &spl_token_2022::id(), - &alice.pubkey(), - &alice.pubkey(), - &[], - ) - .unwrap(), - vec![], - 0, - ) - .unwrap()], - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!(error, client_error(TokenError::CpiGuardSettingsLocked)); - - // guard remains enabled - let alice_state = token.get_account_info(&alice.pubkey()).await.unwrap(); - let extension = alice_state.get_extension::().unwrap(); - assert!(bool::from(extension.lock_cpi)); - - // disable guard properly - token - .disable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - // guard is disabled - let alice_state = token.get_account_info(&alice.pubkey()).await.unwrap(); - let extension = alice_state.get_extension::().unwrap(); - assert!(!bool::from(extension.lock_cpi)); - - // attempt to enable through cpi. this fails - let error = token - .process_ixs( - &[wrap_instruction( - spl_instruction_padding::id(), - cpi_guard::instruction::enable_cpi_guard( - &spl_token_2022::id(), - &alice.pubkey(), - &alice.pubkey(), - &[], - ) - .unwrap(), - vec![], - 0, - ) - .unwrap()], - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!(error, client_error(TokenError::CpiGuardSettingsLocked)); - - // guard remains disabled - let alice_state = token.get_account_info(&alice.pubkey()).await.unwrap(); - let extension = alice_state.get_extension::().unwrap(); - assert!(!bool::from(extension.lock_cpi)); - - // enable works with realloc - token - .reallocate( - &bob.pubkey(), - &bob.pubkey(), - &[ExtensionType::CpiGuard], - &[&bob], - ) - .await - .unwrap(); - - token - .enable_cpi_guard(&bob.pubkey(), &bob.pubkey(), &[&bob]) - .await - .unwrap(); - - let bob_state = token.get_account_info(&bob.pubkey()).await.unwrap(); - let extension = bob_state.get_extension::().unwrap(); - assert!(bool::from(extension.lock_cpi)); -} - -#[tokio::test] -async fn test_cpi_guard_transfer() { - let context = make_context().await; - let TokenContext { - token, - token_unchecked, - mint_authority, - alice, - bob, - .. - } = context.token_context.unwrap(); - - let mk_transfer = |authority, do_checked| { - wrap_instruction( - spl_instruction_padding::id(), - if do_checked { - instruction::transfer_checked( - &spl_token_2022::id(), - &alice.pubkey(), - token.get_address(), - &bob.pubkey(), - &authority, - &[], - 1, - 9, - ) - .unwrap() - } else { - #[allow(deprecated)] - instruction::transfer( - &spl_token_2022::id(), - &alice.pubkey(), - &bob.pubkey(), - &authority, - &[], - 1, - ) - .unwrap() - }, - vec![], - 0, - ) - .unwrap() - }; - - let mut amount = 100; - token - .mint_to( - &alice.pubkey(), - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - for do_checked in [true, false] { - let token_obj = if do_checked { &token } else { &token_unchecked }; - token_obj - .enable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - // transfer works normally with cpi guard enabled - token_obj - .transfer( - &alice.pubkey(), - &bob.pubkey(), - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); - amount -= 1; - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - - // user-auth cpi transfer with cpi guard doesn't work - let error = token_obj - .process_ixs(&[mk_transfer(alice.pubkey(), do_checked)], &[&alice]) - .await - .unwrap_err(); - assert_eq!(error, client_error(TokenError::CpiGuardTransferBlocked)); - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - - // delegate-auth cpi transfer to self does not work - token_obj - .approve( - &alice.pubkey(), - &alice.pubkey(), - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); - - let error = token_obj - .process_ixs(&[mk_transfer(alice.pubkey(), do_checked)], &[&alice]) - .await - .unwrap_err(); - assert_eq!(error, client_error(TokenError::CpiGuardTransferBlocked)); - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - - // delegate-auth cpi transfer with cpi guard works - token_obj - .approve( - &alice.pubkey(), - &bob.pubkey(), - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); - - token_obj - .process_ixs(&[mk_transfer(bob.pubkey(), do_checked)], &[&bob]) - .await - .unwrap(); - amount -= 1; - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - - // transfer still works through cpi with cpi guard off - token_obj - .disable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - token_obj - .process_ixs(&[mk_transfer(alice.pubkey(), do_checked)], &[&alice]) - .await - .unwrap(); - amount -= 1; - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - } -} - -#[tokio::test] -async fn test_cpi_guard_burn() { - let context = make_context().await; - let TokenContext { - token, - token_unchecked, - mint_authority, - alice, - bob, - .. - } = context.token_context.unwrap(); - - let mk_burn = |authority, do_checked| { - wrap_instruction( - spl_instruction_padding::id(), - if do_checked { - instruction::burn_checked( - &spl_token_2022::id(), - &alice.pubkey(), - token.get_address(), - &authority, - &[], - 1, - 9, - ) - .unwrap() - } else { - instruction::burn( - &spl_token_2022::id(), - &alice.pubkey(), - token.get_address(), - &authority, - &[], - 1, - ) - .unwrap() - }, - vec![], - 0, - ) - .unwrap() - }; - - let mut amount = 100; - token - .mint_to( - &alice.pubkey(), - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - for do_checked in [true, false] { - let token_obj = if do_checked { &token } else { &token_unchecked }; - token_obj - .enable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - // burn works normally with cpi guard enabled - token_obj - .burn(&alice.pubkey(), &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); - amount -= 1; - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - - // user-auth cpi burn with cpi guard doesn't work - let error = token_obj - .process_ixs(&[mk_burn(alice.pubkey(), do_checked)], &[&alice]) - .await - .unwrap_err(); - assert_eq!(error, client_error(TokenError::CpiGuardBurnBlocked)); - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - - // delegate-auth cpi burn by self does not work - token_obj - .approve( - &alice.pubkey(), - &alice.pubkey(), - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); - - let error = token_obj - .process_ixs(&[mk_burn(alice.pubkey(), do_checked)], &[&alice]) - .await - .unwrap_err(); - assert_eq!(error, client_error(TokenError::CpiGuardBurnBlocked)); - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - - // delegate-auth cpi burn with cpi guard works - token_obj - .approve( - &alice.pubkey(), - &bob.pubkey(), - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); - - token_obj - .process_ixs(&[mk_burn(bob.pubkey(), do_checked)], &[&bob]) - .await - .unwrap(); - amount -= 1; - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - - // burn still works through cpi with cpi guard off - token_obj - .disable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - token_obj - .process_ixs(&[mk_burn(alice.pubkey(), do_checked)], &[&alice]) - .await - .unwrap(); - amount -= 1; - - let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap(); - assert_eq!(alice_state.base.amount, amount); - } -} - -#[tokio::test] -async fn test_cpi_guard_approve() { - let context = make_context().await; - let TokenContext { - token, - token_unchecked, - alice, - bob, - .. - } = context.token_context.unwrap(); - - let mk_approve = |do_checked| { - wrap_instruction( - spl_instruction_padding::id(), - if do_checked { - instruction::approve_checked( - &spl_token_2022::id(), - &alice.pubkey(), - token.get_address(), - &bob.pubkey(), - &alice.pubkey(), - &[], - 1, - 9, - ) - .unwrap() - } else { - instruction::approve( - &spl_token_2022::id(), - &alice.pubkey(), - &bob.pubkey(), - &alice.pubkey(), - &[], - 1, - ) - .unwrap() - }, - vec![], - 0, - ) - .unwrap() - }; - - for do_checked in [true, false] { - let token_obj = if do_checked { &token } else { &token_unchecked }; - token_obj - .enable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - // approve works normally with cpi guard enabled - token_obj - .approve( - &alice.pubkey(), - &bob.pubkey(), - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); - - token_obj - .revoke(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - // approve doesn't work through cpi - let error = token_obj - .process_ixs(&[mk_approve(do_checked)], &[&alice]) - .await - .unwrap_err(); - assert_eq!(error, client_error(TokenError::CpiGuardApproveBlocked)); - - // approve still works through cpi with cpi guard off - token_obj - .disable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - // refresh the blockhash - token_obj.get_new_latest_blockhash().await.unwrap(); - token_obj - .process_ixs(&[mk_approve(do_checked)], &[&alice]) - .await - .unwrap(); - - token_obj - .revoke(&alice.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - } -} - -async fn make_close_test_account( - token: &Token, - owner: &S, - authority: Option, -) -> Pubkey { - let account = Keypair::new(); - - token - .create_auxiliary_token_account_with_extension_space( - &account, - &owner.pubkey(), - vec![ExtensionType::CpiGuard], - ) - .await - .unwrap(); - - if authority.is_some() { - token - .set_authority( - &account.pubkey(), - &owner.pubkey(), - authority.as_ref(), - AuthorityType::CloseAccount, - &[owner], - ) - .await - .unwrap(); - } - - token - .enable_cpi_guard(&account.pubkey(), &owner.pubkey(), &[owner]) - .await - .unwrap(); - - account.pubkey() -} - -#[tokio::test] -async fn test_cpi_guard_close_account() { - let context = make_context().await; - let TokenContext { - token, alice, bob, .. - } = context.token_context.unwrap(); - - let mk_close = |account, destination, authority| { - wrap_instruction( - spl_instruction_padding::id(), - instruction::close_account( - &spl_token_2022::id(), - &account, - &destination, - &authority, - &[], - ) - .unwrap(), - vec![], - 0, - ) - .unwrap() - }; - - // test closing through owner and closing through close authority - // the result should be the same eitehr way - for maybe_close_authority in [None, Some(bob.pubkey())] { - let authority = if maybe_close_authority.is_none() { - &alice - } else { - &bob - }; - - // closing normally works - let account = make_close_test_account(&token, &alice, maybe_close_authority).await; - token - .close_account(&account, &bob.pubkey(), &authority.pubkey(), &[authority]) - .await - .unwrap(); - - // cpi close with guard enabled fails if lamports diverted to third party - let account = make_close_test_account(&token, &alice, maybe_close_authority).await; - let error = token - .process_ixs( - &[mk_close(account, bob.pubkey(), authority.pubkey())], - &[authority], - ) - .await - .unwrap_err(); - assert_eq!(error, client_error(TokenError::CpiGuardCloseAccountBlocked)); - - // but close succeeds if lamports are returned to owner - token - .process_ixs( - &[mk_close(account, alice.pubkey(), authority.pubkey())], - &[authority], - ) - .await - .unwrap(); - - // close still works through cpi when guard disabled - let account = make_close_test_account(&token, &alice, maybe_close_authority).await; - token - .disable_cpi_guard(&account, &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - token - .process_ixs( - &[mk_close(account, bob.pubkey(), authority.pubkey())], - &[authority], - ) - .await - .unwrap(); - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -enum SetAuthTest { - ChangeOwner, - AddCloseAuth, - ChangeCloseAuth, - RemoveCloseAuth, -} - -#[tokio::test] -async fn test_cpi_guard_set_authority() { - let context = make_context().await; - let TokenContext { - token, alice, bob, .. - } = context.token_context.unwrap(); - - // the behavior of cpi guard and close authority is so complicated that its best - // to test all cases exhaustively - let mut states = vec![]; - for action in [ - SetAuthTest::ChangeOwner, - SetAuthTest::AddCloseAuth, - SetAuthTest::ChangeCloseAuth, - SetAuthTest::RemoveCloseAuth, - ] { - for enable_cpi_guard in [true, false] { - for do_in_cpi in [true, false] { - states.push((action, enable_cpi_guard, do_in_cpi)); - } - } - } - - for state in states { - let (action, enable_cpi_guard, do_in_cpi) = state; - - // make a new account - let account = Keypair::new(); - token - .create_auxiliary_token_account_with_extension_space( - &account, - &alice.pubkey(), - vec![ExtensionType::CpiGuard], - ) - .await - .unwrap(); - - // turn on cpi guard if we are testing that case - // all actions with cpi guard off should succeed unconditionally - // so half of these tests are backwards compat checks - if enable_cpi_guard { - token - .enable_cpi_guard(&account.pubkey(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); - } - - // if we are changing or removing close auth, we need to have one to - // change/remove - if action == SetAuthTest::ChangeCloseAuth || action == SetAuthTest::RemoveCloseAuth { - token - .set_authority( - &account.pubkey(), - &alice.pubkey(), - Some(&bob.pubkey()), - AuthorityType::CloseAccount, - &[&alice], - ) - .await - .unwrap(); - } - - // this produces the token instruction we want to execute - let (current_authority, new_authority) = match action { - SetAuthTest::ChangeOwner | SetAuthTest::AddCloseAuth => { - (keypair_clone(&alice), Some(bob.pubkey())) - } - SetAuthTest::ChangeCloseAuth => (keypair_clone(&bob), Some(alice.pubkey())), - SetAuthTest::RemoveCloseAuth => (keypair_clone(&bob), None), - }; - let token_instruction = instruction::set_authority( - &spl_token_2022::id(), - &account.pubkey(), - new_authority.as_ref(), - if action == SetAuthTest::ChangeOwner { - AuthorityType::AccountOwner - } else { - AuthorityType::CloseAccount - }, - ¤t_authority.pubkey(), - &[], - ) - .unwrap(); - - // this wraps it or doesn't based on the test case - let instruction = if do_in_cpi { - wrap_instruction(spl_instruction_padding::id(), token_instruction, vec![], 0).unwrap() - } else { - token_instruction - }; - - // and here we go - let result = token - .process_ixs(&[instruction], &[¤t_authority]) - .await; - - // truth table for our cases - match (action, enable_cpi_guard, do_in_cpi) { - // all actions succeed with cpi guard off - (_, false, _) => result.unwrap(), - // ownership cannot be transferred with guard - (SetAuthTest::ChangeOwner, true, false) => assert_eq!( - result.unwrap_err(), - client_error(TokenError::CpiGuardOwnerChangeBlocked) - ), - // all other actions succeed outside cpi with guard - (_, true, false) => result.unwrap(), - // removing a close authority succeeds in cpi with guard - (SetAuthTest::RemoveCloseAuth, true, true) => result.unwrap(), - // changing owner, adding close, or changing close all fail in cpi with guard - (_, true, true) => assert_eq!( - result.unwrap_err(), - client_error(TokenError::CpiGuardSetAuthorityBlocked) - ), - } - } -} diff --git a/token/program-2022-test/tests/default_account_state.rs b/token/program-2022-test/tests/default_account_state.rs deleted file mode 100644 index ebee881d9e9..00000000000 --- a/token/program-2022-test/tests/default_account_state.rs +++ /dev/null @@ -1,324 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, program_option::COption, pubkey::Pubkey, signature::Signer, - signer::keypair::Keypair, transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{default_account_state::DefaultAccountState, BaseStateWithExtensions}, - instruction::AuthorityType, - state::AccountState, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - std::convert::TryFrom, -}; - -#[tokio::test] -async fn success_init_default_acct_state_frozen() { - let default_account_state = AccountState::Frozen; - let mut context = TestContext::new().await; - context - .init_token_with_freezing_mint(vec![ExtensionInitializationParams::DefaultAccountState { - state: default_account_state, - }]) - .await - .unwrap(); - let TokenContext { - decimals, - mint_authority, - freeze_authority, - token, - .. - } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - assert_eq!(state.base.decimals, decimals); - assert_eq!( - state.base.mint_authority, - COption::Some(mint_authority.pubkey()) - ); - assert_eq!(state.base.supply, 0); - assert!(state.base.is_initialized); - assert_eq!( - state.base.freeze_authority, - COption::Some(freeze_authority.unwrap().pubkey()) - ); - let extension = state.get_extension::().unwrap(); - assert_eq!( - AccountState::try_from(extension.state).unwrap(), - default_account_state, - ); -} - -#[tokio::test] -async fn fail_init_no_authority_default_acct_state_frozen() { - let default_account_state = AccountState::Frozen; - let mut context = TestContext::new().await; - let err = context - .init_token_with_mint(vec![ExtensionInitializationParams::DefaultAccountState { - state: default_account_state, - }]) - .await - .unwrap_err(); - - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 2, - InstructionError::Custom(TokenError::MintCannotFreeze as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn success_init_default_acct_state_initialized() { - let default_account_state = AccountState::Initialized; - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::DefaultAccountState { - state: default_account_state, - }]) - .await - .unwrap(); - let TokenContext { - decimals, - mint_authority, - token, - .. - } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - assert_eq!(state.base.decimals, decimals); - assert_eq!( - state.base.mint_authority, - COption::Some(mint_authority.pubkey()) - ); - assert_eq!(state.base.supply, 0); - assert!(state.base.is_initialized); - assert_eq!(state.base.freeze_authority, COption::None); - let extension = state.get_extension::().unwrap(); - assert_eq!( - AccountState::try_from(extension.state).unwrap(), - default_account_state, - ); -} - -#[tokio::test] -async fn success_no_authority_init_default_acct_state_initialized() { - let default_account_state = AccountState::Initialized; - let mut context = TestContext::new().await; - context - .init_token_with_freezing_mint(vec![ExtensionInitializationParams::DefaultAccountState { - state: default_account_state, - }]) - .await - .unwrap(); - let TokenContext { - decimals, - mint_authority, - freeze_authority, - token, - .. - } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - assert_eq!(state.base.decimals, decimals); - assert_eq!( - state.base.mint_authority, - COption::Some(mint_authority.pubkey()) - ); - assert_eq!(state.base.supply, 0); - assert!(state.base.is_initialized); - assert_eq!( - state.base.freeze_authority, - COption::Some(freeze_authority.unwrap().pubkey()) - ); - let extension = state.get_extension::().unwrap(); - assert_eq!( - AccountState::try_from(extension.state).unwrap(), - default_account_state, - ); -} - -#[tokio::test] -async fn fail_invalid_default_acct_state() { - let default_account_state = AccountState::Uninitialized; - let mut context = TestContext::new().await; - let err = context - .init_token_with_freezing_mint(vec![ExtensionInitializationParams::DefaultAccountState { - state: default_account_state, - }]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidState as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn end_to_end_default_account_state() { - let default_account_state = AccountState::Frozen; - let mut context = TestContext::new().await; - context - .init_token_with_freezing_mint(vec![ExtensionInitializationParams::DefaultAccountState { - state: default_account_state, - }]) - .await - .unwrap(); - let TokenContext { - mint_authority, - freeze_authority, - token, - .. - } = context.token_context.unwrap(); - - let freeze_authority = freeze_authority.unwrap(); - - let owner = Pubkey::new_unique(); - let account = Keypair::new(); - token - .create_auxiliary_token_account(&account, &owner) - .await - .unwrap(); - let account = account.pubkey(); - let account_state = token.get_account_info(&account).await.unwrap(); - assert_eq!(account_state.base.state, default_account_state); - - // Invalid default state - let err = token - .set_default_account_state( - &mint_authority.pubkey(), - &AccountState::Uninitialized, - &[&mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InvalidState as u32) - ) - ))) - ); - - token - .set_default_account_state( - &freeze_authority.pubkey(), - &AccountState::Initialized, - &[&freeze_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - AccountState::try_from(extension.state).unwrap(), - AccountState::Initialized, - ); - - let owner = Pubkey::new_unique(); - let account = Keypair::new(); - token - .create_auxiliary_token_account(&account, &owner) - .await - .unwrap(); - let account = account.pubkey(); - let account_state = token.get_account_info(&account).await.unwrap(); - assert_eq!(account_state.base.state, AccountState::Initialized); - - // adjusting freeze authority adjusts default state authority - let new_authority = Keypair::new(); - token - .set_authority( - token.get_address(), - &freeze_authority.pubkey(), - Some(&new_authority.pubkey()), - AuthorityType::FreezeAccount, - &[&freeze_authority], - ) - .await - .unwrap(); - - let err = token - .set_default_account_state( - &mint_authority.pubkey(), - &AccountState::Frozen, - &[&mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - token - .set_default_account_state( - &new_authority.pubkey(), - &AccountState::Frozen, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - AccountState::try_from(extension.state).unwrap(), - AccountState::Frozen, - ); - - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - AuthorityType::FreezeAccount, - &[&new_authority], - ) - .await - .unwrap(); - - let err = token - .set_default_account_state( - &new_authority.pubkey(), - &AccountState::Initialized, - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoAuthorityExists as u32) - ) - ))) - ); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - AccountState::try_from(extension.state).unwrap(), - AccountState::Frozen, - ); -} diff --git a/token/program-2022-test/tests/delegate.rs b/token/program-2022-test/tests/delegate.rs deleted file mode 100644 index ee337cb0da3..00000000000 --- a/token/program-2022-test/tests/delegate.rs +++ /dev/null @@ -1,297 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::error::TokenError, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, -}; - -#[derive(PartialEq)] -enum TransferMode { - All, - CheckedOnly, -} - -#[derive(PartialEq)] -enum ApproveMode { - Unchecked, - Checked, -} - -#[derive(PartialEq)] -enum OwnerMode { - SelfOwned, - External, -} - -async fn run_basic( - context: TestContext, - owner_mode: OwnerMode, - transfer_mode: TransferMode, - approve_mode: ApproveMode, -) { - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - bob, - .. - } = context.token_context.unwrap(); - - let alice_account = match owner_mode { - OwnerMode::SelfOwned => { - token - .create_auxiliary_token_account(&alice, &alice.pubkey()) - .await - .unwrap(); - alice.pubkey() - } - OwnerMode::External => { - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - alice_account.pubkey() - } - }; - let bob_account = Keypair::new(); - token - .create_auxiliary_token_account(&bob_account, &bob.pubkey()) - .await - .unwrap(); - let bob_account = bob_account.pubkey(); - - // mint tokens - let amount = 100; - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - // delegate to bob - let delegated_amount = 10; - match approve_mode { - ApproveMode::Unchecked => token_unchecked - .approve( - &alice_account, - &bob.pubkey(), - &alice.pubkey(), - delegated_amount, - &[&alice], - ) - .await - .unwrap(), - ApproveMode::Checked => token - .approve( - &alice_account, - &bob.pubkey(), - &alice.pubkey(), - delegated_amount, - &[&alice], - ) - .await - .unwrap(), - } - - // transfer too much is not ok - let error = token - .transfer( - &alice_account, - &bob_account, - &bob.pubkey(), - delegated_amount.checked_add(1).unwrap(), - &[&bob], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InsufficientFunds as u32) - ) - ))) - ); - - // transfer is ok - if transfer_mode == TransferMode::All { - token_unchecked - .transfer(&alice_account, &bob_account, &bob.pubkey(), 1, &[&bob]) - .await - .unwrap(); - } - - token - .transfer(&alice_account, &bob_account, &bob.pubkey(), 1, &[&bob]) - .await - .unwrap(); - - // burn is ok - token_unchecked - .burn(&alice_account, &bob.pubkey(), 1, &[&bob]) - .await - .unwrap(); - token - .burn(&alice_account, &bob.pubkey(), 1, &[&bob]) - .await - .unwrap(); - - // wrong signer - let keypair = &Keypair::new(); - let error = token - .transfer( - &alice_account, - &bob_account, - &keypair.pubkey(), - 1, - &[keypair], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // revoke - token - .revoke(&alice_account, &alice.pubkey(), &[&alice]) - .await - .unwrap(); - - // now fails - let error = token - .transfer(&alice_account, &bob_account, &bob.pubkey(), 2, &[&bob]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn basic() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_basic( - context, - OwnerMode::External, - TransferMode::All, - ApproveMode::Unchecked, - ) - .await; -} - -#[tokio::test] -async fn basic_checked() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_basic( - context, - OwnerMode::External, - TransferMode::All, - ApproveMode::Checked, - ) - .await; -} - -#[tokio::test] -async fn basic_self_owned() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_basic( - context, - OwnerMode::SelfOwned, - TransferMode::All, - ApproveMode::Checked, - ) - .await; -} - -#[tokio::test] -async fn basic_with_extension() { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(Pubkey::new_unique()), - transfer_fee_basis_points: 100u16, - maximum_fee: 1_000u64, - }]) - .await - .unwrap(); - run_basic( - context, - OwnerMode::External, - TransferMode::CheckedOnly, - ApproveMode::Unchecked, - ) - .await; -} - -#[tokio::test] -async fn basic_with_extension_checked() { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(Pubkey::new_unique()), - transfer_fee_basis_points: 100u16, - maximum_fee: 1_000u64, - }]) - .await - .unwrap(); - run_basic( - context, - OwnerMode::External, - TransferMode::CheckedOnly, - ApproveMode::Checked, - ) - .await; -} - -#[tokio::test] -async fn basic_self_owned_with_extension() { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(Pubkey::new_unique()), - transfer_fee_basis_points: 100u16, - maximum_fee: 1_000u64, - }]) - .await - .unwrap(); - run_basic( - context, - OwnerMode::SelfOwned, - TransferMode::CheckedOnly, - ApproveMode::Checked, - ) - .await; -} diff --git a/token/program-2022-test/tests/fixtures/spl_instruction_padding.so b/token/program-2022-test/tests/fixtures/spl_instruction_padding.so deleted file mode 100755 index 16a50fee6e7..00000000000 Binary files a/token/program-2022-test/tests/fixtures/spl_instruction_padding.so and /dev/null differ diff --git a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example.so b/token/program-2022-test/tests/fixtures/spl_transfer_hook_example.so deleted file mode 100755 index c3ad4a44acc..00000000000 Binary files a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example.so and /dev/null differ diff --git a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_downgrade.so b/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_downgrade.so deleted file mode 100755 index fdda8fb327b..00000000000 Binary files a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_downgrade.so and /dev/null differ diff --git a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_fail.so b/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_fail.so deleted file mode 100755 index 6e46894064d..00000000000 Binary files a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_fail.so and /dev/null differ diff --git a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_success.so b/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_success.so deleted file mode 100755 index 258c11432a2..00000000000 Binary files a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_success.so and /dev/null differ diff --git a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_swap.so b/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_swap.so deleted file mode 100755 index 8d68f1f2669..00000000000 Binary files a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_swap.so and /dev/null differ diff --git a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_swap_with_fee.so b/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_swap_with_fee.so deleted file mode 100755 index b8184a8aa7f..00000000000 Binary files a/token/program-2022-test/tests/fixtures/spl_transfer_hook_example_swap_with_fee.so and /dev/null differ diff --git a/token/program-2022-test/tests/freeze.rs b/token/program-2022-test/tests/freeze.rs deleted file mode 100644 index f17b5ff3b90..00000000000 --- a/token/program-2022-test/tests/freeze.rs +++ /dev/null @@ -1,45 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{signature::Signer, signer::keypair::Keypair}, - spl_token_2022::state::AccountState, -}; - -#[tokio::test] -async fn basic() { - let mut context = TestContext::new().await; - context.init_token_with_freezing_mint(vec![]).await.unwrap(); - let TokenContext { - freeze_authority, - token, - alice, - .. - } = context.token_context.unwrap(); - let freeze_authority = freeze_authority.unwrap(); - - let account = Keypair::new(); - token - .create_auxiliary_token_account(&account, &alice.pubkey()) - .await - .unwrap(); - let account = account.pubkey(); - let state = token.get_account_info(&account).await.unwrap(); - assert_eq!(state.base.state, AccountState::Initialized); - - token - .freeze(&account, &freeze_authority.pubkey(), &[&freeze_authority]) - .await - .unwrap(); - let state = token.get_account_info(&account).await.unwrap(); - assert_eq!(state.base.state, AccountState::Frozen); - - token - .thaw(&account, &freeze_authority.pubkey(), &[&freeze_authority]) - .await - .unwrap(); - let state = token.get_account_info(&account).await.unwrap(); - assert_eq!(state.base.state, AccountState::Initialized); -} diff --git a/token/program-2022-test/tests/group_member_pointer.rs b/token/program-2022-test/tests/group_member_pointer.rs deleted file mode 100644 index 8f59ee5bc0a..00000000000 --- a/token/program-2022-test/tests/group_member_pointer.rs +++ /dev/null @@ -1,294 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{ - group_member_pointer::GroupMemberPointer, group_pointer::GroupPointer, - BaseStateWithExtensions, - }, - instruction, - processor::Processor, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - std::{convert::TryInto, sync::Arc}, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup( - mint: Keypair, - member_address: &Pubkey, - authority: &Pubkey, - maybe_group_address: Option, -) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let mut extension_init_params = vec![ExtensionInitializationParams::GroupMemberPointer { - authority: Some(*authority), - member_address: Some(*member_address), - }]; - if let Some(group_address) = maybe_group_address { - extension_init_params.push(ExtensionInitializationParams::GroupPointer { - authority: Some(*authority), - group_address: Some(group_address), - }); - } - context - .init_token_with_mint_keypair_and_freeze_authority(mint, extension_init_params, None) - .await - .unwrap(); - context -} - -#[tokio::test] -async fn success_init() { - let authority = Pubkey::new_unique(); - let member_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &member_address, &authority, None) - .await - .token_context - .take() - .unwrap() - .token; - - let state = token.get_mint_info().await.unwrap(); - assert!(state.base.is_initialized); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, Some(authority).try_into().unwrap()); - assert_eq!( - extension.member_address, - Some(member_address).try_into().unwrap() - ); -} - -#[tokio::test] -async fn success_init_with_group() { - let authority = Pubkey::new_unique(); - let group_address = Pubkey::new_unique(); - let member_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup( - mint_keypair, - &member_address, - &authority, - Some(group_address), - ) - .await - .token_context - .take() - .unwrap() - .token; - - let state = token.get_mint_info().await.unwrap(); - assert!(state.base.is_initialized); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, Some(authority).try_into().unwrap()); - assert_eq!( - extension.member_address, - Some(member_address).try_into().unwrap() - ); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, Some(authority).try_into().unwrap()); - assert_eq!( - extension.group_address, - Some(group_address).try_into().unwrap() - ); -} - -#[tokio::test] -async fn fail_init_all_none() { - let mut program_test = ProgramTest::default(); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let err = context - .init_token_with_mint(vec![ExtensionInitializationParams::GroupMemberPointer { - authority: None, - member_address: None, - }]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidInstruction as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn set_authority() { - let authority = Keypair::new(); - let member_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &member_address, &authority.pubkey(), None) - .await - .token_context - .take() - .unwrap() - .token; - let new_authority = Keypair::new(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token - .set_authority( - token.get_address(), - &wrong.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::GroupMemberPointer, - &[&wrong], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .set_authority( - token.get_address(), - &authority.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::GroupMemberPointer, - &[&authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.authority, - Some(new_authority.pubkey()).try_into().unwrap(), - ); - - // set to none - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - instruction::AuthorityType::GroupMemberPointer, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, None.try_into().unwrap(),); - - // fail set again - let err = token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - Some(&authority.pubkey()), - instruction::AuthorityType::GroupMemberPointer, - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn update_member_address() { - let authority = Keypair::new(); - let member_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &member_address, &authority.pubkey(), None) - .await - .token_context - .take() - .unwrap() - .token; - let new_member_address = Pubkey::new_unique(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token - .update_group_member_address(&wrong.pubkey(), Some(new_member_address), &[&wrong]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .update_group_member_address(&authority.pubkey(), Some(new_member_address), &[&authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.member_address, - Some(new_member_address).try_into().unwrap(), - ); - - // set to none - token - .update_group_member_address(&authority.pubkey(), None, &[&authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.member_address, None.try_into().unwrap(),); -} diff --git a/token/program-2022-test/tests/group_pointer.rs b/token/program-2022-test/tests/group_pointer.rs deleted file mode 100644 index 5dc6922919f..00000000000 --- a/token/program-2022-test/tests/group_pointer.rs +++ /dev/null @@ -1,253 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{group_pointer::GroupPointer, BaseStateWithExtensions}, - instruction, - processor::Processor, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - std::{convert::TryInto, sync::Arc}, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, group_address: &Pubkey, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::GroupPointer { - authority: Some(*authority), - group_address: Some(*group_address), - }], - None, - ) - .await - .unwrap(); - context -} - -#[tokio::test] -async fn success_init() { - let authority = Pubkey::new_unique(); - let group_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &group_address, &authority) - .await - .token_context - .take() - .unwrap() - .token; - - let state = token.get_mint_info().await.unwrap(); - assert!(state.base.is_initialized); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, Some(authority).try_into().unwrap()); - assert_eq!( - extension.group_address, - Some(group_address).try_into().unwrap() - ); -} - -#[tokio::test] -async fn fail_init_all_none() { - let mut program_test = ProgramTest::default(); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let err = context - .init_token_with_mint_keypair_and_freeze_authority( - Keypair::new(), - vec![ExtensionInitializationParams::GroupPointer { - authority: None, - group_address: None, - }], - None, - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidInstruction as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn set_authority() { - let authority = Keypair::new(); - let group_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &group_address, &authority.pubkey()) - .await - .token_context - .take() - .unwrap() - .token; - let new_authority = Keypair::new(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token - .set_authority( - token.get_address(), - &wrong.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::GroupPointer, - &[&wrong], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .set_authority( - token.get_address(), - &authority.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::GroupPointer, - &[&authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.authority, - Some(new_authority.pubkey()).try_into().unwrap(), - ); - - // set to none - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - instruction::AuthorityType::GroupPointer, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, None.try_into().unwrap(),); - - // fail set again - let err = token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - Some(&authority.pubkey()), - instruction::AuthorityType::GroupPointer, - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn update_group_address() { - let authority = Keypair::new(); - let group_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &group_address, &authority.pubkey()) - .await - .token_context - .take() - .unwrap() - .token; - let new_group_address = Pubkey::new_unique(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token - .update_group_address(&wrong.pubkey(), Some(new_group_address), &[&wrong]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .update_group_address(&authority.pubkey(), Some(new_group_address), &[&authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.group_address, - Some(new_group_address).try_into().unwrap(), - ); - - // set to none - token - .update_group_address(&authority.pubkey(), None, &[&authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.group_address, None.try_into().unwrap(),); -} diff --git a/token/program-2022-test/tests/initialize_account.rs b/token/program-2022-test/tests/initialize_account.rs deleted file mode 100644 index d4bc24f8baf..00000000000 --- a/token/program-2022-test/tests/initialize_account.rs +++ /dev/null @@ -1,262 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, - program_pack::Pack, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - system_instruction, - transaction::{Transaction, TransactionError}, - }, - spl_token_2022::{ - error::TokenError, - extension::{ - transfer_fee::{self, TransferFeeAmount}, - BaseStateWithExtensions, ExtensionType, StateWithExtensions, - }, - instruction, - state::{Account, Mint}, - }, -}; - -#[tokio::test] -async fn no_extensions() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let space = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - ]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - - let account = Keypair::new(); - let account_owner_pubkey = Pubkey::new_unique(); - let space = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_account3( - &spl_token_2022::id(), - &account.pubkey(), - &mint_account.pubkey(), - &account_owner_pubkey, - ) - .unwrap(), - ]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &account], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - let account_info = ctx - .banks_client - .get_account(account.pubkey()) - .await - .expect("get_account") - .expect("account not none"); - assert_eq!(account_info.data.len(), spl_token_2022::state::Account::LEN); - assert_eq!(account_info.owner, spl_token_2022::id()); - assert_eq!(account_info.lamports, rent.minimum_balance(space)); -} - -#[tokio::test] -async fn fail_on_invalid_mint() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - - let space = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let instructions = vec![system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - )]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - - let account = Keypair::new(); - let account_owner_pubkey = Pubkey::new_unique(); - let space = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_account3( - &spl_token_2022::id(), - &account.pubkey(), - &mint_account.pubkey(), - &account_owner_pubkey, - ) - .unwrap(), - ]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidMint as u32) - ) - ); -} - -#[tokio::test] -async fn single_extension() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let space = - ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferFeeConfig]) - .unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - transfer_fee::instruction::initialize_transfer_fee_config( - &spl_token_2022::id(), - &mint_account.pubkey(), - None, - None, - 10, - 4242, - ) - .unwrap(), - instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - ]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - - let account = Keypair::new(); - let account_owner_pubkey = Pubkey::new_unique(); - let space = - ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferFeeAmount]) - .unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_account3( - &spl_token_2022::id(), - &account.pubkey(), - &mint_account.pubkey(), - &account_owner_pubkey, - ) - .unwrap(), - ]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &account], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - let account_info = ctx - .banks_client - .get_account(account.pubkey()) - .await - .expect("get_account") - .expect("account not none"); - assert_eq!( - account_info.data.len(), - ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferFeeAmount]) - .unwrap(), - ); - assert_eq!(account_info.owner, spl_token_2022::id()); - assert_eq!(account_info.lamports, rent.minimum_balance(space)); - let state = StateWithExtensions::::unpack(&account_info.data).unwrap(); - assert_eq!(state.base.mint, mint_account.pubkey()); - assert_eq!( - &state.get_extension_types().unwrap(), - &[ExtensionType::TransferFeeAmount] - ); - let unpacked_extension = state.get_extension::().unwrap(); - assert_eq!( - *unpacked_extension, - TransferFeeAmount { - withheld_amount: 0.into() - } - ); -} - -// TODO: add test for multiple Account extensions when memo extension is present diff --git a/token/program-2022-test/tests/initialize_mint.rs b/token/program-2022-test/tests/initialize_mint.rs deleted file mode 100644 index a14641f211d..00000000000 --- a/token/program-2022-test/tests/initialize_mint.rs +++ /dev/null @@ -1,654 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, - program_option::COption, - program_pack::Pack, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - system_instruction, - transaction::{Transaction, TransactionError}, - }, - spl_token_2022::{ - error::TokenError, - extension::{ - confidential_transfer, confidential_transfer_fee, - mint_close_authority::MintCloseAuthority, transfer_fee, BaseStateWithExtensions, - ExtensionType, - }, - instruction, native_mint, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, - state::Mint, - }, - spl_token_client::token::ExtensionInitializationParams, - std::convert::TryInto, -}; - -#[tokio::test] -async fn success_base() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - let TokenContext { - decimals, - mint_authority, - token, - .. - } = context.token_context.unwrap(); - - let mint = token.get_mint_info().await.unwrap(); - assert_eq!(mint.base.decimals, decimals); - assert_eq!( - mint.base.mint_authority, - COption::Some(mint_authority.pubkey()) - ); - assert_eq!(mint.base.supply, 0); - assert!(mint.base.is_initialized); - assert_eq!(mint.base.freeze_authority, COption::None); -} - -#[tokio::test] -async fn fail_extension_no_space() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let space = Mint::LEN; - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_mint_close_authority( - &spl_token_2022::id(), - &mint_account.pubkey(), - Some(&mint_authority_pubkey), - ) - .unwrap(), - instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError(1, InstructionError::InvalidAccountData) - ); -} - -#[tokio::test] -async fn fail_extension_after_mint_init() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let space = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MintCloseAuthority]) - .unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - instruction::initialize_mint_close_authority( - &spl_token_2022::id(), - &mint_account.pubkey(), - Some(&mint_authority_pubkey), - ) - .unwrap(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError(1, InstructionError::InvalidAccountData) - ); -} - -#[tokio::test] -async fn success_extension_and_base() { - let close_authority = Some(Pubkey::new_unique()); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::MintCloseAuthority { - close_authority, - }]) - .await - .unwrap(); - let TokenContext { - decimals, - mint_authority, - token, - .. - } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - assert_eq!(state.base.decimals, decimals); - assert_eq!( - state.base.mint_authority, - COption::Some(mint_authority.pubkey()) - ); - assert_eq!(state.base.supply, 0); - assert!(state.base.is_initialized); - assert_eq!(state.base.freeze_authority, COption::None); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.close_authority, - close_authority.try_into().unwrap(), - ); -} - -#[tokio::test] -async fn fail_init_overallocated_mint() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let space = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MintCloseAuthority]) - .unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError(1, InstructionError::InvalidAccountData) - ); -} - -#[tokio::test] -async fn fail_account_init_after_mint_extension() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - let token_account = Keypair::new(); - - let mint_space = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let account_space = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MintCloseAuthority]) - .unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(mint_space), - mint_space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - system_instruction::create_account( - &ctx.payer.pubkey(), - &token_account.pubkey(), - rent.minimum_balance(account_space), - account_space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_mint_close_authority( - &spl_token_2022::id(), - &token_account.pubkey(), - Some(&mint_authority_pubkey), - ) - .unwrap(), - instruction::initialize_account( - &spl_token_2022::id(), - &token_account.pubkey(), - &mint_account.pubkey(), - &mint_authority_pubkey, - ) - .unwrap(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account, &token_account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError( - 4, - InstructionError::Custom(TokenError::ExtensionBaseMismatch as u32) - ) - ); -} - -#[tokio::test] -async fn fail_account_init_after_mint_init() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let mint_space = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(mint_space), - mint_space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - instruction::initialize_account( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_account.pubkey(), - &mint_authority_pubkey, - ) - .unwrap(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError(2, InstructionError::InvalidAccountData) - ); -} - -#[tokio::test] -async fn fail_account_init_after_mint_init_with_extension() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let mint_space = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MintCloseAuthority]) - .unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(mint_space), - mint_space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_mint_close_authority( - &spl_token_2022::id(), - &mint_account.pubkey(), - Some(&mint_authority_pubkey), - ) - .unwrap(), - instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - instruction::initialize_account( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_account.pubkey(), - &mint_authority_pubkey, - ) - .unwrap(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError(3, InstructionError::InvalidAccountData) - ); -} - -#[tokio::test] -async fn fail_fee_init_after_mint_init() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let space = - ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferFeeConfig]) - .unwrap(); - let instructions = vec![ - system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(space), - space as u64, - &spl_token_2022::id(), - ), - instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - transfer_fee::instruction::initialize_transfer_fee_config( - &spl_token_2022::id(), - &mint_account.pubkey(), - Some(&Pubkey::new_unique()), - Some(&Pubkey::new_unique()), - 10, - 100, - ) - .unwrap(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError(1, InstructionError::InvalidAccountData) - ); -} - -#[tokio::test] -async fn create_native_mint() { - let mut context = TestContext::new().await; - context.init_token_with_native_mint().await.unwrap(); - let TokenContext { token, .. } = context.token_context.unwrap(); - - let mint = token.get_mint_info().await.unwrap(); - assert_eq!(mint.base.decimals, native_mint::DECIMALS); - assert_eq!(mint.base.mint_authority, COption::None,); - assert_eq!(mint.base.supply, 0); - assert!(mint.base.is_initialized); - assert_eq!(mint.base.freeze_authority, COption::None); -} - -#[tokio::test] -async fn fail_invalid_extensions_combination() { - let context = TestContext::new().await; - let ctx = context.context.lock().await; - let rent = ctx.banks_client.get_rent().await.unwrap(); - let mint_account = Keypair::new(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let transfer_fee_config_init_instruction = - transfer_fee::instruction::initialize_transfer_fee_config( - &spl_token_2022::id(), - &mint_account.pubkey(), - Some(&Pubkey::new_unique()), - Some(&Pubkey::new_unique()), - 10, - 100, - ) - .unwrap(); - - let confidential_transfer_mint_init_instruction = - confidential_transfer::instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - Some(Pubkey::new_unique()), - true, - None, - ) - .unwrap(); - - let confidential_transfer_fee_config_init_instruction = - confidential_transfer_fee::instruction::initialize_confidential_transfer_fee_config( - &spl_token_2022::id(), - &mint_account.pubkey(), - Some(Pubkey::new_unique()), - &PodElGamalPubkey::default(), - ) - .unwrap(); - - let initialize_mint_instruction = instruction::initialize_mint( - &spl_token_2022::id(), - &mint_account.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(); - - // initialize transfer fee and confidential transfers, but no confidential - // transfer fee - let mint_space = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::TransferFeeConfig, - ExtensionType::ConfidentialTransferMint, - ]) - .unwrap(); - let create_account_instruction = system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(mint_space), - mint_space as u64, - &spl_token_2022::id(), - ); - - let instructions = vec![ - create_account_instruction.clone(), - transfer_fee_config_init_instruction.clone(), - confidential_transfer_mint_init_instruction.clone(), - initialize_mint_instruction.clone(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError( - 3, - InstructionError::Custom(TokenError::InvalidExtensionCombination as u32) - ) - ); - - // initialize transfer fee and confidential transfer fees, but no confidential - // transfers - let mint_space = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::TransferFeeConfig, - ExtensionType::ConfidentialTransferFeeConfig, - ]) - .unwrap(); - let create_account_instruction = system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(mint_space), - mint_space as u64, - &spl_token_2022::id(), - ); - - let instructions = vec![ - create_account_instruction.clone(), - transfer_fee_config_init_instruction.clone(), - confidential_transfer_fee_config_init_instruction.clone(), - initialize_mint_instruction.clone(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - err, - TransactionError::InstructionError( - 3, - InstructionError::Custom(TokenError::InvalidExtensionCombination as u32) - ) - ); - - // initialize all of transfer fee, confidential transfers, and confidential - // transfer fees (success case) - let mint_space = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::TransferFeeConfig, - ExtensionType::ConfidentialTransferMint, - ExtensionType::ConfidentialTransferFeeConfig, - ]) - .unwrap(); - let create_account_instruction = system_instruction::create_account( - &ctx.payer.pubkey(), - &mint_account.pubkey(), - rent.minimum_balance(mint_space), - mint_space as u64, - &spl_token_2022::id(), - ); - - let instructions = vec![ - create_account_instruction.clone(), - transfer_fee_config_init_instruction.clone(), - confidential_transfer_mint_init_instruction.clone(), - confidential_transfer_fee_config_init_instruction.clone(), - initialize_mint_instruction.clone(), - ]; - - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &mint_account], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); -} diff --git a/token/program-2022-test/tests/interest_bearing_mint.rs b/token/program-2022-test/tests/interest_bearing_mint.rs deleted file mode 100644 index 5a2299bb1a5..00000000000 --- a/token/program-2022-test/tests/interest_bearing_mint.rs +++ /dev/null @@ -1,350 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{keypair_clone, TestContext, TokenContext}, - solana_program_test::{ - processor, - tokio::{self, sync::Mutex}, - ProgramTest, - }, - solana_sdk::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - instruction::{AccountMeta, Instruction, InstructionError}, - msg, - program::{get_return_data, invoke}, - program_error::ProgramError, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{interest_bearing_mint::InterestBearingConfig, BaseStateWithExtensions}, - instruction::{amount_to_ui_amount, ui_amount_to_amount, AuthorityType}, - processor::Processor, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - std::{convert::TryInto, sync::Arc}, -}; - -#[tokio::test] -async fn success_initialize() { - for (rate, rate_authority) in [(i16::MIN, None), (i16::MAX, Some(Pubkey::new_unique()))] { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::InterestBearingConfig { - rate_authority, - rate, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - Option::::from(extension.rate_authority), - rate_authority, - ); - assert_eq!(i16::from(extension.current_rate), rate,); - assert_eq!(i16::from(extension.pre_update_average_rate), rate,); - } -} - -#[tokio::test] -async fn update_rate() { - let rate_authority = Keypair::new(); - let initial_rate = 500; - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::InterestBearingConfig { - rate_authority: Some(rate_authority.pubkey()), - rate: initial_rate, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(i16::from(extension.current_rate), initial_rate); - assert_eq!(i16::from(extension.pre_update_average_rate), initial_rate); - let initialization_timestamp = i64::from(extension.initialization_timestamp); - assert_eq!( - extension.initialization_timestamp, - extension.last_update_timestamp - ); - - // warp forward, so last update timestamp is advanced during update - let warp_slot = 1_000; - let initial_num_warps = 10; - for i in 1..initial_num_warps { - context - .context - .lock() - .await - .warp_to_slot(i * warp_slot) - .unwrap(); - } - - // correct - let middle_rate = 1_000; - token - .update_interest_rate(&rate_authority.pubkey(), middle_rate, &[&rate_authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(i16::from(extension.current_rate), middle_rate); - assert_eq!(i16::from(extension.pre_update_average_rate), initial_rate); - let last_update_timestamp = i64::from(extension.last_update_timestamp); - assert!(last_update_timestamp > initialization_timestamp); - - // warp forward - let final_num_warps = 20; - for i in initial_num_warps..final_num_warps { - context - .context - .lock() - .await - .warp_to_slot(i * warp_slot) - .unwrap(); - } - - // update again, pre_update_average_rate is between the two previous - let new_rate = 2_000; - token - .update_interest_rate(&rate_authority.pubkey(), new_rate, &[&rate_authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(i16::from(extension.current_rate), new_rate); - let pre_update_average_rate = i16::from(extension.pre_update_average_rate); - assert!(pre_update_average_rate > initial_rate); - assert!(middle_rate > pre_update_average_rate); - let final_update_timestamp = i64::from(extension.last_update_timestamp); - assert!(final_update_timestamp > last_update_timestamp); - - // wrong signer - let wrong_signer = Keypair::new(); - let err = token - .update_interest_rate(&wrong_signer.pubkey(), 0, &[&wrong_signer]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn set_authority() { - let rate_authority = Keypair::new(); - let initial_rate = 500; - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::InterestBearingConfig { - rate_authority: Some(rate_authority.pubkey()), - rate: initial_rate, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - - // success - let new_rate_authority = Keypair::new(); - token - .set_authority( - token.get_address(), - &rate_authority.pubkey(), - Some(&new_rate_authority.pubkey()), - AuthorityType::InterestRate, - &[&rate_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.rate_authority, - Some(new_rate_authority.pubkey()).try_into().unwrap(), - ); - token - .update_interest_rate(&new_rate_authority.pubkey(), 10, &[&new_rate_authority]) - .await - .unwrap(); - let err = token - .update_interest_rate(&rate_authority.pubkey(), 100, &[&rate_authority]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // set to none - token - .set_authority( - token.get_address(), - &new_rate_authority.pubkey(), - None, - AuthorityType::InterestRate, - &[&new_rate_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.rate_authority, None.try_into().unwrap(),); - - // now all fail - let err = token - .update_interest_rate(&new_rate_authority.pubkey(), 50, &[&new_rate_authority]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoAuthorityExists as u32) - ) - ))) - ); - let err = token - .update_interest_rate(&rate_authority.pubkey(), 5, &[&rate_authority]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoAuthorityExists as u32) - ) - ))) - ); -} - -// test program to CPI into token to get ui amounts -fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - _input: &[u8], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - let token_program = next_account_info(account_info_iter)?; - // 10 tokens, with 9 decimal places - let test_amount = 10_000_000_000; - // "10" as an amount should be smaller than test_amount due to interest - invoke( - &ui_amount_to_amount(token_program.key, mint_info.key, "10")?, - &[mint_info.clone(), token_program.clone()], - )?; - let (_, return_data) = get_return_data().unwrap(); - let amount = u64::from_le_bytes(return_data[0..8].try_into().unwrap()); - msg!("amount: {}", amount); - if amount >= test_amount { - return Err(ProgramError::InvalidInstructionData); - } - - // test_amount as a UI amount should be larger due to interest - invoke( - &amount_to_ui_amount(token_program.key, mint_info.key, test_amount)?, - &[mint_info.clone(), token_program.clone()], - )?; - let (_, return_data) = get_return_data().unwrap(); - let ui_amount = String::from_utf8(return_data).unwrap(); - msg!("ui amount: {}", ui_amount); - let float_ui_amount = ui_amount.parse::().unwrap(); - if float_ui_amount <= 10.0 { - return Err(ProgramError::InvalidInstructionData); - } - Ok(()) -} - -#[tokio::test] -async fn amount_conversions() { - let rate_authority = Keypair::new(); - let mut program_test = ProgramTest::default(); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - let program_id = Pubkey::new_unique(); - program_test.add_program( - "ui_amount_to_amount", - program_id, - processor!(process_instruction), - ); - - let context = program_test.start_with_context().await; - let payer = keypair_clone(&context.payer); - let last_blockhash = context.last_blockhash; - let context = Arc::new(Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let initial_rate = i16::MAX; - context - .init_token_with_mint(vec![ExtensionInitializationParams::InterestBearingConfig { - rate_authority: Some(rate_authority.pubkey()), - rate: initial_rate, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - - // warp forward, so interest is accrued - let warp_slot: u64 = 1_000; - let initial_num_warps: u64 = 10; - for i in 1..initial_num_warps { - context - .context - .lock() - .await - .warp_to_slot(i.checked_mul(warp_slot).unwrap()) - .unwrap(); - } - - let transaction = Transaction::new_signed_with_payer( - &[Instruction { - program_id, - accounts: vec![ - AccountMeta::new_readonly(*token.get_address(), false), - AccountMeta::new_readonly(spl_token_2022::id(), false), - ], - data: vec![], - }], - Some(&payer.pubkey()), - &[&payer], - last_blockhash, - ); - context - .context - .lock() - .await - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} diff --git a/token/program-2022-test/tests/memo_transfer.rs b/token/program-2022-test/tests/memo_transfer.rs deleted file mode 100644 index b8585d8d236..00000000000 --- a/token/program-2022-test/tests/memo_transfer.rs +++ /dev/null @@ -1,247 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::{ - tokio::{self, sync::Mutex}, - ProgramTestContext, - }, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - system_instruction, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{memo_transfer::MemoTransfer, BaseStateWithExtensions, ExtensionType}, - }, - spl_token_client::token::TokenError as TokenClientError, - std::sync::Arc, -}; - -async fn test_memo_transfers( - context: Arc>, - token_context: TokenContext, - alice_account: Pubkey, - bob_account: Pubkey, -) { - let TokenContext { - mint_authority, - token, - alice, - bob, - .. - } = token_context; - - // mint tokens - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - 4242, - &[&mint_authority], - ) - .await - .unwrap(); - - // require memo transfers into bob_account - token - .enable_required_transfer_memos(&bob_account, &bob.pubkey(), &[&bob]) - .await - .unwrap(); - - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - let extension = bob_state.get_extension::().unwrap(); - assert!(bool::from(extension.require_incoming_transfer_memos)); - - // attempt to transfer from alice to bob without memo - let err = token - .transfer(&alice_account, &bob_account, &alice.pubkey(), 10, &[&alice]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoMemo as u32) - ) - ))) - ); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, 0); - - // attempt to transfer from bob to bob without memo - let err = token - .transfer(&bob_account, &bob_account, &bob.pubkey(), 0, &[&bob]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoMemo as u32) - ) - ))) - ); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, 0); - - // attempt to transfer from alice to bob with misplaced memo, v1 and current - let mut memo_ix = spl_memo::build_memo(&[240, 159, 166, 150], &[]); - for program_id in [spl_memo::id(), spl_memo::v1::id()] { - let ctx = context.lock().await; - memo_ix.program_id = program_id; - #[allow(deprecated)] - let instructions = vec![ - memo_ix.clone(), - system_instruction::transfer(&ctx.payer.pubkey(), &alice.pubkey(), 42), - spl_token_2022::instruction::transfer( - &spl_token_2022::id(), - &alice_account, - &bob_account, - &alice.pubkey(), - &[], - 10, - ) - .unwrap(), - ]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &alice], - ctx.last_blockhash, - ); - let err = ctx - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - drop(ctx); - assert_eq!( - err, - TransactionError::InstructionError( - 2, - InstructionError::Custom(TokenError::NoMemo as u32) - ) - ); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, 0); - } - - // transfer with memo - token - .with_memo("🦖", vec![alice.pubkey()]) - .transfer(&alice_account, &bob_account, &alice.pubkey(), 10, &[&alice]) - .await - .unwrap(); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, 10); - - // transfer with memo v1 - let ctx = context.lock().await; - memo_ix.program_id = spl_memo::v1::id(); - #[allow(deprecated)] - let instructions = vec![ - memo_ix, - spl_token_2022::instruction::transfer( - &spl_token_2022::id(), - &alice_account, - &bob_account, - &alice.pubkey(), - &[], - 11, - ) - .unwrap(), - ]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&ctx.payer.pubkey()), - &[&ctx.payer, &alice], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - drop(ctx); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, 21); - - // stop requiring memo transfers into bob_account - token - .disable_required_transfer_memos(&bob_account, &bob.pubkey(), &[&bob]) - .await - .unwrap(); - - // transfer from alice to bob without memo - token - .transfer(&alice_account, &bob_account, &alice.pubkey(), 12, &[&alice]) - .await - .unwrap(); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, 33); -} - -#[tokio::test] -async fn require_memo_transfers_without_realloc() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - let token_context = context.token_context.unwrap(); - - // create token accounts - token_context - .token - .create_auxiliary_token_account(&token_context.alice, &token_context.alice.pubkey()) - .await - .unwrap(); - let alice_account = token_context.alice.pubkey(); - token_context - .token - .create_auxiliary_token_account_with_extension_space( - &token_context.bob, - &token_context.bob.pubkey(), - vec![ExtensionType::MemoTransfer], - ) - .await - .unwrap(); - let bob_account = token_context.bob.pubkey(); - - test_memo_transfers(context.context, token_context, alice_account, bob_account).await; -} - -#[tokio::test] -async fn require_memo_transfers_with_realloc() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - let token_context = context.token_context.unwrap(); - - // create token accounts - token_context - .token - .create_auxiliary_token_account(&token_context.alice, &token_context.alice.pubkey()) - .await - .unwrap(); - let alice_account = token_context.alice.pubkey(); - token_context - .token - .create_auxiliary_token_account(&token_context.bob, &token_context.bob.pubkey()) - .await - .unwrap(); - let bob_account = token_context.bob.pubkey(); - token_context - .token - .reallocate( - &token_context.bob.pubkey(), - &token_context.bob.pubkey(), - &[ExtensionType::MemoTransfer], - &[&token_context.bob], - ) - .await - .unwrap(); - - test_memo_transfers(context.context, token_context, alice_account, bob_account).await; -} diff --git a/token/program-2022-test/tests/metadata_pointer.rs b/token/program-2022-test/tests/metadata_pointer.rs deleted file mode 100644 index 32f61f2271c..00000000000 --- a/token/program-2022-test/tests/metadata_pointer.rs +++ /dev/null @@ -1,257 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{metadata_pointer::MetadataPointer, BaseStateWithExtensions}, - instruction, - processor::Processor, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - std::{convert::TryInto, sync::Arc}, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, metadata_address: &Pubkey, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::MetadataPointer { - authority: Some(*authority), - metadata_address: Some(*metadata_address), - }], - None, - ) - .await - .unwrap(); - context -} - -#[tokio::test] -async fn success_init() { - let authority = Pubkey::new_unique(); - let metadata_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &metadata_address, &authority) - .await - .token_context - .take() - .unwrap() - .token; - - let state = token.get_mint_info().await.unwrap(); - assert!(state.base.is_initialized); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, Some(authority).try_into().unwrap()); - assert_eq!( - extension.metadata_address, - Some(metadata_address).try_into().unwrap() - ); -} - -#[tokio::test] -async fn fail_init_all_none() { - let mut program_test = ProgramTest::default(); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let err = context - .init_token_with_mint_keypair_and_freeze_authority( - Keypair::new(), - vec![ExtensionInitializationParams::MetadataPointer { - authority: None, - metadata_address: None, - }], - None, - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidInstruction as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn set_authority() { - let authority = Keypair::new(); - let metadata_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &metadata_address, &authority.pubkey()) - .await - .token_context - .take() - .unwrap() - .token; - let new_authority = Keypair::new(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token - .set_authority( - token.get_address(), - &wrong.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::MetadataPointer, - &[&wrong], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .set_authority( - token.get_address(), - &authority.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::MetadataPointer, - &[&authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.authority, - Some(new_authority.pubkey()).try_into().unwrap(), - ); - - // set to none - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - instruction::AuthorityType::MetadataPointer, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, None.try_into().unwrap(),); - - // fail set again - let err = token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - Some(&authority.pubkey()), - instruction::AuthorityType::MetadataPointer, - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn update_metadata_address() { - let authority = Keypair::new(); - let metadata_address = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &metadata_address, &authority.pubkey()) - .await - .token_context - .take() - .unwrap() - .token; - let new_metadata_address = Pubkey::new_unique(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token - .update_metadata_address(&wrong.pubkey(), Some(new_metadata_address), &[&wrong]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .update_metadata_address( - &authority.pubkey(), - Some(new_metadata_address), - &[&authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.metadata_address, - Some(new_metadata_address).try_into().unwrap(), - ); - - // set to none - token - .update_metadata_address(&authority.pubkey(), None, &[&authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.metadata_address, None.try_into().unwrap(),); -} diff --git a/token/program-2022-test/tests/mint_close_authority.rs b/token/program-2022-test/tests/mint_close_authority.rs deleted file mode 100644 index 1ea2bca1b2d..00000000000 --- a/token/program-2022-test/tests/mint_close_authority.rs +++ /dev/null @@ -1,287 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, program_option::COption, pubkey::Pubkey, signature::Signer, - signer::keypair::Keypair, transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{mint_close_authority::MintCloseAuthority, BaseStateWithExtensions}, - instruction, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - std::convert::TryInto, -}; - -#[tokio::test] -async fn success_init() { - let close_authority = Some(Pubkey::new_unique()); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::MintCloseAuthority { - close_authority, - }]) - .await - .unwrap(); - let TokenContext { - decimals, - mint_authority, - token, - .. - } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - assert_eq!(state.base.decimals, decimals); - assert_eq!( - state.base.mint_authority, - COption::Some(mint_authority.pubkey()) - ); - assert_eq!(state.base.supply, 0); - assert!(state.base.is_initialized); - assert_eq!(state.base.freeze_authority, COption::None); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.close_authority, - close_authority.try_into().unwrap(), - ); -} - -#[tokio::test] -async fn set_authority() { - let close_authority = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::MintCloseAuthority { - close_authority: Some(close_authority.pubkey()), - }]) - .await - .unwrap(); - let token = context.token_context.unwrap().token; - let new_authority = Keypair::new(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token - .set_authority( - token.get_address(), - &wrong.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::CloseMint, - &[&wrong], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .set_authority( - token.get_address(), - &close_authority.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::CloseMint, - &[&close_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.close_authority, - Some(new_authority.pubkey()).try_into().unwrap(), - ); - - // set to none - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - instruction::AuthorityType::CloseMint, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.close_authority, None.try_into().unwrap(),); - - // fail set again - let err = token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - Some(&close_authority.pubkey()), - instruction::AuthorityType::CloseMint, - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) - ) - ))) - ); - - // fail close - let destination = Pubkey::new_unique(); - let err = token - .close_account( - token.get_address(), - &destination, - &new_authority.pubkey(), - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn success_close() { - let close_authority = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::MintCloseAuthority { - close_authority: Some(close_authority.pubkey()), - }]) - .await - .unwrap(); - let token = context.token_context.unwrap().token; - - let destination = Pubkey::new_unique(); - token - .close_account( - token.get_address(), - &destination, - &close_authority.pubkey(), - &[&close_authority], - ) - .await - .unwrap(); - let destination = token.get_account(destination).await.unwrap(); - assert!(destination.lamports > 0); -} - -#[tokio::test] -async fn fail_without_extension() { - let close_authority = Pubkey::new_unique(); - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - let TokenContext { - mint_authority, - token, - .. - } = context.token_context.unwrap(); - - // fail set - let err = token - .set_authority( - token.get_address(), - &mint_authority.pubkey(), - Some(&close_authority), - instruction::AuthorityType::CloseMint, - &[&mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidAccountData) - ))) - ); - - // fail close - let destination = Pubkey::new_unique(); - let err = token - .close_account( - token.get_address(), - &destination, - &mint_authority.pubkey(), - &[&mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidAccountData) - ))) - ); -} - -#[tokio::test] -async fn fail_close_with_supply() { - let close_authority = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::MintCloseAuthority { - close_authority: Some(close_authority.pubkey()), - }]) - .await - .unwrap(); - let TokenContext { - mint_authority, - token, - .. - } = context.token_context.unwrap(); - - // mint a token - let owner = Pubkey::new_unique(); - let account = Keypair::new(); - token - .create_auxiliary_token_account(&account, &owner) - .await - .unwrap(); - let account = account.pubkey(); - token - .mint_to(&account, &mint_authority.pubkey(), 1, &[&mint_authority]) - .await - .unwrap(); - - // fail close - let destination = Pubkey::new_unique(); - let err = token - .close_account( - token.get_address(), - &destination, - &close_authority.pubkey(), - &[&close_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintHasSupply as u32) - ) - ))) - ); -} diff --git a/token/program-2022-test/tests/non_transferable.rs b/token/program-2022-test/tests/non_transferable.rs deleted file mode 100644 index d0d36706196..00000000000 --- a/token/program-2022-test/tests/non_transferable.rs +++ /dev/null @@ -1,334 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{ - immutable_owner::ImmutableOwner, transfer_fee::TransferFee, BaseStateWithExtensions, - ExtensionType, - }, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, -}; - -#[tokio::test] -async fn transfer() { - let test_transfer_amount = 100; - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::NonTransferable]) - .await - .unwrap(); - - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - bob, - .. - } = context.token_context.unwrap(); - - // create token accounts - token - .create_auxiliary_token_account(&alice, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice.pubkey(); - - // immutable ownership is added to alice's account during initialization - token - .get_account_info(&alice_account) - .await - .unwrap() - .get_extension::() - .unwrap(); - - token - .create_auxiliary_token_account_with_extension_space( - &bob, - &bob.pubkey(), - vec![ExtensionType::ImmutableOwner], - ) - .await - .unwrap(); - let bob_account = bob.pubkey(); - - // mint to alice should be successful - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - test_transfer_amount, - &[&mint_authority], - ) - .await - .unwrap(); - - token - .mint_to( - &bob_account, - &mint_authority.pubkey(), - test_transfer_amount, - &[&mint_authority], - ) - .await - .unwrap(); - - // self-transfer fails - let error = token - .transfer( - &bob_account, - &bob_account, - &bob.pubkey(), - test_transfer_amount, - &[&bob], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonTransferable as u32) - ) - ))) - ); - - // regular transfer fails - let error = token - .transfer( - &bob_account, - &alice_account, - &bob.pubkey(), - test_transfer_amount, - &[&bob], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonTransferable as u32) - ) - ))) - ); - - // regular unchecked transfer fails - let error = token_unchecked - .transfer( - &bob_account, - &alice_account, - &bob.pubkey(), - test_transfer_amount, - &[&bob], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonTransferable as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn transfer_checked_with_fee() { - let test_transfer_amount = 100; - let maximum_fee = 10; - let transfer_fee_basis_points = 100; - - let transfer_fee_config_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - - let transfer_fee = TransferFee { - epoch: 0.into(), - transfer_fee_basis_points: transfer_fee_basis_points.into(), - maximum_fee: maximum_fee.into(), - }; - - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), - withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), - transfer_fee_basis_points, - maximum_fee, - }, - ExtensionInitializationParams::NonTransferable, - ]) - .await - .unwrap(); - - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - bob, - .. - } = context.token_context.unwrap(); - - // create token accounts - token - .create_auxiliary_token_account_with_extension_space( - &alice, - &alice.pubkey(), - vec![ExtensionType::ImmutableOwner], - ) - .await - .unwrap(); - let alice_account = alice.pubkey(); - - token - .create_auxiliary_token_account_with_extension_space( - &bob, - &bob.pubkey(), - vec![ExtensionType::ImmutableOwner], - ) - .await - .unwrap(); - let bob_account = bob.pubkey(); - - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - test_transfer_amount, - &[&mint_authority], - ) - .await - .unwrap(); - - // self-transfer fails - let error = token - .transfer( - &alice_account, - &alice_account, - &alice.pubkey(), - test_transfer_amount, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonTransferable as u32) - ) - ))) - ); - - // regular transfer fails - let error = token - .transfer( - &alice_account, - &bob_account, - &alice.pubkey(), - test_transfer_amount, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonTransferable as u32) - ) - ))) - ); - - // unchecked transfer fails - let error = token_unchecked - .transfer( - &alice_account, - &bob_account, - &alice.pubkey(), - test_transfer_amount, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonTransferable as u32) - ) - ))) - ); - - // self-transfer checked with fee fails - let fee = transfer_fee.calculate_fee(test_transfer_amount).unwrap(); - let error = token - .transfer_with_fee( - &alice_account, - &alice_account, - &alice.pubkey(), - test_transfer_amount, - fee, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonTransferable as u32) - ) - ))) - ); - - // transfer checked with fee fails - let fee = transfer_fee.calculate_fee(test_transfer_amount).unwrap(); - let error = token - .transfer_with_fee( - &alice_account, - &bob_account, - &alice.pubkey(), - test_transfer_amount, - fee, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NonTransferable as u32) - ) - ))) - ); -} diff --git a/token/program-2022-test/tests/pausable.rs b/token/program-2022-test/tests/pausable.rs deleted file mode 100644 index 724758d413d..00000000000 --- a/token/program-2022-test/tests/pausable.rs +++ /dev/null @@ -1,356 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{ - pausable::{PausableAccount, PausableConfig}, - BaseStateWithExtensions, - }, - instruction::AuthorityType, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - std::convert::TryInto, -}; - -#[tokio::test] -async fn success_initialize() { - let authority = Pubkey::new_unique(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { - authority, - }]) - .await - .unwrap(); - let TokenContext { token, alice, .. } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(Option::::from(extension.authority), Some(authority)); - assert!(!bool::from(extension.paused)); - - let account = Keypair::new(); - token - .create_auxiliary_token_account(&account, &alice.pubkey()) - .await - .unwrap(); - let state = token.get_account_info(&account.pubkey()).await.unwrap(); - let _ = state.get_extension::().unwrap(); -} - -#[tokio::test] -async fn set_authority() { - let authority = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { - authority: authority.pubkey(), - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - - // success - let new_authority = Keypair::new(); - token - .set_authority( - token.get_address(), - &authority.pubkey(), - Some(&new_authority.pubkey()), - AuthorityType::Pause, - &[&authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.authority, - Some(new_authority.pubkey()).try_into().unwrap(), - ); - token - .pause(&new_authority.pubkey(), &[&new_authority]) - .await - .unwrap(); - let err = token - .pause(&authority.pubkey(), &[&authority]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // set to none - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - AuthorityType::Pause, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, None.try_into().unwrap(),); -} - -#[tokio::test] -async fn pause_mint() { - let authority = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { - authority: authority.pubkey(), - }]) - .await - .unwrap(); - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - .. - } = context.token_context.take().unwrap(); - - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - - token - .pause(&authority.pubkey(), &[&authority]) - .await - .unwrap(); - - let amount = 10; - let error = token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); - - let error = token_unchecked - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn pause_burn() { - let authority = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { - authority: authority.pubkey(), - }]) - .await - .unwrap(); - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - .. - } = context.token_context.take().unwrap(); - - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - - let amount = 10; - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - token - .pause(&authority.pubkey(), &[&authority]) - .await - .unwrap(); - - let error = token_unchecked - .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); - - let error = token - .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn pause_transfer() { - let authority = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { - authority: authority.pubkey(), - }]) - .await - .unwrap(); - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - bob, - .. - } = context.token_context.take().unwrap(); - - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - - let bob_account = Keypair::new(); - token - .create_auxiliary_token_account(&bob_account, &bob.pubkey()) - .await - .unwrap(); - let bob_account = bob_account.pubkey(); - - let amount = 10; - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - token - .pause(&authority.pubkey(), &[&authority]) - .await - .unwrap(); - - let error = token_unchecked - .transfer(&alice_account, &bob_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap_err(); - - // need to use checked transfer - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintRequiredForTransfer as u32) - ) - ))) - ); - - let error = token - .transfer(&alice_account, &bob_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); - - let error = token - .transfer_with_fee( - &alice_account, - &bob_account, - &alice.pubkey(), - 1, - 0, - &[&alice], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintPaused as u32) - ) - ))) - ); -} diff --git a/token/program-2022-test/tests/permanent_delegate.rs b/token/program-2022-test/tests/permanent_delegate.rs deleted file mode 100644 index 0543fadd0af..00000000000 --- a/token/program-2022-test/tests/permanent_delegate.rs +++ /dev/null @@ -1,276 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{permanent_delegate::PermanentDelegate, BaseStateWithExtensions}, - instruction, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - std::convert::TryInto, -}; - -async fn setup_accounts(token_context: &TokenContext, amount: u64) -> (Pubkey, Pubkey) { - let alice_account = Keypair::new(); - token_context - .token - .create_auxiliary_token_account(&alice_account, &token_context.alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - let bob_account = Keypair::new(); - token_context - .token - .create_auxiliary_token_account(&bob_account, &token_context.bob.pubkey()) - .await - .unwrap(); - let bob_account = bob_account.pubkey(); - - // mint tokens - token_context - .token - .mint_to( - &alice_account, - &token_context.mint_authority.pubkey(), - amount, - &[&token_context.mint_authority], - ) - .await - .unwrap(); - (alice_account, bob_account) -} - -#[tokio::test] -async fn success_init() { - let delegate = Pubkey::new_unique(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::PermanentDelegate { - delegate, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - assert!(state.base.is_initialized); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.delegate, Some(delegate).try_into().unwrap(),); -} - -#[tokio::test] -async fn set_authority() { - let delegate = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::PermanentDelegate { - delegate: delegate.pubkey(), - }]) - .await - .unwrap(); - let token_context = context.token_context.unwrap(); - let new_delegate = Keypair::new(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token_context - .token - .set_authority( - token_context.token.get_address(), - &wrong.pubkey(), - Some(&new_delegate.pubkey()), - instruction::AuthorityType::PermanentDelegate, - &[&wrong], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token_context - .token - .set_authority( - token_context.token.get_address(), - &delegate.pubkey(), - Some(&new_delegate.pubkey()), - instruction::AuthorityType::PermanentDelegate, - &[&delegate], - ) - .await - .unwrap(); - let state = token_context.token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.delegate, - Some(new_delegate.pubkey()).try_into().unwrap(), - ); - - // set to none - token_context - .token - .set_authority( - token_context.token.get_address(), - &new_delegate.pubkey(), - None, - instruction::AuthorityType::PermanentDelegate, - &[&new_delegate], - ) - .await - .unwrap(); - let state = token_context.token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.delegate, None.try_into().unwrap(),); - - // fail set again - let err = token_context - .token - .set_authority( - token_context.token.get_address(), - &new_delegate.pubkey(), - Some(&delegate.pubkey()), - instruction::AuthorityType::PermanentDelegate, - &[&new_delegate], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) - ) - ))) - ); - - // setup accounts - let amount = 10; - let (alice_account, bob_account) = setup_accounts(&token_context, amount).await; - - // fail transfer - let error = token_context - .token - .transfer( - &alice_account, - &bob_account, - &new_delegate.pubkey(), - amount, - &[&new_delegate], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn success_transfer() { - let delegate = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::PermanentDelegate { - delegate: delegate.pubkey(), - }]) - .await - .unwrap(); - let token_context = context.token_context.unwrap(); - let amount = 10; - let (alice_account, bob_account) = setup_accounts(&token_context, amount).await; - - token_context - .token - .transfer( - &alice_account, - &bob_account, - &delegate.pubkey(), - amount, - &[&delegate], - ) - .await - .unwrap(); - - let destination = token_context - .token - .get_account_info(&bob_account) - .await - .unwrap(); - assert_eq!(destination.base.amount, amount); -} - -#[tokio::test] -async fn success_burn() { - let delegate = Keypair::new(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::PermanentDelegate { - delegate: delegate.pubkey(), - }]) - .await - .unwrap(); - let token_context = context.token_context.unwrap(); - let amount = 10; - let (alice_account, _) = setup_accounts(&token_context, amount).await; - - token_context - .token - .burn(&alice_account, &delegate.pubkey(), amount, &[&delegate]) - .await - .unwrap(); - - let destination = token_context - .token - .get_account_info(&alice_account) - .await - .unwrap(); - assert_eq!(destination.base.amount, 0); -} - -#[tokio::test] -async fn fail_without_extension() { - let delegate = Pubkey::new_unique(); - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - let token_context = context.token_context.unwrap(); - - // fail set - let err = token_context - .token - .set_authority( - token_context.token.get_address(), - &token_context.mint_authority.pubkey(), - Some(&delegate), - instruction::AuthorityType::PermanentDelegate, - &[&token_context.mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidAccountData) - ))) - ); -} diff --git a/token/program-2022-test/tests/program_test.rs b/token/program-2022-test/tests/program_test.rs deleted file mode 100644 index 1fbd749dae6..00000000000 --- a/token/program-2022-test/tests/program_test.rs +++ /dev/null @@ -1,376 +0,0 @@ -#![allow(dead_code)] - -use { - solana_program_test::{processor, tokio::sync::Mutex, ProgramTest, ProgramTestContext}, - solana_sdk::{ - pubkey::Pubkey, - signer::{keypair::Keypair, Signer}, - }, - spl_token_2022::{ - extension::{ - confidential_transfer::ConfidentialTransferAccount, BaseStateWithExtensions, - ExtensionType, - }, - id, native_mint, - processor::Processor, - solana_zk_sdk::encryption::{auth_encryption::*, elgamal::*}, - }, - spl_token_client::{ - client::{ - ProgramBanksClient, ProgramBanksClientProcessTransaction, ProgramClient, - SendTransaction, SimulateTransaction, - }, - token::{ComputeUnitLimit, ExtensionInitializationParams, Token, TokenResult}, - }, - std::sync::Arc, -}; - -pub struct TokenContext { - pub decimals: u8, - pub mint_authority: Keypair, - pub token: Token, - pub token_unchecked: Token, - pub alice: Keypair, - pub bob: Keypair, - pub freeze_authority: Option, -} - -pub struct TestContext { - pub context: Arc>, - pub token_context: Option, -} - -impl TestContext { - pub async fn new() -> Self { - let mut program_test = - ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_record", - spl_record::id(), - processor!(spl_record::processor::process_instruction), - ); - program_test.add_program( - "spl_elgamal_registry", - spl_elgamal_registry::id(), - processor!(spl_elgamal_registry::processor::process_instruction), - ); - let context = program_test.start_with_context().await; - let context = Arc::new(Mutex::new(context)); - - Self { - context, - token_context: None, - } - } - - pub async fn init_token_with_mint( - &mut self, - extension_init_params: Vec, - ) -> TokenResult<()> { - self.init_token_with_mint_and_freeze_authority(extension_init_params, None) - .await - } - - pub async fn init_token_with_freezing_mint( - &mut self, - extension_init_params: Vec, - ) -> TokenResult<()> { - let freeze_authority = Keypair::new(); - self.init_token_with_mint_and_freeze_authority( - extension_init_params, - Some(freeze_authority), - ) - .await - } - - pub async fn init_token_with_mint_and_freeze_authority( - &mut self, - extension_init_params: Vec, - freeze_authority: Option, - ) -> TokenResult<()> { - let mint_account = Keypair::new(); - self.init_token_with_mint_keypair_and_freeze_authority( - mint_account, - extension_init_params, - freeze_authority, - ) - .await - } - - pub async fn init_token_with_mint_keypair_and_freeze_authority( - &mut self, - mint_account: Keypair, - extension_init_params: Vec, - freeze_authority: Option, - ) -> TokenResult<()> { - let payer = keypair_clone(&self.context.lock().await.payer); - let client: Arc> = - Arc::new(ProgramBanksClient::new_from_context( - Arc::clone(&self.context), - ProgramBanksClientProcessTransaction, - )); - - let decimals: u8 = 9; - - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - let freeze_authority_pubkey = freeze_authority - .as_ref() - .map(|authority| authority.pubkey()); - - let token = Token::new( - Arc::clone(&client), - &id(), - &mint_account.pubkey(), - Some(decimals), - Arc::new(keypair_clone(&payer)), - ) - .with_compute_unit_limit(ComputeUnitLimit::Simulated); - - let token_unchecked = Token::new( - Arc::clone(&client), - &id(), - &mint_account.pubkey(), - None, - Arc::new(payer), - ) - .with_compute_unit_limit(ComputeUnitLimit::Simulated); - - token - .create_mint( - &mint_authority_pubkey, - freeze_authority_pubkey.as_ref(), - extension_init_params, - &[&mint_account], - ) - .await?; - - self.token_context = Some(TokenContext { - decimals, - mint_authority, - token, - token_unchecked, - alice: Keypair::new(), - bob: Keypair::new(), - freeze_authority, - }); - - Ok(()) - } - - pub async fn init_token_with_native_mint(&mut self) -> TokenResult<()> { - let payer = keypair_clone(&self.context.lock().await.payer); - let client: Arc> = - Arc::new(ProgramBanksClient::new_from_context( - Arc::clone(&self.context), - ProgramBanksClientProcessTransaction, - )); - - let token = - Token::create_native_mint(Arc::clone(&client), &id(), Arc::new(keypair_clone(&payer))) - .await?; - // unchecked native is never needed because decimals is known statically - let token_unchecked = Token::new_native(Arc::clone(&client), &id(), Arc::new(payer)) - .with_compute_unit_limit(ComputeUnitLimit::Simulated); - self.token_context = Some(TokenContext { - decimals: native_mint::DECIMALS, - mint_authority: Keypair::new(), /* bogus */ - token, - token_unchecked, - alice: Keypair::new(), - bob: Keypair::new(), - freeze_authority: None, - }); - Ok(()) - } -} - -pub(crate) fn keypair_clone(kp: &Keypair) -> Keypair { - Keypair::from_bytes(&kp.to_bytes()).expect("failed to copy keypair") -} - -pub(crate) struct ConfidentialTokenAccountMeta { - pub(crate) token_account: Pubkey, - pub(crate) elgamal_keypair: ElGamalKeypair, - pub(crate) aes_key: AeKey, -} - -impl ConfidentialTokenAccountMeta { - pub(crate) async fn new( - token: &Token, - owner: &Keypair, - maximum_pending_balance_credit_counter: Option, - require_memo: bool, - require_fee: bool, - ) -> Self - where - T: SendTransaction + SimulateTransaction, - { - let token_account_keypair = Keypair::new(); - - let mut extensions = vec![ExtensionType::ConfidentialTransferAccount]; - if require_memo { - extensions.push(ExtensionType::MemoTransfer); - } - if require_fee { - extensions.push(ExtensionType::ConfidentialTransferFeeAmount); - } - - token - .create_auxiliary_token_account_with_extension_space( - &token_account_keypair, - &owner.pubkey(), - extensions, - ) - .await - .unwrap(); - let token_account = token_account_keypair.pubkey(); - - let elgamal_keypair = - ElGamalKeypair::new_from_signer(owner, &token_account.to_bytes()).unwrap(); - let aes_key = AeKey::new_from_signer(owner, &token_account.to_bytes()).unwrap(); - - token - .confidential_transfer_configure_token_account( - &token_account, - &owner.pubkey(), - None, - maximum_pending_balance_credit_counter, - &elgamal_keypair, - &aes_key, - &[owner], - ) - .await - .unwrap(); - - if require_memo { - token - .enable_required_transfer_memos(&token_account, &owner.pubkey(), &[owner]) - .await - .unwrap(); - } - - Self { - token_account, - elgamal_keypair, - aes_key, - } - } - - #[allow(clippy::too_many_arguments)] - #[cfg(feature = "zk-ops")] - pub(crate) async fn new_with_tokens( - token: &Token, - owner: &Keypair, - maximum_pending_balance_credit_counter: Option, - require_memo: bool, - require_fee: bool, - mint_authority: &Keypair, - amount: u64, - decimals: u8, - ) -> Self - where - T: SendTransaction + SimulateTransaction, - { - let meta = Self::new( - token, - owner, - maximum_pending_balance_credit_counter, - require_memo, - require_fee, - ) - .await; - - token - .mint_to( - &meta.token_account, - &mint_authority.pubkey(), - amount, - &[mint_authority], - ) - .await - .unwrap(); - - token - .confidential_transfer_deposit( - &meta.token_account, - &owner.pubkey(), - amount, - decimals, - &[owner], - ) - .await - .unwrap(); - - token - .confidential_transfer_apply_pending_balance( - &meta.token_account, - &owner.pubkey(), - None, - meta.elgamal_keypair.secret(), - &meta.aes_key, - &[owner], - ) - .await - .unwrap(); - meta - } - - #[cfg(feature = "zk-ops")] - pub(crate) async fn check_balances( - &self, - token: &Token, - expected: ConfidentialTokenAccountBalances, - ) where - T: SendTransaction + SimulateTransaction, - { - let state = token.get_account_info(&self.token_account).await.unwrap(); - let extension = state - .get_extension::() - .unwrap(); - - assert_eq!( - self.elgamal_keypair - .secret() - .decrypt_u32(&extension.pending_balance_lo.try_into().unwrap()) - .unwrap(), - expected.pending_balance_lo, - ); - assert_eq!( - self.elgamal_keypair - .secret() - .decrypt_u32(&extension.pending_balance_hi.try_into().unwrap()) - .unwrap(), - expected.pending_balance_hi, - ); - assert_eq!( - self.elgamal_keypair - .secret() - .decrypt_u32(&extension.available_balance.try_into().unwrap()) - .unwrap(), - expected.available_balance, - ); - assert_eq!( - self.aes_key - .decrypt(&extension.decryptable_available_balance.try_into().unwrap()) - .unwrap(), - expected.decryptable_available_balance, - ); - } -} - -#[cfg(feature = "zk-ops")] -pub(crate) struct ConfidentialTokenAccountBalances { - pub(crate) pending_balance_lo: u64, - pub(crate) pending_balance_hi: u64, - pub(crate) available_balance: u64, - pub(crate) decryptable_available_balance: u64, -} - -#[derive(Clone, Copy)] -pub enum ConfidentialTransferOption { - InstructionData, - RecordAccount, - ContextStateAccount, -} diff --git a/token/program-2022-test/tests/reallocate.rs b/token/program-2022-test/tests/reallocate.rs deleted file mode 100644 index 52b0a3290a6..00000000000 --- a/token/program-2022-test/tests/reallocate.rs +++ /dev/null @@ -1,287 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, - program_option::COption, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - system_instruction, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_token_2022::{error::TokenError, extension::ExtensionType, state::Account}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - test_case::test_case, -}; - -#[tokio::test] -async fn reallocate() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - let TokenContext { - token, - alice, - mint_authority, - .. - } = context.token_context.unwrap(); - - // reallocate fails on wrong account type - let error = token - .reallocate( - token.get_address(), - &mint_authority.pubkey(), - &[ExtensionType::ImmutableOwner], - &[&mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidAccountData) - ))) - ); - - // create account just large enough for base - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - - // reallocate fails on invalid extension type - let error = token - .reallocate( - &alice_account, - &alice.pubkey(), - &[ExtensionType::MintCloseAuthority], - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InvalidState as u32) - ) - ))) - ); - - // reallocate fails on invalid authority - let error = token - .reallocate( - &alice_account, - &mint_authority.pubkey(), - &[ExtensionType::ImmutableOwner], - &[&mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // reallocate succeeds - token - .reallocate( - &alice_account, - &alice.pubkey(), - &[ExtensionType::ImmutableOwner], - &[&alice], - ) - .await - .unwrap(); - let account = token.get_account(alice_account).await.unwrap(); - assert_eq!( - account.data.len(), - ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner]) - .unwrap() - ); - - // reallocate succeeds with noop if account is already large enough - token.get_new_latest_blockhash().await.unwrap(); - token - .reallocate( - &alice_account, - &alice.pubkey(), - &[ExtensionType::ImmutableOwner], - &[&alice], - ) - .await - .unwrap(); - let account = token.get_account(alice_account).await.unwrap(); - assert_eq!( - account.data.len(), - ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner]) - .unwrap() - ); - - // reallocate only reallocates enough for new extension, and dedupes extensions - token - .reallocate( - &alice_account, - &alice.pubkey(), - &[ - ExtensionType::ImmutableOwner, - ExtensionType::ImmutableOwner, - ExtensionType::TransferFeeAmount, - ExtensionType::TransferFeeAmount, - ], - &[&alice], - ) - .await - .unwrap(); - let account = token.get_account(alice_account).await.unwrap(); - assert_eq!( - account.data.len(), - ExtensionType::try_calculate_account_len::(&[ - ExtensionType::ImmutableOwner, - ExtensionType::TransferFeeAmount - ]) - .unwrap() - ); -} - -#[tokio::test] -async fn reallocate_without_current_extension_knowledge() { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: COption::Some(Pubkey::new_unique()).into(), - withdraw_withheld_authority: COption::Some(Pubkey::new_unique()).into(), - transfer_fee_basis_points: 250, - maximum_fee: 10_000_000, - }]) - .await - .unwrap(); - let TokenContext { token, alice, .. } = context.token_context.unwrap(); - - // create account just large enough for TransferFeeAmount extension - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - - // reallocate resizes account to accommodate new and existing extensions - token - .reallocate( - &alice_account, - &alice.pubkey(), - &[ExtensionType::ImmutableOwner], - &[&alice], - ) - .await - .unwrap(); - let account = token.get_account(alice_account).await.unwrap(); - assert_eq!( - account.data.len(), - ExtensionType::try_calculate_account_len::(&[ - ExtensionType::TransferFeeAmount, - ExtensionType::ImmutableOwner - ]) - .unwrap() - ); -} - -#[test_case(&[ExtensionType::CpiGuard], 1_000_000_000, true ; "transfer more than new rent and sync")] -#[test_case(&[ExtensionType::CpiGuard], 1_000_000_000, false ; "transfer more than new rent")] -#[test_case(&[ExtensionType::CpiGuard], 1, true ; "transfer less than new rent and sync")] -#[test_case(&[ExtensionType::CpiGuard], 1, false ; "transfer less than new rent")] -#[test_case(&[ExtensionType::CpiGuard], 0, false ; "no transfer with extension")] -#[test_case(&[], 1_000_000_000, true ; "transfer lamports and sync without extension")] -#[test_case(&[], 1_000_000_000, false ; "transfer lamports without extension")] -#[test_case(&[], 0, false ; "no transfer without extension")] -#[tokio::test] -async fn reallocate_updates_native_rent_exemption( - extensions: &[ExtensionType], - transfer_lamports: u64, - sync_native: bool, -) { - let mut context = TestContext::new().await; - context.init_token_with_native_mint().await.unwrap(); - let TokenContext { token, alice, .. } = context.token_context.unwrap(); - let context = context.context.clone(); - - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - - // transfer more lamports - if transfer_lamports > 0 { - let context = context.lock().await; - let instructions = vec![system_instruction::transfer( - &context.payer.pubkey(), - &alice_account, - transfer_lamports, - )]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - context.banks_client.process_transaction(tx).await.unwrap(); - } - - // amount in the account should be 0 no matter what - let account_info = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(account_info.base.amount, 0); - - if sync_native { - token.sync_native(&alice_account).await.unwrap(); - let account_info = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(account_info.base.amount, transfer_lamports); - } - - let token_account = token.get_account_info(&alice_account).await.unwrap(); - let pre_amount = token_account.base.amount; - let pre_rent_exempt_reserve = token_account.base.is_native.unwrap(); - - // reallocate resizes account to accommodate new extension - token - .reallocate(&alice_account, &alice.pubkey(), extensions, &[&alice]) - .await - .unwrap(); - - let account = token.get_account(alice_account).await.unwrap(); - assert_eq!( - account.data.len(), - ExtensionType::try_calculate_account_len::(extensions).unwrap() - ); - let expected_rent_exempt_reserve = { - let context = context.lock().await; - let rent = context.banks_client.get_rent().await.unwrap(); - rent.minimum_balance(account.data.len()) - }; - let token_account = token.get_account_info(&alice_account).await.unwrap(); - let post_amount = token_account.base.amount; - let post_rent_exempt_reserve = token_account.base.is_native.unwrap(); - // amount of lamports should be totally unchanged - assert_eq!(pre_amount, post_amount); - // but rent exempt reserve should change - assert_eq!(post_rent_exempt_reserve, expected_rent_exempt_reserve); - if extensions.is_empty() { - assert_eq!(pre_rent_exempt_reserve, post_rent_exempt_reserve); - } else { - assert!(pre_rent_exempt_reserve < post_rent_exempt_reserve); - } -} diff --git a/token/program-2022-test/tests/scaled_ui_amount.rs b/token/program-2022-test/tests/scaled_ui_amount.rs deleted file mode 100644 index 062e6fafad8..00000000000 --- a/token/program-2022-test/tests/scaled_ui_amount.rs +++ /dev/null @@ -1,379 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{keypair_clone, TestContext, TokenContext}, - solana_program_test::{ - processor, - tokio::{self, sync::Mutex}, - ProgramTest, - }, - solana_sdk::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - instruction::{AccountMeta, Instruction, InstructionError}, - msg, - program::{get_return_data, invoke}, - program_error::ProgramError, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{scaled_ui_amount::ScaledUiAmountConfig, BaseStateWithExtensions}, - instruction::{amount_to_ui_amount, ui_amount_to_amount, AuthorityType}, - processor::Processor, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - std::{convert::TryInto, sync::Arc}, -}; - -#[tokio::test] -async fn success_initialize() { - for (multiplier, authority) in [ - (f64::MIN_POSITIVE, None), - (f64::MAX, Some(Pubkey::new_unique())), - ] { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig { - authority, - multiplier, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(Option::::from(extension.authority), authority,); - assert_eq!(f64::from(extension.multiplier), multiplier); - assert_eq!(f64::from(extension.new_multiplier), multiplier); - assert_eq!(i64::from(extension.new_multiplier_effective_timestamp), 0); - } -} - -#[tokio::test] -async fn fail_initialize_with_interest_bearing() { - let authority = None; - let mut context = TestContext::new().await; - let err = context - .init_token_with_mint(vec![ - ExtensionInitializationParams::ScaledUiAmountConfig { - authority, - multiplier: 1.0, - }, - ExtensionInitializationParams::InterestBearingConfig { - rate_authority: None, - rate: 0, - }, - ]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 3, - InstructionError::Custom(TokenError::InvalidExtensionCombination as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_initialize_with_bad_multiplier() { - let mut context = TestContext::new().await; - let err = context - .init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig { - authority: None, - multiplier: 0.0, - }]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidScale as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn update_multiplier() { - let authority = Keypair::new(); - let initial_multiplier = 5.0; - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig { - authority: Some(authority.pubkey()), - multiplier: initial_multiplier, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(f64::from(extension.multiplier), initial_multiplier); - assert_eq!(f64::from(extension.new_multiplier), initial_multiplier); - - // correct - let new_multiplier = 10.0; - token - .update_multiplier(&authority.pubkey(), new_multiplier, 0, &[&authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(f64::from(extension.multiplier), new_multiplier); - assert_eq!(f64::from(extension.new_multiplier), new_multiplier); - assert_eq!(i64::from(extension.new_multiplier_effective_timestamp), 0); - - // fail, bad number - let err = token - .update_multiplier(&authority.pubkey(), f64::INFINITY, 0, &[&authority]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InvalidScale as u32) - ) - ))) - ); - - // correct in the future - let newest_multiplier = 100.0; - token - .update_multiplier( - &authority.pubkey(), - newest_multiplier, - i64::MAX, - &[&authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(f64::from(extension.multiplier), new_multiplier); - assert_eq!(f64::from(extension.new_multiplier), newest_multiplier); - assert_eq!( - i64::from(extension.new_multiplier_effective_timestamp), - i64::MAX - ); - - // wrong signer - let wrong_signer = Keypair::new(); - let err = token - .update_multiplier(&wrong_signer.pubkey(), 1.0, 0, &[&wrong_signer]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn set_authority() { - let authority = Keypair::new(); - let initial_multiplier = 500.0; - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig { - authority: Some(authority.pubkey()), - multiplier: initial_multiplier, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - - // success - let new_authority = Keypair::new(); - token - .set_authority( - token.get_address(), - &authority.pubkey(), - Some(&new_authority.pubkey()), - AuthorityType::ScaledUiAmount, - &[&authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.authority, - Some(new_authority.pubkey()).try_into().unwrap(), - ); - token - .update_multiplier(&new_authority.pubkey(), 10.0, 0, &[&new_authority]) - .await - .unwrap(); - let err = token - .update_multiplier(&authority.pubkey(), 100.0, 0, &[&authority]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // set to none - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - AuthorityType::ScaledUiAmount, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, None.try_into().unwrap(),); - - // now all fail - let err = token - .update_multiplier(&new_authority.pubkey(), 50.0, 0, &[&new_authority]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoAuthorityExists as u32) - ) - ))) - ); - let err = token - .update_multiplier(&authority.pubkey(), 5.5, 0, &[&authority]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoAuthorityExists as u32) - ) - ))) - ); -} - -// test program to CPI into token to get ui amounts -fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - _input: &[u8], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - let token_program = next_account_info(account_info_iter)?; - // 10 tokens, with 9 decimal places - let test_amount = 10_000_000_000; - // "10" as an amount should be smaller than test_amount due to interest - invoke( - &ui_amount_to_amount(token_program.key, mint_info.key, "50")?, - &[mint_info.clone(), token_program.clone()], - )?; - let (_, return_data) = get_return_data().unwrap(); - let amount = u64::from_le_bytes(return_data[0..8].try_into().unwrap()); - msg!("amount: {}", amount); - if amount != test_amount { - return Err(ProgramError::InvalidInstructionData); - } - - // test_amount as a UI amount should be larger due to interest - invoke( - &amount_to_ui_amount(token_program.key, mint_info.key, test_amount)?, - &[mint_info.clone(), token_program.clone()], - )?; - let (_, return_data) = get_return_data().unwrap(); - let ui_amount = String::from_utf8(return_data).unwrap(); - msg!("ui amount: {}", ui_amount); - let float_ui_amount = ui_amount.parse::().unwrap(); - if float_ui_amount != 50.0 { - return Err(ProgramError::InvalidInstructionData); - } - Ok(()) -} - -#[tokio::test] -async fn amount_conversions() { - let authority = Keypair::new(); - let mut program_test = ProgramTest::default(); - program_test.prefer_bpf(false); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - let program_id = Pubkey::new_unique(); - program_test.add_program( - "ui_amount_to_amount", - program_id, - processor!(process_instruction), - ); - - let context = program_test.start_with_context().await; - let payer = keypair_clone(&context.payer); - let last_blockhash = context.last_blockhash; - let context = Arc::new(Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let initial_multiplier = 5.0; - context - .init_token_with_mint(vec![ExtensionInitializationParams::ScaledUiAmountConfig { - authority: Some(authority.pubkey()), - multiplier: initial_multiplier, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[Instruction { - program_id, - accounts: vec![ - AccountMeta::new_readonly(*token.get_address(), false), - AccountMeta::new_readonly(spl_token_2022::id(), false), - ], - data: vec![], - }], - Some(&payer.pubkey()), - &[&payer], - last_blockhash, - ); - context - .context - .lock() - .await - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} diff --git a/token/program-2022-test/tests/sync_native.rs b/token/program-2022-test/tests/sync_native.rs deleted file mode 100644 index ef5bc56eb2e..00000000000 --- a/token/program-2022-test/tests/sync_native.rs +++ /dev/null @@ -1,86 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::{ - tokio::{self, sync::Mutex}, - ProgramTestContext, - }, - solana_sdk::{ - pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, system_instruction, - transaction::Transaction, - }, - spl_token_2022::extension::ExtensionType, - spl_token_client::{client::ProgramBanksClientProcessTransaction, token::Token}, - std::sync::Arc, -}; - -async fn run_basic( - token: Token, - context: Arc>, - account: Pubkey, -) { - let account_info = token.get_account_info(&account).await.unwrap(); - assert_eq!(account_info.base.amount, 0); - - // system transfer to account - let amount = 1_000; - { - let context = context.lock().await; - let instructions = vec![system_instruction::transfer( - &context.payer.pubkey(), - &account, - amount, - )]; - let tx = Transaction::new_signed_with_payer( - &instructions, - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - context.banks_client.process_transaction(tx).await.unwrap(); - } - let account_info = token.get_account_info(&account).await.unwrap(); - assert_eq!(account_info.base.amount, 0); - - token.sync_native(&account).await.unwrap(); - let account_info = token.get_account_info(&account).await.unwrap(); - assert_eq!(account_info.base.amount, amount); -} - -#[tokio::test] -async fn basic() { - let mut context = TestContext::new().await; - context.init_token_with_native_mint().await.unwrap(); - let TokenContext { token, alice, .. } = context.token_context.unwrap(); - let context = context.context.clone(); - - let account = Keypair::new(); - token - .create_auxiliary_token_account(&account, &alice.pubkey()) - .await - .unwrap(); - let account = account.pubkey(); - run_basic(token, context, account).await; -} - -#[tokio::test] -async fn basic_with_extension() { - let mut context = TestContext::new().await; - context.init_token_with_native_mint().await.unwrap(); - let TokenContext { token, alice, .. } = context.token_context.unwrap(); - let context = context.context.clone(); - - let account = Keypair::new(); - token - .create_auxiliary_token_account_with_extension_space( - &account, - &alice.pubkey(), - vec![ExtensionType::ImmutableOwner], - ) - .await - .unwrap(); - let account = account.pubkey(); - run_basic(token, context, account).await; -} diff --git a/token/program-2022-test/tests/token_group_initialize.rs b/token/program-2022-test/tests/token_group_initialize.rs deleted file mode 100644 index e9d1296c31f..00000000000 --- a/token/program-2022-test/tests/token_group_initialize.rs +++ /dev/null @@ -1,271 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_pod::bytemuck::pod_from_bytes, - spl_token_2022::{error::TokenError, extension::BaseStateWithExtensions, processor::Processor}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_token_group_interface::{error::TokenGroupError, state::TokenGroup}, - std::{convert::TryInto, sync::Arc}, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let group_address = Some(mint.pubkey()); - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::GroupPointer { - authority: Some(*authority), - group_address, - }], - None, - ) - .await - .unwrap(); - context -} - -#[tokio::test] -async fn success_initialize() { - let authority = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Pubkey::new_unique(); - let max_size = 10; - let token_group = TokenGroup::new( - token_context.token.get_address(), - Some(update_authority).try_into().unwrap(), - max_size, - ); - - // fails without more lamports for new rent-exemption - let error = token_context - .token - .token_group_initialize( - &token_context.mint_authority.pubkey(), - &update_authority, - max_size, - &[&token_context.mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InsufficientFundsForRent { account_index: 2 } - ))) - ); - - // fail wrong signer - let not_mint_authority = Keypair::new(); - let error = token_context - .token - .token_group_initialize_with_rent_transfer( - &payer_pubkey, - ¬_mint_authority.pubkey(), - &update_authority, - max_size, - &[¬_mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenGroupError::IncorrectMintAuthority as u32) - ) - ))) - ); - - token_context - .token - .token_group_initialize_with_rent_transfer( - &payer_pubkey, - &token_context.mint_authority.pubkey(), - &update_authority, - max_size, - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - // check that the data is correct - let mint_info = token_context.token.get_mint_info().await.unwrap(); - let group_bytes = mint_info.get_extension_bytes::().unwrap(); - let fetched_group = pod_from_bytes::(group_bytes).unwrap(); - assert_eq!(fetched_group, &token_group); - - // fail double-init - let error = token_context - .token - .token_group_initialize_with_rent_transfer( - &payer_pubkey, - &token_context.mint_authority.pubkey(), - &update_authority, - 12, // Change so we get a different transaction - &[&token_context.mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, // No additional rent - InstructionError::Custom(TokenError::ExtensionAlreadyInitialized as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_without_group_pointer() { - let mut test_context = { - let mint_keypair = Keypair::new(); - let program_test = setup_program_test(); - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority(mint_keypair, vec![], None) - .await - .unwrap(); - context - }; - - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let error = token_context - .token - .token_group_initialize_with_rent_transfer( - &payer_pubkey, - &token_context.mint_authority.pubkey(), - &Pubkey::new_unique(), - 5, - &[&token_context.mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidExtensionCombination as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_init_in_another_mint() { - let authority = Pubkey::new_unique(); - let first_mint_keypair = Keypair::new(); - let first_mint = first_mint_keypair.pubkey(); - let mut test_context = setup(first_mint_keypair, &authority).await; - let second_mint_keypair = Keypair::new(); - let second_mint = second_mint_keypair.pubkey(); - test_context - .init_token_with_mint_keypair_and_freeze_authority( - second_mint_keypair, - vec![ExtensionInitializationParams::GroupPointer { - authority: Some(authority), - group_address: Some(second_mint), - }], - None, - ) - .await - .unwrap(); - - let token_context = test_context.token_context.take().unwrap(); - - let error = token_context - .token - .process_ixs( - &[spl_token_group_interface::instruction::initialize_group( - &spl_token_2022::id(), - &first_mint, - token_context.token.get_address(), - &token_context.mint_authority.pubkey(), - Some(Pubkey::new_unique()), - 5, - )], - &[&token_context.mint_authority], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_without_signature() { - let authority = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority).await; - - let token_context = test_context.token_context.take().unwrap(); - - let mut instruction = spl_token_group_interface::instruction::initialize_group( - &spl_token_2022::id(), - token_context.token.get_address(), - token_context.token.get_address(), - &token_context.mint_authority.pubkey(), - Some(Pubkey::new_unique()), - 5, - ); - instruction.accounts[2].is_signer = false; - let error = token_context - .token - .process_ixs(&[instruction], &[] as &[&dyn Signer; 0]) // yuck, but the compiler needs it - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ))) - ); -} diff --git a/token/program-2022-test/tests/token_group_initialize_member.rs b/token/program-2022-test/tests/token_group_initialize_member.rs deleted file mode 100644 index b25a8a6331d..00000000000 --- a/token/program-2022-test/tests/token_group_initialize_member.rs +++ /dev/null @@ -1,490 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_pod::bytemuck::pod_from_bytes, - spl_token_2022::{error::TokenError, extension::BaseStateWithExtensions, processor::Processor}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_token_group_interface::{error::TokenGroupError, state::TokenGroupMember}, - std::sync::Arc, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -type SetupConfig = (Keypair, Pubkey); // Mint, Authority - -async fn setup(group: SetupConfig, members: Vec) -> (TestContext, Vec) { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut group_context = TestContext { - context: context.clone(), - token_context: None, - }; - - let (group_mint, group_authority) = group; - let group_address = Some(group_mint.pubkey()); - group_context - .init_token_with_mint_keypair_and_freeze_authority( - group_mint, - vec![ExtensionInitializationParams::GroupPointer { - authority: Some(group_authority), - group_address, - }], - None, - ) - .await - .unwrap(); - - let mut member_contexts = vec![]; - for member in members.into_iter() { - let (member_mint, member_authority) = member; - let member_address = Some(member_mint.pubkey()); - let mut member_context = TestContext { - context: context.clone(), - token_context: None, - }; - member_context - .init_token_with_mint_keypair_and_freeze_authority( - member_mint, - vec![ExtensionInitializationParams::GroupMemberPointer { - authority: Some(member_authority), - member_address, - }], - None, - ) - .await - .unwrap(); - member_contexts.push(member_context); - } - - let payer_pubkey = group_context.context.lock().await.payer.pubkey(); - let group_token_context = group_context.token_context.as_ref().unwrap(); - group_token_context - .token - .token_group_initialize_with_rent_transfer( - &payer_pubkey, - &group_token_context.mint_authority.pubkey(), - &group_authority, - 2, - &[&group_token_context.mint_authority], - ) - .await - .unwrap(); - - (group_context, member_contexts) -} - -#[tokio::test] -async fn success_initialize() { - let group_authority = Keypair::new(); - let group_mint_keypair = Keypair::new(); - let member1_authority = Keypair::new(); - let member1_mint_keypair = Keypair::new(); - let member2_authority = Keypair::new(); - let member2_mint_keypair = Keypair::new(); - let member3_authority = Keypair::new(); - let member3_mint_keypair = Keypair::new(); - - let (_, mut member_contexts) = setup( - ( - group_mint_keypair.insecure_clone(), - group_authority.pubkey(), - ), - vec![ - ( - member1_mint_keypair.insecure_clone(), - member1_authority.pubkey(), - ), - ( - member2_mint_keypair.insecure_clone(), - member2_authority.pubkey(), - ), - ( - member3_mint_keypair.insecure_clone(), - member3_authority.pubkey(), - ), - ], - ) - .await; - - let member1_token_context = member_contexts[0].token_context.take().unwrap(); - - // fails without more lamports for new rent-exemption - let error = member1_token_context - .token - .token_group_initialize_member( - &member1_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - &[&member1_token_context.mint_authority, &group_authority], - ) - .await - .unwrap_err(); - let member_index = if group_mint_keypair - .pubkey() - .cmp(&member1_mint_keypair.pubkey()) - .is_le() - { - 4 - } else { - 3 - }; - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InsufficientFundsForRent { - account_index: member_index - } - ))) - ); - - // fail wrong mint authority signer - let payer_pubkey = member_contexts[0].context.lock().await.payer.pubkey(); - let not_mint_authority = Keypair::new(); - let error = member1_token_context - .token - .token_group_initialize_member_with_rent_transfer( - &payer_pubkey, - ¬_mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - &[¬_mint_authority, &group_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenGroupError::IncorrectMintAuthority as u32) - ) - ))) - ); - - // fail wrong group update authority signer - let not_group_update_authority = Keypair::new(); - let error = member1_token_context - .token - .token_group_initialize_member_with_rent_transfer( - &payer_pubkey, - &member1_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - ¬_group_update_authority.pubkey(), - &[ - &member1_token_context.mint_authority, - ¬_group_update_authority, - ], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenGroupError::IncorrectUpdateAuthority as u32) - ) - ))) - ); - - // fail group and member same mint - let error = member1_token_context - .token - .token_group_initialize_member_with_rent_transfer( - &payer_pubkey, - &member1_token_context.mint_authority.pubkey(), - member1_token_context.token.get_address(), - &group_authority.pubkey(), - &[&member1_token_context.mint_authority, &group_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenGroupError::MemberAccountIsGroupAccount as u32) - ) - ))) - ); - - member1_token_context - .token - .token_group_initialize_member_with_rent_transfer( - &payer_pubkey, - &member1_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - &[&member1_token_context.mint_authority, &group_authority], - ) - .await - .unwrap(); - - // check that the data is correct - let mint_info = member1_token_context.token.get_mint_info().await.unwrap(); - let member_bytes = mint_info.get_extension_bytes::().unwrap(); - let fetched_member = pod_from_bytes::(member_bytes).unwrap(); - assert_eq!( - fetched_member, - &TokenGroupMember { - mint: member1_mint_keypair.pubkey(), - group: group_mint_keypair.pubkey(), - member_number: 1.into(), - } - ); - - // fail double-init - { - let mut context = member_contexts[0].context.lock().await; - context.get_new_latest_blockhash().await.unwrap(); - context.get_new_latest_blockhash().await.unwrap(); - } - let error = member1_token_context - .token - .token_group_initialize_member( - &member1_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - &[&member1_token_context.mint_authority, &group_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::ExtensionAlreadyInitialized as u32) - ) - ))) - ); - - // Now the second - let member2_token_context = member_contexts[1].token_context.take().unwrap(); - member2_token_context - .token - .token_group_initialize_member_with_rent_transfer( - &payer_pubkey, - &member2_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - &[&member2_token_context.mint_authority, &group_authority], - ) - .await - .unwrap(); - let mint_info = member2_token_context.token.get_mint_info().await.unwrap(); - let member_bytes = mint_info.get_extension_bytes::().unwrap(); - let fetched_member = pod_from_bytes::(member_bytes).unwrap(); - assert_eq!( - fetched_member, - &TokenGroupMember { - mint: member2_mint_keypair.pubkey(), - group: group_mint_keypair.pubkey(), - member_number: 2.into(), - } - ); - - // Third should fail on max size - let member3_token_context = member_contexts[2].token_context.take().unwrap(); - let error = member3_token_context - .token - .token_group_initialize_member_with_rent_transfer( - &payer_pubkey, - &member3_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - &[&member3_token_context.mint_authority, &group_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenGroupError::SizeExceedsMaxSize as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_without_member_pointer() { - let group_authority = Keypair::new(); - let group_mint_keypair = Keypair::new(); - let member_mint_keypair = Keypair::new(); - - let (group_context, _) = setup( - ( - group_mint_keypair.insecure_clone(), - group_authority.pubkey(), - ), - vec![], - ) - .await; - - let mut member_test_context = TestContext { - context: group_context.context.clone(), - token_context: None, - }; - member_test_context - .init_token_with_mint_keypair_and_freeze_authority(member_mint_keypair, vec![], None) - .await - .unwrap(); - - let payer_pubkey = member_test_context.context.lock().await.payer.pubkey(); - let member_token_context = member_test_context.token_context.take().unwrap(); - - let error = member_token_context - .token - .token_group_initialize_member_with_rent_transfer( - &payer_pubkey, - &member_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - &[&member_token_context.mint_authority, &group_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidExtensionCombination as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_init_in_another_mint() { - let group_authority = Keypair::new(); - let group_mint_keypair = Keypair::new(); - let member_authority = Keypair::new(); - let first_member_mint_keypair = Keypair::new(); - let second_member_mint_keypair = Keypair::new(); - - let (_, mut member_contexts) = setup( - ( - group_mint_keypair.insecure_clone(), - group_authority.pubkey(), - ), - vec![( - second_member_mint_keypair.insecure_clone(), - member_authority.pubkey(), - )], - ) - .await; - - let member_token_context = member_contexts[0].token_context.take().unwrap(); - let error = member_token_context - .token - .process_ixs( - &[spl_token_group_interface::instruction::initialize_member( - &spl_token_2022::id(), - &first_member_mint_keypair.pubkey(), - member_token_context.token.get_address(), - &member_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - )], - &[&member_token_context.mint_authority, &group_authority], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_without_signatures() { - let group_authority = Keypair::new(); - let group_mint_keypair = Keypair::new(); - let member_authority = Keypair::new(); - let member_mint_keypair = Keypair::new(); - - let (_, mut member_contexts) = setup( - ( - group_mint_keypair.insecure_clone(), - group_authority.pubkey(), - ), - vec![( - member_mint_keypair.insecure_clone(), - member_authority.pubkey(), - )], - ) - .await; - - let member_token_context = member_contexts[0].token_context.take().unwrap(); - - // Missing mint authority - let mut instruction = spl_token_group_interface::instruction::initialize_member( - &spl_token_2022::id(), - &member_mint_keypair.pubkey(), - member_token_context.token.get_address(), - &member_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - ); - instruction.accounts[2].is_signer = false; - let error = member_token_context - .token - .process_ixs(&[instruction], &[&group_authority]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ))) - ); - - // Missing group update authority - let mut instruction = spl_token_group_interface::instruction::initialize_member( - &spl_token_2022::id(), - &member_mint_keypair.pubkey(), - member_token_context.token.get_address(), - &member_token_context.mint_authority.pubkey(), - &group_mint_keypair.pubkey(), - &group_authority.pubkey(), - ); - instruction.accounts[4].is_signer = false; - let error = member_token_context - .token - .process_ixs(&[instruction], &[&member_token_context.mint_authority]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ))) - ); -} diff --git a/token/program-2022-test/tests/token_group_update_authority.rs b/token/program-2022-test/tests/token_group_update_authority.rs deleted file mode 100644 index 99688fb3dd0..00000000000 --- a/token/program-2022-test/tests/token_group_update_authority.rs +++ /dev/null @@ -1,202 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_2022::{extension::BaseStateWithExtensions, processor::Processor}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_token_group_interface::{ - error::TokenGroupError, instruction::update_group_authority, state::TokenGroup, - }, - std::{convert::TryInto, sync::Arc}, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let group_address = Some(mint.pubkey()); - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::GroupPointer { - authority: Some(*authority), - group_address, - }], - None, - ) - .await - .unwrap(); - context -} - -#[tokio::test] -async fn success_update() { - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair.insecure_clone(), &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Keypair::new(); - let mut token_group = TokenGroup::new( - &mint_keypair.pubkey(), - Some(update_authority.pubkey()).try_into().unwrap(), - 10, - ); - - token_context - .token - .token_group_initialize_with_rent_transfer( - &payer_pubkey, - &token_context.mint_authority.pubkey(), - &update_authority.pubkey(), - 10, - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - let new_update_authority = Keypair::new(); - let new_update_authority_pubkey = - OptionalNonZeroPubkey::try_from(Some(new_update_authority.pubkey())).unwrap(); - token_group.update_authority = new_update_authority_pubkey; - - token_context - .token - .token_group_update_authority( - &update_authority.pubkey(), - Some(new_update_authority.pubkey()), - &[&update_authority], - ) - .await - .unwrap(); - - // check that the data is correct - let mint = token_context.token.get_mint_info().await.unwrap(); - let fetched_group = mint.get_extension::().unwrap(); - assert_eq!(fetched_group, &token_group); - - // unset - token_group.update_authority = None.try_into().unwrap(); - token_context - .token - .token_group_update_authority( - &new_update_authority.pubkey(), - None, - &[&new_update_authority], - ) - .await - .unwrap(); - - let mint = token_context.token.get_mint_info().await.unwrap(); - let fetched_group = mint.get_extension::().unwrap(); - assert_eq!(fetched_group, &token_group); - - // fail to update - let error = token_context - .token - .token_group_update_authority( - &new_update_authority.pubkey(), - Some(new_update_authority.pubkey()), - &[&new_update_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenGroupError::ImmutableGroup as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_authority_checks() { - let program_id = spl_token_2022::id(); - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mint_pubkey = mint_keypair.pubkey(); - let mut test_context = setup(mint_keypair, &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Keypair::new(); - token_context - .token - .token_group_initialize_with_rent_transfer( - &payer_pubkey, - &token_context.mint_authority.pubkey(), - &update_authority.pubkey(), - 10, - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - // wrong authority - let error = token_context - .token - .token_group_update_authority(&payer_pubkey, None, &[] as &[&dyn Signer; 0]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenGroupError::IncorrectUpdateAuthority as u32), - ) - ))) - ); - - // no signature - let context = test_context.context.lock().await; - let mut instruction = - update_group_authority(&program_id, &mint_pubkey, &authority.pubkey(), None); - instruction.accounts[1].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature,) - ); -} diff --git a/token/program-2022-test/tests/token_group_update_max_size.rs b/token/program-2022-test/tests/token_group_update_max_size.rs deleted file mode 100644 index fef4db7e35e..00000000000 --- a/token/program-2022-test/tests/token_group_update_max_size.rs +++ /dev/null @@ -1,249 +0,0 @@ -#![cfg(feature = "test-sbf")] -#![allow(clippy::items_after_test_module)] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - account::Account as SolanaAccount, instruction::InstructionError, pubkey::Pubkey, - signature::Signer, signer::keypair::Keypair, transaction::TransactionError, - transport::TransportError, - }, - spl_token_2022::{extension::BaseStateWithExtensions, processor::Processor}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_token_group_interface::{ - error::TokenGroupError, instruction::update_group_max_size, state::TokenGroup, - }, - std::{convert::TryInto, sync::Arc}, - test_case::test_case, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let group_address = Some(mint.pubkey()); - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::GroupPointer { - authority: Some(*authority), - group_address, - }], - None, - ) - .await - .unwrap(); - context -} - -// Successful attempts to set higher than size -#[test_case(0, 0, 10)] -#[test_case(5, 0, 10)] -#[test_case(50, 0, 200_000)] -#[test_case(100_000, 100_000, 200_000)] -#[test_case(50, 0, 300_000_000)] -#[test_case(100_000, 100_000, 300_000_000)] -#[test_case(100_000_000, 100_000_000, 300_000_000)] -#[test_case(0, 0, u64::MAX)] -#[test_case(200_000, 200_000, u64::MAX)] -#[test_case(300_000_000, 300_000_000, u64::MAX)] -// Attempts to set lower than size -#[test_case(5, 5, 4)] -#[test_case(200_000, 200_000, 50)] -#[test_case(200_000, 200_000, 100_000)] -#[test_case(300_000_000, 300_000_000, 50)] -#[test_case(u64::MAX, u64::MAX, 0)] -#[tokio::test] -async fn test_update_group_max_size(max_size: u64, size: u64, new_max_size: u64) { - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair.insecure_clone(), &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Keypair::new(); - let mut token_group = TokenGroup::new( - &mint_keypair.pubkey(), - Some(update_authority.pubkey()).try_into().unwrap(), - max_size, - ); - - token_context - .token - .token_group_initialize_with_rent_transfer( - &payer_pubkey, - &token_context.mint_authority.pubkey(), - &update_authority.pubkey(), - max_size, - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - { - // Update the group's size manually - let mut context = test_context.context.lock().await; - - let group_mint_account = context - .banks_client - .get_account(mint_keypair.pubkey()) - .await - .unwrap() - .unwrap(); - - let old_data = context - .banks_client - .get_account(mint_keypair.pubkey()) - .await - .unwrap() - .unwrap() - .data; - - let data = { - // 0....81: mint - // 82...164: padding - // 165..166: account type - // 167..170: extension discriminator (GroupPointer) - // 171..202: authority - // 203..234: group pointer - // 235..238: extension discriminator (TokenGroup) - // 239..270: mint - // 271..302: update_authority - // 303..306: size - // 307..310: max_size - let (front, back) = old_data.split_at(302); - let (_, back) = back.split_at(4); - let size_bytes = size.to_le_bytes(); - let mut bytes = vec![]; - bytes.extend_from_slice(front); - bytes.extend_from_slice(&size_bytes); - bytes.extend_from_slice(back); - bytes - }; - - context.set_account( - &mint_keypair.pubkey(), - &SolanaAccount { - data, - ..group_mint_account - } - .into(), - ); - - token_group.size = size.into(); - } - - token_group.max_size = new_max_size.into(); - - if new_max_size < size { - let error = token_context - .token - .token_group_update_max_size( - &update_authority.pubkey(), - new_max_size, - &[&update_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenGroupError::SizeExceedsNewMaxSize as u32) - ) - ))), - ); - } else { - token_context - .token - .token_group_update_max_size( - &update_authority.pubkey(), - new_max_size, - &[&update_authority], - ) - .await - .unwrap(); - - let mint_info = token_context.token.get_mint_info().await.unwrap(); - let fetched_group = mint_info.get_extension::().unwrap(); - assert_eq!(fetched_group, &token_group); - } -} - -#[tokio::test] -async fn fail_authority_checks() { - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Keypair::new(); - token_context - .token - .token_group_initialize_with_rent_transfer( - &payer_pubkey, - &token_context.mint_authority.pubkey(), - &update_authority.pubkey(), - 10, - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - // no signature - let mut instruction = update_group_max_size( - &spl_token_2022::id(), - token_context.token.get_address(), - &update_authority.pubkey(), - 20, - ); - instruction.accounts[1].is_signer = false; - - let error = token_context - .token - .process_ixs(&[instruction], &[] as &[&dyn Signer; 0]) // yuck, but the compiler needs it - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ))) - ); - - // wrong authority - let wrong_authority = Keypair::new(); - let error = token_context - .token - .token_group_update_max_size(&wrong_authority.pubkey(), 20, &[&wrong_authority]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenGroupError::IncorrectUpdateAuthority as u32) - ) - ))) - ); -} diff --git a/token/program-2022-test/tests/token_metadata_emit.rs b/token/program-2022-test/tests/token_metadata_emit.rs deleted file mode 100644 index 42fdbd9b5b8..00000000000 --- a/token/program-2022-test/tests/token_metadata_emit.rs +++ /dev/null @@ -1,141 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - borsh1::try_from_slice_unchecked, program::MAX_RETURN_DATA, pubkey::Pubkey, - signature::Signer, signer::keypair::Keypair, transaction::Transaction, - }, - spl_token_2022::processor::Processor, - spl_token_client::token::ExtensionInitializationParams, - spl_token_metadata_interface::{instruction::emit, state::TokenMetadata}, - std::{convert::TryInto, sync::Arc}, - test_case::test_case, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let metadata_address = Some(mint.pubkey()); - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::MetadataPointer { - authority: Some(*authority), - metadata_address, - }], - None, - ) - .await - .unwrap(); - context -} - -#[test_case(Some(40), Some(40) ; "zero bytes")] -#[test_case(Some(40), Some(41) ; "one byte")] -#[test_case(Some(1_000_000), Some(1_000_001) ; "too far")] -#[test_case(Some(50), Some(49) ; "wrong way")] -#[test_case(Some(50), None ; "truncate start")] -#[test_case(None, Some(50) ; "truncate end")] -#[test_case(None, None ; "full data")] -#[tokio::test] -async fn success(start: Option, end: Option) { - let program_id = spl_token_2022::id(); - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority.pubkey()).try_into().unwrap(), - mint: *token_context.token.get_address(), - ..Default::default() - }; - - token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &update_authority.pubkey(), - &token_context.mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - let context = test_context.context.lock().await; - - let transaction = Transaction::new_signed_with_payer( - &[emit( - &program_id, - token_context.token.get_address(), - start, - end, - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let simulation = context - .banks_client - .simulate_transaction(transaction) - .await - .unwrap(); - - let metadata_buffer = borsh::to_vec(&token_metadata).unwrap(); - if let Some(check_buffer) = TokenMetadata::get_slice(&metadata_buffer, start, end) { - if !check_buffer.is_empty() { - // pad the data if necessary - let mut return_data = vec![0; MAX_RETURN_DATA]; - if let Some(simulation_details) = simulation.simulation_details { - if let Some(simulation_return_data) = simulation_details.return_data { - assert_eq!(simulation_return_data.program_id, program_id); - return_data[..simulation_return_data.data.len()] - .copy_from_slice(&simulation_return_data.data); - } - } - - assert_eq!(*check_buffer, return_data[..check_buffer.len()]); - // we're sure that we're getting the full data, so also compare the deserialized - // type - if start.is_none() && end.is_none() { - let emitted_token_metadata = - try_from_slice_unchecked::(&return_data).unwrap(); - assert_eq!(token_metadata, emitted_token_metadata); - } - } else { - assert!(simulation.simulation_details.unwrap().return_data.is_none()); - } - } else { - assert!(simulation.simulation_details.unwrap().return_data.is_none()); - } -} diff --git a/token/program-2022-test/tests/token_metadata_initialize.rs b/token/program-2022-test/tests/token_metadata_initialize.rs deleted file mode 100644 index e6576d68f68..00000000000 --- a/token/program-2022-test/tests/token_metadata_initialize.rs +++ /dev/null @@ -1,290 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - borsh::BorshDeserialize, - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{error::TokenError, extension::BaseStateWithExtensions, processor::Processor}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_token_metadata_interface::{error::TokenMetadataError, state::TokenMetadata}, - std::{convert::TryInto, sync::Arc}, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let metadata_address = Some(mint.pubkey()); - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::MetadataPointer { - authority: Some(*authority), - metadata_address, - }], - None, - ) - .await - .unwrap(); - context -} - -#[tokio::test] -async fn success_initialize() { - let authority = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Pubkey::new_unique(); - let name = "MyTokenNeedsMetadata".to_string(); - let symbol = "NEEDS".to_string(); - let uri = "my.token.needs.metadata".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority).try_into().unwrap(), - mint: *token_context.token.get_address(), - ..Default::default() - }; - - // fails without more lamports for new rent-exemption - let error = token_context - .token - .token_metadata_initialize( - &update_authority, - &token_context.mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[&token_context.mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InsufficientFundsForRent { account_index: 2 } - ))) - ); - - // fail wrong signer - let not_mint_authority = Keypair::new(); - let error = token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &update_authority, - ¬_mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[¬_mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenMetadataError::IncorrectMintAuthority as u32) - ) - ))) - ); - - token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &update_authority, - &token_context.mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - // check that the data is correct - let mint_info = token_context.token.get_mint_info().await.unwrap(); - let metadata_bytes = mint_info.get_extension_bytes::().unwrap(); - let fetched_metadata = TokenMetadata::try_from_slice(metadata_bytes).unwrap(); - assert_eq!(fetched_metadata, token_metadata); - - // fail double-init - let error = token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &update_authority, - &token_context.mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[&token_context.mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::ExtensionAlreadyInitialized as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_without_metadata_pointer() { - let mut test_context = { - let mint_keypair = Keypair::new(); - let program_test = setup_program_test(); - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority(mint_keypair, vec![], None) - .await - .unwrap(); - context - }; - - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let error = token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &Pubkey::new_unique(), - &token_context.mint_authority.pubkey(), - "Name".to_string(), - "Symbol".to_string(), - "URI".to_string(), - &[&token_context.mint_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidExtensionCombination as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_init_in_another_mint() { - let authority = Pubkey::new_unique(); - let first_mint_keypair = Keypair::new(); - let first_mint = first_mint_keypair.pubkey(); - let mut test_context = setup(first_mint_keypair, &authority).await; - let second_mint_keypair = Keypair::new(); - let second_mint = second_mint_keypair.pubkey(); - test_context - .init_token_with_mint_keypair_and_freeze_authority( - second_mint_keypair, - vec![ExtensionInitializationParams::MetadataPointer { - authority: Some(authority), - metadata_address: Some(second_mint), - }], - None, - ) - .await - .unwrap(); - - let token_context = test_context.token_context.take().unwrap(); - - let error = token_context - .token - .process_ixs( - &[spl_token_metadata_interface::instruction::initialize( - &spl_token_2022::id(), - &first_mint, - &Pubkey::new_unique(), - token_context.token.get_address(), - &token_context.mint_authority.pubkey(), - "Name".to_string(), - "Symbol".to_string(), - "URI".to_string(), - )], - &[&token_context.mint_authority], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_without_signature() { - let authority = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority).await; - - let token_context = test_context.token_context.take().unwrap(); - - let mut instruction = spl_token_metadata_interface::instruction::initialize( - &spl_token_2022::id(), - token_context.token.get_address(), - &Pubkey::new_unique(), - token_context.token.get_address(), - &token_context.mint_authority.pubkey(), - "Name".to_string(), - "Symbol".to_string(), - "URI".to_string(), - ); - instruction.accounts[3].is_signer = false; - let error = token_context - .token - .process_ixs(&[instruction], &[] as &[&dyn Signer; 0]) // yuck, but the compiler needs it - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ))) - ); -} diff --git a/token/program-2022-test/tests/token_metadata_remove_key.rs b/token/program-2022-test/tests/token_metadata_remove_key.rs deleted file mode 100644 index 04354e3ae0e..00000000000 --- a/token/program-2022-test/tests/token_metadata_remove_key.rs +++ /dev/null @@ -1,258 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_token_2022::{extension::BaseStateWithExtensions, processor::Processor}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_token_metadata_interface::{ - error::TokenMetadataError, - instruction::remove_key, - state::{Field, TokenMetadata}, - }, - std::{convert::TryInto, sync::Arc}, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let metadata_address = Some(mint.pubkey()); - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::MetadataPointer { - authority: Some(*authority), - metadata_address, - }], - None, - ) - .await - .unwrap(); - context -} - -#[tokio::test] -async fn success_remove() { - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let mut token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority.pubkey()).try_into().unwrap(), - mint: *token_context.token.get_address(), - ..Default::default() - }; - - token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &update_authority.pubkey(), - &token_context.mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - let key = "new_field, wow!".to_string(); - let field = Field::Key(key.clone()); - let value = "so impressed with the new field, don't know what to put here".to_string(); - token_metadata.update(field.clone(), value.clone()); - - // add the field - token_context - .token - .token_metadata_update_field_with_rent_transfer( - &payer_pubkey, - &update_authority.pubkey(), - field, - value, - None, - &[&update_authority], - ) - .await - .unwrap(); - - // now remove it - token_context - .token - .token_metadata_remove_key( - &update_authority.pubkey(), - key.clone(), - false, // idempotent - &[&update_authority], - ) - .await - .unwrap(); - - // check that the data is correct - token_metadata.remove_key(&key); - let mint = token_context.token.get_mint_info().await.unwrap(); - let fetched_metadata = mint.get_variable_len_extension::().unwrap(); - assert_eq!(fetched_metadata, token_metadata); - - // succeed again with idempotent flag - token_context - .token - .token_metadata_remove_key( - &update_authority.pubkey(), - key.clone(), - true, // idempotent - &[&update_authority], - ) - .await - .unwrap(); - - // fail doing it again without idempotent flag - { - // Be really sure to have a new latest blockhash since this keeps failing in CI - let mut context = test_context.context.lock().await; - context.get_new_latest_blockhash().await.unwrap(); - context.get_new_latest_blockhash().await.unwrap(); - } - let error = token_context - .token - .token_metadata_remove_key( - &update_authority.pubkey(), - key.clone(), - false, // idempotent - &[&update_authority], - ) - .await - .unwrap_err(); - - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::KeyNotFound as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_authority_checks() { - let program_id = spl_token_2022::id(); - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mint_pubkey = mint_keypair.pubkey(); - let mut test_context = setup(mint_keypair, &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority.pubkey()).try_into().unwrap(), - mint: *token_context.token.get_address(), - ..Default::default() - }; - - token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &update_authority.pubkey(), - &token_context.mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - let key = "new_field, wow!".to_string(); - - // wrong authority - let error = token_context - .token - .token_metadata_remove_key( - &payer_pubkey, - key, - true, // idempotent - &[] as &[&dyn Signer; 0], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::IncorrectUpdateAuthority as u32) - ) - ))) - ); - - // no signature - let context = test_context.context.lock().await; - let mut instruction = remove_key( - &program_id, - &mint_pubkey, - &update_authority.pubkey(), - "new_name".to_string(), - true, // idempotent - ); - instruction.accounts[1].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature,) - ); -} diff --git a/token/program-2022-test/tests/token_metadata_update_authority.rs b/token/program-2022-test/tests/token_metadata_update_authority.rs deleted file mode 100644 index 5efe8a7c3d7..00000000000 --- a/token/program-2022-test/tests/token_metadata_update_authority.rs +++ /dev/null @@ -1,228 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - transaction::{Transaction, TransactionError}, - transport::TransportError, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_2022::{extension::BaseStateWithExtensions, processor::Processor}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_token_metadata_interface::{ - error::TokenMetadataError, instruction::update_authority, state::TokenMetadata, - }, - std::{convert::TryInto, sync::Arc}, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let metadata_address = Some(mint.pubkey()); - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::MetadataPointer { - authority: Some(*authority), - metadata_address, - }], - None, - ) - .await - .unwrap(); - context -} - -#[tokio::test] -async fn success_update() { - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let mut token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(authority.pubkey()).try_into().unwrap(), - mint: *token_context.token.get_address(), - ..Default::default() - }; - - token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &authority.pubkey(), - &token_context.mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - let new_update_authority = Keypair::new(); - let new_update_authority_pubkey = - OptionalNonZeroPubkey::try_from(Some(new_update_authority.pubkey())).unwrap(); - token_metadata.update_authority = new_update_authority_pubkey; - - token_context - .token - .token_metadata_update_authority( - &authority.pubkey(), - Some(new_update_authority.pubkey()), - &[&authority], - ) - .await - .unwrap(); - - // check that the data is correct - let mint = token_context.token.get_mint_info().await.unwrap(); - let fetched_metadata = mint.get_variable_len_extension::().unwrap(); - assert_eq!(fetched_metadata, token_metadata); - - // unset - token_metadata.update_authority = None.try_into().unwrap(); - token_context - .token - .token_metadata_update_authority( - &new_update_authority.pubkey(), - None, - &[&new_update_authority], - ) - .await - .unwrap(); - - let mint = token_context.token.get_mint_info().await.unwrap(); - let fetched_metadata = mint.get_variable_len_extension::().unwrap(); - assert_eq!(fetched_metadata, token_metadata); - - // fail to update - let error = token_context - .token - .token_metadata_update_authority( - &new_update_authority.pubkey(), - Some(new_update_authority.pubkey()), - &[&new_update_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::ImmutableMetadata as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_authority_checks() { - let program_id = spl_token_2022::id(); - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mint_pubkey = mint_keypair.pubkey(); - let mut test_context = setup(mint_keypair, &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(authority.pubkey()).try_into().unwrap(), - mint: *token_context.token.get_address(), - ..Default::default() - }; - - token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &authority.pubkey(), - &token_context.mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - // wrong authority - let error = token_context - .token - .token_metadata_update_authority(&payer_pubkey, None, &[] as &[&dyn Signer; 0]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::IncorrectUpdateAuthority as u32), - ) - ))) - ); - - // no signature - let context = test_context.context.lock().await; - let mut instruction = update_authority( - &program_id, - &mint_pubkey, - &authority.pubkey(), - None.try_into().unwrap(), - ); - instruction.accounts[1].is_signer = false; - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature,) - ); -} diff --git a/token/program-2022-test/tests/token_metadata_update_field.rs b/token/program-2022-test/tests/token_metadata_update_field.rs deleted file mode 100644 index 83417d376f4..00000000000 --- a/token/program-2022-test/tests/token_metadata_update_field.rs +++ /dev/null @@ -1,206 +0,0 @@ -#![cfg(feature = "test-sbf")] -#![allow(clippy::items_after_test_module)] - -mod program_test; -use { - program_test::TestContext, - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{extension::BaseStateWithExtensions, processor::Processor}, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_token_metadata_interface::{ - error::TokenMetadataError, - instruction::update_field, - state::{Field, TokenMetadata}, - }, - std::{convert::TryInto, sync::Arc}, - test_case::test_case, -}; - -fn setup_program_test() -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(Processor::process), - ); - program_test -} - -async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext { - let program_test = setup_program_test(); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let metadata_address = Some(mint.pubkey()); - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::MetadataPointer { - authority: Some(*authority), - metadata_address, - }], - None, - ) - .await - .unwrap(); - context -} - -#[test_case(Field::Name, "This is my larger name".to_string() ; "larger name")] -#[test_case(Field::Name, "Smaller".to_string() ; "smaller name")] -#[test_case(Field::Key("my new field".to_string()), "Some data for the new field!".to_string() ; "new field")] -#[tokio::test] -async fn success_update(field: Field, value: String) { - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Keypair::new(); - let name = "MySuperCoolToken".to_string(); - let symbol = "MINE".to_string(); - let uri = "my.super.cool.token".to_string(); - let mut token_metadata = TokenMetadata { - name, - symbol, - uri, - update_authority: Some(update_authority.pubkey()).try_into().unwrap(), - mint: *token_context.token.get_address(), - ..Default::default() - }; - - token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &update_authority.pubkey(), - &token_context.mint_authority.pubkey(), - token_metadata.name.clone(), - token_metadata.symbol.clone(), - token_metadata.uri.clone(), - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - let old_space = token_metadata.tlv_size_of().unwrap(); - token_metadata.update(field.clone(), value.clone()); - let new_space = token_metadata.tlv_size_of().unwrap(); - - if new_space > old_space { - let error = token_context - .token - .token_metadata_update_field( - &update_authority.pubkey(), - field.clone(), - value.clone(), - &[&update_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InsufficientFundsForRent { account_index: 2 } - ))) - ); - } - - // transfer required lamports - token_context - .token - .token_metadata_update_field_with_rent_transfer( - &payer_pubkey, - &update_authority.pubkey(), - field, - value, - None, - &[&update_authority], - ) - .await - .unwrap(); - - // check that the account looks good - let mint_info = token_context.token.get_mint_info().await.unwrap(); - let fetched_metadata = mint_info - .get_variable_len_extension::() - .unwrap(); - assert_eq!(fetched_metadata, token_metadata); -} - -#[tokio::test] -async fn fail_authority_checks() { - let authority = Keypair::new(); - let mint_keypair = Keypair::new(); - let mut test_context = setup(mint_keypair, &authority.pubkey()).await; - let payer_pubkey = test_context.context.lock().await.payer.pubkey(); - let token_context = test_context.token_context.take().unwrap(); - - let update_authority = Keypair::new(); - token_context - .token - .token_metadata_initialize_with_rent_transfer( - &payer_pubkey, - &update_authority.pubkey(), - &token_context.mint_authority.pubkey(), - "MySuperCoolToken".to_string(), - "MINE".to_string(), - "my.super.cool.token".to_string(), - &[&token_context.mint_authority], - ) - .await - .unwrap(); - - // no signature - let mut instruction = update_field( - &spl_token_2022::id(), - token_context.token.get_address(), - &update_authority.pubkey(), - Field::Name, - "new_name".to_string(), - ); - instruction.accounts[1].is_signer = false; - - let error = token_context - .token - .process_ixs(&[instruction], &[] as &[&dyn Signer; 0]) // yuck, but the compiler needs it - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) - ))) - ); - - // wrong authority - let wrong_authority = Keypair::new(); - let error = token_context - .token - .token_metadata_update_field( - &wrong_authority.pubkey(), - Field::Name, - "new_name".to_string(), - &[&wrong_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenMetadataError::IncorrectUpdateAuthority as u32) - ) - ))) - ); -} diff --git a/token/program-2022-test/tests/transfer.rs b/token/program-2022-test/tests/transfer.rs deleted file mode 100644 index 8c3a395fdf5..00000000000 --- a/token/program-2022-test/tests/transfer.rs +++ /dev/null @@ -1,379 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::error::TokenError, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, -}; - -#[derive(PartialEq)] -enum TestMode { - All, - CheckedOnly, -} - -async fn run_basic_transfers(context: TestContext, test_mode: TestMode) { - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - bob, - .. - } = context.token_context.unwrap(); - - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - let bob_account = Keypair::new(); - token - .create_auxiliary_token_account(&bob_account, &bob.pubkey()) - .await - .unwrap(); - let bob_account = bob_account.pubkey(); - - // mint a token - let amount = 10; - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - if test_mode == TestMode::All { - // unchecked is ok - token_unchecked - .transfer(&alice_account, &bob_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); - } - - // checked is ok - token - .transfer(&alice_account, &bob_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); - - // transfer too much is not ok - let error = token - .transfer( - &alice_account, - &bob_account, - &alice.pubkey(), - amount, - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InsufficientFunds as u32) - ) - ))) - ); - - // wrong signer - let error = token - .transfer(&alice_account, &bob_account, &bob.pubkey(), 1, &[&bob]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn basic() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_basic_transfers(context, TestMode::All).await; -} - -#[tokio::test] -async fn basic_with_extension() { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(Pubkey::new_unique()), - transfer_fee_basis_points: 100u16, - maximum_fee: 1_000_000u64, - }]) - .await - .unwrap(); - run_basic_transfers(context, TestMode::CheckedOnly).await; -} - -async fn run_self_transfers(context: TestContext, test_mode: TestMode) { - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - .. - } = context.token_context.unwrap(); - - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - - // mint a token - let amount = 10; - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - // self transfer is ok - token - .transfer( - &alice_account, - &alice_account, - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); - if test_mode == TestMode::All { - token_unchecked - .transfer( - &alice_account, - &alice_account, - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); - } - - // too much self transfer is not ok - let error = token - .transfer( - &alice_account, - &alice_account, - &alice.pubkey(), - amount.checked_add(1).unwrap(), - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InsufficientFunds as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn self_transfer() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_self_transfers(context, TestMode::All).await; -} - -#[tokio::test] -async fn self_transfer_with_extension() { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(Pubkey::new_unique()), - transfer_fee_basis_points: 100u16, - maximum_fee: 1_000_000u64, - }]) - .await - .unwrap(); - run_self_transfers(context, TestMode::CheckedOnly).await; -} - -async fn run_self_owned(context: TestContext, test_mode: TestMode) { - let TokenContext { - mint_authority, - token, - token_unchecked, - alice, - bob, - .. - } = context.token_context.unwrap(); - - token - .create_auxiliary_token_account(&alice, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice.pubkey(); - let bob_account = Keypair::new(); - token - .create_auxiliary_token_account(&bob_account, &bob.pubkey()) - .await - .unwrap(); - let bob_account = bob_account.pubkey(); - - // mint a token - let amount = 10; - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - if test_mode == TestMode::All { - // unchecked is ok - token_unchecked - .transfer(&alice_account, &bob_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); - } - - // checked is ok - token - .transfer(&alice_account, &bob_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); - - // self transfer is ok - token - .transfer( - &alice_account, - &alice_account, - &alice.pubkey(), - 1, - &[&alice], - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn self_owned() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - run_self_owned(context, TestMode::All).await; -} - -#[tokio::test] -async fn self_owned_with_extension() { - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(Pubkey::new_unique()), - transfer_fee_basis_points: 100u16, - maximum_fee: 1_000_000u64, - }]) - .await - .unwrap(); - run_self_owned(context, TestMode::CheckedOnly).await; -} - -#[tokio::test] -async fn transfer_with_fee_on_mint_without_fee_configured() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - let TokenContext { - mint_authority, - token, - alice, - bob, - .. - } = context.token_context.unwrap(); - - let alice_account = Keypair::new(); - token - .create_auxiliary_token_account(&alice_account, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - let bob_account = Keypair::new(); - token - .create_auxiliary_token_account(&bob_account, &bob.pubkey()) - .await - .unwrap(); - let bob_account = bob_account.pubkey(); - - // mint some tokens - let amount = 10; - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - // success if expected fee is 0 - token - .transfer_with_fee( - &alice_account, - &bob_account, - &alice.pubkey(), - 1, - 0, - &[&alice], - ) - .await - .unwrap(); - - // fail for anything else - let error = token - .transfer_with_fee( - &alice_account, - &bob_account, - &alice.pubkey(), - 2, - 1, - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::FeeMismatch as u32) - ) - ))) - ); -} diff --git a/token/program-2022-test/tests/transfer_fee.rs b/token/program-2022-test/tests/transfer_fee.rs deleted file mode 100644 index 2249cac0337..00000000000 --- a/token/program-2022-test/tests/transfer_fee.rs +++ /dev/null @@ -1,1715 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, program_option::COption, pubkey::Pubkey, signature::Signer, - signer::keypair::Keypair, transaction::TransactionError, transport::TransportError, - }, - spl_token_2022::{ - error::TokenError, - extension::{ - transfer_fee::{ - TransferFee, TransferFeeAmount, TransferFeeConfig, MAX_FEE_BASIS_POINTS, - }, - BaseStateWithExtensions, - }, - instruction, - }, - spl_token_client::{ - client::ProgramBanksClientProcessTransaction, - token::{ExtensionInitializationParams, Token, TokenError as TokenClientError}, - }, - std::convert::TryInto, -}; - -const TEST_MAXIMUM_FEE: u64 = 10_000_000; -const TEST_FEE_BASIS_POINTS: u16 = 250; - -fn test_transfer_fee() -> TransferFee { - TransferFee { - epoch: 0.into(), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), - maximum_fee: TEST_MAXIMUM_FEE.into(), - } -} - -fn test_transfer_fee_config() -> TransferFeeConfig { - let transfer_fee = test_transfer_fee(); - TransferFeeConfig { - transfer_fee_config_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), - withdraw_withheld_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), - withheld_amount: 0.into(), - older_transfer_fee: transfer_fee, - newer_transfer_fee: transfer_fee, - } -} - -struct TransferFeeConfigWithKeypairs { - transfer_fee_config: TransferFeeConfig, - transfer_fee_config_authority: Keypair, - withdraw_withheld_authority: Keypair, -} - -fn test_transfer_fee_config_with_keypairs() -> TransferFeeConfigWithKeypairs { - let transfer_fee = test_transfer_fee(); - let transfer_fee_config_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - let transfer_fee_config = TransferFeeConfig { - transfer_fee_config_authority: COption::Some(transfer_fee_config_authority.pubkey()) - .try_into() - .unwrap(), - withdraw_withheld_authority: COption::Some(withdraw_withheld_authority.pubkey()) - .try_into() - .unwrap(), - withheld_amount: 0.into(), - older_transfer_fee: transfer_fee, - newer_transfer_fee: transfer_fee, - }; - TransferFeeConfigWithKeypairs { - transfer_fee_config, - transfer_fee_config_authority, - withdraw_withheld_authority, - } -} - -struct TokenWithAccounts { - context: TestContext, - token: Token, - token_unchecked: Token, - transfer_fee_config: TransferFeeConfig, - withdraw_withheld_authority: Keypair, - freeze_authority: Keypair, - alice: Keypair, - alice_account: Pubkey, - bob_account: Pubkey, -} - -async fn create_mint_with_accounts(alice_amount: u64) -> TokenWithAccounts { - let TransferFeeConfigWithKeypairs { - transfer_fee_config_authority, - withdraw_withheld_authority, - transfer_fee_config, - .. - } = test_transfer_fee_config_with_keypairs(); - let mut context = TestContext::new().await; - let transfer_fee_basis_points = u16::from( - transfer_fee_config - .newer_transfer_fee - .transfer_fee_basis_points, - ); - let maximum_fee = u64::from(transfer_fee_config.newer_transfer_fee.maximum_fee); - context - .init_token_with_freezing_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), - withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), - transfer_fee_basis_points, - maximum_fee, - }]) - .await - .unwrap(); - let TokenContext { - mint_authority, - freeze_authority, - token, - token_unchecked, - alice, - bob, - .. - } = context.token_context.take().unwrap(); - - // token account is self-owned just to test another case - token - .create_auxiliary_token_account(&alice, &alice.pubkey()) - .await - .unwrap(); - let alice_account = alice.pubkey(); - let bob_account = Keypair::new(); - token - .create_auxiliary_token_account(&bob_account, &bob.pubkey()) - .await - .unwrap(); - let bob_account = bob_account.pubkey(); - - // mint tokens - token - .mint_to( - &alice_account, - &mint_authority.pubkey(), - alice_amount, - &[&mint_authority], - ) - .await - .unwrap(); - TokenWithAccounts { - context, - token, - token_unchecked, - transfer_fee_config, - withdraw_withheld_authority, - freeze_authority: freeze_authority.unwrap(), - alice, - alice_account, - bob_account, - } -} - -#[tokio::test] -async fn success_init() { - let TransferFeeConfig { - transfer_fee_config_authority, - withdraw_withheld_authority, - newer_transfer_fee, - .. - } = test_transfer_fee_config(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.into(), - withdraw_withheld_authority: withdraw_withheld_authority.into(), - transfer_fee_basis_points: newer_transfer_fee.transfer_fee_basis_points.into(), - maximum_fee: newer_transfer_fee.maximum_fee.into(), - }]) - .await - .unwrap(); - let TokenContext { - decimals, - mint_authority, - token, - .. - } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - assert_eq!(state.base.decimals, decimals); - assert_eq!( - state.base.mint_authority, - COption::Some(mint_authority.pubkey()) - ); - assert_eq!(state.base.supply, 0); - assert!(state.base.is_initialized); - assert_eq!(state.base.freeze_authority, COption::None); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.transfer_fee_config_authority, - transfer_fee_config_authority, - ); - assert_eq!( - extension.withdraw_withheld_authority, - withdraw_withheld_authority, - ); - assert_eq!(extension.newer_transfer_fee, newer_transfer_fee); - assert_eq!(extension.older_transfer_fee, newer_transfer_fee); -} - -#[tokio::test] -async fn fail_init_default_pubkey_as_authority() { - let TransferFeeConfig { - transfer_fee_config_authority, - newer_transfer_fee, - .. - } = test_transfer_fee_config(); - let mut context = TestContext::new().await; - let err = context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.into(), - withdraw_withheld_authority: Some(Pubkey::default()), - transfer_fee_basis_points: newer_transfer_fee.transfer_fee_basis_points.into(), - maximum_fee: newer_transfer_fee.maximum_fee.into(), - }]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(1, InstructionError::InvalidArgument) - ))) - ); -} - -#[tokio::test] -async fn fail_init_fee_too_high() { - let TransferFeeConfig { - transfer_fee_config_authority, - withdraw_withheld_authority, - newer_transfer_fee, - .. - } = test_transfer_fee_config(); - let mut context = TestContext::new().await; - let err = context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.into(), - withdraw_withheld_authority: withdraw_withheld_authority.into(), - transfer_fee_basis_points: MAX_FEE_BASIS_POINTS + 1, - maximum_fee: newer_transfer_fee.maximum_fee.into(), - }]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::TransferFeeExceedsMaximum as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn set_fee() { - let TransferFeeConfigWithKeypairs { - transfer_fee_config_authority, - withdraw_withheld_authority, - transfer_fee_config: TransferFeeConfig { - newer_transfer_fee, .. - }, - .. - } = test_transfer_fee_config_with_keypairs(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), - withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), - transfer_fee_basis_points: newer_transfer_fee.transfer_fee_basis_points.into(), - maximum_fee: newer_transfer_fee.maximum_fee.into(), - }]) - .await - .unwrap(); - - // warp to first normal slot to easily calculate epochs - let (first_normal_slot, slots_per_epoch) = { - let context = context.context.lock().await; - ( - context.genesis_config().epoch_schedule.first_normal_slot, - context.genesis_config().epoch_schedule.slots_per_epoch, - ) - }; - - context - .context - .lock() - .await - .warp_to_slot(first_normal_slot) - .unwrap(); - - let token = context.token_context.unwrap().token; - - // set to something new, old fee not touched - let new_transfer_fee_basis_points = MAX_FEE_BASIS_POINTS; - let new_maximum_fee = u64::MAX; - token - .set_transfer_fee( - &transfer_fee_config_authority.pubkey(), - new_transfer_fee_basis_points, - new_maximum_fee, - &[&transfer_fee_config_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.newer_transfer_fee.transfer_fee_basis_points, - new_transfer_fee_basis_points.into() - ); - assert_eq!( - extension.newer_transfer_fee.maximum_fee, - new_maximum_fee.into() - ); - assert_eq!(extension.older_transfer_fee, newer_transfer_fee); - - // set again, old fee still not touched - let new_transfer_fee_basis_points = 0; - let new_maximum_fee = 0; - token - .set_transfer_fee( - &transfer_fee_config_authority.pubkey(), - new_transfer_fee_basis_points, - new_maximum_fee, - &[&transfer_fee_config_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.newer_transfer_fee.transfer_fee_basis_points, - new_transfer_fee_basis_points.into() - ); - assert_eq!( - extension.newer_transfer_fee.maximum_fee, - new_maximum_fee.into() - ); - assert_eq!(extension.older_transfer_fee, newer_transfer_fee); - - // warp forward one epoch, old fee still not touched when set - let new_transfer_fee_basis_points = 10; - let new_maximum_fee = 10; - context - .context - .lock() - .await - .warp_to_slot(first_normal_slot + slots_per_epoch) - .unwrap(); - token - .set_transfer_fee( - &transfer_fee_config_authority.pubkey(), - new_transfer_fee_basis_points, - new_maximum_fee, - &[&transfer_fee_config_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.newer_transfer_fee.transfer_fee_basis_points, - new_transfer_fee_basis_points.into() - ); - assert_eq!( - extension.newer_transfer_fee.maximum_fee, - new_maximum_fee.into() - ); - assert_eq!(extension.older_transfer_fee, newer_transfer_fee); - - // warp forward two epochs, old fee is replaced on set - let newer_transfer_fee = extension.newer_transfer_fee; - context - .context - .lock() - .await - .warp_to_slot(first_normal_slot + 3 * slots_per_epoch) - .unwrap(); - let new_transfer_fee_basis_points = MAX_FEE_BASIS_POINTS; - let new_maximum_fee = u64::MAX; - token - .set_transfer_fee( - &transfer_fee_config_authority.pubkey(), - new_transfer_fee_basis_points, - new_maximum_fee, - &[&transfer_fee_config_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.newer_transfer_fee.transfer_fee_basis_points, - new_transfer_fee_basis_points.into() - ); - assert_eq!( - extension.newer_transfer_fee.maximum_fee, - new_maximum_fee.into() - ); - assert_eq!(extension.older_transfer_fee, newer_transfer_fee); - - // fail, wrong signer - let error = token - .set_transfer_fee( - &withdraw_withheld_authority.pubkey(), - new_transfer_fee_basis_points, - new_maximum_fee, - &[&withdraw_withheld_authority], - ) - .await - .err() - .unwrap(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // fail, set too high - let error = token - .set_transfer_fee( - &transfer_fee_config_authority.pubkey(), - MAX_FEE_BASIS_POINTS + 1, - new_maximum_fee, - &[&transfer_fee_config_authority], - ) - .await - .err() - .unwrap(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::TransferFeeExceedsMaximum as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_unsupported_mint() { - let mut context = TestContext::new().await; - context.init_token_with_mint(vec![]).await.unwrap(); - let TokenContext { - mint_authority, - token, - .. - } = context.token_context.unwrap(); - let transfer_fee_basis_points = u16::MAX; - let maximum_fee = u64::MAX; - let error = token - .set_transfer_fee( - &mint_authority.pubkey(), - transfer_fee_basis_points, - maximum_fee, - &[&mint_authority], - ) - .await - .err() - .unwrap(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidAccountData) - ))) - ); - let error = token - .harvest_withheld_tokens_to_mint(&[]) - .await - .err() - .unwrap(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidAccountData) - ))) - ); - let error = token - .withdraw_withheld_tokens_from_mint( - &Pubkey::new_unique(), - &mint_authority.pubkey(), - &[&mint_authority], - ) - .await - .err() - .unwrap(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidAccountData) - ))) - ); -} - -#[tokio::test] -async fn set_transfer_fee_config_authority() { - let TransferFeeConfigWithKeypairs { - transfer_fee_config_authority, - withdraw_withheld_authority, - transfer_fee_config: TransferFeeConfig { - newer_transfer_fee, .. - }, - .. - } = test_transfer_fee_config_with_keypairs(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), - withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), - transfer_fee_basis_points: newer_transfer_fee.transfer_fee_basis_points.into(), - maximum_fee: newer_transfer_fee.maximum_fee.into(), - }]) - .await - .unwrap(); - let token = context.token_context.unwrap().token; - - let new_authority = Keypair::new(); - let wrong = Keypair::new(); - - // fail, wrong signer - let err = token - .set_authority( - token.get_address(), - &wrong.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::TransferFeeConfig, - &[&wrong], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .set_authority( - token.get_address(), - &transfer_fee_config_authority.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::TransferFeeConfig, - &[&transfer_fee_config_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.transfer_fee_config_authority, - Some(new_authority.pubkey()).try_into().unwrap(), - ); - - // assert new_authority can update transfer fee config, and old cannot - let transfer_fee_basis_points = MAX_FEE_BASIS_POINTS; - let maximum_fee = u64::MAX; - let err = token - .set_transfer_fee( - &transfer_fee_config_authority.pubkey(), - transfer_fee_basis_points, - maximum_fee, - &[&transfer_fee_config_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - token - .set_transfer_fee( - &new_authority.pubkey(), - transfer_fee_basis_points, - maximum_fee, - &[&new_authority], - ) - .await - .unwrap(); - - // set to none - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - instruction::AuthorityType::TransferFeeConfig, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.transfer_fee_config_authority, - None.try_into().unwrap(), - ); - - // fail set again - let err = token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - Some(&transfer_fee_config_authority.pubkey()), - instruction::AuthorityType::TransferFeeConfig, - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) - ) - ))) - ); - - // fail update transfer fee config - let err = token - .set_transfer_fee( - &transfer_fee_config_authority.pubkey(), - 0, - 0, - &[&transfer_fee_config_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoAuthorityExists as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn set_withdraw_withheld_authority() { - let TransferFeeConfigWithKeypairs { - transfer_fee_config_authority, - withdraw_withheld_authority, - transfer_fee_config: TransferFeeConfig { - newer_transfer_fee, .. - }, - .. - } = test_transfer_fee_config_with_keypairs(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), - withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), - transfer_fee_basis_points: newer_transfer_fee.transfer_fee_basis_points.into(), - maximum_fee: newer_transfer_fee.maximum_fee.into(), - }]) - .await - .unwrap(); - let token = context.token_context.unwrap().token; - - let new_authority = Keypair::new(); - let wrong = Keypair::new(); - - // fail, wrong signer - let err = token - .set_authority( - token.get_address(), - &wrong.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::WithheldWithdraw, - &[&wrong], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .set_authority( - token.get_address(), - &withdraw_withheld_authority.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::WithheldWithdraw, - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.withdraw_withheld_authority, - Some(new_authority.pubkey()).try_into().unwrap(), - ); - - // new authority can withdraw tokens - let account = Keypair::new(); - token - .create_auxiliary_token_account(&account, &new_authority.pubkey()) - .await - .unwrap(); - let account = account.pubkey(); - token - .withdraw_withheld_tokens_from_accounts( - &account, - &new_authority.pubkey(), - &[&account], - &[&new_authority], - ) - .await - .unwrap(); - // old one cannot - let error = token - .withdraw_withheld_tokens_from_accounts( - &account, - &withdraw_withheld_authority.pubkey(), - &[&account], - &[&withdraw_withheld_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // set to none - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - instruction::AuthorityType::WithheldWithdraw, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.withdraw_withheld_authority, - None.try_into().unwrap(), - ); - - // fail set again - let err = token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - Some(&withdraw_withheld_authority.pubkey()), - instruction::AuthorityType::WithheldWithdraw, - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) - ) - ))) - ); - - // assert no authority can withdraw withheld fees - let account = Keypair::new(); - token - .create_auxiliary_token_account(&account, &new_authority.pubkey()) - .await - .unwrap(); - let account = account.pubkey(); - let error = token - .withdraw_withheld_tokens_from_accounts( - &account, - &withdraw_withheld_authority.pubkey(), - &[&account], - &[&withdraw_withheld_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoAuthorityExists as u32) - ) - ))) - ); - let error = token - .withdraw_withheld_tokens_from_accounts( - &account, - &new_authority.pubkey(), - &[&account], - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoAuthorityExists as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn transfer_checked() { - let maximum_fee = TEST_MAXIMUM_FEE; - let mut alice_amount = maximum_fee * 100; - let TokenWithAccounts { - token, - token_unchecked, - transfer_fee_config, - alice, - alice_account, - bob_account, - .. - } = create_mint_with_accounts(alice_amount).await; - - // fail unchecked always - let error = token_unchecked - .transfer( - &alice_account, - &bob_account, - &alice.pubkey(), - maximum_fee, - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintRequiredForTransfer as u32) - ) - ))) - ); - - // fail because amount too high - let error = token - .transfer( - &alice_account, - &bob_account, - &alice.pubkey(), - alice_amount + 1, - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InsufficientFunds as u32) - ) - ))) - ); - - let mut withheld_amount = 0; - let mut transferred_amount = 0; - - // success, clean calculation for transfer fee - let fee = transfer_fee_config - .calculate_epoch_fee(0, maximum_fee) - .unwrap(); - token - .transfer( - &alice_account, - &bob_account, - &alice.pubkey(), - maximum_fee, - &[&alice], - ) - .await - .unwrap(); - alice_amount -= maximum_fee; - withheld_amount += fee; - transferred_amount += maximum_fee - fee; - - let alice_state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(alice_state.base.amount, alice_amount); - let extension = alice_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, transferred_amount); - let extension = bob_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, withheld_amount.into()); - - // success, rounded up transfer fee - let transfer_amount = maximum_fee - 1; - let fee = transfer_fee_config - .calculate_epoch_fee(0, transfer_amount) - .unwrap(); - token - .transfer( - &alice_account, - &bob_account, - &alice.pubkey(), - transfer_amount, - &[&alice], - ) - .await - .unwrap(); - alice_amount -= transfer_amount; - withheld_amount += fee; - transferred_amount += transfer_amount - fee; - let alice_state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(alice_state.base.amount, alice_amount); - let extension = alice_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, transferred_amount); - let extension = bob_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, withheld_amount.into()); - - // success, maximum fee kicks in - let transfer_amount = 1 + maximum_fee * (MAX_FEE_BASIS_POINTS as u64) - / (u16::from( - transfer_fee_config - .newer_transfer_fee - .transfer_fee_basis_points, - ) as u64); - let fee = transfer_fee_config - .calculate_epoch_fee(0, transfer_amount) - .unwrap(); - assert_eq!(fee, maximum_fee); // sanity - token - .transfer( - &alice_account, - &bob_account, - &alice.pubkey(), - transfer_amount, - &[&alice], - ) - .await - .unwrap(); - alice_amount -= transfer_amount; - withheld_amount += fee; - transferred_amount += transfer_amount - fee; - let alice_state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(alice_state.base.amount, alice_amount); - let extension = alice_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, transferred_amount); - let extension = bob_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, withheld_amount.into()); - - // transfer down to 1 token - token - .transfer( - &alice_account, - &bob_account, - &alice.pubkey(), - alice_amount - 1, - &[&alice], - ) - .await - .unwrap(); - transferred_amount += alice_amount - 1 - maximum_fee; - alice_amount = 1; - withheld_amount += maximum_fee; - let alice_state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(alice_state.base.amount, alice_amount); - let extension = alice_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, transferred_amount); - let extension = bob_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, withheld_amount.into()); - - // final transfer, only move tokens to withheld amount, nothing received - token - .transfer(&alice_account, &bob_account, &alice.pubkey(), 1, &[&alice]) - .await - .unwrap(); - withheld_amount += 1; - let alice_state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(alice_state.base.amount, 0); - let extension = alice_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, transferred_amount); - let extension = bob_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, withheld_amount.into()); -} - -#[tokio::test] -async fn transfer_checked_with_fee() { - let maximum_fee = TEST_MAXIMUM_FEE; - let alice_amount = maximum_fee * 100; - let TokenWithAccounts { - token, - transfer_fee_config, - alice, - alice_account, - bob_account, - .. - } = create_mint_with_accounts(alice_amount).await; - - // incorrect fee, too high - let transfer_amount = maximum_fee; - let fee = transfer_fee_config - .calculate_epoch_fee(0, transfer_amount) - .unwrap() - + 1; - let error = token - .transfer_with_fee( - &alice_account, - &bob_account, - &alice.pubkey(), - transfer_amount, - fee, - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::FeeMismatch as u32) - ) - ))) - ); - - // incorrect fee, too low - let fee = transfer_fee_config - .calculate_epoch_fee(0, transfer_amount) - .unwrap() - - 1; - let error = token - .transfer_with_fee( - &alice_account, - &bob_account, - &alice.pubkey(), - transfer_amount, - fee, - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::FeeMismatch as u32) - ) - ))) - ); - - // correct fee, not enough tokens - let fee = transfer_fee_config - .calculate_epoch_fee(0, alice_amount + 1) - .unwrap() - - 1; - let error = token - .transfer_with_fee( - &alice_account, - &bob_account, - &alice.pubkey(), - alice_amount + 1, - fee, - &[&alice], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::InsufficientFunds as u32) - ) - ))) - ); - - // correct fee - let fee = transfer_fee_config - .calculate_epoch_fee(0, transfer_amount) - .unwrap(); - token - .transfer_with_fee( - &alice_account, - &bob_account, - &alice.pubkey(), - transfer_amount, - fee, - &[&alice], - ) - .await - .unwrap(); - let alice_state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(alice_state.base.amount, alice_amount - transfer_amount); - let extension = alice_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - let bob_state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(bob_state.base.amount, transfer_amount - fee); - let extension = bob_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, fee.into()); -} - -#[tokio::test] -async fn no_fees_from_self_transfer() { - let amount = TEST_MAXIMUM_FEE; - let alice_amount = amount * 100; - let TokenWithAccounts { - token, - transfer_fee_config, - alice, - alice_account, - .. - } = create_mint_with_accounts(alice_amount).await; - - // self transfer, no fee assessed - let fee = transfer_fee_config.calculate_epoch_fee(0, amount).unwrap(); - token - .transfer_with_fee( - &alice_account, - &alice_account, - &alice.pubkey(), - amount, - fee, - &[&alice], - ) - .await - .unwrap(); - let alice_state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(alice_state.base.amount, alice_amount); - let extension = alice_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); -} - -async fn create_and_transfer_to_account( - token: &Token, - source: &Pubkey, - authority: &Keypair, - owner: &Pubkey, - amount: u64, -) -> Pubkey { - let account = Keypair::new(); - token - .create_auxiliary_token_account(&account, owner) - .await - .unwrap(); - let account = account.pubkey(); - token - .transfer(source, &account, &authority.pubkey(), amount, &[authority]) - .await - .unwrap(); - account -} - -#[tokio::test] -async fn harvest_withheld_tokens_to_mint() { - let amount = TEST_MAXIMUM_FEE; - let alice_amount = amount * 100; - let TokenWithAccounts { - mut context, - token, - transfer_fee_config, - alice, - alice_account, - .. - } = create_mint_with_accounts(alice_amount).await; - - // harvest from zero accounts - token.harvest_withheld_tokens_to_mint(&[]).await.unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - - // harvest from one account - let accumulated_fees = transfer_fee_config.calculate_epoch_fee(0, amount).unwrap(); - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - token - .harvest_withheld_tokens_to_mint(&[&account]) - .await - .unwrap(); - let state = token.get_account_info(&account).await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, accumulated_fees.into()); - - // no fail harvesting from account belonging to different mint, but nothing - // happens - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(Pubkey::new_unique()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - token - .harvest_withheld_tokens_to_mint(&[&account]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); -} - -#[tokio::test] -async fn max_harvest_withheld_tokens_to_mint() { - let amount = TEST_MAXIMUM_FEE; - let alice_amount = amount * 100; - let TokenWithAccounts { - token, - transfer_fee_config, - alice, - alice_account, - .. - } = create_mint_with_accounts(alice_amount).await; - - // harvest from max accounts, which is around 35, AKA 34 accounts + 1 mint - // see https://docs.solana.com/proposals/transactions-v2#problem - let mut accounts = vec![]; - let max_accounts = 34; - for _ in 0..max_accounts { - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - accounts.push(account); - } - let accounts: Vec<_> = accounts.iter().collect(); - let accumulated_fees = - max_accounts * transfer_fee_config.calculate_epoch_fee(0, amount).unwrap(); - token - .harvest_withheld_tokens_to_mint(&accounts) - .await - .unwrap(); - for account in accounts { - let state = token.get_account_info(account).await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - } - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, accumulated_fees.into()); -} - -#[tokio::test] -async fn max_withdraw_withheld_tokens_from_accounts() { - let amount = TEST_MAXIMUM_FEE; - let alice_amount = amount * 100; - let TokenWithAccounts { - token, - withdraw_withheld_authority, - transfer_fee_config, - alice, - alice_account, - .. - } = create_mint_with_accounts(alice_amount).await; - - // withdraw from max accounts, which is around 35: 1 mint, 1 destination, 1 - // authority, 32 accounts - // see https://docs.solana.com/proposals/transactions-v2#problem - let destination = Keypair::new(); - token - .create_auxiliary_token_account(&destination, &alice.pubkey()) - .await - .unwrap(); - let destination = destination.pubkey(); - let mut accounts = vec![]; - let max_accounts = 32; - for _ in 0..max_accounts { - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - accounts.push(account); - } - let accounts: Vec<_> = accounts.iter().collect(); - let accumulated_fees = - max_accounts * transfer_fee_config.calculate_epoch_fee(0, amount).unwrap(); - token - .withdraw_withheld_tokens_from_accounts( - &destination, - &withdraw_withheld_authority.pubkey(), - &accounts, - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - for account in accounts { - let state = token.get_account_info(account).await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - } - let state = token.get_account_info(&destination).await.unwrap(); - assert_eq!(state.base.amount, accumulated_fees); -} - -#[tokio::test] -async fn withdraw_withheld_tokens_from_mint() { - let amount = TEST_MAXIMUM_FEE; - let alice_amount = amount * 100; - let TokenWithAccounts { - mut context, - token, - transfer_fee_config, - withdraw_withheld_authority, - freeze_authority, - alice, - alice_account, - bob_account, - .. - } = create_mint_with_accounts(alice_amount).await; - - // no tokens withheld on mint - token - .withdraw_withheld_tokens_from_mint( - &alice_account, - &withdraw_withheld_authority.pubkey(), - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - let state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(state.base.amount, alice_amount); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - - // transfer + harvest to mint - let fee = transfer_fee_config.calculate_epoch_fee(0, amount).unwrap(); - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - - let state = token.get_account_info(&account).await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, fee.into()); - - token - .harvest_withheld_tokens_to_mint(&[&account]) - .await - .unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, fee.into()); - - // success - token - .withdraw_withheld_tokens_from_mint( - &bob_account, - &withdraw_withheld_authority.pubkey(), - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - let state = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(state.base.amount, fee); - let state = token.get_account_info(&account).await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - - // fail wrong signer - let error = token - .withdraw_withheld_tokens_from_mint(&alice_account, &alice.pubkey(), &[&alice]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // fail frozen account - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - token - .freeze(&account, &freeze_authority.pubkey(), &[&freeze_authority]) - .await - .unwrap(); - let error = token - .withdraw_withheld_tokens_from_mint( - &account, - &withdraw_withheld_authority.pubkey(), - &[&withdraw_withheld_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AccountFrozen as u32) - ) - ))) - ); - - // set to none, fail - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - token - .set_authority( - token.get_address(), - &withdraw_withheld_authority.pubkey(), - None, - instruction::AuthorityType::WithheldWithdraw, - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - let error = token - .withdraw_withheld_tokens_from_mint( - &account, - &withdraw_withheld_authority.pubkey(), - &[&withdraw_withheld_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::NoAuthorityExists as u32) - ) - ))) - ); - - // fail on new mint with mint mismatch - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - let error = token - .withdraw_withheld_tokens_from_mint( - &account, - &withdraw_withheld_authority.pubkey(), - &[&withdraw_withheld_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn withdraw_withheld_tokens_from_accounts() { - let amount = TEST_MAXIMUM_FEE; - let alice_amount = amount * 100; - let TokenWithAccounts { - mut context, - token, - withdraw_withheld_authority, - alice, - alice_account, - .. - } = create_mint_with_accounts(alice_amount).await; - - // wrong signer - let wrong_signer = Keypair::new(); - let error = token - .withdraw_withheld_tokens_from_accounts( - &alice_account, - &wrong_signer.pubkey(), - &[], - &[&wrong_signer], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // withdraw from zero accounts - token - .withdraw_withheld_tokens_from_accounts( - &alice_account, - &withdraw_withheld_authority.pubkey(), - &[], - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - let state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(state.base.amount, alice_amount); - - // self-harvest from one account - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - token - .withdraw_withheld_tokens_from_accounts( - &account, - &withdraw_withheld_authority.pubkey(), - &[&account], - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - let state = token.get_account_info(&account).await.unwrap(); - let extension = state.get_extension::().unwrap(); - // we transferred to this account, and then withdrew the fee to it, so it's - // like doing a fee-less transfer! - assert_eq!(extension.withheld_amount, 0.into()); - assert_eq!(state.base.amount, amount); - - // harvest again from the same account - token - .withdraw_withheld_tokens_from_accounts( - &alice_account, - &withdraw_withheld_authority.pubkey(), - &[&account], - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - let state = token.get_account_info(&account).await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - assert_eq!(state.base.amount, amount); - let state = token.get_account_info(&alice_account).await.unwrap(); - assert_eq!(state.base.amount, alice_amount - amount); - - // no fail harvesting from account belonging to different mint, but nothing - // happens - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - context - .init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: Some(Pubkey::new_unique()), - withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, - maximum_fee: TEST_MAXIMUM_FEE, - }]) - .await - .unwrap(); - let TokenContext { token, .. } = context.token_context.take().unwrap(); - let withdraw_account = Keypair::new(); - token - .create_auxiliary_token_account(&withdraw_account, &alice.pubkey()) - .await - .unwrap(); - let withdraw_account = withdraw_account.pubkey(); - token - .withdraw_withheld_tokens_from_accounts( - &withdraw_account, - &withdraw_withheld_authority.pubkey(), - &[&account], - &[&withdraw_withheld_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - - // fail withdrawing into account on different mint - let error = token - .withdraw_withheld_tokens_from_accounts( - &account, - &withdraw_withheld_authority.pubkey(), - &[&withdraw_account], - &[&withdraw_withheld_authority], - ) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::MintMismatch as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn fail_close_with_withheld() { - let amount = TEST_MAXIMUM_FEE; - let alice_amount = amount * 100; - let TokenWithAccounts { - token, - transfer_fee_config, - alice, - alice_account, - .. - } = create_mint_with_accounts(alice_amount).await; - - // accrue withheld fees on new account - let account = - create_and_transfer_to_account(&token, &alice_account, &alice, &alice.pubkey(), amount) - .await; - - // empty the account - let fee = transfer_fee_config.calculate_epoch_fee(0, amount).unwrap(); - token - .transfer( - &account, - &alice_account, - &alice.pubkey(), - amount - fee, - &[&alice], - ) - .await - .unwrap(); - - // fail to close - let error = token - .close_account(&account, &Pubkey::new_unique(), &alice.pubkey(), &[&alice]) - .await - .unwrap_err(); - assert_eq!( - error, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AccountHasWithheldTransferFees as u32) - ) - ))) - ); - - // harvest the fees to the mint - token - .harvest_withheld_tokens_to_mint(&[&account]) - .await - .unwrap(); - - // successfully close - token - .close_account(&account, &Pubkey::new_unique(), &alice.pubkey(), &[&alice]) - .await - .unwrap(); -} diff --git a/token/program-2022-test/tests/transfer_hook.rs b/token/program-2022-test/tests/transfer_hook.rs deleted file mode 100644 index 32f929857a4..00000000000 --- a/token/program-2022-test/tests/transfer_hook.rs +++ /dev/null @@ -1,1112 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod program_test; -use { - futures_util::TryFutureExt, - program_test::{ - ConfidentialTokenAccountBalances, ConfidentialTokenAccountMeta, TestContext, TokenContext, - }, - solana_program_test::{tokio, ProgramTest}, - solana_sdk::{ - account::Account, - instruction::{AccountMeta, Instruction, InstructionError}, - program_option::COption, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - transaction::TransactionError, - transport::TransportError, - }, - spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}, - spl_token_2022::{ - error::TokenError, - extension::{ - transfer_fee::{TransferFee, TransferFeeAmount, TransferFeeConfig}, - transfer_hook::{TransferHook, TransferHookAccount}, - BaseStateWithExtensions, - }, - instruction, offchain, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, - spl_transfer_hook_interface::{ - get_extra_account_metas_address, offchain::add_extra_account_metas_for_execute, - }, - std::{convert::TryInto, sync::Arc}, -}; - -const TEST_MAXIMUM_FEE: u64 = 10_000_000; -const TEST_FEE_BASIS_POINTS: u16 = 100; - -async fn setup_accounts( - token_context: &TokenContext, - alice_account: Keypair, - bob_account: Keypair, - amount: u64, -) -> (Pubkey, Pubkey) { - token_context - .token - .create_auxiliary_token_account(&alice_account, &token_context.alice.pubkey()) - .await - .unwrap(); - let alice_account = alice_account.pubkey(); - token_context - .token - .create_auxiliary_token_account(&bob_account, &token_context.bob.pubkey()) - .await - .unwrap(); - let bob_account = bob_account.pubkey(); - - // mint tokens - token_context - .token - .mint_to( - &alice_account, - &token_context.mint_authority.pubkey(), - amount, - &[&token_context.mint_authority], - ) - .await - .unwrap(); - (alice_account, bob_account) -} - -fn setup_program_test(program_id: &Pubkey) -> ProgramTest { - let mut program_test = ProgramTest::default(); - program_test.add_program("spl_token_2022", spl_token_2022::id(), None); - program_test.add_program("spl_transfer_hook_example", *program_id, None); - program_test -} - -fn add_validation_account(program_test: &mut ProgramTest, mint: &Pubkey, program_id: &Pubkey) { - let validation_address = get_extra_account_metas_address(mint, program_id); - let extra_account_metas = vec![ - AccountMeta { - pubkey: Pubkey::new_unique(), - is_signer: false, - is_writable: false, - } - .into(), - AccountMeta { - pubkey: Pubkey::new_unique(), - is_signer: false, - is_writable: false, - } - .into(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::AccountKey { index: 0 }, // source - Seed::AccountKey { index: 2 }, // destination - Seed::AccountKey { index: 4 }, // validation state - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: vec![1, 2, 3, 4, 5, 6], - }, - Seed::AccountKey { index: 2 }, // destination - Seed::AccountKey { index: 5 }, // extra meta 1 - ], - false, - true, - ) - .unwrap(), - ]; - program_test.add_account( - validation_address, - Account { - lamports: 1_000_000_000, // a lot, just to be safe - data: spl_transfer_hook_example::state::example_data(&extra_account_metas).unwrap(), - owner: *program_id, - ..Account::default() - }, - ); -} - -async fn setup(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) -> TestContext { - let mut program_test = setup_program_test(program_id); - add_validation_account(&mut program_test, &mint.pubkey(), program_id); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::TransferHook { - authority: Some(*authority), - program_id: Some(*program_id), - }], - None, - ) - .await - .unwrap(); - context -} - -async fn setup_with_fee(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) -> TestContext { - let mut program_test = setup_program_test(program_id); - - let transfer_fee_config_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - let transfer_fee_basis_points = TEST_FEE_BASIS_POINTS; - let maximum_fee = TEST_MAXIMUM_FEE; - add_validation_account(&mut program_test, &mint.pubkey(), program_id); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ - ExtensionInitializationParams::TransferHook { - authority: Some(*authority), - program_id: Some(*program_id), - }, - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), - withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), - transfer_fee_basis_points, - maximum_fee, - }, - ], - None, - ) - .await - .unwrap(); - context -} - -async fn setup_with_confidential_transfers( - mint: Keypair, - program_id: &Pubkey, - authority: &Pubkey, -) -> TestContext { - let mut program_test = setup_program_test(program_id); - add_validation_account(&mut program_test, &mint.pubkey(), program_id); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ - ExtensionInitializationParams::TransferHook { - authority: Some(*authority), - program_id: Some(*program_id), - }, - ExtensionInitializationParams::ConfidentialTransferMint { - authority: Some(*authority), - auto_approve_new_accounts: true, - auditor_elgamal_pubkey: None, - }, - ], - None, - ) - .await - .unwrap(); - context -} - -#[tokio::test] -async fn success_init() { - let authority = Pubkey::new_unique(); - let program_id = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &program_id, &authority) - .await - .token_context - .take() - .unwrap() - .token; - - let state = token.get_mint_info().await.unwrap(); - assert!(state.base.is_initialized); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, Some(authority).try_into().unwrap()); - assert_eq!(extension.program_id, Some(program_id).try_into().unwrap()); -} - -#[tokio::test] -async fn fail_init_all_none() { - let mut program_test = ProgramTest::default(); - program_test.add_program("spl_token_2022", spl_token_2022::id(), None); - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - let err = context - .init_token_with_mint_keypair_and_freeze_authority( - Keypair::new(), - vec![ExtensionInitializationParams::TransferHook { - authority: None, - program_id: None, - }], - None, - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 1, - InstructionError::Custom(TokenError::InvalidInstruction as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn set_authority() { - let authority = Keypair::new(); - let program_id = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &program_id, &authority.pubkey()) - .await - .token_context - .take() - .unwrap() - .token; - let new_authority = Keypair::new(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token - .set_authority( - token.get_address(), - &wrong.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::TransferHookProgramId, - &[&wrong], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .set_authority( - token.get_address(), - &authority.pubkey(), - Some(&new_authority.pubkey()), - instruction::AuthorityType::TransferHookProgramId, - &[&authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.authority, - Some(new_authority.pubkey()).try_into().unwrap(), - ); - - // set to none - token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - None, - instruction::AuthorityType::TransferHookProgramId, - &[&new_authority], - ) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, None.try_into().unwrap(),); - - // fail set again - let err = token - .set_authority( - token.get_address(), - &new_authority.pubkey(), - Some(&authority.pubkey()), - instruction::AuthorityType::TransferHookProgramId, - &[&new_authority], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::AuthorityTypeNotSupported as u32) - ) - ))) - ); -} - -#[tokio::test] -async fn update_transfer_hook_program_id() { - let authority = Keypair::new(); - let program_id = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token = setup(mint_keypair, &program_id, &authority.pubkey()) - .await - .token_context - .take() - .unwrap() - .token; - let new_program_id = Pubkey::new_unique(); - - // fail, wrong signature - let wrong = Keypair::new(); - let err = token - .update_transfer_hook_program_id(&wrong.pubkey(), Some(new_program_id), &[&wrong]) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError( - 0, - InstructionError::Custom(TokenError::OwnerMismatch as u32) - ) - ))) - ); - - // success - token - .update_transfer_hook_program_id(&authority.pubkey(), Some(new_program_id), &[&authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!( - extension.program_id, - Some(new_program_id).try_into().unwrap(), - ); - - // set to none - token - .update_transfer_hook_program_id(&authority.pubkey(), None, &[&authority]) - .await - .unwrap(); - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.program_id, None.try_into().unwrap(),); -} - -#[tokio::test] -async fn success_transfer() { - let authority = Keypair::new(); - let program_id = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token_context = setup(mint_keypair, &program_id, &authority.pubkey()) - .await - .token_context - .take() - .unwrap(); - let amount = 10; - let (alice_account, bob_account) = - setup_accounts(&token_context, Keypair::new(), Keypair::new(), amount).await; - - token_context - .token - .transfer( - &alice_account, - &bob_account, - &token_context.alice.pubkey(), - amount, - &[&token_context.alice], - ) - .await - .unwrap(); - - let destination = token_context - .token - .get_account_info(&bob_account) - .await - .unwrap(); - assert_eq!(destination.base.amount, amount); - - // the example program checks that the transferring flag was set to true, - // so make sure that it was correctly unset by the token program - assert_eq!( - destination - .get_extension::() - .unwrap() - .transferring, - false.into() - ); - let source = token_context - .token - .get_account_info(&alice_account) - .await - .unwrap(); - assert_eq!( - source - .get_extension::() - .unwrap() - .transferring, - false.into() - ); -} - -#[tokio::test] -async fn success_transfer_with_fee() { - let authority = Keypair::new(); - let program_id = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - - let maximum_fee = TEST_MAXIMUM_FEE; - let alice_amount = maximum_fee * 100; - let transfer_amount = maximum_fee; - - let token_context = setup_with_fee(mint_keypair, &program_id, &authority.pubkey()) - .await - .token_context - .take() - .unwrap(); - - let (alice_account, bob_account) = - setup_accounts(&token_context, Keypair::new(), Keypair::new(), alice_amount).await; - - let transfer_fee = TransferFee { - epoch: 0.into(), - transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), - maximum_fee: TEST_MAXIMUM_FEE.into(), - }; - let transfer_fee_config = TransferFeeConfig { - transfer_fee_config_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), - withdraw_withheld_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), - withheld_amount: 0.into(), - older_transfer_fee: transfer_fee, - newer_transfer_fee: transfer_fee, - }; - let fee = transfer_fee_config - .calculate_epoch_fee(0, transfer_amount) - .unwrap(); - - token_context - .token - .transfer_with_fee( - &alice_account, - &bob_account, - &token_context.alice.pubkey(), - transfer_amount, - fee, - &[&token_context.alice], - ) - .await - .unwrap(); - - // Get the accounts' state after the transfer - let alice_state = token_context - .token - .get_account_info(&alice_account) - .await - .unwrap(); - let bob_state = token_context - .token - .get_account_info(&bob_account) - .await - .unwrap(); - - // Check that the correct amount was deducted from Alice's account - assert_eq!(alice_state.base.amount, alice_amount - transfer_amount); - - // Check the there are no tokens withheld in Alice's account - let extension = alice_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, 0.into()); - - // Check the fee tokens are withheld in Bobs's account - let extension = bob_state.get_extension::().unwrap(); - assert_eq!(extension.withheld_amount, fee.into()); - - // Check that the correct amount was added to Bobs's account - assert_eq!(bob_state.base.amount, transfer_amount - fee); - - // the example program checks that the transferring flag was set to true, - // so make sure that it was correctly unset by the token program - assert_eq!( - bob_state - .get_extension::() - .unwrap() - .transferring, - false.into() - ); - - assert_eq!( - alice_state - .get_extension::() - .unwrap() - .transferring, - false.into() - ); -} - -#[tokio::test] -async fn fail_transfer_hook_program() { - let authority = Pubkey::new_unique(); - let program_id = Pubkey::new_unique(); - let mint = Keypair::new(); - let mut program_test = ProgramTest::default(); - program_test.add_program("spl_token_2022", spl_token_2022::id(), None); - program_test.add_program("spl_transfer_hook_example_fail", program_id, None); - let validation_address = get_extra_account_metas_address(&mint.pubkey(), &program_id); - program_test.add_account( - validation_address, - Account { - lamports: 1_000_000_000, // a lot, just to be safe - data: spl_transfer_hook_example::state::example_data(&[]).unwrap(), - owner: program_id, - ..Account::default() - }, - ); - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::TransferHook { - authority: Some(authority), - program_id: Some(program_id), - }], - None, - ) - .await - .unwrap(); - let token_context = context.token_context.take().unwrap(); - - let amount = 10; - let (alice_account, bob_account) = - setup_accounts(&token_context, Keypair::new(), Keypair::new(), amount).await; - - let err = token_context - .token - .transfer( - &alice_account, - &bob_account, - &token_context.alice.pubkey(), - amount, - &[&token_context.alice], - ) - .await - .unwrap_err(); - assert_eq!( - err, - TokenClientError::Client(Box::new(TransportError::TransactionError( - TransactionError::InstructionError(0, InstructionError::InvalidInstructionData) - ))) - ); -} - -#[tokio::test] -async fn success_downgrade_writable_and_signer_accounts() { - let authority = Pubkey::new_unique(); - let program_id = Pubkey::new_unique(); - let mint = Keypair::new(); - let mut program_test = ProgramTest::default(); - program_test.add_program("spl_token_2022", spl_token_2022::id(), None); - program_test.add_program("spl_transfer_hook_example_downgrade", program_id, None); - let alice = Keypair::new(); - let alice_account = Keypair::new(); - let validation_address = get_extra_account_metas_address(&mint.pubkey(), &program_id); - let extra_account_metas = vec![ - AccountMeta { - pubkey: alice_account.pubkey(), - is_signer: false, - is_writable: true, - } - .into(), - AccountMeta { - pubkey: alice.pubkey(), - is_signer: true, - is_writable: false, - } - .into(), - ]; - program_test.add_account( - validation_address, - Account { - lamports: 1_000_000_000, // a lot, just to be safe - data: spl_transfer_hook_example::state::example_data(&extra_account_metas).unwrap(), - owner: program_id, - ..Account::default() - }, - ); - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::TransferHook { - authority: Some(authority), - program_id: Some(program_id), - }], - None, - ) - .await - .unwrap(); - let mut token_context = context.token_context.take().unwrap(); - token_context.alice = alice; - - let amount = 10; - let (alice_account, bob_account) = - setup_accounts(&token_context, alice_account, Keypair::new(), amount).await; - - token_context - .token - .transfer( - &alice_account, - &bob_account, - &token_context.alice.pubkey(), - amount, - &[&token_context.alice], - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn success_transfers_using_onchain_helper() { - let authority = Pubkey::new_unique(); - let program_id = Pubkey::new_unique(); - let mint_a_keypair = Keypair::new(); - let mint_a = mint_a_keypair.pubkey(); - let mint_b_keypair = Keypair::new(); - let mint_b = mint_b_keypair.pubkey(); - let amount = 10; - - let swap_program_id = Pubkey::new_unique(); - let mut program_test = setup_program_test(&program_id); - program_test.add_program("spl_transfer_hook_example_swap", swap_program_id, None); - add_validation_account(&mut program_test, &mint_a, &program_id); - add_validation_account(&mut program_test, &mint_b, &program_id); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context_a = TestContext { - context: context.clone(), - token_context: None, - }; - context_a - .init_token_with_mint_keypair_and_freeze_authority( - mint_a_keypair, - vec![ExtensionInitializationParams::TransferHook { - authority: Some(authority), - program_id: Some(program_id), - }], - None, - ) - .await - .unwrap(); - let token_a_context = context_a.token_context.unwrap(); - let (source_a_account, destination_a_account) = - setup_accounts(&token_a_context, Keypair::new(), Keypair::new(), amount).await; - let authority_a = token_a_context.alice; - let token_a = token_a_context.token; - let mut context_b = TestContext { - context, - token_context: None, - }; - context_b - .init_token_with_mint_keypair_and_freeze_authority( - mint_b_keypair, - vec![ExtensionInitializationParams::TransferHook { - authority: Some(authority), - program_id: Some(program_id), - }], - None, - ) - .await - .unwrap(); - let token_b_context = context_b.token_context.unwrap(); - let (source_b_account, destination_b_account) = - setup_accounts(&token_b_context, Keypair::new(), Keypair::new(), amount).await; - let authority_b = token_b_context.alice; - let account_metas = vec![ - AccountMeta::new(source_a_account, false), - AccountMeta::new_readonly(mint_a, false), - AccountMeta::new(destination_a_account, false), - AccountMeta::new_readonly(authority_a.pubkey(), true), - AccountMeta::new_readonly(spl_token_2022::id(), false), - AccountMeta::new(source_b_account, false), - AccountMeta::new_readonly(mint_b, false), - AccountMeta::new(destination_b_account, false), - AccountMeta::new_readonly(authority_b.pubkey(), true), - AccountMeta::new_readonly(spl_token_2022::id(), false), - ]; - - let mut instruction = Instruction::new_with_bytes(swap_program_id, &[], account_metas); - - add_extra_account_metas_for_execute( - &mut instruction, - &program_id, - &source_a_account, - &mint_a, - &destination_a_account, - &authority_a.pubkey(), - amount, - |address| { - token_a.get_account(address).map_ok_or_else( - |e| match e { - TokenClientError::AccountNotFound => Ok(None), - _ => Err(offchain::AccountFetchError::from(e)), - }, - |acc| Ok(Some(acc.data)), - ) - }, - ) - .await - .unwrap(); - add_extra_account_metas_for_execute( - &mut instruction, - &program_id, - &source_b_account, - &mint_b, - &destination_b_account, - &authority_b.pubkey(), - amount, - |address| { - token_a.get_account(address).map_ok_or_else( - |e| match e { - TokenClientError::AccountNotFound => Ok(None), - _ => Err(offchain::AccountFetchError::from(e)), - }, - |acc| Ok(Some(acc.data)), - ) - }, - ) - .await - .unwrap(); - - token_a - .process_ixs(&[instruction], &[&authority_a, &authority_b]) - .await - .unwrap(); -} - -#[tokio::test] -async fn success_transfers_with_fee_using_onchain_helper() { - let authority = Pubkey::new_unique(); - let program_id = Pubkey::new_unique(); - let mint_a_keypair = Keypair::new(); - let mint_a = mint_a_keypair.pubkey(); - let mint_b_keypair = Keypair::new(); - let mint_b = mint_b_keypair.pubkey(); - let amount = 10_000_000_000; - - let transfer_fee_config_authority = Keypair::new(); - let withdraw_withheld_authority = Keypair::new(); - let transfer_fee_basis_points = TEST_FEE_BASIS_POINTS; - let maximum_fee = TEST_MAXIMUM_FEE; - - let swap_program_id = Pubkey::new_unique(); - let mut program_test = setup_program_test(&program_id); - program_test.add_program( - "spl_transfer_hook_example_swap_with_fee", - swap_program_id, - None, - ); - add_validation_account(&mut program_test, &mint_a, &program_id); - add_validation_account(&mut program_test, &mint_b, &program_id); - - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context_a = TestContext { - context: context.clone(), - token_context: None, - }; - context_a - .init_token_with_mint_keypair_and_freeze_authority( - mint_a_keypair, - vec![ - ExtensionInitializationParams::TransferHook { - authority: Some(authority), - program_id: Some(program_id), - }, - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), - withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), - transfer_fee_basis_points, - maximum_fee, - }, - ], - None, - ) - .await - .unwrap(); - let token_a_context = context_a.token_context.unwrap(); - let (source_a_account, destination_a_account) = - setup_accounts(&token_a_context, Keypair::new(), Keypair::new(), amount).await; - let authority_a = token_a_context.alice; - let token_a = token_a_context.token; - let mut context_b = TestContext { - context, - token_context: None, - }; - context_b - .init_token_with_mint_keypair_and_freeze_authority( - mint_b_keypair, - vec![ - ExtensionInitializationParams::TransferHook { - authority: Some(authority), - program_id: Some(program_id), - }, - ExtensionInitializationParams::TransferFeeConfig { - transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), - withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), - transfer_fee_basis_points, - maximum_fee, - }, - ], - None, - ) - .await - .unwrap(); - let token_b_context = context_b.token_context.unwrap(); - let (source_b_account, destination_b_account) = - setup_accounts(&token_b_context, Keypair::new(), Keypair::new(), amount).await; - let authority_b = token_b_context.alice; - let account_metas = vec![ - AccountMeta::new(source_a_account, false), - AccountMeta::new_readonly(mint_a, false), - AccountMeta::new(destination_a_account, false), - AccountMeta::new_readonly(authority_a.pubkey(), true), - AccountMeta::new_readonly(spl_token_2022::id(), false), - AccountMeta::new(source_b_account, false), - AccountMeta::new_readonly(mint_b, false), - AccountMeta::new(destination_b_account, false), - AccountMeta::new_readonly(authority_b.pubkey(), true), - AccountMeta::new_readonly(spl_token_2022::id(), false), - ]; - - let mut instruction = Instruction::new_with_bytes(swap_program_id, &[], account_metas); - - add_extra_account_metas_for_execute( - &mut instruction, - &program_id, - &source_a_account, - &mint_a, - &destination_a_account, - &authority_a.pubkey(), - amount, - |address| { - token_a.get_account(address).map_ok_or_else( - |e| match e { - TokenClientError::AccountNotFound => Ok(None), - _ => Err(offchain::AccountFetchError::from(e)), - }, - |acc| Ok(Some(acc.data)), - ) - }, - ) - .await - .unwrap(); - add_extra_account_metas_for_execute( - &mut instruction, - &program_id, - &source_b_account, - &mint_b, - &destination_b_account, - &authority_b.pubkey(), - amount, - |address| { - token_a.get_account(address).map_ok_or_else( - |e| match e { - TokenClientError::AccountNotFound => Ok(None), - _ => Err(offchain::AccountFetchError::from(e)), - }, - |acc| Ok(Some(acc.data)), - ) - }, - ) - .await - .unwrap(); - - token_a - .process_ixs(&[instruction], &[&authority_a, &authority_b]) - .await - .unwrap(); -} - -#[tokio::test] -async fn success_confidential_transfer() { - let authority = Keypair::new(); - let program_id = Pubkey::new_unique(); - let mint_keypair = Keypair::new(); - let token_context = - setup_with_confidential_transfers(mint_keypair, &program_id, &authority.pubkey()) - .await - .token_context - .take() - .unwrap(); - let amount = 10; - - let TokenContext { - token, - alice, - bob, - mint_authority, - decimals, - .. - } = token_context; - - let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( - &token, - &alice, - None, - false, - false, - &mint_authority, - amount, - decimals, - ) - .await; - - let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, Some(2), false, false).await; - - token - .confidential_transfer_transfer( - &alice_meta.token_account, - &bob_meta.token_account, - &alice.pubkey(), - None, - None, - None, - amount, - None, - &alice_meta.elgamal_keypair, - &alice_meta.aes_key, - bob_meta.elgamal_keypair.pubkey(), - None, // auditor - &[&alice], - ) - .await - .unwrap(); - - let destination = token - .get_account_info(&bob_meta.token_account) - .await - .unwrap(); - alice_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: 0, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - bob_meta - .check_balances( - &token, - ConfidentialTokenAccountBalances { - pending_balance_lo: amount, - pending_balance_hi: 0, - available_balance: 0, - decryptable_available_balance: 0, - }, - ) - .await; - - // the example program checks that the transferring flag was set to true, - // so make sure that it was correctly unset by the token program - assert_eq!( - destination - .get_extension::() - .unwrap() - .transferring, - false.into() - ); - let source = token - .get_account_info(&alice_meta.token_account) - .await - .unwrap(); - assert_eq!( - source - .get_extension::() - .unwrap() - .transferring, - false.into() - ); -} - -#[tokio::test] -async fn success_without_validation_account() { - let authority = Pubkey::new_unique(); - let program_id = Pubkey::new_unique(); - let mint = Keypair::new(); - let mut program_test = ProgramTest::default(); - program_test.add_program("spl_token_2022", spl_token_2022::id(), None); - program_test.add_program("spl_transfer_hook_example_success", program_id, None); - let context = program_test.start_with_context().await; - let context = Arc::new(tokio::sync::Mutex::new(context)); - let mut context = TestContext { - context, - token_context: None, - }; - context - .init_token_with_mint_keypair_and_freeze_authority( - mint, - vec![ExtensionInitializationParams::TransferHook { - authority: Some(authority), - program_id: Some(program_id), - }], - None, - ) - .await - .unwrap(); - let token_context = context.token_context.take().unwrap(); - - let amount = 10; - let (alice_account, bob_account) = - setup_accounts(&token_context, Keypair::new(), Keypair::new(), amount).await; - - // only add the transfer hook program id, nothing else - let token = token_context - .token - .with_transfer_hook_accounts(vec![AccountMeta::new_readonly(program_id, false)]); - token - .transfer( - &alice_account, - &bob_account, - &token_context.alice.pubkey(), - amount, - &[&token_context.alice], - ) - .await - .unwrap(); - let destination = token.get_account_info(&bob_account).await.unwrap(); - assert_eq!(destination.base.amount, amount); -} diff --git a/token/program-2022-test/transfer-hook-test-programs/downgrade/Cargo.toml b/token/program-2022-test/transfer-hook-test-programs/downgrade/Cargo.toml deleted file mode 100644 index 8434af65498..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/downgrade/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "spl-transfer-hook-example-downgrade" -version = "1.0.0" -description = "Solana Program Library Transfer Hook Example: Downgrade" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" -publish = false - -[dependencies] -solana-account-info = "2.1.0" -solana-program-entrypoint = "2.1.0" -solana-program-error = "2.1.0" -solana-pubkey = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token/program-2022-test/transfer-hook-test-programs/downgrade/src/lib.rs b/token/program-2022-test/transfer-hook-test-programs/downgrade/src/lib.rs deleted file mode 100644 index 9b2ab4e156a..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/downgrade/src/lib.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Program implementation - -use { - solana_account_info::{next_account_info, AccountInfo}, - solana_program_error::{ProgramError, ProgramResult}, - solana_pubkey::Pubkey, -}; - -solana_program_entrypoint::entrypoint!(process_instruction); -fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - _instruction_data: &[u8], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_account_info = next_account_info(account_info_iter)?; - let _mint_info = next_account_info(account_info_iter)?; - let _destination_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let _extra_account_metas_info = next_account_info(account_info_iter)?; - - let source_account_info_again = next_account_info(account_info_iter)?; - let authority_info_again = next_account_info(account_info_iter)?; - - if source_account_info.key != source_account_info_again.key { - return Err(ProgramError::InvalidAccountData); - } - - if source_account_info_again.is_writable { - return Err(ProgramError::InvalidAccountData); - } - - if authority_info.key != authority_info_again.key { - return Err(ProgramError::InvalidAccountData); - } - - if authority_info.is_signer { - return Err(ProgramError::InvalidAccountData); - } - Ok(()) -} diff --git a/token/program-2022-test/transfer-hook-test-programs/fail/Cargo.toml b/token/program-2022-test/transfer-hook-test-programs/fail/Cargo.toml deleted file mode 100644 index b8e6d4bfe68..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/fail/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "spl-transfer-hook-example-fail" -version = "1.0.0" -description = "Solana Program Library Transfer Hook Example: Fail" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" -publish = false - -[dependencies] -solana-account-info = "2.1.0" -solana-program-entrypoint = "2.1.0" -solana-program-error = "2.1.0" -solana-pubkey = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token/program-2022-test/transfer-hook-test-programs/fail/src/lib.rs b/token/program-2022-test/transfer-hook-test-programs/fail/src/lib.rs deleted file mode 100644 index e82ab842ae6..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/fail/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Program implementation - -use { - solana_account_info::AccountInfo, - solana_program_error::{ProgramError, ProgramResult}, - solana_pubkey::Pubkey, -}; - -solana_program_entrypoint::entrypoint!(process_instruction); -fn process_instruction( - _program_id: &Pubkey, - _accounts: &[AccountInfo], - _instruction_data: &[u8], -) -> ProgramResult { - Err(ProgramError::InvalidInstructionData) -} diff --git a/token/program-2022-test/transfer-hook-test-programs/success/Cargo.toml b/token/program-2022-test/transfer-hook-test-programs/success/Cargo.toml deleted file mode 100644 index 26c7a507aa5..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/success/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "spl-transfer-hook-example-success" -version = "1.0.0" -description = "Solana Program Library Transfer Hook Example: Success" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" -publish = false - -[dependencies] -solana-account-info = "2.1.0" -solana-program-entrypoint = "2.1.0" -solana-program-error = "2.1.0" -solana-pubkey = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token/program-2022-test/transfer-hook-test-programs/success/src/lib.rs b/token/program-2022-test/transfer-hook-test-programs/success/src/lib.rs deleted file mode 100644 index 5baa0db1f18..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/success/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Program implementation - -use { - solana_account_info::AccountInfo, solana_program_error::ProgramResult, solana_pubkey::Pubkey, -}; - -solana_program_entrypoint::entrypoint!(process_instruction); -fn process_instruction( - _program_id: &Pubkey, - _accounts: &[AccountInfo], - _instruction_data: &[u8], -) -> ProgramResult { - Ok(()) -} diff --git a/token/program-2022-test/transfer-hook-test-programs/swap-with-fee/Cargo.toml b/token/program-2022-test/transfer-hook-test-programs/swap-with-fee/Cargo.toml deleted file mode 100644 index 6a263434801..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/swap-with-fee/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "spl-transfer-hook-example-swap-with-fee" -version = "1.0.0" -description = "Solana Program Library Transfer Hook Example: Swap with Fee" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" -publish = false - -[dependencies] -solana-account-info = "2.1.0" -solana-program-entrypoint = "2.1.0" -solana-program-error = "2.1.0" -solana-pubkey = "2.1.0" -spl-token-2022 = { path = "../../../program-2022", features = ["no-entrypoint"] } - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token/program-2022-test/transfer-hook-test-programs/swap-with-fee/src/lib.rs b/token/program-2022-test/transfer-hook-test-programs/swap-with-fee/src/lib.rs deleted file mode 100644 index 5d21fb88440..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/swap-with-fee/src/lib.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Program implementation - -use { - solana_account_info::{next_account_info, AccountInfo}, - solana_program_error::ProgramResult, - solana_pubkey::Pubkey, - spl_token_2022::onchain, -}; - -solana_program_entrypoint::entrypoint!(process_instruction); -fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - _instruction_data: &[u8], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_a_account_info = next_account_info(account_info_iter)?; - let mint_a_info = next_account_info(account_info_iter)?; - let destination_a_account_info = next_account_info(account_info_iter)?; - let authority_a_info = next_account_info(account_info_iter)?; - let token_program_a_info = next_account_info(account_info_iter)?; - - let source_b_account_info = next_account_info(account_info_iter)?; - let mint_b_info = next_account_info(account_info_iter)?; - let destination_b_account_info = next_account_info(account_info_iter)?; - let authority_b_info = next_account_info(account_info_iter)?; - let token_program_b_info = next_account_info(account_info_iter)?; - - let remaining_accounts = account_info_iter.as_slice(); - - onchain::invoke_transfer_checked_with_fee( - token_program_a_info.key, - source_a_account_info.clone(), - mint_a_info.clone(), - destination_a_account_info.clone(), - authority_a_info.clone(), - remaining_accounts, - 1_000_000_000, - 9, - 10_000_000, - &[], - )?; - - onchain::invoke_transfer_checked_with_fee( - token_program_b_info.key, - source_b_account_info.clone(), - mint_b_info.clone(), - destination_b_account_info.clone(), - authority_b_info.clone(), - remaining_accounts, - 1_000_000_000, - 9, - 10_000_000, - &[], - )?; - - Ok(()) -} diff --git a/token/program-2022-test/transfer-hook-test-programs/swap/Cargo.toml b/token/program-2022-test/transfer-hook-test-programs/swap/Cargo.toml deleted file mode 100644 index 05d4d40d4a7..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/swap/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "spl-transfer-hook-example-swap" -version = "1.0.0" -description = "Solana Program Library Transfer Hook Example: Swap" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" -publish = false - -[dependencies] -solana-account-info = "2.1.0" -solana-program-entrypoint = "2.1.0" -solana-program-error = "2.1.0" -solana-pubkey = "2.1.0" -spl-token-2022 = { path = "../../../program-2022", features = ["no-entrypoint"] } - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token/program-2022-test/transfer-hook-test-programs/swap/src/lib.rs b/token/program-2022-test/transfer-hook-test-programs/swap/src/lib.rs deleted file mode 100644 index c299f6217b0..00000000000 --- a/token/program-2022-test/transfer-hook-test-programs/swap/src/lib.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Program implementation - -use { - solana_account_info::{next_account_info, AccountInfo}, - solana_program_error::ProgramResult, - solana_pubkey::Pubkey, - spl_token_2022::onchain, -}; - -solana_program_entrypoint::entrypoint!(process_instruction); -fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - _instruction_data: &[u8], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_a_account_info = next_account_info(account_info_iter)?; - let mint_a_info = next_account_info(account_info_iter)?; - let destination_a_account_info = next_account_info(account_info_iter)?; - let authority_a_info = next_account_info(account_info_iter)?; - let token_program_a_info = next_account_info(account_info_iter)?; - - let source_b_account_info = next_account_info(account_info_iter)?; - let mint_b_info = next_account_info(account_info_iter)?; - let destination_b_account_info = next_account_info(account_info_iter)?; - let authority_b_info = next_account_info(account_info_iter)?; - let token_program_b_info = next_account_info(account_info_iter)?; - - let remaining_accounts = account_info_iter.as_slice(); - - onchain::invoke_transfer_checked( - token_program_a_info.key, - source_a_account_info.clone(), - mint_a_info.clone(), - destination_a_account_info.clone(), - authority_a_info.clone(), - remaining_accounts, - 1, - 9, - &[], - )?; - - onchain::invoke_transfer_checked( - token_program_b_info.key, - source_b_account_info.clone(), - mint_b_info.clone(), - destination_b_account_info.clone(), - authority_b_info.clone(), - remaining_accounts, - 1, - 9, - &[], - )?; - - Ok(()) -} diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml deleted file mode 100644 index a6b72df1a45..00000000000 --- a/token/program-2022/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "spl-token-2022" -version = "6.0.0" -description = "Solana Program Library Token 2022" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" -exclude = ["js/**"] - -[features] -no-entrypoint = [] -test-sbf = [] -serde-traits = ["dep:serde", "dep:serde_with", "dep:base64", "spl-pod/serde-traits"] -default = ["zk-ops"] -# Remove this feature once the underlying syscalls are released on all networks -zk-ops = [] - -[dependencies] -arrayref = "0.3.9" -bytemuck = { version = "1.21.0", features = ["derive"] } -num-derive = "0.4" -num-traits = "0.2" -num_enum = "0.7.3" -solana-program = "2.1.0" -solana-security-txt = "1.1.1" -solana-zk-sdk = "2.1.0" -spl-elgamal-registry = { version = "0.1.0", path = "../confidential-transfer/elgamal-registry", features = ["no-entrypoint"] } -spl-memo = { version = "6.0", features = ["no-entrypoint"] } -spl-token = { version = "7.0", path = "../program", features = ["no-entrypoint"] } -spl-token-confidential-transfer-ciphertext-arithmetic = { version = "0.2.0", path = "../confidential-transfer/ciphertext-arithmetic" } -spl-token-confidential-transfer-proof-extraction = { version = "0.2.0", path = "../confidential-transfer/proof-extraction" } -spl-token-group-interface = { version = "0.5.0", path = "../../token-group/interface" } -spl-token-metadata-interface = { version = "0.6.0", path = "../../token-metadata/interface" } -spl-transfer-hook-interface = { version = "0.9.0", path = "../transfer-hook/interface" } -spl-type-length-value = { version = "0.7.0", path = "../../libraries/type-length-value" } -spl-pod = { version = "0.5.0", path = "../../libraries/pod" } -thiserror = "2.0" -serde = { version = "1.0.217", optional = true } -serde_with = { version = "3.12.0", optional = true } -base64 = { version = "0.22.1", optional = true } - -[target.'cfg(not(target_os = "solana"))'.dependencies] -spl-token-confidential-transfer-proof-generation = { version = "0.2.0", path = "../confidential-transfer/proof-generation"} - -[dev-dependencies] -lazy_static = "1.5.0" -proptest = "1.6" -serial_test = "3.2.0" -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" -spl-tlv-account-resolution = { version = "0.9.0", path = "../../libraries/tlv-account-resolution" } -serde_json = "1.0.135" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[lints] -workspace = true diff --git a/token/program-2022/README.md b/token/program-2022/README.md deleted file mode 100644 index 54289de68e4..00000000000 --- a/token/program-2022/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Token-2022 program - -A token program on the Solana blockchain, usable for fungible and non-fungible tokens. - -This program provides an interface and implementation that third parties can -utilize to create and use their tokens. - -Full documentation is available at https://spl.solana.com/token-2022 - -## Audit - -The repository [README](https://github.com/solana-labs/solana-program-library#audits) -contains information about program audits. diff --git a/token/program-2022/Xargo.toml b/token/program-2022/Xargo.toml deleted file mode 100644 index 1744f098ae1..00000000000 --- a/token/program-2022/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] \ No newline at end of file diff --git a/token/program-2022/inc/token.h b/token/program-2022/inc/token.h deleted file mode 100644 index 145c0c5e07b..00000000000 --- a/token/program-2022/inc/token.h +++ /dev/null @@ -1,687 +0,0 @@ -/* Autogenerated SPL Token program C Bindings */ - -#pragma once - -#include -#include -#include -#include - -/** - * Minimum number of multisignature signers (min N) - */ -#define Token_MIN_SIGNERS 1 - -/** - * Maximum number of multisignature signers (max N) - */ -#define Token_MAX_SIGNERS 11 - -/** - * Account state. - */ -enum Token_AccountState -#ifdef __cplusplus - : uint8_t -#endif // __cplusplus - { - /** - * Account is not yet initialized - */ - Token_AccountState_Uninitialized, - /** - * Account is initialized; the account owner and/or delegate may perform permitted operations - * on this account - */ - Token_AccountState_Initialized, - /** - * Account has been frozen by the mint freeze authority. Neither the account owner nor - * the delegate are able to perform operations on this account. - */ - Token_AccountState_Frozen, -}; -#ifndef __cplusplus -typedef uint8_t Token_AccountState; -#endif // __cplusplus - -/** - * Specifies the authority type for SetAuthority instructions - */ -enum Token_AuthorityType -#ifdef __cplusplus - : uint8_t -#endif // __cplusplus - { - /** - * Authority to mint new tokens - */ - Token_AuthorityType_MintTokens, - /** - * Authority to freeze any account associated with the Mint - */ - Token_AuthorityType_FreezeAccount, - /** - * Owner of a given token account - */ - Token_AuthorityType_AccountOwner, - /** - * Authority to close a token account - */ - Token_AuthorityType_CloseAccount, -}; -#ifndef __cplusplus -typedef uint8_t Token_AuthorityType; -#endif // __cplusplus - -typedef uint8_t Token_Pubkey[32]; - -/** - * A C representation of Rust's `std::option::Option` - */ -typedef enum Token_COption_Pubkey_Tag { - /** - * No value - */ - Token_COption_Pubkey_None_Pubkey, - /** - * Some value `T` - */ - Token_COption_Pubkey_Some_Pubkey, -} Token_COption_Pubkey_Tag; - -typedef struct Token_COption_Pubkey { - Token_COption_Pubkey_Tag tag; - union { - struct { - Token_Pubkey some; - }; - }; -} Token_COption_Pubkey; - -/** - * Instructions supported by the token program. - */ -typedef enum Token_TokenInstruction_Tag { - /** - * Initializes a new mint and optionally deposits all the newly minted - * tokens in an account. - * - * The `InitializeMint` instruction requires no signers and MUST be - * included within the same Transaction as the system program's - * `CreateAccount` instruction that creates the account being initialized. - * Otherwise another party can acquire ownership of the uninitialized - * account. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The mint to initialize. - * 1. `[]` Rent sysvar - * - */ - Token_TokenInstruction_InitializeMint, - /** - * Initializes a new account to hold tokens. If this account is associated - * with the native mint then the token balance of the initialized account - * will be equal to the amount of SOL in the account. If this account is - * associated with another mint, that mint must be initialized before this - * command can succeed. - * - * The `InitializeAccount` instruction requires no signers and MUST be - * included within the same Transaction as the system program's - * `CreateAccount` instruction that creates the account being initialized. - * Otherwise another party can acquire ownership of the uninitialized - * account. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The account to initialize. - * 1. `[]` The mint this account will be associated with. - * 2. `[]` The new account's owner/multisignature. - * 3. `[]` Rent sysvar - */ - Token_TokenInstruction_InitializeAccount, - /** - * Initializes a multisignature account with N provided signers. - * - * Multisignature accounts can used in place of any single owner/delegate - * accounts in any token instruction that require an owner/delegate to be - * present. The variant field represents the number of signers (M) - * required to validate this multisignature account. - * - * The `InitializeMultisig` instruction requires no signers and MUST be - * included within the same Transaction as the system program's - * `CreateAccount` instruction that creates the account being initialized. - * Otherwise another party can acquire ownership of the uninitialized - * account. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The multisignature account to initialize. - * 1. `[]` Rent sysvar - * 2. ..2+N. `[]` The signer accounts, must equal to N where 1 <= N <= - * 11. - */ - Token_TokenInstruction_InitializeMultisig, - /** - * Transfers tokens from one account to another either directly or via a - * delegate. If this account is associated with the native mint then equal - * amounts of SOL and Tokens will be transferred to the destination - * account. - * - * Accounts expected by this instruction: - * - * * Single owner/delegate - * 0. `[writable]` The source account. - * 1. `[writable]` The destination account. - * 2. `[signer]` The source account's owner/delegate. - * - * * Multisignature owner/delegate - * 0. `[writable]` The source account. - * 1. `[writable]` The destination account. - * 2. `[]` The source account's multisignature owner/delegate. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_Transfer, - /** - * Approves a delegate. A delegate is given the authority over tokens on - * behalf of the source account's owner. - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The source account. - * 1. `[]` The delegate. - * 2. `[signer]` The source account owner. - * - * * Multisignature owner - * 0. `[writable]` The source account. - * 1. `[]` The delegate. - * 2. `[]` The source account's multisignature owner. - * 3. ..3+M `[signer]` M signer accounts - */ - Token_TokenInstruction_Approve, - /** - * Revokes the delegate's authority. - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The source account. - * 1. `[signer]` The source account owner. - * - * * Multisignature owner - * 0. `[writable]` The source account. - * 1. `[]` The source account's multisignature owner. - * 2. ..2+M `[signer]` M signer accounts - */ - Token_TokenInstruction_Revoke, - /** - * Sets a new authority of a mint or account. - * - * Accounts expected by this instruction: - * - * * Single authority - * 0. `[writable]` The mint or account to change the authority of. - * 1. `[signer]` The current authority of the mint or account. - * - * * Multisignature authority - * 0. `[writable]` The mint or account to change the authority of. - * 1. `[]` The mint's or account's current multisignature authority. - * 2. ..2+M `[signer]` M signer accounts - */ - Token_TokenInstruction_SetAuthority, - /** - * Mints new tokens to an account. The native mint does not support - * minting. - * - * Accounts expected by this instruction: - * - * * Single authority - * 0. `[writable]` The mint. - * 1. `[writable]` The account to mint tokens to. - * 2. `[signer]` The mint's minting authority. - * - * * Multisignature authority - * 0. `[writable]` The mint. - * 1. `[writable]` The account to mint tokens to. - * 2. `[]` The mint's multisignature mint-tokens authority. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_MintTo, - /** - * Burns tokens by removing them from an account. `Burn` does not support - * accounts associated with the native mint, use `CloseAccount` instead. - * - * Accounts expected by this instruction: - * - * * Single owner/delegate - * 0. `[writable]` The account to burn from. - * 1. `[writable]` The token mint. - * 2. `[signer]` The account's owner/delegate. - * - * * Multisignature owner/delegate - * 0. `[writable]` The account to burn from. - * 1. `[writable]` The token mint. - * 2. `[]` The account's multisignature owner/delegate. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_Burn, - /** - * Close an account by transferring all its SOL to the destination account. - * Non-native accounts may only be closed if its token amount is zero. - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The account to close. - * 1. `[writable]` The destination account. - * 2. `[signer]` The account's owner. - * - * * Multisignature owner - * 0. `[writable]` The account to close. - * 1. `[writable]` The destination account. - * 2. `[]` The account's multisignature owner. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_CloseAccount, - /** - * Freeze an Initialized account using the Mint's freeze_authority (if - * set). - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The account to freeze. - * 1. `[]` The token mint. - * 2. `[signer]` The mint freeze authority. - * - * * Multisignature owner - * 0. `[writable]` The account to freeze. - * 1. `[]` The token mint. - * 2. `[]` The mint's multisignature freeze authority. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_FreezeAccount, - /** - * Thaw a Frozen account using the Mint's freeze_authority (if set). - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The account to freeze. - * 1. `[]` The token mint. - * 2. `[signer]` The mint freeze authority. - * - * * Multisignature owner - * 0. `[writable]` The account to freeze. - * 1. `[]` The token mint. - * 2. `[]` The mint's multisignature freeze authority. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_ThawAccount, - /** - * Transfers tokens from one account to another either directly or via a - * delegate. If this account is associated with the native mint then equal - * amounts of SOL and Tokens will be transferred to the destination - * account. - * - * This instruction differs from Transfer in that the token mint and - * decimals value is checked by the caller. This may be useful when - * creating transactions offline or within a hardware wallet. - * - * Accounts expected by this instruction: - * - * * Single owner/delegate - * 0. `[writable]` The source account. - * 1. `[]` The token mint. - * 2. `[writable]` The destination account. - * 3. `[signer]` The source account's owner/delegate. - * - * * Multisignature owner/delegate - * 0. `[writable]` The source account. - * 1. `[]` The token mint. - * 2. `[writable]` The destination account. - * 3. `[]` The source account's multisignature owner/delegate. - * 4. ..4+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_TransferChecked, - /** - * Approves a delegate. A delegate is given the authority over tokens on - * behalf of the source account's owner. - * - * This instruction differs from Approve in that the token mint and - * decimals value is checked by the caller. This may be useful when - * creating transactions offline or within a hardware wallet. - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The source account. - * 1. `[]` The token mint. - * 2. `[]` The delegate. - * 3. `[signer]` The source account owner. - * - * * Multisignature owner - * 0. `[writable]` The source account. - * 1. `[]` The token mint. - * 2. `[]` The delegate. - * 3. `[]` The source account's multisignature owner. - * 4. ..4+M `[signer]` M signer accounts - */ - Token_TokenInstruction_ApproveChecked, - /** - * Mints new tokens to an account. The native mint does not support - * minting. - * - * This instruction differs from MintTo in that the decimals value is - * checked by the caller. This may be useful when creating transactions - * offline or within a hardware wallet. - * - * Accounts expected by this instruction: - * - * * Single authority - * 0. `[writable]` The mint. - * 1. `[writable]` The account to mint tokens to. - * 2. `[signer]` The mint's minting authority. - * - * * Multisignature authority - * 0. `[writable]` The mint. - * 1. `[writable]` The account to mint tokens to. - * 2. `[]` The mint's multisignature mint-tokens authority. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_MintToChecked, - /** - * Burns tokens by removing them from an account. `BurnChecked` does not - * support accounts associated with the native mint, use `CloseAccount` - * instead. - * - * This instruction differs from Burn in that the decimals value is checked - * by the caller. This may be useful when creating transactions offline or - * within a hardware wallet. - * - * Accounts expected by this instruction: - * - * * Single owner/delegate - * 0. `[writable]` The account to burn from. - * 1. `[writable]` The token mint. - * 2. `[signer]` The account's owner/delegate. - * - * * Multisignature owner/delegate - * 0. `[writable]` The account to burn from. - * 1. `[writable]` The token mint. - * 2. `[]` The account's multisignature owner/delegate. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_BurnChecked, - /** - * Like InitializeAccount, but the owner pubkey is passed via instruction data - * rather than the accounts list. This variant may be preferable when using - * Cross Program Invocation from an instruction that does not need the owner's - * `AccountInfo` otherwise. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The account to initialize. - * 1. `[]` The mint this account will be associated with. - * 3. `[]` Rent sysvar - */ - Token_TokenInstruction_InitializeAccount2, - /** - * Given a wrapped / native token account (a token account containing SOL) - * updates its amount field based on the account's underlying `lamports`. - * This is useful if a non-wrapped SOL account uses `system_instruction::transfer` - * to move lamports to a wrapped token account, and needs to have its token - * `amount` field updated. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The native token account to sync with its underlying lamports. - */ - Token_TokenInstruction_SyncNative, -} Token_TokenInstruction_Tag; - -typedef struct Token_TokenInstruction_Token_InitializeMint_Body { - /** - * Number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; - /** - * The authority/multisignature to mint tokens. - */ - Token_Pubkey mint_authority; - /** - * The freeze authority/multisignature of the mint. - */ - struct Token_COption_Pubkey freeze_authority; -} Token_TokenInstruction_Token_InitializeMint_Body; - -typedef struct Token_TokenInstruction_Token_InitializeMultisig_Body { - /** - * The number of signers (M) required to validate this multisignature - * account. - */ - uint8_t m; -} Token_TokenInstruction_Token_InitializeMultisig_Body; - -typedef struct Token_TokenInstruction_Token_Transfer_Body { - /** - * The amount of tokens to transfer. - */ - uint64_t amount; -} Token_TokenInstruction_Token_Transfer_Body; - -typedef struct Token_TokenInstruction_Token_Approve_Body { - /** - * The amount of tokens the delegate is approved for. - */ - uint64_t amount; -} Token_TokenInstruction_Token_Approve_Body; - -typedef struct Token_TokenInstruction_Token_SetAuthority_Body { - /** - * The type of authority to update. - */ - Token_AuthorityType authority_type; - /** - * The new authority - */ - struct Token_COption_Pubkey new_authority; -} Token_TokenInstruction_Token_SetAuthority_Body; - -typedef struct Token_TokenInstruction_Token_MintTo_Body { - /** - * The amount of new tokens to mint. - */ - uint64_t amount; -} Token_TokenInstruction_Token_MintTo_Body; - -typedef struct Token_TokenInstruction_Token_Burn_Body { - /** - * The amount of tokens to burn. - */ - uint64_t amount; -} Token_TokenInstruction_Token_Burn_Body; - -typedef struct Token_TokenInstruction_Token_TransferChecked_Body { - /** - * The amount of tokens to transfer. - */ - uint64_t amount; - /** - * Expected number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; -} Token_TokenInstruction_Token_TransferChecked_Body; - -typedef struct Token_TokenInstruction_Token_ApproveChecked_Body { - /** - * The amount of tokens the delegate is approved for. - */ - uint64_t amount; - /** - * Expected number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; -} Token_TokenInstruction_Token_ApproveChecked_Body; - -typedef struct Token_TokenInstruction_Token_MintToChecked_Body { - /** - * The amount of new tokens to mint. - */ - uint64_t amount; - /** - * Expected number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; -} Token_TokenInstruction_Token_MintToChecked_Body; - -typedef struct Token_TokenInstruction_Token_BurnChecked_Body { - /** - * The amount of tokens to burn. - */ - uint64_t amount; - /** - * Expected number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; -} Token_TokenInstruction_Token_BurnChecked_Body; - -typedef struct Token_TokenInstruction_Token_InitializeAccount2_Body { - /** - * The new account's owner/multisignature. - */ - Token_Pubkey owner; -} Token_TokenInstruction_Token_InitializeAccount2_Body; - -typedef struct Token_TokenInstruction { - Token_TokenInstruction_Tag tag; - union { - Token_TokenInstruction_Token_InitializeMint_Body initialize_mint; - Token_TokenInstruction_Token_InitializeMultisig_Body initialize_multisig; - Token_TokenInstruction_Token_Transfer_Body transfer; - Token_TokenInstruction_Token_Approve_Body approve; - Token_TokenInstruction_Token_SetAuthority_Body set_authority; - Token_TokenInstruction_Token_MintTo_Body mint_to; - Token_TokenInstruction_Token_Burn_Body burn; - Token_TokenInstruction_Token_TransferChecked_Body transfer_checked; - Token_TokenInstruction_Token_ApproveChecked_Body approve_checked; - Token_TokenInstruction_Token_MintToChecked_Body mint_to_checked; - Token_TokenInstruction_Token_BurnChecked_Body burn_checked; - Token_TokenInstruction_Token_InitializeAccount2_Body initialize_account2; - }; -} Token_TokenInstruction; - -/** - * Mint data. - */ -typedef struct Token_Mint { - /** - * Optional authority used to mint new tokens. The mint authority may only be provided during - * mint creation. If no mint authority is present then the mint has a fixed supply and no - * further tokens may be minted. - */ - struct Token_COption_Pubkey mint_authority; - /** - * Total supply of tokens. - */ - uint64_t supply; - /** - * Number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; - /** - * Is `true` if this structure has been initialized - */ - bool is_initialized; - /** - * Optional authority to freeze token accounts. - */ - struct Token_COption_Pubkey freeze_authority; -} Token_Mint; - -/** - * A C representation of Rust's `std::option::Option` - */ -typedef enum Token_COption_u64_Tag { - /** - * No value - */ - Token_COption_u64_None_u64, - /** - * Some value `T` - */ - Token_COption_u64_Some_u64, -} Token_COption_u64_Tag; - -typedef struct Token_COption_u64 { - Token_COption_u64_Tag tag; - union { - struct { - uint64_t some; - }; - }; -} Token_COption_u64; - -/** - * Account data. - */ -typedef struct Token_Account { - /** - * The mint associated with this account - */ - Token_Pubkey mint; - /** - * The owner of this account. - */ - Token_Pubkey owner; - /** - * The amount of tokens this account holds. - */ - uint64_t amount; - /** - * If `delegate` is `Some` then `delegated_amount` represents - * the amount authorized by the delegate - */ - struct Token_COption_Pubkey delegate; - /** - * The account's state - */ - Token_AccountState state; - /** - * If is_some, this is a native token, and the value logs the rent-exempt reserve. An Account - * is required to be rent-exempt, so the value is used by the Processor to ensure that wrapped - * SOL accounts do not drop below this threshold. - */ - struct Token_COption_u64 is_native; - /** - * The amount delegated - */ - uint64_t delegated_amount; - /** - * Optional authority to close the account. - */ - struct Token_COption_Pubkey close_authority; -} Token_Account; - -/** - * Multisignature data. - */ -typedef struct Token_Multisig { - /** - * Number of signers required - */ - uint8_t m; - /** - * Number of valid signers - */ - uint8_t n; - /** - * Is `true` if this structure has been initialized - */ - bool is_initialized; - /** - * Signer public keys - */ - Token_Pubkey signers[Token_MAX_SIGNERS]; -} Token_Multisig; diff --git a/token/program-2022/program-id.md b/token/program-2022/program-id.md deleted file mode 100644 index 7d999512781..00000000000 --- a/token/program-2022/program-id.md +++ /dev/null @@ -1 +0,0 @@ -TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb diff --git a/token/program-2022/src/entrypoint.rs b/token/program-2022/src/entrypoint.rs deleted file mode 100644 index 78694845ab4..00000000000 --- a/token/program-2022/src/entrypoint.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Program entrypoint - -use { - crate::{error::TokenError, processor::Processor}, - solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError, - pubkey::Pubkey, - }, - solana_security_txt::security_txt, -}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - if let Err(error) = Processor::process(program_id, accounts, instruction_data) { - // catch the error so we can print it - error.print::(); - return Err(error); - } - Ok(()) -} - -security_txt! { - // Required fields - name: "SPL Token-2022", - project_url: "https://spl.solana.com/token-2022", - contacts: "link:https://github.com/solana-labs/solana-program-library/security/advisories/new,mailto:security@solana.com,discord:https://solana.com/discord", - policy: "https://github.com/solana-labs/solana-program-library/blob/master/SECURITY.md", - - // Optional Fields - preferred_languages: "en", - source_code: "https://github.com/solana-labs/solana-program-library/tree/master/token/program-2022", - source_revision: "070934ae4f2975d602caa6bd1e88b2c010e4cab5", - source_release: "token-2022-v5.0.2", - auditors: "https://github.com/solana-labs/security-audits#token-2022" -} diff --git a/token/program-2022/src/error.rs b/token/program-2022/src/error.rs deleted file mode 100644 index f7ae93c942c..00000000000 --- a/token/program-2022/src/error.rs +++ /dev/null @@ -1,502 +0,0 @@ -//! Error types - -#[cfg(not(target_os = "solana"))] -use spl_token_confidential_transfer_proof_generation::errors::TokenProofGenerationError; -use { - num_derive::FromPrimitive, - solana_program::{ - decode_error::DecodeError, - msg, - program_error::{PrintProgramError, ProgramError}, - }, - spl_token_confidential_transfer_proof_extraction::errors::TokenProofExtractionError, - thiserror::Error, -}; - -/// Errors that may be returned by the Token program. -#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] -pub enum TokenError { - // 0 - /// Lamport balance below rent-exempt threshold. - #[error("Lamport balance below rent-exempt threshold")] - NotRentExempt, - /// Insufficient funds for the operation requested. - #[error("Insufficient funds")] - InsufficientFunds, - /// Invalid Mint. - #[error("Invalid Mint")] - InvalidMint, - /// Account not associated with this Mint. - #[error("Account not associated with this Mint")] - MintMismatch, - /// Owner does not match. - #[error("Owner does not match")] - OwnerMismatch, - - // 5 - /// This token's supply is fixed and new tokens cannot be minted. - #[error("Fixed supply")] - FixedSupply, - /// The account cannot be initialized because it is already being used. - #[error("Already in use")] - AlreadyInUse, - /// Invalid number of provided signers. - #[error("Invalid number of provided signers")] - InvalidNumberOfProvidedSigners, - /// Invalid number of required signers. - #[error("Invalid number of required signers")] - InvalidNumberOfRequiredSigners, - /// State is uninitialized. - #[error("State is uninitialized")] - UninitializedState, - - // 10 - /// Instruction does not support native tokens - #[error("Instruction does not support native tokens")] - NativeNotSupported, - /// Non-native account can only be closed if its balance is zero - #[error("Non-native account can only be closed if its balance is zero")] - NonNativeHasBalance, - /// Invalid instruction - #[error("Invalid instruction")] - InvalidInstruction, - /// State is invalid for requested operation. - #[error("State is invalid for requested operation")] - InvalidState, - /// Operation overflowed - #[error("Operation overflowed")] - Overflow, - - // 15 - /// Account does not support specified authority type. - #[error("Account does not support specified authority type")] - AuthorityTypeNotSupported, - /// This token mint cannot freeze accounts. - #[error("This token mint cannot freeze accounts")] - MintCannotFreeze, - /// Account is frozen; all account operations will fail - #[error("Account is frozen")] - AccountFrozen, - /// Mint decimals mismatch between the client and mint - #[error("The provided decimals value different from the Mint decimals")] - MintDecimalsMismatch, - /// Instruction does not support non-native tokens - #[error("Instruction does not support non-native tokens")] - NonNativeNotSupported, - - // 20 - /// Extension type does not match already existing extensions - #[error("Extension type does not match already existing extensions")] - ExtensionTypeMismatch, - /// Extension does not match the base type provided - #[error("Extension does not match the base type provided")] - ExtensionBaseMismatch, - /// Extension already initialized on this account - #[error("Extension already initialized on this account")] - ExtensionAlreadyInitialized, - /// An account can only be closed if its confidential balance is zero - #[error("An account can only be closed if its confidential balance is zero")] - ConfidentialTransferAccountHasBalance, - /// Account not approved for confidential transfers - #[error("Account not approved for confidential transfers")] - ConfidentialTransferAccountNotApproved, - - // 25 - /// Account not accepting deposits or transfers - #[error("Account not accepting deposits or transfers")] - ConfidentialTransferDepositsAndTransfersDisabled, - /// ElGamal public key mismatch - #[error("ElGamal public key mismatch")] - ConfidentialTransferElGamalPubkeyMismatch, - /// Balance mismatch - #[error("Balance mismatch")] - ConfidentialTransferBalanceMismatch, - /// Mint has non-zero supply. Burn all tokens before closing the mint. - #[error("Mint has non-zero supply. Burn all tokens before closing the mint")] - MintHasSupply, - /// No authority exists to perform the desired operation - #[error("No authority exists to perform the desired operation")] - NoAuthorityExists, - - // 30 - /// Transfer fee exceeds maximum of 10,000 basis points - #[error("Transfer fee exceeds maximum of 10,000 basis points")] - TransferFeeExceedsMaximum, - /// Mint required for this account to transfer tokens, use - /// `transfer_checked` or `transfer_checked_with_fee` - #[error("Mint required for this account to transfer tokens, use `transfer_checked` or `transfer_checked_with_fee`")] - MintRequiredForTransfer, - /// Calculated fee does not match expected fee - #[error("Calculated fee does not match expected fee")] - FeeMismatch, - /// Fee parameters associated with confidential transfer zero-knowledge - /// proofs do not match fee parameters in mint - #[error( - "Fee parameters associated with zero-knowledge proofs do not match fee parameters in mint" - )] - FeeParametersMismatch, - /// The owner authority cannot be changed - #[error("The owner authority cannot be changed")] - ImmutableOwner, - - // 35 - /// An account can only be closed if its withheld fee balance is zero, - /// harvest fees to the mint and try again - #[error("An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again")] - AccountHasWithheldTransferFees, - /// No memo in previous instruction; required for recipient to receive a - /// transfer - #[error("No memo in previous instruction; required for recipient to receive a transfer")] - NoMemo, - /// Transfer is disabled for this mint - #[error("Transfer is disabled for this mint")] - NonTransferable, - /// Non-transferable tokens can't be minted to an account without immutable - /// ownership - #[error("Non-transferable tokens can't be minted to an account without immutable ownership")] - NonTransferableNeedsImmutableOwnership, - /// The total number of `Deposit` and `Transfer` instructions to an account - /// cannot exceed the associated - /// `maximum_pending_balance_credit_counter` - #[error( - "The total number of `Deposit` and `Transfer` instructions to an account cannot exceed - the associated `maximum_pending_balance_credit_counter`" - )] - MaximumPendingBalanceCreditCounterExceeded, - - // 40 - /// The deposit amount for the confidential extension exceeds the maximum - /// limit - #[error("Deposit amount exceeds maximum limit")] - MaximumDepositAmountExceeded, - /// CPI Guard cannot be enabled or disabled in CPI - #[error("CPI Guard cannot be enabled or disabled in CPI")] - CpiGuardSettingsLocked, - /// CPI Guard is enabled, and a program attempted to transfer user funds - /// without using a delegate - #[error("CPI Guard is enabled, and a program attempted to transfer user funds via CPI without using a delegate")] - CpiGuardTransferBlocked, - /// CPI Guard is enabled, and a program attempted to burn user funds without - /// using a delegate - #[error( - "CPI Guard is enabled, and a program attempted to burn user funds via CPI without using a delegate" - )] - CpiGuardBurnBlocked, - /// CPI Guard is enabled, and a program attempted to close an account - /// without returning lamports to owner - #[error("CPI Guard is enabled, and a program attempted to close an account via CPI without returning lamports to owner")] - CpiGuardCloseAccountBlocked, - - // 45 - /// CPI Guard is enabled, and a program attempted to approve a delegate - #[error("CPI Guard is enabled, and a program attempted to approve a delegate via CPI")] - CpiGuardApproveBlocked, - /// CPI Guard is enabled, and a program attempted to add or replace an - /// authority - #[error( - "CPI Guard is enabled, and a program attempted to add or replace an authority via CPI" - )] - CpiGuardSetAuthorityBlocked, - /// Account ownership cannot be changed while CPI Guard is enabled - #[error("Account ownership cannot be changed while CPI Guard is enabled")] - CpiGuardOwnerChangeBlocked, - /// Extension not found in account data - #[error("Extension not found in account data")] - ExtensionNotFound, - /// Account does not accept non-confidential transfers - #[error("Non-confidential transfers disabled")] - NonConfidentialTransfersDisabled, - - // 50 - /// An account can only be closed if the confidential withheld fee is zero - #[error("An account can only be closed if the confidential withheld fee is zero")] - ConfidentialTransferFeeAccountHasWithheldFee, - /// A mint or an account is initialized to an invalid combination of - /// extensions - #[error("A mint or an account is initialized to an invalid combination of extensions")] - InvalidExtensionCombination, - /// Extension allocation with overwrite must use the same length - #[error("Extension allocation with overwrite must use the same length")] - InvalidLengthForAlloc, - /// Failed to decrypt a confidential transfer account - #[error("Failed to decrypt a confidential transfer account")] - AccountDecryption, - /// Failed to generate a zero-knowledge proof needed for a token instruction - #[error("Failed to generate proof")] - ProofGeneration, - - // 55 - /// An invalid proof instruction offset was provided - #[error("An invalid proof instruction offset was provided")] - InvalidProofInstructionOffset, - /// Harvest of withheld tokens to mint is disabled - #[error("Harvest of withheld tokens to mint is disabled")] - HarvestToMintDisabled, - /// Split proof context state accounts not supported for instruction - #[error("Split proof context state accounts not supported for instruction")] - SplitProofContextStateAccountsNotSupported, - /// Not enough proof context state accounts provided - #[error("Not enough proof context state accounts provided")] - NotEnoughProofContextStateAccounts, - /// Ciphertext is malformed - #[error("Ciphertext is malformed")] - MalformedCiphertext, - - // 60 - /// Ciphertext arithmetic failed - #[error("Ciphertext arithmetic failed")] - CiphertextArithmeticFailed, - /// Pedersen commitments did not match - #[error("Pedersen commitment mismatch")] - PedersenCommitmentMismatch, - /// Range proof length did not match - #[error("Range proof length mismatch")] - RangeProofLengthMismatch, - /// Illegal transfer amount bit length - #[error("Illegal transfer amount bit length")] - IllegalBitLength, - /// Fee calculation failed - #[error("Fee calculation failed")] - FeeCalculation, - - //65 - /// Withdraw / Deposit not allowed for confidential-mint-burn - #[error("Withdraw / Deposit not allowed for confidential-mint-burn")] - IllegalMintBurnConversion, - /// Invalid scale for scaled ui amount - #[error("Invalid scale for scaled ui amount")] - InvalidScale, - /// Transferring, minting, and burning is paused on this mint - #[error("Transferring, minting, and burning is paused on this mint")] - MintPaused, -} -impl From for ProgramError { - fn from(e: TokenError) -> Self { - ProgramError::Custom(e as u32) - } -} -impl DecodeError for TokenError { - fn type_of() -> &'static str { - "TokenError" - } -} - -impl PrintProgramError for TokenError { - fn print(&self) - where - E: 'static + std::error::Error + DecodeError + num_traits::FromPrimitive, - { - match self { - TokenError::NotRentExempt => msg!("Error: Lamport balance below rent-exempt threshold"), - TokenError::InsufficientFunds => msg!("Error: insufficient funds"), - TokenError::InvalidMint => msg!("Error: Invalid Mint"), - TokenError::MintMismatch => msg!("Error: Account not associated with this Mint"), - TokenError::OwnerMismatch => msg!("Error: owner does not match"), - TokenError::FixedSupply => msg!("Error: the total supply of this token is fixed"), - TokenError::AlreadyInUse => msg!("Error: account or token already in use"), - TokenError::InvalidNumberOfProvidedSigners => { - msg!("Error: Invalid number of provided signers") - } - TokenError::InvalidNumberOfRequiredSigners => { - msg!("Error: Invalid number of required signers") - } - TokenError::UninitializedState => msg!("Error: State is uninitialized"), - TokenError::NativeNotSupported => { - msg!("Error: Instruction does not support native tokens") - } - TokenError::NonNativeHasBalance => { - msg!("Error: Non-native account can only be closed if its balance is zero") - } - TokenError::InvalidInstruction => msg!("Error: Invalid instruction"), - TokenError::InvalidState => msg!("Error: Invalid account state for operation"), - TokenError::Overflow => msg!("Error: Operation overflowed"), - TokenError::AuthorityTypeNotSupported => { - msg!("Error: Account does not support specified authority type") - } - TokenError::MintCannotFreeze => msg!("Error: This token mint cannot freeze accounts"), - TokenError::AccountFrozen => msg!("Error: Account is frozen"), - TokenError::MintDecimalsMismatch => { - msg!("Error: decimals different from the Mint decimals") - } - TokenError::NonNativeNotSupported => { - msg!("Error: Instruction does not support non-native tokens") - } - TokenError::ExtensionTypeMismatch => { - msg!("Error: New extension type does not match already existing extensions") - } - TokenError::ExtensionBaseMismatch => { - msg!("Error: Extension does not match the base type provided") - } - TokenError::ExtensionAlreadyInitialized => { - msg!("Error: Extension already initialized on this account") - } - TokenError::ConfidentialTransferAccountHasBalance => { - msg!("Error: An account can only be closed if its confidential balance is zero") - } - TokenError::ConfidentialTransferAccountNotApproved => { - msg!("Error: Account not approved for confidential transfers") - } - TokenError::ConfidentialTransferDepositsAndTransfersDisabled => { - msg!("Error: Account not accepting deposits or transfers") - } - TokenError::ConfidentialTransferElGamalPubkeyMismatch => { - msg!("Error: ElGamal public key mismatch") - } - TokenError::ConfidentialTransferBalanceMismatch => { - msg!("Error: Balance mismatch") - } - TokenError::MintHasSupply => { - msg!("Error: Mint has non-zero supply. Burn all tokens before closing the mint") - } - TokenError::NoAuthorityExists => { - msg!("Error: No authority exists to perform the desired operation"); - } - TokenError::TransferFeeExceedsMaximum => { - msg!("Error: Transfer fee exceeds maximum of 10,000 basis points"); - } - TokenError::MintRequiredForTransfer => { - msg!("Mint required for this account to transfer tokens, use `transfer_checked` or `transfer_checked_with_fee`"); - } - TokenError::FeeMismatch => { - msg!("Calculated fee does not match expected fee"); - } - TokenError::FeeParametersMismatch => { - msg!("Fee parameters associated with zero-knowledge proofs do not match fee parameters in mint") - } - TokenError::ImmutableOwner => { - msg!("The owner authority cannot be changed"); - } - TokenError::AccountHasWithheldTransferFees => { - msg!("Error: An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again"); - } - TokenError::NoMemo => { - msg!("Error: No memo in previous instruction; required for recipient to receive a transfer"); - } - TokenError::NonTransferable => { - msg!("Transfer is disabled for this mint"); - } - TokenError::NonTransferableNeedsImmutableOwnership => { - msg!("Non-transferable tokens can't be minted to an account without immutable ownership"); - } - TokenError::MaximumPendingBalanceCreditCounterExceeded => { - msg!("The total number of `Deposit` and `Transfer` instructions to an account cannot exceed the associated `maximum_pending_balance_credit_counter`"); - } - TokenError::MaximumDepositAmountExceeded => { - msg!("Deposit amount exceeds maximum limit") - } - TokenError::CpiGuardSettingsLocked => { - msg!("CPI Guard status cannot be changed in CPI") - } - TokenError::CpiGuardTransferBlocked => { - msg!("CPI Guard is enabled, and a program attempted to transfer user funds without using a delegate") - } - TokenError::CpiGuardBurnBlocked => { - msg!("CPI Guard is enabled, and a program attempted to burn user funds without using a delegate") - } - TokenError::CpiGuardCloseAccountBlocked => { - msg!("CPI Guard is enabled, and a program attempted to close an account without returning lamports to owner") - } - TokenError::CpiGuardApproveBlocked => { - msg!("CPI Guard is enabled, and a program attempted to approve a delegate") - } - TokenError::CpiGuardSetAuthorityBlocked => { - msg!("CPI Guard is enabled, and a program attempted to add or change an authority") - } - TokenError::CpiGuardOwnerChangeBlocked => { - msg!("Account ownership cannot be changed while CPI Guard is enabled") - } - TokenError::ExtensionNotFound => { - msg!("Extension not found in account data") - } - TokenError::NonConfidentialTransfersDisabled => { - msg!("Non-confidential transfers disabled") - } - TokenError::ConfidentialTransferFeeAccountHasWithheldFee => { - msg!("Account has non-zero confidential withheld fee") - } - TokenError::InvalidExtensionCombination => { - msg!("Mint or account is initialized to an invalid combination of extensions") - } - TokenError::InvalidLengthForAlloc => { - msg!("Extension allocation with overwrite must use the same length") - } - TokenError::AccountDecryption => { - msg!("Failed to decrypt a confidential transfer account") - } - TokenError::ProofGeneration => { - msg!("Failed to generate proof") - } - TokenError::InvalidProofInstructionOffset => { - msg!("An invalid proof instruction offset was provided") - } - TokenError::HarvestToMintDisabled => { - msg!("Harvest of withheld tokens to mint is disabled") - } - TokenError::SplitProofContextStateAccountsNotSupported => { - msg!("Split proof context state accounts not supported for instruction") - } - TokenError::NotEnoughProofContextStateAccounts => { - msg!("Not enough proof context state accounts provided") - } - TokenError::MalformedCiphertext => { - msg!("Ciphertext is malformed") - } - TokenError::CiphertextArithmeticFailed => { - msg!("Ciphertext arithmetic failed") - } - TokenError::PedersenCommitmentMismatch => { - msg!("Pedersen commitments did not match") - } - TokenError::RangeProofLengthMismatch => { - msg!("Range proof lengths did not match") - } - TokenError::IllegalBitLength => { - msg!("Illegal transfer amount bit length") - } - TokenError::FeeCalculation => { - msg!("Transfer fee calculation failed") - } - TokenError::IllegalMintBurnConversion => { - msg!("Conversions from normal to confidential token balance and vice versa are illegal if the confidential-mint-burn extension is enabled") - } - TokenError::InvalidScale => { - msg!("Invalid scale for scaled ui amount") - } - TokenError::MintPaused => { - msg!("Transferring, minting, and burning is paused on this mint") - } - } - } -} - -#[cfg(not(target_os = "solana"))] -impl From for TokenError { - fn from(e: TokenProofGenerationError) -> Self { - match e { - TokenProofGenerationError::ProofGeneration(_) => TokenError::ProofGeneration, - TokenProofGenerationError::NotEnoughFunds => TokenError::InsufficientFunds, - TokenProofGenerationError::IllegalAmountBitLength => TokenError::IllegalBitLength, - TokenProofGenerationError::FeeCalculation => TokenError::FeeCalculation, - TokenProofGenerationError::CiphertextExtraction => TokenError::MalformedCiphertext, - } - } -} - -impl From for TokenError { - fn from(e: TokenProofExtractionError) -> Self { - match e { - TokenProofExtractionError::ElGamalPubkeyMismatch => { - TokenError::ConfidentialTransferElGamalPubkeyMismatch - } - TokenProofExtractionError::PedersenCommitmentMismatch => { - TokenError::PedersenCommitmentMismatch - } - TokenProofExtractionError::RangeProofLengthMismatch => { - TokenError::RangeProofLengthMismatch - } - TokenProofExtractionError::FeeParametersMismatch => TokenError::FeeParametersMismatch, - TokenProofExtractionError::CurveArithmetic => TokenError::CiphertextArithmeticFailed, - TokenProofExtractionError::CiphertextExtraction => TokenError::MalformedCiphertext, - } - } -} diff --git a/token/program-2022/src/extension/confidential_mint_burn/account_info.rs b/token/program-2022/src/extension/confidential_mint_burn/account_info.rs deleted file mode 100644 index 53f678d1082..00000000000 --- a/token/program-2022/src/extension/confidential_mint_burn/account_info.rs +++ /dev/null @@ -1,208 +0,0 @@ -use { - super::ConfidentialMintBurn, - crate::{ - error::TokenError, - extension::confidential_transfer::{ - ConfidentialTransferAccount, DecryptableBalance, EncryptedBalance, - }, - }, - bytemuck::{Pod, Zeroable}, - solana_zk_sdk::{ - encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, - pedersen::PedersenOpening, - pod::{ - auth_encryption::PodAeCiphertext, - elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - }, - }, - zk_elgamal_proof_program::proof_data::CiphertextCiphertextEqualityProofData, - }, - spl_token_confidential_transfer_proof_generation::{ - burn::{burn_split_proof_data, BurnProofData}, - mint::{mint_split_proof_data, MintProofData}, - }, -}; - -/// Confidential Mint Burn extension information needed to construct a -/// `RotateSupplyElgamalPubkey` instruction. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct SupplyAccountInfo { - /// The available balance (encrypted by `supply_elgamal_pubkey`) - pub current_supply: PodElGamalCiphertext, - /// The decryptable supply - pub decryptable_supply: PodAeCiphertext, - /// The supply's ElGamal pubkey - pub supply_elgamal_pubkey: PodElGamalPubkey, -} - -impl SupplyAccountInfo { - /// Creates a `SupplyAccountInfo` from `ConfidentialMintBurn` extension - /// account data - pub fn new(extension: &ConfidentialMintBurn) -> Self { - Self { - current_supply: extension.confidential_supply, - decryptable_supply: extension.decryptable_supply, - supply_elgamal_pubkey: extension.supply_elgamal_pubkey, - } - } - - /// Computes the current supply from the decryptable supply and the - /// difference between the decryptable supply and the ElGamal encrypted - /// supply ciphertext - pub fn decrypted_current_supply( - &self, - aes_key: &AeKey, - elgamal_keypair: &ElGamalKeypair, - ) -> Result { - // decrypt the decryptable supply - let current_decyptable_supply = AeCiphertext::try_from(self.decryptable_supply) - .map_err(|_| TokenError::MalformedCiphertext)? - .decrypt(aes_key) - .ok_or(TokenError::MalformedCiphertext)?; - - // get the difference between the supply ciphertext and the decryptable supply - // explanation see https://github.com/solana-labs/solana-program-library/pull/6881#issuecomment-2385579058 - let decryptable_supply_ciphertext = - elgamal_keypair.pubkey().encrypt(current_decyptable_supply); - #[allow(clippy::arithmetic_side_effects)] - let supply_delta_ciphertext = decryptable_supply_ciphertext - - ElGamalCiphertext::try_from(self.current_supply) - .map_err(|_| TokenError::MalformedCiphertext)?; - let decryptable_to_current_diff = elgamal_keypair - .secret() - .decrypt_u32(&supply_delta_ciphertext) - .ok_or(TokenError::MalformedCiphertext)?; - - // compute the current supply - current_decyptable_supply - .checked_sub(decryptable_to_current_diff) - .ok_or(TokenError::Overflow) - } - - /// Generates the `CiphertextCiphertextEqualityProofData` needed for a - /// `RotateSupplyElgamalPubkey` instruction - pub fn generate_rotate_supply_elgamal_pubkey_proof( - &self, - current_supply_elgamal_keypair: &ElGamalKeypair, - new_supply_elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - ) -> Result { - let current_supply = - self.decrypted_current_supply(aes_key, current_supply_elgamal_keypair)?; - - let new_supply_opening = PedersenOpening::new_rand(); - let new_supply_ciphertext = new_supply_elgamal_keypair - .pubkey() - .encrypt_with(current_supply, &new_supply_opening); - - CiphertextCiphertextEqualityProofData::new( - current_supply_elgamal_keypair, - new_supply_elgamal_keypair.pubkey(), - &self - .current_supply - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?, - &new_supply_ciphertext, - &new_supply_opening, - current_supply, - ) - .map_err(|_| TokenError::ProofGeneration) - } - - /// Create a mint proof data that is split into equality, ciphertext - /// validity, and range proof. - pub fn generate_split_mint_proof_data( - &self, - mint_amount: u64, - current_supply: u64, - supply_elgamal_keypair: &ElGamalKeypair, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - ) -> Result { - let current_supply_ciphertext = self - .current_supply - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - - mint_split_proof_data( - ¤t_supply_ciphertext, - mint_amount, - current_supply, - supply_elgamal_keypair, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ) - .map_err(|e| -> TokenError { e.into() }) - } - - /// Compute the new decryptable supply. - pub fn new_decryptable_supply( - &self, - mint_amount: u64, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - ) -> Result { - let current_decrypted_supply = self.decrypted_current_supply(aes_key, elgamal_keypair)?; - let new_decrypted_available_balance = current_decrypted_supply - .checked_add(mint_amount) - .ok_or(TokenError::Overflow)?; - - Ok(aes_key.encrypt(new_decrypted_available_balance)) - } -} - -/// Confidential Mint Burn extension information needed to construct a -/// `Burn` instruction. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct BurnAccountInfo { - /// The available balance (encrypted by `encryption_pubkey`) - pub available_balance: EncryptedBalance, - /// The decryptable available balance - pub decryptable_available_balance: DecryptableBalance, -} - -impl BurnAccountInfo { - /// Create the `ApplyPendingBalance` instruction account information from - /// `ConfidentialTransferAccount`. - pub fn new(account: &ConfidentialTransferAccount) -> Self { - Self { - available_balance: account.available_balance, - decryptable_available_balance: account.decryptable_available_balance, - } - } - - /// Create a burn proof data that is split into equality, ciphertext - /// validity, and range proof. - pub fn generate_split_burn_proof_data( - &self, - burn_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - supply_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - ) -> Result { - let current_available_balance_ciphertext = self - .available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - let current_decryptable_available_balance = self - .decryptable_available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - - burn_split_proof_data( - ¤t_available_balance_ciphertext, - ¤t_decryptable_available_balance, - burn_amount, - source_elgamal_keypair, - aes_key, - auditor_elgamal_pubkey, - supply_elgamal_pubkey, - ) - .map_err(|e| -> TokenError { e.into() }) - } -} diff --git a/token/program-2022/src/extension/confidential_mint_burn/instruction.rs b/token/program-2022/src/extension/confidential_mint_burn/instruction.rs deleted file mode 100644 index 065e7ff9aeb..00000000000 --- a/token/program-2022/src/extension/confidential_mint_burn/instruction.rs +++ /dev/null @@ -1,574 +0,0 @@ -#[cfg(feature = "serde-traits")] -use { - crate::serialization::{ - aeciphertext_fromstr, elgamalciphertext_fromstr, elgamalpubkey_fromstr, - }, - serde::{Deserialize, Serialize}, -}; -use { - crate::{ - check_program_account, - extension::confidential_transfer::DecryptableBalance, - instruction::{encode_instruction, TokenInstruction}, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, - solana_zk_sdk::encryption::pod::{ - auth_encryption::PodAeCiphertext, - elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - }, -}; -#[cfg(not(target_os = "solana"))] -use { - solana_zk_sdk::{ - encryption::elgamal::ElGamalPubkey, - zk_elgamal_proof_program::{ - instruction::ProofInstruction, - proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, - CiphertextCiphertextEqualityProofData, CiphertextCommitmentEqualityProofData, - }, - }, - }, - spl_token_confidential_transfer_proof_extraction::instruction::{ - process_proof_location, ProofLocation, - }, -}; - -/// Confidential Transfer extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] -#[repr(u8)] -pub enum ConfidentialMintBurnInstruction { - /// Initializes confidential mints and burns for a mint. - /// - /// The `ConfidentialMintBurnInstruction::InitializeMint` instruction - /// requires no signers and MUST be included within the same Transaction - /// as `TokenInstruction::InitializeMint`. Otherwise another party can - /// initialize the configuration. - /// - /// The instruction fails if the `TokenInstruction::InitializeMint` - /// instruction has already executed for the mint. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The SPL Token mint. - /// - /// Data expected by this instruction: - /// `InitializeMintData` - InitializeMint, - /// Rotates the ElGamal pubkey used to encrypt confidential supply - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The SPL Token mint. - /// 1. `[]` Instructions sysvar if `CiphertextCiphertextEquality` is - /// included in the same transaction or context state account if - /// `CiphertextCiphertextEquality` is pre-verified into a context state - /// account. - /// 2. `[signer]` Confidential mint authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The SPL Token mint. - /// 1. `[]` Instructions sysvar if `CiphertextCiphertextEquality` is - /// included in the same transaction or context state account if - /// `CiphertextCiphertextEquality` is pre-verified into a context state - /// account. - /// 2. `[]` The multisig authority account owner. - /// 3. ..`[signer]` Required M signer accounts for the SPL Token Multisig - /// - /// Data expected by this instruction: - /// `RotateSupplyElGamalPubkeyData` - RotateSupplyElGamalPubkey, - /// Updates the decryptable supply of the mint - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The SPL Token mint. - /// 1. `[signer]` Confidential mint authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The SPL Token mint. - /// 1. `[]` The multisig authority account owner. - /// 2. ..`[signer]` Required M signer accounts for the SPL Token Multisig - /// - /// Data expected by this instruction: - /// `UpdateDecryptableSupplyData` - UpdateDecryptableSupply, - /// Mints tokens to confidential balance - /// - /// Fails if the destination account is frozen. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The SPL Token mint. `[writable]` if the mint has a non-zero - /// supply elgamal-pubkey - /// 2. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 3. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyCiphertextCommitmentEquality` proof - /// 4. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyBatchedGroupedCiphertext3HandlesValidity` proof - /// 5. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyBatchedRangeProofU128` - /// 6. `[signer]` The single account owner. - /// - /// * Multisignature authority - /// 0. `[writable]` The SPL Token mint. - /// 1. `[]` The SPL Token mint. `[writable]` if the mint has a non-zero - /// supply elgamal-pubkey - /// 2. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 3. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyCiphertextCommitmentEquality` proof - /// 4. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyBatchedGroupedCiphertext3HandlesValidity` proof - /// 5. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyBatchedRangeProofU128` - /// 6. `[]` The multisig account owner. - /// 7. ..`[signer]` Required M signer accounts for the SPL Token Multisig - /// - /// Data expected by this instruction: - /// `MintInstructionData` - Mint, - /// Burn tokens from confidential balance - /// - /// Fails if the destination account is frozen. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The SPL Token mint. `[writable]` if the mint has a non-zero - /// supply elgamal-pubkey - /// 2. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 3. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyCiphertextCommitmentEquality` proof - /// 4. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyBatchedGroupedCiphertext3HandlesValidity` proof - /// 5. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyBatchedRangeProofU128` - /// 6. `[signer]` The single account owner. - /// - /// * Multisignature authority - /// 0. `[writable]` The SPL Token mint. - /// 1. `[]` The SPL Token mint. `[writable]` if the mint has a non-zero - /// supply elgamal-pubkey - /// 2. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 3. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyCiphertextCommitmentEquality` proof - /// 4. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyBatchedGroupedCiphertext3HandlesValidity` proof - /// 5. `[]` (Optional) The context state account containing the - /// pre-verified `VerifyBatchedRangeProofU128` - /// 6. `[]` The multisig account owner. - /// 7. ..`[signer]` Required M signer accounts for the SPL Token Multisig - /// - /// Data expected by this instruction: - /// `BurnInstructionData` - Burn, -} - -/// Data expected by `ConfidentialMintBurnInstruction::InitializeMint` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeMintData { - /// The ElGamal pubkey used to encrypt the confidential supply - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalpubkey_fromstr"))] - pub supply_elgamal_pubkey: PodElGamalPubkey, - /// The initial 0 supply encrypted with the supply aes key - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub decryptable_supply: PodAeCiphertext, -} - -/// Data expected by `ConfidentialMintBurnInstruction::RotateSupplyElGamal` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct RotateSupplyElGamalPubkeyData { - /// The new ElGamal pubkey for supply encryption - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalpubkey_fromstr"))] - pub new_supply_elgamal_pubkey: PodElGamalPubkey, - /// The location of the - /// `ProofInstruction::VerifyCiphertextCiphertextEquality` instruction - /// relative to the `RotateSupplyElGamal` instruction in the transaction - pub proof_instruction_offset: i8, -} - -/// Data expected by `ConfidentialMintBurnInstruction::UpdateDecryptableSupply` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct UpdateDecryptableSupplyData { - /// The new decryptable supply - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub new_decryptable_supply: PodAeCiphertext, -} - -/// Data expected by `ConfidentialMintBurnInstruction::ConfidentialMint` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct MintInstructionData { - /// The new decryptable supply if the mint succeeds - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub new_decryptable_supply: PodAeCiphertext, - /// The transfer amount encrypted under the auditor ElGamal public key - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalciphertext_fromstr"))] - pub mint_amount_auditor_ciphertext_lo: PodElGamalCiphertext, - /// The transfer amount encrypted under the auditor ElGamal public key - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalciphertext_fromstr"))] - pub mint_amount_auditor_ciphertext_hi: PodElGamalCiphertext, - /// Relative location of the - /// `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction - /// to the `ConfidentialMint` instruction in the transaction. 0 if the - /// proof is in a pre-verified context account - pub equality_proof_instruction_offset: i8, - /// Relative location of the - /// `ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity` - /// instruction to the `ConfidentialMint` instruction in the - /// transaction. 0 if the proof is in a pre-verified context account - pub ciphertext_validity_proof_instruction_offset: i8, - /// Relative location of the `ProofInstruction::VerifyBatchedRangeProofU128` - /// instruction to the `ConfidentialMint` instruction in the - /// transaction. 0 if the proof is in a pre-verified context account - pub range_proof_instruction_offset: i8, -} - -/// Data expected by `ConfidentialMintBurnInstruction::ConfidentialBurn` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct BurnInstructionData { - /// The new decryptable balance of the burner if the burn succeeds - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub new_decryptable_available_balance: DecryptableBalance, - /// The transfer amount encrypted under the auditor ElGamal public key - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalciphertext_fromstr"))] - pub burn_amount_auditor_ciphertext_lo: PodElGamalCiphertext, - /// The transfer amount encrypted under the auditor ElGamal public key - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalciphertext_fromstr"))] - pub burn_amount_auditor_ciphertext_hi: PodElGamalCiphertext, - /// Relative location of the - /// `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction - /// to the `ConfidentialMint` instruction in the transaction. 0 if the - /// proof is in a pre-verified context account - pub equality_proof_instruction_offset: i8, - /// Relative location of the - /// `ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity` - /// instruction to the `ConfidentialMint` instruction in the - /// transaction. 0 if the proof is in a pre-verified context account - pub ciphertext_validity_proof_instruction_offset: i8, - /// Relative location of the `ProofInstruction::VerifyBatchedRangeProofU128` - /// instruction to the `ConfidentialMint` instruction in the - /// transaction. 0 if the proof is in a pre-verified context account - pub range_proof_instruction_offset: i8, -} - -/// Create a `InitializeMint` instruction -pub fn initialize_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - supply_elgamal_pubkey: &PodElGamalPubkey, - decryptable_supply: &DecryptableBalance, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialMintBurnExtension, - ConfidentialMintBurnInstruction::InitializeMint, - &InitializeMintData { - supply_elgamal_pubkey: *supply_elgamal_pubkey, - decryptable_supply: *decryptable_supply, - }, - )) -} - -/// Create a `RotateSupplyElGamal` instruction -#[allow(clippy::too_many_arguments)] -#[cfg(not(target_os = "solana"))] -pub fn rotate_supply_elgamal_pubkey( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - new_supply_elgamal_pubkey: &PodElGamalPubkey, - ciphertext_equality_proof: ProofLocation, -) -> Result, ProgramError> { - check_program_account(token_program_id)?; - let mut accounts = vec![AccountMeta::new(*mint, false)]; - - let mut expected_instruction_offset = 1; - let mut proof_instructions = vec![]; - - let proof_instruction_offset = process_proof_location( - &mut accounts, - &mut expected_instruction_offset, - &mut proof_instructions, - ciphertext_equality_proof, - true, - ProofInstruction::VerifyCiphertextCiphertextEquality, - )?; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - let mut instructions = vec![encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialMintBurnExtension, - ConfidentialMintBurnInstruction::RotateSupplyElGamalPubkey, - &RotateSupplyElGamalPubkeyData { - new_supply_elgamal_pubkey: *new_supply_elgamal_pubkey, - proof_instruction_offset, - }, - )]; - - instructions.extend(proof_instructions); - - Ok(instructions) -} - -/// Create a `UpdateMint` instruction -#[cfg(not(target_os = "solana"))] -pub fn update_decryptable_supply( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - new_decryptable_supply: &DecryptableBalance, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), - ]; - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialMintBurnExtension, - ConfidentialMintBurnInstruction::UpdateDecryptableSupply, - &UpdateDecryptableSupplyData { - new_decryptable_supply: *new_decryptable_supply, - }, - )) -} - -/// Context state accounts used in confidential mint -#[derive(Clone, Copy)] -pub struct MintSplitContextStateAccounts<'a> { - /// Location of equality proof - pub equality_proof: &'a Pubkey, - /// Location of ciphertext validity proof - pub ciphertext_validity_proof: &'a Pubkey, - /// Location of range proof - pub range_proof: &'a Pubkey, - /// Authority able to close proof accounts - pub authority: &'a Pubkey, -} - -/// Create a `ConfidentialMint` instruction -#[allow(clippy::too_many_arguments)] -#[cfg(not(target_os = "solana"))] -pub fn confidential_mint_with_split_proofs( - token_program_id: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - supply_elgamal_pubkey: Option, - mint_amount_auditor_ciphertext_lo: &PodElGamalCiphertext, - mint_amount_auditor_ciphertext_hi: &PodElGamalCiphertext, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - equality_proof_location: ProofLocation, - ciphertext_validity_proof_location: ProofLocation< - BatchedGroupedCiphertext3HandlesValidityProofData, - >, - range_proof_location: ProofLocation, - new_decryptable_supply: &DecryptableBalance, -) -> Result, ProgramError> { - check_program_account(token_program_id)?; - let mut accounts = vec![AccountMeta::new(*token_account, false)]; - // we only need write lock to adjust confidential suppy on - // mint if a value for supply_elgamal_pubkey has been set - if supply_elgamal_pubkey.is_some() { - accounts.push(AccountMeta::new(*mint, false)); - } else { - accounts.push(AccountMeta::new_readonly(*mint, false)); - } - - let mut expected_instruction_offset = 1; - let mut proof_instructions = vec![]; - - let equality_proof_instruction_offset = process_proof_location( - &mut accounts, - &mut expected_instruction_offset, - &mut proof_instructions, - equality_proof_location, - true, - ProofInstruction::VerifyCiphertextCommitmentEquality, - )?; - - let ciphertext_validity_proof_instruction_offset = process_proof_location( - &mut accounts, - &mut expected_instruction_offset, - &mut proof_instructions, - ciphertext_validity_proof_location, - false, - ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity, - )?; - - let range_proof_instruction_offset = process_proof_location( - &mut accounts, - &mut expected_instruction_offset, - &mut proof_instructions, - range_proof_location, - false, - ProofInstruction::VerifyBatchedRangeProofU128, - )?; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - let mut instructions = vec![encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialMintBurnExtension, - ConfidentialMintBurnInstruction::Mint, - &MintInstructionData { - new_decryptable_supply: *new_decryptable_supply, - mint_amount_auditor_ciphertext_lo: *mint_amount_auditor_ciphertext_lo, - mint_amount_auditor_ciphertext_hi: *mint_amount_auditor_ciphertext_hi, - equality_proof_instruction_offset, - ciphertext_validity_proof_instruction_offset, - range_proof_instruction_offset, - }, - )]; - - instructions.extend(proof_instructions); - - Ok(instructions) -} - -/// Create a inner `ConfidentialBurn` instruction -#[allow(clippy::too_many_arguments)] -#[cfg(not(target_os = "solana"))] -pub fn confidential_burn_with_split_proofs( - token_program_id: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - supply_elgamal_pubkey: Option, - new_decryptable_available_balance: &DecryptableBalance, - burn_amount_auditor_ciphertext_lo: &PodElGamalCiphertext, - burn_amount_auditor_ciphertext_hi: &PodElGamalCiphertext, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - equality_proof_location: ProofLocation, - ciphertext_validity_proof_location: ProofLocation< - BatchedGroupedCiphertext3HandlesValidityProofData, - >, - range_proof_location: ProofLocation, -) -> Result, ProgramError> { - check_program_account(token_program_id)?; - let mut accounts = vec![AccountMeta::new(*token_account, false)]; - if supply_elgamal_pubkey.is_some() { - accounts.push(AccountMeta::new(*mint, false)); - } else { - accounts.push(AccountMeta::new_readonly(*mint, false)); - } - - let mut expected_instruction_offset = 1; - let mut proof_instructions = vec![]; - - let equality_proof_instruction_offset = process_proof_location( - &mut accounts, - &mut expected_instruction_offset, - &mut proof_instructions, - equality_proof_location, - true, - ProofInstruction::VerifyCiphertextCommitmentEquality, - )?; - - let ciphertext_validity_proof_instruction_offset = process_proof_location( - &mut accounts, - &mut expected_instruction_offset, - &mut proof_instructions, - ciphertext_validity_proof_location, - false, - ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity, - )?; - - let range_proof_instruction_offset = process_proof_location( - &mut accounts, - &mut expected_instruction_offset, - &mut proof_instructions, - range_proof_location, - false, - ProofInstruction::VerifyBatchedRangeProofU128, - )?; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - let mut instructions = vec![encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialMintBurnExtension, - ConfidentialMintBurnInstruction::Burn, - &BurnInstructionData { - new_decryptable_available_balance: *new_decryptable_available_balance, - burn_amount_auditor_ciphertext_lo: *burn_amount_auditor_ciphertext_lo, - burn_amount_auditor_ciphertext_hi: *burn_amount_auditor_ciphertext_hi, - equality_proof_instruction_offset, - ciphertext_validity_proof_instruction_offset, - range_proof_instruction_offset, - }, - )]; - - instructions.extend(proof_instructions); - - Ok(instructions) -} diff --git a/token/program-2022/src/extension/confidential_mint_burn/mod.rs b/token/program-2022/src/extension/confidential_mint_burn/mod.rs deleted file mode 100644 index 3ced158923d..00000000000 --- a/token/program-2022/src/extension/confidential_mint_burn/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, - solana_zk_sdk::encryption::pod::{ - auth_encryption::PodAeCiphertext, - elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - }, -}; - -/// Maximum bit length of any mint or burn amount -/// -/// Any mint or burn amount must be less than `2^48` -pub const MAXIMUM_DEPOSIT_TRANSFER_AMOUNT: u64 = (u16::MAX as u64) + (1 << 16) * (u32::MAX as u64); - -/// Bit length of the low bits of pending balance plaintext -pub const PENDING_BALANCE_LO_BIT_LENGTH: u32 = 16; - -/// Confidential Mint-Burn Extension instructions -pub mod instruction; - -/// Confidential Mint-Burn Extension processor -pub mod processor; - -/// Confidential Mint-Burn proof verification -pub mod verify_proof; - -/// Confidential Mint Burn Extension supply information needed for instructions -#[cfg(not(target_os = "solana"))] -pub mod account_info; - -/// Confidential mint-burn mint configuration -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct ConfidentialMintBurn { - /// The confidential supply of the mint (encrypted by `encryption_pubkey`) - pub confidential_supply: PodElGamalCiphertext, - /// The decryptable confidential supply of the mint - pub decryptable_supply: PodAeCiphertext, - /// The ElGamal pubkey used to encrypt the confidential supply - pub supply_elgamal_pubkey: PodElGamalPubkey, -} - -impl Extension for ConfidentialMintBurn { - const TYPE: ExtensionType = ExtensionType::ConfidentialMintBurn; -} diff --git a/token/program-2022/src/extension/confidential_mint_burn/processor.rs b/token/program-2022/src/extension/confidential_mint_burn/processor.rs deleted file mode 100644 index 1f63a7c4720..00000000000 --- a/token/program-2022/src/extension/confidential_mint_burn/processor.rs +++ /dev/null @@ -1,445 +0,0 @@ -#[cfg(feature = "zk-ops")] -use spl_token_confidential_transfer_ciphertext_arithmetic as ciphertext_arithmetic; -use { - crate::{ - check_auditor_ciphertext, check_program_account, - error::TokenError, - extension::{ - confidential_mint_burn::{ - instruction::{ - BurnInstructionData, ConfidentialMintBurnInstruction, InitializeMintData, - MintInstructionData, RotateSupplyElGamalPubkeyData, - UpdateDecryptableSupplyData, - }, - verify_proof::{verify_burn_proof, verify_mint_proof}, - ConfidentialMintBurn, - }, - confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, - pausable::PausableConfig, - BaseStateWithExtensions, BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::{PodAccount, PodMint}, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program_error::ProgramError, - pubkey::Pubkey, - }, - solana_zk_sdk::{ - encryption::pod::{auth_encryption::PodAeCiphertext, elgamal::PodElGamalPubkey}, - zk_elgamal_proof_program::proof_data::{ - CiphertextCiphertextEqualityProofContext, CiphertextCiphertextEqualityProofData, - }, - }, - spl_token_confidential_transfer_proof_extraction::instruction::verify_and_extract_context, -}; - -/// Processes an [`InitializeMint`] instruction. -fn process_initialize_mint(accounts: &[AccountInfo], data: &InitializeMintData) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - - check_program_account(mint_info.owner)?; - - let mint_data = &mut mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(mint_data)?; - let mint_burn_extension = mint.init_extension::(true)?; - - mint_burn_extension.supply_elgamal_pubkey = data.supply_elgamal_pubkey; - mint_burn_extension.decryptable_supply = data.decryptable_supply; - - Ok(()) -} - -/// Processes an [`RotateSupplyElGamal`] instruction. -#[cfg(feature = "zk-ops")] -fn process_rotate_supply_elgamal_pubkey( - program_id: &Pubkey, - accounts: &[AccountInfo], - data: &RotateSupplyElGamalPubkeyData, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - - check_program_account(mint_info.owner)?; - let mint_data = &mut mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; - let mint_authority = mint.base.mint_authority; - let mint_burn_extension = mint.get_extension_mut::()?; - - let proof_context = verify_and_extract_context::< - CiphertextCiphertextEqualityProofData, - CiphertextCiphertextEqualityProofContext, - >( - account_info_iter, - data.proof_instruction_offset as i64, - None, - )?; - - let supply_elgamal_pubkey: Option = - mint_burn_extension.supply_elgamal_pubkey.into(); - let Some(supply_elgamal_pubkey) = supply_elgamal_pubkey else { - return Err(TokenError::InvalidState.into()); - }; - - if !supply_elgamal_pubkey.eq(&proof_context.first_pubkey) { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - if mint_burn_extension.confidential_supply != proof_context.first_ciphertext { - return Err(ProgramError::InvalidInstructionData); - } - - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - let authority = mint_authority.ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - mint_burn_extension.supply_elgamal_pubkey = proof_context.second_pubkey; - mint_burn_extension.confidential_supply = proof_context.second_ciphertext; - - Ok(()) -} - -/// Processes an [`UpdateAuthority`] instruction. -fn process_update_decryptable_supply( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_decryptable_supply: PodAeCiphertext, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - - check_program_account(mint_info.owner)?; - let mint_data = &mut mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; - let mint_authority = mint.base.mint_authority; - let mint_burn_extension = mint.get_extension_mut::()?; - - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - let authority = mint_authority.ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - mint_burn_extension.decryptable_supply = new_decryptable_supply; - - Ok(()) -} - -/// Processes a [`ConfidentialMint`] instruction. -#[cfg(feature = "zk-ops")] -fn process_confidential_mint( - program_id: &Pubkey, - accounts: &[AccountInfo], - data: &MintInstructionData, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - - check_program_account(mint_info.owner)?; - let mint_data = &mut mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; - let mint_authority = mint.base.mint_authority; - - let auditor_elgamal_pubkey = mint - .get_extension::()? - .auditor_elgamal_pubkey; - if let Ok(extension) = mint.get_extension::() { - if extension.paused.into() { - return Err(TokenError::MintPaused.into()); - } - } - let mint_burn_extension = mint.get_extension_mut::()?; - - let proof_context = verify_mint_proof( - account_info_iter, - data.equality_proof_instruction_offset, - data.ciphertext_validity_proof_instruction_offset, - data.range_proof_instruction_offset, - )?; - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - let authority = mint_authority.ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - if token_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if token_account.base.mint != *mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - assert!(!token_account.base.is_native()); - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - confidential_transfer_account.valid_as_destination()?; - - if proof_context.mint_pubkeys.destination != confidential_transfer_account.elgamal_pubkey { - return Err(ProgramError::InvalidInstructionData); - } - - if let Some(auditor_pubkey) = Option::::from(auditor_elgamal_pubkey) { - if auditor_pubkey != proof_context.mint_pubkeys.auditor { - return Err(ProgramError::InvalidInstructionData); - } - } - - let proof_context_auditor_ciphertext_lo = proof_context - .mint_amount_ciphertext_lo - .try_extract_ciphertext(2) - .map_err(TokenError::from)?; - let proof_context_auditor_ciphertext_hi = proof_context - .mint_amount_ciphertext_hi - .try_extract_ciphertext(2) - .map_err(TokenError::from)?; - - check_auditor_ciphertext( - &data.mint_amount_auditor_ciphertext_lo, - &data.mint_amount_auditor_ciphertext_hi, - &proof_context_auditor_ciphertext_lo, - &proof_context_auditor_ciphertext_hi, - )?; - - confidential_transfer_account.pending_balance_lo = ciphertext_arithmetic::add( - &confidential_transfer_account.pending_balance_lo, - &proof_context - .mint_amount_ciphertext_lo - .try_extract_ciphertext(0) - .map_err(TokenError::from)?, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - confidential_transfer_account.pending_balance_hi = ciphertext_arithmetic::add( - &confidential_transfer_account.pending_balance_hi, - &proof_context - .mint_amount_ciphertext_hi - .try_extract_ciphertext(0) - .map_err(TokenError::from)?, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - confidential_transfer_account.increment_pending_balance_credit_counter()?; - - // update supply - if mint_burn_extension.supply_elgamal_pubkey != proof_context.mint_pubkeys.supply { - return Err(ProgramError::InvalidInstructionData); - } - let current_supply = mint_burn_extension.confidential_supply; - mint_burn_extension.confidential_supply = ciphertext_arithmetic::add_with_lo_hi( - ¤t_supply, - &proof_context - .mint_amount_ciphertext_lo - .try_extract_ciphertext(2) - .map_err(|_| ProgramError::InvalidAccountData)?, - &proof_context - .mint_amount_ciphertext_hi - .try_extract_ciphertext(2) - .map_err(|_| ProgramError::InvalidAccountData)?, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - mint_burn_extension.decryptable_supply = data.new_decryptable_supply; - - Ok(()) -} - -/// Processes a [`ConfidentialBurn`] instruction. -#[cfg(feature = "zk-ops")] -fn process_confidential_burn( - program_id: &Pubkey, - accounts: &[AccountInfo], - data: &BurnInstructionData, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - - check_program_account(mint_info.owner)?; - let mint_data = &mut mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; - - let auditor_elgamal_pubkey = mint - .get_extension::()? - .auditor_elgamal_pubkey; - if let Ok(extension) = mint.get_extension::() { - if extension.paused.into() { - return Err(TokenError::MintPaused.into()); - } - } - let mint_burn_extension = mint.get_extension_mut::()?; - - let proof_context = verify_burn_proof( - account_info_iter, - data.equality_proof_instruction_offset, - data.ciphertext_validity_proof_instruction_offset, - data.range_proof_instruction_offset, - )?; - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - if token_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if token_account.base.mint != *mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - confidential_transfer_account.valid_as_source()?; - - // Check that the source encryption public key is consistent with what was - // actually used to generate the zkp. - if proof_context.burn_pubkeys.source != confidential_transfer_account.elgamal_pubkey { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - let proof_context_auditor_ciphertext_lo = proof_context - .burn_amount_ciphertext_lo - .try_extract_ciphertext(2) - .map_err(TokenError::from)?; - let proof_context_auditor_ciphertext_hi = proof_context - .burn_amount_ciphertext_hi - .try_extract_ciphertext(2) - .map_err(TokenError::from)?; - - check_auditor_ciphertext( - &data.burn_amount_auditor_ciphertext_lo, - &data.burn_amount_auditor_ciphertext_hi, - &proof_context_auditor_ciphertext_lo, - &proof_context_auditor_ciphertext_hi, - )?; - - let burn_amount_lo = &proof_context - .burn_amount_ciphertext_lo - .try_extract_ciphertext(0) - .map_err(TokenError::from)?; - let burn_amount_hi = &proof_context - .burn_amount_ciphertext_hi - .try_extract_ciphertext(0) - .map_err(TokenError::from)?; - - let new_source_available_balance = ciphertext_arithmetic::subtract_with_lo_hi( - &confidential_transfer_account.available_balance, - burn_amount_lo, - burn_amount_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - // Check that the computed available balance is consistent with what was - // actually used to generate the zkp on the client side. - if new_source_available_balance != proof_context.remaining_balance_ciphertext { - return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); - } - - confidential_transfer_account.available_balance = new_source_available_balance; - confidential_transfer_account.decryptable_available_balance = - data.new_decryptable_available_balance; - - if let Some(auditor_pubkey) = Option::::from(auditor_elgamal_pubkey) { - if auditor_pubkey != proof_context.burn_pubkeys.auditor { - return Err(ProgramError::InvalidInstructionData); - } - } - - // update supply - if mint_burn_extension.supply_elgamal_pubkey != proof_context.burn_pubkeys.supply { - return Err(ProgramError::InvalidInstructionData); - } - let current_supply = mint_burn_extension.confidential_supply; - mint_burn_extension.confidential_supply = ciphertext_arithmetic::subtract_with_lo_hi( - ¤t_supply, - &proof_context - .burn_amount_ciphertext_lo - .try_extract_ciphertext(2) - .map_err(|_| ProgramError::InvalidAccountData)?, - &proof_context - .burn_amount_ciphertext_hi - .try_extract_ciphertext(2) - .map_err(|_| ProgramError::InvalidAccountData)?, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - Ok(()) -} - -#[allow(dead_code)] -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - - match decode_instruction_type(input)? { - ConfidentialMintBurnInstruction::InitializeMint => { - msg!("ConfidentialMintBurnInstruction::InitializeMint"); - let data = decode_instruction_data::(input)?; - process_initialize_mint(accounts, data) - } - ConfidentialMintBurnInstruction::RotateSupplyElGamalPubkey => { - msg!("ConfidentialMintBurnInstruction::RotateSupplyElGamal"); - let data = decode_instruction_data::(input)?; - process_rotate_supply_elgamal_pubkey(program_id, accounts, data) - } - ConfidentialMintBurnInstruction::UpdateDecryptableSupply => { - msg!("ConfidentialMintBurnInstruction::UpdateDecryptableSupply"); - let data = decode_instruction_data::(input)?; - process_update_decryptable_supply(program_id, accounts, data.new_decryptable_supply) - } - ConfidentialMintBurnInstruction::Mint => { - msg!("ConfidentialMintBurnInstruction::ConfidentialMint"); - let data = decode_instruction_data::(input)?; - process_confidential_mint(program_id, accounts, data) - } - ConfidentialMintBurnInstruction::Burn => { - msg!("ConfidentialMintBurnInstruction::ConfidentialBurn"); - let data = decode_instruction_data::(input)?; - process_confidential_burn(program_id, accounts, data) - } - } -} diff --git a/token/program-2022/src/extension/confidential_mint_burn/verify_proof.rs b/token/program-2022/src/extension/confidential_mint_burn/verify_proof.rs deleted file mode 100644 index 47192117077..00000000000 --- a/token/program-2022/src/extension/confidential_mint_burn/verify_proof.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::error::TokenError; -#[cfg(feature = "zk-ops")] -use { - solana_program::{ - account_info::{next_account_info, AccountInfo}, - program_error::ProgramError, - }, - solana_zk_sdk::zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofContext, - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofContext, - BatchedRangeProofU128Data, CiphertextCommitmentEqualityProofContext, - CiphertextCommitmentEqualityProofData, - }, - spl_token_confidential_transfer_proof_extraction::{ - burn::BurnProofContext, instruction::verify_and_extract_context, mint::MintProofContext, - }, - std::slice::Iter, -}; - -/// Verify zero-knowledge proofs needed for a [`ConfidentialMint`] instruction -/// and return the corresponding proof context information. -#[cfg(feature = "zk-ops")] -pub fn verify_mint_proof( - account_info_iter: &mut Iter<'_, AccountInfo<'_>>, - equality_proof_instruction_offset: i8, - ciphertext_validity_proof_instruction_offset: i8, - range_proof_instruction_offset: i8, -) -> Result { - let sysvar_account_info = if equality_proof_instruction_offset != 0 { - Some(next_account_info(account_info_iter)?) - } else { - None - }; - - let equality_proof_context = verify_and_extract_context::< - CiphertextCommitmentEqualityProofData, - CiphertextCommitmentEqualityProofContext, - >( - account_info_iter, - equality_proof_instruction_offset as i64, - sysvar_account_info, - )?; - - let ciphertext_validity_proof_context = verify_and_extract_context::< - BatchedGroupedCiphertext3HandlesValidityProofData, - BatchedGroupedCiphertext3HandlesValidityProofContext, - >( - account_info_iter, - ciphertext_validity_proof_instruction_offset as i64, - sysvar_account_info, - )?; - - let range_proof_context = - verify_and_extract_context::( - account_info_iter, - range_proof_instruction_offset as i64, - sysvar_account_info, - )?; - - Ok(MintProofContext::verify_and_extract( - &equality_proof_context, - &ciphertext_validity_proof_context, - &range_proof_context, - ) - .map_err(|e| -> TokenError { e.into() })?) -} - -/// Verify zero-knowledge proofs needed for a [`ConfidentialBurn`] instruction -/// and return the corresponding proof context information. -#[cfg(feature = "zk-ops")] -pub fn verify_burn_proof( - account_info_iter: &mut Iter<'_, AccountInfo<'_>>, - equality_proof_instruction_offset: i8, - ciphertext_validity_proof_instruction_offset: i8, - range_proof_instruction_offset: i8, -) -> Result { - let sysvar_account_info = if equality_proof_instruction_offset != 0 { - Some(next_account_info(account_info_iter)?) - } else { - None - }; - - let equality_proof_context = verify_and_extract_context::< - CiphertextCommitmentEqualityProofData, - CiphertextCommitmentEqualityProofContext, - >( - account_info_iter, - equality_proof_instruction_offset as i64, - sysvar_account_info, - )?; - - let ciphertext_validity_proof_context = verify_and_extract_context::< - BatchedGroupedCiphertext3HandlesValidityProofData, - BatchedGroupedCiphertext3HandlesValidityProofContext, - >( - account_info_iter, - ciphertext_validity_proof_instruction_offset as i64, - sysvar_account_info, - )?; - - let range_proof_context = - verify_and_extract_context::( - account_info_iter, - range_proof_instruction_offset as i64, - sysvar_account_info, - )?; - - Ok(BurnProofContext::verify_and_extract( - &equality_proof_context, - &ciphertext_validity_proof_context, - &range_proof_context, - ) - .map_err(|e| -> TokenError { e.into() })?) -} diff --git a/token/program-2022/src/extension/confidential_transfer/account_info.rs b/token/program-2022/src/extension/confidential_transfer/account_info.rs deleted file mode 100644 index 672a8028904..00000000000 --- a/token/program-2022/src/extension/confidential_transfer/account_info.rs +++ /dev/null @@ -1,332 +0,0 @@ -use { - crate::{ - error::TokenError, - extension::confidential_transfer::{ - ConfidentialTransferAccount, DecryptableBalance, EncryptedBalance, - PENDING_BALANCE_LO_BIT_LENGTH, - }, - }, - bytemuck::{Pod, Zeroable}, - solana_zk_sdk::{ - encryption::{ - auth_encryption::{AeCiphertext, AeKey}, - elgamal::{ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey}, - }, - zk_elgamal_proof_program::proof_data::ZeroCiphertextProofData, - }, - spl_pod::primitives::PodU64, - spl_token_confidential_transfer_proof_generation::{ - transfer::{transfer_split_proof_data, TransferProofData}, - transfer_with_fee::{transfer_with_fee_split_proof_data, TransferWithFeeProofData}, - withdraw::{withdraw_proof_data, WithdrawProofData}, - }, -}; - -/// Confidential transfer extension information needed to construct an -/// `EmptyAccount` instruction. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct EmptyAccountAccountInfo { - /// The available balance - pub(crate) available_balance: EncryptedBalance, -} -impl EmptyAccountAccountInfo { - /// Create the `EmptyAccount` instruction account information from - /// `ConfidentialTransferAccount`. - pub fn new(account: &ConfidentialTransferAccount) -> Self { - Self { - available_balance: account.available_balance, - } - } - - /// Create an empty account proof data. - pub fn generate_proof_data( - &self, - elgamal_keypair: &ElGamalKeypair, - ) -> Result { - let available_balance = self - .available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - - ZeroCiphertextProofData::new(elgamal_keypair, &available_balance) - .map_err(|_| TokenError::ProofGeneration) - } -} - -/// Confidential Transfer extension information needed to construct an -/// `ApplyPendingBalance` instruction. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct ApplyPendingBalanceAccountInfo { - /// The total number of `Deposit` and `Transfer` instructions that have - /// credited `pending_balance` - pub(crate) pending_balance_credit_counter: PodU64, - /// The low 16 bits of the pending balance (encrypted by `elgamal_pubkey`) - pub(crate) pending_balance_lo: EncryptedBalance, - /// The high 48 bits of the pending balance (encrypted by `elgamal_pubkey`) - pub(crate) pending_balance_hi: EncryptedBalance, - /// The decryptable available balance - pub(crate) decryptable_available_balance: DecryptableBalance, -} -impl ApplyPendingBalanceAccountInfo { - /// Create the `ApplyPendingBalance` instruction account information from - /// `ConfidentialTransferAccount`. - pub fn new(account: &ConfidentialTransferAccount) -> Self { - Self { - pending_balance_credit_counter: account.pending_balance_credit_counter, - pending_balance_lo: account.pending_balance_lo, - pending_balance_hi: account.pending_balance_hi, - decryptable_available_balance: account.decryptable_available_balance, - } - } - - /// Return the pending balance credit counter of the account. - pub fn pending_balance_credit_counter(&self) -> u64 { - self.pending_balance_credit_counter.into() - } - - fn decrypted_pending_balance_lo( - &self, - elgamal_secret_key: &ElGamalSecretKey, - ) -> Result { - let pending_balance_lo = self - .pending_balance_lo - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - elgamal_secret_key - .decrypt_u32(&pending_balance_lo) - .ok_or(TokenError::AccountDecryption) - } - - fn decrypted_pending_balance_hi( - &self, - elgamal_secret_key: &ElGamalSecretKey, - ) -> Result { - let pending_balance_hi = self - .pending_balance_hi - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - elgamal_secret_key - .decrypt_u32(&pending_balance_hi) - .ok_or(TokenError::AccountDecryption) - } - - fn decrypted_available_balance(&self, aes_key: &AeKey) -> Result { - let decryptable_available_balance = self - .decryptable_available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - aes_key - .decrypt(&decryptable_available_balance) - .ok_or(TokenError::AccountDecryption) - } - - /// Update the decryptable available balance. - pub fn new_decryptable_available_balance( - &self, - elgamal_secret_key: &ElGamalSecretKey, - aes_key: &AeKey, - ) -> Result { - let decrypted_pending_balance_lo = self.decrypted_pending_balance_lo(elgamal_secret_key)?; - let decrypted_pending_balance_hi = self.decrypted_pending_balance_hi(elgamal_secret_key)?; - let pending_balance = - combine_balances(decrypted_pending_balance_lo, decrypted_pending_balance_hi) - .ok_or(TokenError::AccountDecryption)?; - let current_available_balance = self.decrypted_available_balance(aes_key)?; - let new_decrypted_available_balance = current_available_balance - .checked_add(pending_balance) - .unwrap(); // total balance cannot exceed `u64` - - Ok(aes_key.encrypt(new_decrypted_available_balance)) - } -} - -/// Confidential Transfer extension information needed to construct a `Withdraw` -/// instruction. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct WithdrawAccountInfo { - /// The available balance (encrypted by `encryption_pubkey`) - pub available_balance: EncryptedBalance, - /// The decryptable available balance - pub decryptable_available_balance: DecryptableBalance, -} -impl WithdrawAccountInfo { - /// Create the `ApplyPendingBalance` instruction account information from - /// `ConfidentialTransferAccount`. - pub fn new(account: &ConfidentialTransferAccount) -> Self { - Self { - available_balance: account.available_balance, - decryptable_available_balance: account.decryptable_available_balance, - } - } - - fn decrypted_available_balance(&self, aes_key: &AeKey) -> Result { - let decryptable_available_balance = self - .decryptable_available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - aes_key - .decrypt(&decryptable_available_balance) - .ok_or(TokenError::AccountDecryption) - } - - /// Create a withdraw proof data. - pub fn generate_proof_data( - &self, - withdraw_amount: u64, - elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - ) -> Result { - let current_available_balance = self - .available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - let current_decrypted_available_balance = self.decrypted_available_balance(aes_key)?; - - withdraw_proof_data( - ¤t_available_balance, - current_decrypted_available_balance, - withdraw_amount, - elgamal_keypair, - ) - .map_err(|e| -> TokenError { e.into() }) - } - - /// Update the decryptable available balance. - pub fn new_decryptable_available_balance( - &self, - withdraw_amount: u64, - aes_key: &AeKey, - ) -> Result { - let current_decrypted_available_balance = self.decrypted_available_balance(aes_key)?; - let new_decrypted_available_balance = current_decrypted_available_balance - .checked_sub(withdraw_amount) - .ok_or(TokenError::InsufficientFunds)?; - - Ok(aes_key.encrypt(new_decrypted_available_balance)) - } -} - -/// Confidential Transfer extension information needed to construct a `Transfer` -/// instruction. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct TransferAccountInfo { - /// The available balance (encrypted by `encryption_pubkey`) - pub available_balance: EncryptedBalance, - /// The decryptable available balance - pub decryptable_available_balance: DecryptableBalance, -} -impl TransferAccountInfo { - /// Create the `Transfer` instruction account information from - /// `ConfidentialTransferAccount`. - pub fn new(account: &ConfidentialTransferAccount) -> Self { - Self { - available_balance: account.available_balance, - decryptable_available_balance: account.decryptable_available_balance, - } - } - - fn decrypted_available_balance(&self, aes_key: &AeKey) -> Result { - let decryptable_available_balance = self - .decryptable_available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - aes_key - .decrypt(&decryptable_available_balance) - .ok_or(TokenError::AccountDecryption) - } - - /// Create a transfer proof data that is split into equality, ciphertext - /// validity, and range proofs. - pub fn generate_split_transfer_proof_data( - &self, - transfer_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - ) -> Result { - let current_available_balance = self - .available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - let current_decryptable_available_balance = self - .decryptable_available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - - transfer_split_proof_data( - ¤t_available_balance, - ¤t_decryptable_available_balance, - transfer_amount, - source_elgamal_keypair, - aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - ) - .map_err(|e| -> TokenError { e.into() }) - } - - /// Create a transfer proof data that is split into equality, ciphertext - /// validity (transfer amount), percentage-with-cap, ciphertext validity - /// (fee), and range proofs. - #[allow(clippy::too_many_arguments)] - pub fn generate_split_transfer_with_fee_proof_data( - &self, - transfer_amount: u64, - source_elgamal_keypair: &ElGamalKeypair, - aes_key: &AeKey, - destination_elgamal_pubkey: &ElGamalPubkey, - auditor_elgamal_pubkey: Option<&ElGamalPubkey>, - withdraw_withheld_authority_elgamal_pubkey: &ElGamalPubkey, - fee_rate_basis_points: u16, - maximum_fee: u64, - ) -> Result { - let current_available_balance = self - .available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - let current_decryptable_available_balance = self - .decryptable_available_balance - .try_into() - .map_err(|_| TokenError::MalformedCiphertext)?; - - transfer_with_fee_split_proof_data( - ¤t_available_balance, - ¤t_decryptable_available_balance, - transfer_amount, - source_elgamal_keypair, - aes_key, - destination_elgamal_pubkey, - auditor_elgamal_pubkey, - withdraw_withheld_authority_elgamal_pubkey, - fee_rate_basis_points, - maximum_fee, - ) - .map_err(|e| -> TokenError { e.into() }) - } - - /// Update the decryptable available balance. - pub fn new_decryptable_available_balance( - &self, - transfer_amount: u64, - aes_key: &AeKey, - ) -> Result { - let current_decrypted_available_balance = self.decrypted_available_balance(aes_key)?; - let new_decrypted_available_balance = current_decrypted_available_balance - .checked_sub(transfer_amount) - .ok_or(TokenError::InsufficientFunds)?; - - Ok(aes_key.encrypt(new_decrypted_available_balance)) - } -} - -/// Combines pending balances low and high bits into singular pending balance -pub fn combine_balances(balance_lo: u64, balance_hi: u64) -> Option { - balance_hi - .checked_shl(PENDING_BALANCE_LO_BIT_LENGTH)? - .checked_add(balance_lo) -} diff --git a/token/program-2022/src/extension/confidential_transfer/instruction.rs b/token/program-2022/src/extension/confidential_transfer/instruction.rs deleted file mode 100644 index 5fa822529b5..00000000000 --- a/token/program-2022/src/extension/confidential_transfer/instruction.rs +++ /dev/null @@ -1,1801 +0,0 @@ -pub use solana_zk_sdk::zk_elgamal_proof_program::{ - instruction::ProofInstruction, proof_data::*, state::ProofContextState, -}; -#[cfg(feature = "serde-traits")] -use { - crate::serialization::{aeciphertext_fromstr, elgamalciphertext_fromstr}, - serde::{Deserialize, Serialize}, -}; -use { - crate::{ - check_program_account, - extension::confidential_transfer::*, - instruction::{encode_instruction, TokenInstruction}, - }, - bytemuck::Zeroable, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - system_program, sysvar, - }, - spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation}, -}; - -/// Confidential Transfer extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] -#[repr(u8)] -pub enum ConfidentialTransferInstruction { - /// Initializes confidential transfers for a mint. - /// - /// The `ConfidentialTransferInstruction::InitializeMint` instruction - /// requires no signers and MUST be included within the same Transaction - /// as `TokenInstruction::InitializeMint`. Otherwise another party can - /// initialize the configuration. - /// - /// The instruction fails if the `TokenInstruction::InitializeMint` - /// instruction has already executed for the mint. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The SPL Token mint. - /// - /// Data expected by this instruction: - /// `InitializeMintData` - InitializeMint, - - /// Updates the confidential transfer mint configuration for a mint. - /// - /// Use `TokenInstruction::SetAuthority` to update the confidential transfer - /// mint authority. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The SPL Token mint. - /// 1. `[signer]` Confidential transfer mint authority. - /// - /// Data expected by this instruction: - /// `UpdateMintData` - UpdateMint, - - /// Configures confidential transfers for a token account. - /// - /// The instruction fails if the confidential transfers are already - /// configured, or if the mint was not initialized with confidential - /// transfer support. - /// - /// The instruction fails if the `TokenInstruction::InitializeAccount` - /// instruction has not yet successfully executed for the token account. - /// - /// Upon success, confidential and non-confidential deposits and transfers - /// are enabled. Use the `DisableConfidentialCredits` and - /// `DisableNonConfidentialCredits` instructions to disable. - /// - /// In order for this instruction to be successfully processed, it must be - /// accompanied by the `VerifyPubkeyValidity` instruction of the - /// `zk_elgamal_proof` program in the same transaction or the address of a - /// context state account for the proof must be provided. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writeable]` The SPL Token account. - /// 1. `[]` The corresponding SPL Token mint. - /// 2. `[]` Instructions sysvar if `VerifyPubkeyValidity` is included in - /// the same transaction or context state account if - /// `VerifyPubkeyValidity` is pre-verified into a context state - /// account. - /// 3. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 4. `[signer]` The single source account owner. - /// - /// * Multisignature owner/delegate - /// 0. `[writeable]` The SPL Token account. - /// 1. `[]` The corresponding SPL Token mint. - /// 2. `[]` Instructions sysvar if `VerifyPubkeyValidity` is included in - /// the same transaction or context state account if - /// `VerifyPubkeyValidity` is pre-verified into a context state - /// account. - /// 3. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 4. `[]` The multisig source account owner. - /// 5. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// `ConfigureAccountInstructionData` - ConfigureAccount, - - /// Approves a token account for confidential transfers. - /// - /// Approval is only required when the - /// `ConfidentialTransferMint::approve_new_accounts` field is set in the - /// SPL Token mint. This instruction must be executed after the account - /// owner configures their account for confidential transfers with - /// `ConfidentialTransferInstruction::ConfigureAccount`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The SPL Token account to approve. - /// 1. `[]` The SPL Token mint. - /// 2. `[signer]` Confidential transfer mint authority. - /// - /// Data expected by this instruction: - /// None - ApproveAccount, - - /// Empty the available balance in a confidential token account. - /// - /// A token account that is extended for confidential transfers can only be - /// closed if the pending and available balance ciphertexts are emptied. - /// The pending balance can be emptied - /// via the `ConfidentialTransferInstruction::ApplyPendingBalance` - /// instruction. Use the `ConfidentialTransferInstruction::EmptyAccount` - /// instruction to empty the available balance ciphertext. - /// - /// Note that a newly configured account is always empty, so this - /// instruction is not required prior to account closing if no - /// instructions beyond - /// `ConfidentialTransferInstruction::ConfigureAccount` have affected the - /// token account. - /// - /// In order for this instruction to be successfully processed, it must be - /// accompanied by the `VerifyZeroCiphertext` instruction of the - /// `zk_elgamal_proof` program in the same transaction or the address of a - /// context state account for the proof must be provided. - /// - /// * Single owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` Instructions sysvar if `VerifyZeroCiphertext` is included in - /// the same transaction or context state account if - /// `VerifyZeroCiphertext` is pre-verified into a context state - /// account. - /// 2. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 3. `[signer]` The single account owner. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` Instructions sysvar if `VerifyZeroCiphertext` is included in - /// the same transaction or context state account if - /// `VerifyZeroCiphertext` is pre-verified into a context state - /// account. - /// 2. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 3. `[]` The multisig account owner. - /// 4. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// `EmptyAccountInstructionData` - EmptyAccount, - - /// Deposit SPL Tokens into the pending balance of a confidential token - /// account. - /// - /// The account owner can then invoke the `ApplyPendingBalance` instruction - /// to roll the deposit into their available balance at a time of their - /// choosing. - /// - /// Fails if the source or destination accounts are frozen. - /// Fails if the associated mint is extended as `NonTransferable`. - /// Fails if the associated mint is extended as `ConfidentialMintBurn`. - /// Fails if the associated mint is paused with the `Pausable` extension. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The token mint. - /// 2. `[signer]` The single account owner or delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The token mint. - /// 2. `[]` The multisig account owner or delegate. - /// 3. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// `DepositInstructionData` - Deposit, - - /// Withdraw SPL Tokens from the available balance of a confidential token - /// account. - /// - /// In order for this instruction to be successfully processed, it must be - /// accompanied by the following list of `zk_elgamal_proof` program - /// instructions: - /// - /// - `VerifyCiphertextCommitmentEquality` - /// - `VerifyBatchedRangeProofU64` - /// - /// These instructions can be accompanied in the same transaction or can be - /// pre-verified into a context state account, in which case, only their - /// context state account address need to be provided. - /// - /// Fails if the source or destination accounts are frozen. - /// Fails if the associated mint is extended as `NonTransferable`. - /// Fails if the associated mint is extended as `ConfidentialMintBurn`. - /// Fails if the associated mint is paused with the `Pausable` extension. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The token mint. - /// 2. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 3. `[]` (Optional) Equality proof record account or context state - /// account. - /// 4. `[]` (Optional) Range proof record account or context state - /// account. - /// 5. `[signer]` The single source account owner. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The token mint. - /// 2. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 3. `[]` (Optional) Equality proof record account or context state - /// account. - /// 4. `[]` (Optional) Range proof record account or context state - /// account. - /// 5. `[]` The multisig source account owner. - /// 6. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// `WithdrawInstructionData` - Withdraw, - - /// Transfer tokens confidentially. - /// - /// In order for this instruction to be successfully processed, it must be - /// accompanied by the following list of `zk_elgamal_proof` program - /// instructions: - /// - /// - `VerifyCiphertextCommitmentEquality` - /// - `VerifyBatchedGroupedCiphertext3HandlesValidity` - /// - `VerifyBatchedRangeProofU128` - /// - /// These instructions can be accompanied in the same transaction or can be - /// pre-verified into a context state account, in which case, only their - /// context state account addresses need to be provided. - /// - /// Fails if the associated mint is extended as `NonTransferable`. - /// - /// * Single owner/delegate - /// 1. `[writable]` The source SPL Token account. - /// 2. `[]` The token mint. - /// 3. `[writable]` The destination SPL Token account. - /// 4. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 5. `[]` (Optional) Equality proof record account or context state - /// account. - /// 6. `[]` (Optional) Ciphertext validity proof record account or context - /// state account. - /// 7. `[]` (Optional) Range proof record account or context state - /// account. - /// 8. `[signer]` The single source account owner. - /// - /// * Multisignature owner/delegate - /// 1. `[writable]` The source SPL Token account. - /// 2. `[]` The token mint. - /// 3. `[writable]` The destination SPL Token account. - /// 4. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 5. `[]` (Optional) Equality proof record account or context state - /// account. - /// 6. `[]` (Optional) Ciphertext validity proof record account or context - /// state account. - /// 7. `[]` (Optional) Range proof record account or context state - /// account. - /// 8. `[]` The multisig source account owner. - /// 9. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// `TransferInstructionData` - Transfer, - - /// Applies the pending balance to the available balance, based on the - /// history of `Deposit` and/or `Transfer` instructions. - /// - /// After submitting `ApplyPendingBalance`, the client should compare - /// `ConfidentialTransferAccount::expected_pending_balance_credit_counter` - /// with - /// `ConfidentialTransferAccount::actual_applied_pending_balance_instructions`. If they are - /// equal then the - /// `ConfidentialTransferAccount::decryptable_available_balance` is - /// consistent with `ConfidentialTransferAccount::available_balance`. If - /// they differ then there is more pending balance to be applied. - /// - /// Account expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[signer]` The single account owner. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The multisig account owner. - /// 2. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// `ApplyPendingBalanceData` - ApplyPendingBalance, - - /// Configure a confidential extension account to accept incoming - /// confidential transfers. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[signer]` Single authority. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` Multisig authority. - /// 2. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// None - EnableConfidentialCredits, - - /// Configure a confidential extension account to reject any incoming - /// confidential transfers. - /// - /// If the `allow_non_confidential_credits` field is `true`, then the base - /// account can still receive non-confidential transfers. - /// - /// This instruction can be used to disable confidential payments after a - /// token account has already been extended for confidential transfers. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[signer]` The single account owner. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The multisig account owner. - /// 2. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// None - DisableConfidentialCredits, - - /// Configure an account with the confidential extension to accept incoming - /// non-confidential transfers. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[signer]` The single account owner. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The multisig account owner. - /// 2. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// None - EnableNonConfidentialCredits, - - /// Configure an account with the confidential extension to reject any - /// incoming non-confidential transfers. - /// - /// This instruction can be used to configure a confidential extension - /// account to exclusively receive confidential payments. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[signer]` The single account owner. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The multisig account owner. - /// 2. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// None - DisableNonConfidentialCredits, - - /// Transfer tokens confidentially with fee. - /// - /// In order for this instruction to be successfully processed, it must be - /// accompanied by the following list of `zk_elgamal_proof` program - /// instructions: - /// - /// - `VerifyCiphertextCommitmentEquality` - /// - `VerifyBatchedGroupedCiphertext3HandlesValidity` (transfer amount - /// ciphertext) - /// - `VerifyPercentageWithFee` - /// - `VerifyBatchedGroupedCiphertext2HandlesValidity` (fee ciphertext) - /// - `VerifyBatchedRangeProofU256` - /// - /// These instructions can be accompanied in the same transaction or can be - /// pre-verified into a context state account, in which case, only their - /// context state account addresses need to be provided. - /// - /// The same restrictions for the `Transfer` applies to - /// `TransferWithFee`. Namely, the instruction fails if the - /// associated mint is extended as `NonTransferable`. - /// - /// * Transfer without fee - /// 1. `[writable]` The source SPL Token account. - /// 2. `[]` The token mint. - /// 3. `[writable]` The destination SPL Token account. - /// 4. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 5. `[]` (Optional) Equality proof record account or context state - /// account. - /// 6. `[]` (Optional) Transfer amount ciphertext validity proof record - /// account or context state account. - /// 7. `[]` (Optional) Fee sigma proof record account or context state - /// account. - /// 8. `[]` (Optional) Fee ciphertext validity proof record account or - /// context state account. - /// 9. `[]` (Optional) Range proof record account or context state - /// account. - /// 10. `[signer]` The source account owner. - /// - /// * Transfer with fee - /// 1. `[writable]` The source SPL Token account. - /// 2. `[]` The token mint. - /// 3. `[writable]` The destination SPL Token account. - /// 4. `[]` (Optional) Instructions sysvar if at least one of the - /// `zk_elgamal_proof` instructions are included in the same - /// transaction. - /// 5. `[]` (Optional) Equality proof record account or context state - /// account. - /// 6. `[]` (Optional) Transfer amount ciphertext validity proof record - /// account or context state account. - /// 7. `[]` (Optional) Fee sigma proof record account or context state - /// account. - /// 8. `[]` (Optional) Fee ciphertext validity proof record account or - /// context state account. - /// 9. `[]` (Optional) Range proof record account or context state - /// account. - /// 10. `[]` The multisig source account owner. - /// 11. .. `[signer]` Required M signer accounts for the SPL Token - /// Multisig - /// - /// Data expected by this instruction: - /// `TransferWithFeeInstructionData` - TransferWithFee, - - /// Configures confidential transfers for a token account. - /// - /// This instruction is identical to the `ConfigureAccount` account except - /// that a valid `ElGamalRegistry` account is expected in place of the - /// `VerifyPubkeyValidity` proof. - /// - /// An `ElGamalRegistry` account is valid if it shares the same owner with - /// the token account. If a valid `ElGamalRegistry` account is provided, - /// then the program skips the verification of the ElGamal pubkey - /// validity proof as well as the token owner signature. - /// - /// If the token account is not large enough to include the new - /// confidential transfer extension, then optionally reallocate the - /// account to increase the data size. To reallocate, a payer account to - /// fund the reallocation and the system account should be included in the - /// instruction. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The SPL Token account. - /// 1. `[]` The corresponding SPL Token mint. - /// 2. `[]` The ElGamal registry account. - /// 3. `[signer, writable]` (Optional) The payer account to fund - /// reallocation - /// 4. `[]` (Optional) System program for reallocation funding - /// - /// Data expected by this instruction: - /// None - ConfigureAccountWithRegistry, -} - -/// Data expected by `ConfidentialTransferInstruction::InitializeMint` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeMintData { - /// Authority to modify the `ConfidentialTransferMint` configuration and to - /// approve new accounts. - pub authority: OptionalNonZeroPubkey, - /// Determines if newly configured accounts must be approved by the - /// `authority` before they may be used by the user. - pub auto_approve_new_accounts: PodBool, - /// New authority to decode any transfer amount in a confidential transfer. - pub auditor_elgamal_pubkey: OptionalNonZeroElGamalPubkey, -} - -/// Data expected by `ConfidentialTransferInstruction::UpdateMint` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct UpdateMintData { - /// Determines if newly configured accounts must be approved by the - /// `authority` before they may be used by the user. - pub auto_approve_new_accounts: PodBool, - /// New authority to decode any transfer amount in a confidential transfer. - pub auditor_elgamal_pubkey: OptionalNonZeroElGamalPubkey, -} - -/// Data expected by `ConfidentialTransferInstruction::ConfigureAccount` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct ConfigureAccountInstructionData { - /// The decryptable balance (always 0) once the configure account succeeds - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub decryptable_zero_balance: DecryptableBalance, - /// The maximum number of despots and transfers that an account can receiver - /// before the `ApplyPendingBalance` is executed - pub maximum_pending_balance_credit_counter: PodU64, - /// Relative location of the `ProofInstruction::ZeroCiphertextProof` - /// instruction to the `ConfigureAccount` instruction in the - /// transaction. If the offset is `0`, then use a context state account - /// for the proof. - pub proof_instruction_offset: i8, -} - -/// Data expected by `ConfidentialTransferInstruction::EmptyAccount` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct EmptyAccountInstructionData { - /// Relative location of the `ProofInstruction::VerifyCloseAccount` - /// instruction to the `EmptyAccount` instruction in the transaction. If - /// the offset is `0`, then use a context state account for the proof. - pub proof_instruction_offset: i8, -} - -/// Data expected by `ConfidentialTransferInstruction::Deposit` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct DepositInstructionData { - /// The amount of tokens to deposit - pub amount: PodU64, - /// Expected number of base 10 digits to the right of the decimal place - pub decimals: u8, -} - -/// Data expected by `ConfidentialTransferInstruction::Withdraw` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct WithdrawInstructionData { - /// The amount of tokens to withdraw - pub amount: PodU64, - /// Expected number of base 10 digits to the right of the decimal place - pub decimals: u8, - /// The new decryptable balance if the withdrawal succeeds - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub new_decryptable_available_balance: DecryptableBalance, - /// Relative location of the - /// `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction - /// to the `Withdraw` instruction in the transaction. If the offset is - /// `0`, then use a context state account for the proof. - pub equality_proof_instruction_offset: i8, - /// Relative location of the `ProofInstruction::BatchedRangeProofU64` - /// instruction to the `Withdraw` instruction in the transaction. If the - /// offset is `0`, then use a context state account for the proof. - pub range_proof_instruction_offset: i8, -} - -/// Data expected by `ConfidentialTransferInstruction::Transfer` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct TransferInstructionData { - /// The new source decryptable balance if the transfer succeeds - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub new_source_decryptable_available_balance: DecryptableBalance, - /// The transfer amount encrypted under the auditor ElGamal public key - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalciphertext_fromstr"))] - pub transfer_amount_auditor_ciphertext_lo: PodElGamalCiphertext, - /// The transfer amount encrypted under the auditor ElGamal public key - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalciphertext_fromstr"))] - pub transfer_amount_auditor_ciphertext_hi: PodElGamalCiphertext, - /// Relative location of the - /// `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction - /// to the `Transfer` instruction in the transaction. If the offset is - /// `0`, then use a context state account for the proof. - pub equality_proof_instruction_offset: i8, - /// Relative location of the - /// `ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity` - /// instruction to the `Transfer` instruction in the transaction. If the - /// offset is `0`, then use a context state account for the proof. - pub ciphertext_validity_proof_instruction_offset: i8, - /// Relative location of the `ProofInstruction::BatchedRangeProofU128Data` - /// instruction to the `Transfer` instruction in the transaction. If the - /// offset is `0`, then use a context state account for the proof. - pub range_proof_instruction_offset: i8, -} - -/// Data expected by `ConfidentialTransferInstruction::ApplyPendingBalance` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct ApplyPendingBalanceData { - /// The expected number of pending balance credits since the last successful - /// `ApplyPendingBalance` instruction - pub expected_pending_balance_credit_counter: PodU64, - /// The new decryptable balance if the pending balance is applied - /// successfully - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub new_decryptable_available_balance: DecryptableBalance, -} - -/// Data expected by `ConfidentialTransferInstruction::TransferWithFee` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct TransferWithFeeInstructionData { - /// The new source decryptable balance if the transfer succeeds - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub new_source_decryptable_available_balance: DecryptableBalance, - /// The transfer amount encrypted under the auditor ElGamal public key - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalciphertext_fromstr"))] - pub transfer_amount_auditor_ciphertext_lo: PodElGamalCiphertext, - /// The transfer amount encrypted under the auditor ElGamal public key - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalciphertext_fromstr"))] - pub transfer_amount_auditor_ciphertext_hi: PodElGamalCiphertext, - /// Relative location of the - /// `ProofInstruction::VerifyCiphertextCommitmentEquality` instruction - /// to the `TransferWithFee` instruction in the transaction. If the offset - /// is `0`, then use a context state account for the proof. - pub equality_proof_instruction_offset: i8, - /// Relative location of the - /// `ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity` - /// instruction to the `TransferWithFee` instruction in the transaction. - /// If the offset is `0`, then use a context state account for the - /// proof. - pub transfer_amount_ciphertext_validity_proof_instruction_offset: i8, - /// Relative location of the `ProofInstruction::VerifyPercentageWithFee` - /// instruction to the `TransferWithFee` instruction in the transaction. - /// If the offset is `0`, then use a context state account for the - /// proof. - pub fee_sigma_proof_instruction_offset: i8, - /// Relative location of the - /// `ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity` - /// instruction to the `TransferWithFee` instruction in the transaction. - /// If the offset is `0`, then use a context state account for the - /// proof. - pub fee_ciphertext_validity_proof_instruction_offset: i8, - /// Relative location of the `ProofInstruction::BatchedRangeProofU256Data` - /// instruction to the `TransferWithFee` instruction in the transaction. - /// If the offset is `0`, then use a context state account for the - /// proof. - pub range_proof_instruction_offset: i8, -} - -/// Create a `InitializeMint` instruction -pub fn initialize_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: Option, - auto_approve_new_accounts: bool, - auditor_elgamal_pubkey: Option, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::InitializeMint, - &InitializeMintData { - authority: authority.try_into()?, - auto_approve_new_accounts: auto_approve_new_accounts.into(), - auditor_elgamal_pubkey: auditor_elgamal_pubkey.try_into()?, - }, - )) -} - -/// Create a `UpdateMint` instruction -pub fn update_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - auto_approve_new_accounts: bool, - auditor_elgamal_pubkey: Option, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), - ]; - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::UpdateMint, - &UpdateMintData { - auto_approve_new_accounts: auto_approve_new_accounts.into(), - auditor_elgamal_pubkey: auditor_elgamal_pubkey.try_into()?, - }, - )) -} - -/// Create a `ConfigureAccount` instruction -/// -/// This instruction is suitable for use with a cross-program `invoke` -#[allow(clippy::too_many_arguments)] -pub fn inner_configure_account( - token_program_id: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - decryptable_zero_balance: &DecryptableBalance, - maximum_pending_balance_credit_counter: u64, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, -) -> Result { - check_program_account(token_program_id)?; - - let mut accounts = vec![ - AccountMeta::new(*token_account, false), - AccountMeta::new_readonly(*mint, false), - ]; - - let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::ConfigureAccount, - &ConfigureAccountInstructionData { - decryptable_zero_balance: *decryptable_zero_balance, - maximum_pending_balance_credit_counter: maximum_pending_balance_credit_counter.into(), - proof_instruction_offset, - }, - )) -} - -/// Create a `ConfigureAccount` instruction -#[allow(clippy::too_many_arguments)] -pub fn configure_account( - token_program_id: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - decryptable_zero_balance: &DecryptableBalance, - maximum_pending_balance_credit_counter: u64, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, -) -> Result, ProgramError> { - let mut instructions = vec![inner_configure_account( - token_program_id, - token_account, - mint, - decryptable_zero_balance, - maximum_pending_balance_credit_counter, - authority, - multisig_signers, - proof_data_location, - )?]; - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - proof_data_location - { - // This constructor appends the proof instruction right after the - // `ConfigureAccount` instruction. This means that the proof instruction - // offset must be always be 1. To use an arbitrary proof instruction - // offset, use the `inner_configure_account` constructor. - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 1 { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions - .push(ProofInstruction::VerifyPubkeyValidity.encode_verify_proof(None, data)), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyPubkeyValidity - .encode_verify_proof_from_account(None, address, offset), - ), - }; - } - - Ok(instructions) -} - -/// Create an `ApproveAccount` instruction -pub fn approve_account( - token_program_id: &Pubkey, - account_to_approve: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*account_to_approve, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), - ]; - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::ApproveAccount, - &(), - )) -} - -/// Create an inner `EmptyAccount` instruction -/// -/// This instruction is suitable for use with a cross-program `invoke` -pub fn inner_empty_account( - token_program_id: &Pubkey, - token_account: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![AccountMeta::new(*token_account, false)]; - - let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::EmptyAccount, - &EmptyAccountInstructionData { - proof_instruction_offset, - }, - )) -} - -/// Create a `EmptyAccount` instruction -pub fn empty_account( - token_program_id: &Pubkey, - token_account: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, -) -> Result, ProgramError> { - let mut instructions = vec![inner_empty_account( - token_program_id, - token_account, - authority, - multisig_signers, - proof_data_location, - )?]; - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - proof_data_location - { - // This constructor appends the proof instruction right after the `EmptyAccount` - // instruction. This means that the proof instruction offset must be always be - // 1. To use an arbitrary proof instruction offset, use the - // `inner_empty_account` constructor. - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 1 { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions - .push(ProofInstruction::VerifyZeroCiphertext.encode_verify_proof(None, data)), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyZeroCiphertext - .encode_verify_proof_from_account(None, address, offset), - ), - }; - }; - - Ok(instructions) -} - -/// Create a `Deposit` instruction -#[allow(clippy::too_many_arguments)] -pub fn deposit( - token_program_id: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - amount: u64, - decimals: u8, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*token_account, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), - ]; - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::Deposit, - &DepositInstructionData { - amount: amount.into(), - decimals, - }, - )) -} - -/// Create a inner `Withdraw` instruction -/// -/// This instruction is suitable for use with a cross-program `invoke` -#[allow(clippy::too_many_arguments)] -pub fn inner_withdraw( - token_program_id: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - amount: u64, - decimals: u8, - new_decryptable_available_balance: &DecryptableBalance, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - equality_proof_data_location: ProofLocation, - range_proof_data_location: ProofLocation, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*token_account, false), - AccountMeta::new_readonly(*mint, false), - ]; - - // if at least one of the proof locations is an instruction offset, sysvar - // account is needed - if equality_proof_data_location.is_instruction_offset() - || range_proof_data_location.is_instruction_offset() - { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - } - - let equality_proof_instruction_offset = match equality_proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - let range_proof_instruction_offset = match range_proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::Withdraw, - &WithdrawInstructionData { - amount: amount.into(), - decimals, - new_decryptable_available_balance: *new_decryptable_available_balance, - equality_proof_instruction_offset, - range_proof_instruction_offset, - }, - )) -} - -/// Create a `Withdraw` instruction -#[allow(clippy::too_many_arguments)] -pub fn withdraw( - token_program_id: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - amount: u64, - decimals: u8, - new_decryptable_available_balance: &DecryptableBalance, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - equality_proof_data_location: ProofLocation, - range_proof_data_location: ProofLocation, -) -> Result, ProgramError> { - let mut instructions = vec![inner_withdraw( - token_program_id, - token_account, - mint, - amount, - decimals, - new_decryptable_available_balance, - authority, - multisig_signers, - equality_proof_data_location, - range_proof_data_location, - )?]; - - let mut expected_instruction_offset = 1; - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - equality_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyCiphertextCommitmentEquality - .encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyCiphertextCommitmentEquality - .encode_verify_proof_from_account(None, address, offset), - ), - }; - - expected_instruction_offset += 1; - }; - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - range_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions - .push(ProofInstruction::VerifyBatchedRangeProofU64.encode_verify_proof(None, data)), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyBatchedRangeProofU64 - .encode_verify_proof_from_account(None, address, offset), - ), - }; - }; - - Ok(instructions) -} - -/// Create an inner `Transfer` instruction -/// -/// This instruction is suitable for use with a cross-program `invoke` -#[allow(clippy::too_many_arguments)] -pub fn inner_transfer( - token_program_id: &Pubkey, - source_token_account: &Pubkey, - mint: &Pubkey, - destination_token_account: &Pubkey, - new_source_decryptable_available_balance: &DecryptableBalance, - transfer_amount_auditor_ciphertext_lo: &PodElGamalCiphertext, - transfer_amount_auditor_ciphertext_hi: &PodElGamalCiphertext, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - equality_proof_data_location: ProofLocation, - ciphertext_validity_proof_data_location: ProofLocation< - BatchedGroupedCiphertext3HandlesValidityProofData, - >, - range_proof_data_location: ProofLocation, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*source_token_account, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new(*destination_token_account, false), - ]; - - // if at least one of the proof locations is an instruction offset, sysvar - // account is needed - if equality_proof_data_location.is_instruction_offset() - || ciphertext_validity_proof_data_location.is_instruction_offset() - || range_proof_data_location.is_instruction_offset() - { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - } - - let equality_proof_instruction_offset = match equality_proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - let ciphertext_validity_proof_instruction_offset = match ciphertext_validity_proof_data_location - { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - let range_proof_instruction_offset = match range_proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::Transfer, - &TransferInstructionData { - new_source_decryptable_available_balance: *new_source_decryptable_available_balance, - transfer_amount_auditor_ciphertext_lo: *transfer_amount_auditor_ciphertext_lo, - transfer_amount_auditor_ciphertext_hi: *transfer_amount_auditor_ciphertext_hi, - equality_proof_instruction_offset, - ciphertext_validity_proof_instruction_offset, - range_proof_instruction_offset, - }, - )) -} - -/// Create a `Transfer` instruction -#[allow(clippy::too_many_arguments)] -pub fn transfer( - token_program_id: &Pubkey, - source_token_account: &Pubkey, - mint: &Pubkey, - destination_token_account: &Pubkey, - new_source_decryptable_available_balance: &DecryptableBalance, - transfer_amount_auditor_ciphertext_lo: &PodElGamalCiphertext, - transfer_amount_auditor_ciphertext_hi: &PodElGamalCiphertext, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - equality_proof_data_location: ProofLocation, - ciphertext_validity_proof_data_location: ProofLocation< - BatchedGroupedCiphertext3HandlesValidityProofData, - >, - range_proof_data_location: ProofLocation, -) -> Result, ProgramError> { - let mut instructions = vec![inner_transfer( - token_program_id, - source_token_account, - mint, - destination_token_account, - new_source_decryptable_available_balance, - transfer_amount_auditor_ciphertext_lo, - transfer_amount_auditor_ciphertext_hi, - authority, - multisig_signers, - equality_proof_data_location, - ciphertext_validity_proof_data_location, - range_proof_data_location, - )?]; - - let mut expected_instruction_offset = 1; - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - equality_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyCiphertextCommitmentEquality - .encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyCiphertextCommitmentEquality - .encode_verify_proof_from_account(None, address, offset), - ), - }; - - expected_instruction_offset += 1; - } - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - ciphertext_validity_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity - .encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity - .encode_verify_proof_from_account(None, address, offset), - ), - }; - - expected_instruction_offset += 1; - } - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - range_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyBatchedRangeProofU128.encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyBatchedRangeProofU128 - .encode_verify_proof_from_account(None, address, offset), - ), - }; - } - - Ok(instructions) -} - -/// Create a inner `ApplyPendingBalance` instruction -/// -/// This instruction is suitable for use with a cross-program `invoke` -pub fn inner_apply_pending_balance( - token_program_id: &Pubkey, - token_account: &Pubkey, - expected_pending_balance_credit_counter: u64, - new_decryptable_available_balance: &DecryptableBalance, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*token_account, false), - AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), - ]; - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::ApplyPendingBalance, - &ApplyPendingBalanceData { - expected_pending_balance_credit_counter: expected_pending_balance_credit_counter.into(), - new_decryptable_available_balance: *new_decryptable_available_balance, - }, - )) -} - -/// Create a `ApplyPendingBalance` instruction -pub fn apply_pending_balance( - token_program_id: &Pubkey, - token_account: &Pubkey, - pending_balance_instructions: u64, - new_decryptable_available_balance: &DecryptableBalance, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - inner_apply_pending_balance( - token_program_id, - token_account, - pending_balance_instructions, - new_decryptable_available_balance, - authority, - multisig_signers, - ) // calls check_program_account -} - -fn enable_or_disable_balance_credits( - instruction: ConfidentialTransferInstruction, - token_program_id: &Pubkey, - token_account: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*token_account, false), - AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), - ]; - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - instruction, - &(), - )) -} - -/// Create a `EnableConfidentialCredits` instruction -pub fn enable_confidential_credits( - token_program_id: &Pubkey, - token_account: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - enable_or_disable_balance_credits( - ConfidentialTransferInstruction::EnableConfidentialCredits, - token_program_id, - token_account, - authority, - multisig_signers, - ) -} - -/// Create a `DisableConfidentialCredits` instruction -pub fn disable_confidential_credits( - token_program_id: &Pubkey, - token_account: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - enable_or_disable_balance_credits( - ConfidentialTransferInstruction::DisableConfidentialCredits, - token_program_id, - token_account, - authority, - multisig_signers, - ) -} - -/// Create a `EnableNonConfidentialCredits` instruction -pub fn enable_non_confidential_credits( - token_program_id: &Pubkey, - token_account: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - enable_or_disable_balance_credits( - ConfidentialTransferInstruction::EnableNonConfidentialCredits, - token_program_id, - token_account, - authority, - multisig_signers, - ) -} - -/// Create a `DisableNonConfidentialCredits` instruction -pub fn disable_non_confidential_credits( - token_program_id: &Pubkey, - token_account: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - enable_or_disable_balance_credits( - ConfidentialTransferInstruction::DisableNonConfidentialCredits, - token_program_id, - token_account, - authority, - multisig_signers, - ) -} - -/// Create an inner `TransferWithFee` instruction -/// -/// This instruction is suitable for use with a cross-program `invoke` -#[allow(clippy::too_many_arguments)] -pub fn inner_transfer_with_fee( - token_program_id: &Pubkey, - source_token_account: &Pubkey, - mint: &Pubkey, - destination_token_account: &Pubkey, - new_source_decryptable_available_balance: &DecryptableBalance, - transfer_amount_auditor_ciphertext_lo: &PodElGamalCiphertext, - transfer_amount_auditor_ciphertext_hi: &PodElGamalCiphertext, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - equality_proof_data_location: ProofLocation, - transfer_amount_ciphertext_validity_proof_data_location: ProofLocation< - BatchedGroupedCiphertext3HandlesValidityProofData, - >, - fee_sigma_proof_data_location: ProofLocation, - fee_ciphertext_validity_proof_data_location: ProofLocation< - BatchedGroupedCiphertext2HandlesValidityProofData, - >, - range_proof_data_location: ProofLocation, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*source_token_account, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new(*destination_token_account, false), - ]; - - // if at least one of the proof locations is an instruction offset, sysvar - // account is needed - if equality_proof_data_location.is_instruction_offset() - || transfer_amount_ciphertext_validity_proof_data_location.is_instruction_offset() - || fee_sigma_proof_data_location.is_instruction_offset() - || fee_ciphertext_validity_proof_data_location.is_instruction_offset() - || range_proof_data_location.is_instruction_offset() - { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - } - - let equality_proof_instruction_offset = match equality_proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - let transfer_amount_ciphertext_validity_proof_instruction_offset = - match transfer_amount_ciphertext_validity_proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - let fee_sigma_proof_instruction_offset = match fee_sigma_proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - let fee_ciphertext_validity_proof_instruction_offset = - match fee_ciphertext_validity_proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - let range_proof_instruction_offset = match range_proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::TransferWithFee, - &TransferWithFeeInstructionData { - new_source_decryptable_available_balance: *new_source_decryptable_available_balance, - transfer_amount_auditor_ciphertext_lo: *transfer_amount_auditor_ciphertext_lo, - transfer_amount_auditor_ciphertext_hi: *transfer_amount_auditor_ciphertext_hi, - equality_proof_instruction_offset, - transfer_amount_ciphertext_validity_proof_instruction_offset, - fee_sigma_proof_instruction_offset, - fee_ciphertext_validity_proof_instruction_offset, - range_proof_instruction_offset, - }, - )) -} - -/// Create a `TransferWithFee` instruction -#[allow(clippy::too_many_arguments)] -pub fn transfer_with_fee( - token_program_id: &Pubkey, - source_token_account: &Pubkey, - mint: &Pubkey, - destination_token_account: &Pubkey, - new_source_decryptable_available_balance: &DecryptableBalance, - transfer_amount_auditor_ciphertext_lo: &PodElGamalCiphertext, - transfer_amount_auditor_ciphertext_hi: &PodElGamalCiphertext, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - equality_proof_data_location: ProofLocation, - transfer_amount_ciphertext_validity_proof_data_location: ProofLocation< - BatchedGroupedCiphertext3HandlesValidityProofData, - >, - fee_sigma_proof_data_location: ProofLocation, - fee_ciphertext_validity_proof_data_location: ProofLocation< - BatchedGroupedCiphertext2HandlesValidityProofData, - >, - range_proof_data_location: ProofLocation, -) -> Result, ProgramError> { - let mut instructions = vec![inner_transfer_with_fee( - token_program_id, - source_token_account, - mint, - destination_token_account, - new_source_decryptable_available_balance, - transfer_amount_auditor_ciphertext_lo, - transfer_amount_auditor_ciphertext_hi, - authority, - multisig_signers, - equality_proof_data_location, - transfer_amount_ciphertext_validity_proof_data_location, - fee_sigma_proof_data_location, - fee_ciphertext_validity_proof_data_location, - range_proof_data_location, - )?]; - - let mut expected_instruction_offset = 1; - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - equality_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyCiphertextCommitmentEquality - .encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyCiphertextCommitmentEquality - .encode_verify_proof_from_account(None, address, offset), - ), - }; - expected_instruction_offset += 1; - } - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - transfer_amount_ciphertext_validity_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity - .encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity - .encode_verify_proof_from_account(None, address, offset), - ), - }; - expected_instruction_offset += 1; - } - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - fee_sigma_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions - .push(ProofInstruction::VerifyPercentageWithCap.encode_verify_proof(None, data)), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyPercentageWithCap - .encode_verify_proof_from_account(None, address, offset), - ), - }; - expected_instruction_offset += 1; - } - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - fee_ciphertext_validity_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity - .encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity - .encode_verify_proof_from_account(None, address, offset), - ), - }; - expected_instruction_offset += 1; - } - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - range_proof_data_location - { - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != expected_instruction_offset { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyBatchedRangeProofU256.encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyBatchedRangeProofU256 - .encode_verify_proof_from_account(None, address, offset), - ), - }; - } - - Ok(instructions) -} - -/// Create a `ConfigureAccountWithRegistry` instruction -pub fn configure_account_with_registry( - token_program_id: &Pubkey, - token_account: &Pubkey, - mint: &Pubkey, - elgamal_registry_account: &Pubkey, - payer: Option<&Pubkey>, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*token_account, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new_readonly(*elgamal_registry_account, false), - ]; - if let Some(payer) = payer { - accounts.push(AccountMeta::new(*payer, true)); - accounts.push(AccountMeta::new_readonly(system_program::id(), false)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferExtension, - ConfidentialTransferInstruction::ConfigureAccountWithRegistry, - &(), - )) -} diff --git a/token/program-2022/src/extension/confidential_transfer/mod.rs b/token/program-2022/src/extension/confidential_transfer/mod.rs deleted file mode 100644 index baa371cf89f..00000000000 --- a/token/program-2022/src/extension/confidential_transfer/mod.rs +++ /dev/null @@ -1,201 +0,0 @@ -use { - crate::{ - error::TokenError, - extension::{Extension, ExtensionType}, - }, - bytemuck::{Pod, Zeroable}, - solana_program::entrypoint::ProgramResult, - solana_zk_sdk::encryption::pod::{ - auth_encryption::PodAeCiphertext, - elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - }, - spl_pod::{ - optional_keys::{OptionalNonZeroElGamalPubkey, OptionalNonZeroPubkey}, - primitives::{PodBool, PodU64}, - }, -}; - -/// Maximum bit length of any deposit or transfer amount -/// -/// Any deposit or transfer amount must be less than `2^48` -pub const MAXIMUM_DEPOSIT_TRANSFER_AMOUNT: u64 = (u16::MAX as u64) + (1 << 16) * (u32::MAX as u64); - -/// Bit length of the low bits of pending balance plaintext -pub const PENDING_BALANCE_LO_BIT_LENGTH: u32 = 16; - -/// The default maximum pending balance credit counter. -pub const DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER: u64 = 65536; - -/// Confidential Transfer Extension instructions -pub mod instruction; - -/// Confidential Transfer Extension processor -pub mod processor; - -/// Helper functions to verify zero-knowledge proofs in the Confidential -/// Transfer Extension -pub mod verify_proof; - -/// Confidential Transfer Extension account information needed for instructions -#[cfg(not(target_os = "solana"))] -pub mod account_info; - -/// ElGamal ciphertext containing an account balance -pub type EncryptedBalance = PodElGamalCiphertext; -/// Authenticated encryption containing an account balance -pub type DecryptableBalance = PodAeCiphertext; - -/// Confidential transfer mint configuration -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct ConfidentialTransferMint { - /// Authority to modify the `ConfidentialTransferMint` configuration and to - /// approve new accounts (if `auto_approve_new_accounts` is true) - /// - /// The legacy Token Multisig account is not supported as the authority - pub authority: OptionalNonZeroPubkey, - - /// Indicate if newly configured accounts must be approved by the - /// `authority` before they may be used by the user. - /// - /// * If `true`, no approval is required and new accounts may be used - /// immediately - /// * If `false`, the authority must approve newly configured accounts (see - /// `ConfidentialTransferInstruction::ConfigureAccount`) - pub auto_approve_new_accounts: PodBool, - - /// Authority to decode any transfer amount in a confidential transfer. - pub auditor_elgamal_pubkey: OptionalNonZeroElGamalPubkey, -} - -impl Extension for ConfidentialTransferMint { - const TYPE: ExtensionType = ExtensionType::ConfidentialTransferMint; -} - -/// Confidential account state -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct ConfidentialTransferAccount { - /// `true` if this account has been approved for use. All confidential - /// transfer operations for the account will fail until approval is - /// granted. - pub approved: PodBool, - - /// The public key associated with ElGamal encryption - pub elgamal_pubkey: PodElGamalPubkey, - - /// The low 16 bits of the pending balance (encrypted by `elgamal_pubkey`) - pub pending_balance_lo: EncryptedBalance, - - /// The high 48 bits of the pending balance (encrypted by `elgamal_pubkey`) - pub pending_balance_hi: EncryptedBalance, - - /// The available balance (encrypted by `encryption_pubkey`) - pub available_balance: EncryptedBalance, - - /// The decryptable available balance - pub decryptable_available_balance: DecryptableBalance, - - /// If `false`, the extended account rejects any incoming confidential - /// transfers - pub allow_confidential_credits: PodBool, - - /// If `false`, the base account rejects any incoming transfers - pub allow_non_confidential_credits: PodBool, - - /// The total number of `Deposit` and `Transfer` instructions that have - /// credited `pending_balance` - pub pending_balance_credit_counter: PodU64, - - /// The maximum number of `Deposit` and `Transfer` instructions that can - /// credit `pending_balance` before the `ApplyPendingBalance` - /// instruction is executed - pub maximum_pending_balance_credit_counter: PodU64, - - /// The `expected_pending_balance_credit_counter` value that was included in - /// the last `ApplyPendingBalance` instruction - pub expected_pending_balance_credit_counter: PodU64, - - /// The actual `pending_balance_credit_counter` when the last - /// `ApplyPendingBalance` instruction was executed - pub actual_pending_balance_credit_counter: PodU64, -} - -impl Extension for ConfidentialTransferAccount { - const TYPE: ExtensionType = ExtensionType::ConfidentialTransferAccount; -} - -impl ConfidentialTransferAccount { - /// Check if a `ConfidentialTransferAccount` has been approved for use. - pub fn approved(&self) -> ProgramResult { - if bool::from(&self.approved) { - Ok(()) - } else { - Err(TokenError::ConfidentialTransferAccountNotApproved.into()) - } - } - - /// Check if a `ConfidentialTransferAccount` is in a closable state. - pub fn closable(&self) -> ProgramResult { - if self.pending_balance_lo == EncryptedBalance::zeroed() - && self.pending_balance_hi == EncryptedBalance::zeroed() - && self.available_balance == EncryptedBalance::zeroed() - { - Ok(()) - } else { - Err(TokenError::ConfidentialTransferAccountHasBalance.into()) - } - } - - /// Check if a base account of a `ConfidentialTransferAccount` accepts - /// non-confidential transfers. - pub fn non_confidential_transfer_allowed(&self) -> ProgramResult { - if bool::from(&self.allow_non_confidential_credits) { - Ok(()) - } else { - Err(TokenError::NonConfidentialTransfersDisabled.into()) - } - } - - /// Checks if a `ConfidentialTransferAccount` is configured to send funds. - pub fn valid_as_source(&self) -> ProgramResult { - self.approved() - } - - /// Checks if a confidential extension is configured to receive funds. - /// - /// A destination account can receive funds if the following conditions are - /// satisfied: - /// 1. The account is approved by the confidential transfer mint authority - /// 2. The account is not disabled by the account owner - /// 3. The number of credits into the account has not reached the maximum - /// credit counter - pub fn valid_as_destination(&self) -> ProgramResult { - self.approved()?; - - if !bool::from(self.allow_confidential_credits) { - return Err(TokenError::ConfidentialTransferDepositsAndTransfersDisabled.into()); - } - - let new_destination_pending_balance_credit_counter = - u64::from(self.pending_balance_credit_counter) - .checked_add(1) - .ok_or(TokenError::Overflow)?; - if new_destination_pending_balance_credit_counter - > u64::from(self.maximum_pending_balance_credit_counter) - { - return Err(TokenError::MaximumPendingBalanceCreditCounterExceeded.into()); - } - - Ok(()) - } - - /// Increments a confidential extension pending balance credit counter. - pub fn increment_pending_balance_credit_counter(&mut self) -> ProgramResult { - self.pending_balance_credit_counter = (u64::from(self.pending_balance_credit_counter) - .checked_add(1) - .ok_or(TokenError::Overflow)?) - .into(); - Ok(()) - } -} diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs deleted file mode 100644 index f9a3920b76c..00000000000 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ /dev/null @@ -1,1416 +0,0 @@ -// Remove feature once zk ops syscalls are enabled on all networks -#[cfg(feature = "zk-ops")] -use { - crate::extension::confidential_mint_burn::ConfidentialMintBurn, - crate::extension::non_transferable::NonTransferableAccount, - spl_token_confidential_transfer_ciphertext_arithmetic as ciphertext_arithmetic, -}; -use { - crate::{ - check_auditor_ciphertext, check_elgamal_registry_program_account, check_program_account, - error::TokenError, - extension::{ - confidential_transfer::{instruction::*, verify_proof::*, *}, - confidential_transfer_fee::{ - ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, - EncryptedWithheldAmount, - }, - memo_transfer::{check_previous_sibling_instruction_is_memo, memo_required}, - pausable::PausableConfig, - set_account_type, - transfer_fee::TransferFeeConfig, - transfer_hook, BaseStateWithExtensions, BaseStateWithExtensionsMut, - PodStateWithExtensions, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::{PodAccount, PodMint}, - processor::Processor, - state::Account, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - clock::Clock, - entrypoint::ProgramResult, - msg, - program::invoke, - program_error::ProgramError, - pubkey::Pubkey, - system_instruction, - sysvar::{rent::Rent, Sysvar}, - }, - spl_elgamal_registry::state::ElGamalRegistry, - spl_pod::bytemuck::pod_from_bytes, - spl_token_confidential_transfer_proof_extraction::{ - instruction::verify_and_extract_context, transfer::TransferProofContext, - transfer_with_fee::TransferWithFeeProofContext, - }, -}; - -/// Processes an [`InitializeMint`] instruction. -fn process_initialize_mint( - accounts: &[AccountInfo], - authority: &OptionalNonZeroPubkey, - auto_approve_new_account: PodBool, - auditor_encryption_pubkey: &OptionalNonZeroElGamalPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - - check_program_account(mint_info.owner)?; - let mint_data = &mut mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(mint_data)?; - let confidential_transfer_mint = mint.init_extension::(true)?; - - confidential_transfer_mint.authority = *authority; - confidential_transfer_mint.auto_approve_new_accounts = auto_approve_new_account; - confidential_transfer_mint.auditor_elgamal_pubkey = *auditor_encryption_pubkey; - - Ok(()) -} - -/// Processes an [`UpdateMint`] instruction. -fn process_update_mint( - accounts: &[AccountInfo], - auto_approve_new_account: PodBool, - auditor_encryption_pubkey: &OptionalNonZeroElGamalPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - check_program_account(mint_info.owner)?; - let mint_data = &mut mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; - let confidential_transfer_mint = mint.get_extension_mut::()?; - let maybe_confidential_transfer_mint_authority: Option = - confidential_transfer_mint.authority.into(); - let confidential_transfer_mint_authority = - maybe_confidential_transfer_mint_authority.ok_or(TokenError::NoAuthorityExists)?; - - if !authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - - if confidential_transfer_mint_authority != *authority_info.key { - return Err(TokenError::OwnerMismatch.into()); - } - - confidential_transfer_mint.auto_approve_new_accounts = auto_approve_new_account; - confidential_transfer_mint.auditor_elgamal_pubkey = *auditor_encryption_pubkey; - Ok(()) -} - -enum ElGamalPubkeySource<'a> { - ProofInstructionOffset(i64), - ElGamalRegistry(&'a ElGamalRegistry), -} - -/// Processes a [`ConfigureAccountWithRegistry`] instruction. -fn process_configure_account_with_registry( - program_id: &Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let _mint_info = next_account_info(account_info_iter)?; - let elgamal_registry_account = next_account_info(account_info_iter)?; - - check_elgamal_registry_program_account(elgamal_registry_account.owner)?; - - // if a payer account for reallcation is provided, then reallocate - if let Ok(payer_info) = next_account_info(account_info_iter) { - let system_program_info = next_account_info(account_info_iter)?; - reallocate_for_configure_account_with_registry( - token_account_info, - payer_info, - system_program_info, - )?; - } - - let elgamal_registry_account_data = &elgamal_registry_account.data.borrow(); - let elgamal_registry_account = - pod_from_bytes::(elgamal_registry_account_data)?; - - let decryptable_zero_balance = PodAeCiphertext::default(); - let maximum_pending_balance_credit_counter = - DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER.into(); - - process_configure_account( - program_id, - accounts, - &decryptable_zero_balance, - &maximum_pending_balance_credit_counter, - ElGamalPubkeySource::ElGamalRegistry(elgamal_registry_account), - ) -} - -fn reallocate_for_configure_account_with_registry<'a>( - token_account_info: &AccountInfo<'a>, - payer_info: &AccountInfo<'a>, - system_program_info: &AccountInfo<'a>, -) -> ProgramResult { - let mut current_extension_types = { - let token_account = token_account_info.data.borrow(); - let account = PodStateWithExtensions::::unpack(&token_account)?; - account.get_extension_types()? - }; - // `try_calculate_account_len` dedupes extension types, so always push - // the `ConfidentialTransferAccount` type - current_extension_types.push(ExtensionType::ConfidentialTransferAccount); - let needed_account_len = - ExtensionType::try_calculate_account_len::(¤t_extension_types)?; - - // if account is already large enough, return early - if token_account_info.data_len() >= needed_account_len { - return Ok(()); - } - - // reallocate - msg!( - "account needs realloc, +{:?} bytes", - needed_account_len - token_account_info.data_len() - ); - token_account_info.realloc(needed_account_len, false)?; - - // if additional lamports needed to remain rent-exempt, transfer them - let rent = Rent::get()?; - let new_rent_exempt_reserve = rent.minimum_balance(needed_account_len); - - let current_lamport_reserve = token_account_info.lamports(); - let lamports_diff = new_rent_exempt_reserve.saturating_sub(current_lamport_reserve); - if lamports_diff > 0 { - invoke( - &system_instruction::transfer(payer_info.key, token_account_info.key, lamports_diff), - &[ - payer_info.clone(), - token_account_info.clone(), - system_program_info.clone(), - ], - )?; - } - - // set account_type, if needed - let mut token_account_data = token_account_info.data.borrow_mut(); - set_account_type::(&mut token_account_data)?; - - Ok(()) -} - -/// Processes a [`ConfigureAccount`] instruction. -fn process_configure_account( - program_id: &Pubkey, - accounts: &[AccountInfo], - decryptable_zero_balance: &DecryptableBalance, - maximum_pending_balance_credit_counter: &PodU64, - elgamal_pubkey_source: ElGamalPubkeySource, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - - let elgamal_pubkey = match elgamal_pubkey_source { - ElGamalPubkeySource::ProofInstructionOffset(offset) => { - // zero-knowledge proof certifies that the supplied ElGamal public key is valid - let proof_context = verify_and_extract_context::< - PubkeyValidityProofData, - PubkeyValidityProofContext, - >(account_info_iter, offset, None)?; - proof_context.pubkey - } - ElGamalPubkeySource::ElGamalRegistry(elgamal_registry_account) => { - let _elgamal_registry_account = next_account_info(account_info_iter)?; - elgamal_registry_account.elgamal_pubkey - } - }; - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - - if token_account.base.mint != *mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - match elgamal_pubkey_source { - ElGamalPubkeySource::ProofInstructionOffset(_) => { - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - } - ElGamalPubkeySource::ElGamalRegistry(elgamal_registry_account) => { - // if ElGamal registry was provided, then just verify that the owners of the - // registry and token accounts match, then skip the signature - // verification check - if elgamal_registry_account.owner != token_account.base.owner { - return Err(TokenError::OwnerMismatch.into()); - } - } - }; - - check_program_account(mint_info.owner)?; - let mint_data = &mut mint_info.data.borrow(); - let mint = PodStateWithExtensions::::unpack(mint_data)?; - let confidential_transfer_mint = mint.get_extension::()?; - - // Note: The caller is expected to use the `Reallocate` instruction to ensure - // there is sufficient room in their token account for the new - // `ConfidentialTransferAccount` extension - let confidential_transfer_account = - token_account.init_extension::(false)?; - - confidential_transfer_account.approved = confidential_transfer_mint.auto_approve_new_accounts; - confidential_transfer_account.elgamal_pubkey = elgamal_pubkey; - confidential_transfer_account.maximum_pending_balance_credit_counter = - *maximum_pending_balance_credit_counter; - - // The all-zero ciphertext [0; 64] is a valid encryption of zero - confidential_transfer_account.pending_balance_lo = EncryptedBalance::zeroed(); - confidential_transfer_account.pending_balance_hi = EncryptedBalance::zeroed(); - confidential_transfer_account.available_balance = EncryptedBalance::zeroed(); - - confidential_transfer_account.decryptable_available_balance = *decryptable_zero_balance; - confidential_transfer_account.allow_confidential_credits = true.into(); - confidential_transfer_account.pending_balance_credit_counter = 0.into(); - confidential_transfer_account.expected_pending_balance_credit_counter = 0.into(); - confidential_transfer_account.actual_pending_balance_credit_counter = 0.into(); - confidential_transfer_account.allow_non_confidential_credits = true.into(); - - // if the mint is extended for fees, then initialize account for confidential - // transfer fees - if mint.get_extension::().is_ok() { - let confidential_transfer_fee_amount = - token_account.init_extension::(false)?; - confidential_transfer_fee_amount.withheld_amount = EncryptedWithheldAmount::zeroed(); - } - - Ok(()) -} - -/// Processes an [`ApproveAccount`] instruction. -fn process_approve_account(accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - - if *mint_info.key != token_account.base.mint { - return Err(TokenError::MintMismatch.into()); - } - - check_program_account(mint_info.owner)?; - let mint_data = &mint_info.data.borrow_mut(); - let mint = PodStateWithExtensions::::unpack(mint_data)?; - let confidential_transfer_mint = mint.get_extension::()?; - let maybe_confidential_transfer_mint_authority: Option = - confidential_transfer_mint.authority.into(); - let confidential_transfer_mint_authority = - maybe_confidential_transfer_mint_authority.ok_or(TokenError::NoAuthorityExists)?; - - if authority_info.is_signer && *authority_info.key == confidential_transfer_mint_authority { - let confidential_transfer_state = - token_account.get_extension_mut::()?; - confidential_transfer_state.approved = true.into(); - Ok(()) - } else { - Err(ProgramError::MissingRequiredSignature) - } -} - -/// Processes an [`EmptyAccount`] instruction. -fn process_empty_account( - program_id: &Pubkey, - accounts: &[AccountInfo], - proof_instruction_offset: i64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - - // zero-knowledge proof certifies that the available balance ciphertext holds - // the balance of 0. - let proof_context = verify_and_extract_context::< - ZeroCiphertextProofData, - ZeroCiphertextProofContext, - >(account_info_iter, proof_instruction_offset, None)?; - - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - - // Check that the encryption public key and ciphertext associated with the - // confidential extension account are consistent with those that were - // actually used to generate the zkp. - if confidential_transfer_account.elgamal_pubkey != proof_context.pubkey { - msg!("Encryption public-key mismatch"); - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - if confidential_transfer_account.available_balance != proof_context.ciphertext { - msg!("Available balance mismatch"); - return Err(ProgramError::InvalidInstructionData); - } - confidential_transfer_account.available_balance = EncryptedBalance::zeroed(); - - // check that all balances are all-zero ciphertexts - confidential_transfer_account.closable()?; - - Ok(()) -} - -/// Processes a [`Deposit`] instruction. -#[cfg(feature = "zk-ops")] -fn process_deposit( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: u8, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - check_program_account(mint_info.owner)?; - let mint_data = &mint_info.data.borrow_mut(); - let mint = PodStateWithExtensions::::unpack(mint_data)?; - - if let Ok(extension) = mint.get_extension::() { - if extension.paused.into() { - return Err(TokenError::MintPaused.into()); - } - } - - if expected_decimals != mint.base.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - - if mint.get_extension::().is_ok() { - return Err(TokenError::IllegalMintBurnConversion.into()); - } - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - if token_account - .get_extension::() - .is_ok() - { - return Err(TokenError::NonTransferable.into()); - } - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - if token_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if token_account.base.mint != *mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - // Wrapped SOL deposits are not supported because lamports cannot be vanished. - assert!(!token_account.base.is_native()); - - token_account.base.amount = u64::from(token_account.base.amount) - .checked_sub(amount) - .ok_or(TokenError::Overflow)? - .into(); - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - confidential_transfer_account.valid_as_destination()?; - - // A deposit amount must be a 48-bit number - let (amount_lo, amount_hi) = verify_and_split_deposit_amount(amount)?; - - // Prevent unnecessary ciphertext arithmetic syscalls if `amount_lo` or - // `amount_hi` is zero - if amount_lo > 0 { - confidential_transfer_account.pending_balance_lo = ciphertext_arithmetic::add_to( - &confidential_transfer_account.pending_balance_lo, - amount_lo, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - } - if amount_hi > 0 { - confidential_transfer_account.pending_balance_hi = ciphertext_arithmetic::add_to( - &confidential_transfer_account.pending_balance_hi, - amount_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - } - - confidential_transfer_account.increment_pending_balance_credit_counter()?; - - Ok(()) -} - -/// Verifies that a deposit amount is a 48-bit number and returns the least -/// significant 16 bits and most significant 32 bits of the amount. -#[cfg(feature = "zk-ops")] -pub fn verify_and_split_deposit_amount(amount: u64) -> Result<(u64, u64), TokenError> { - if amount > MAXIMUM_DEPOSIT_TRANSFER_AMOUNT { - return Err(TokenError::MaximumDepositAmountExceeded); - } - let deposit_amount_lo = amount & (u16::MAX as u64); - let deposit_amount_hi = amount.checked_shr(u16::BITS).unwrap(); - Ok((deposit_amount_lo, deposit_amount_hi)) -} - -/// Processes a [`Withdraw`] instruction. -#[cfg(feature = "zk-ops")] -fn process_withdraw( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: u8, - new_decryptable_available_balance: DecryptableBalance, - equality_proof_instruction_offset: i64, - range_proof_instruction_offset: i64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - - // zero-knowledge proof certifies that the account has enough available balance - // to withdraw the amount. - let proof_context = verify_withdraw_proof( - account_info_iter, - equality_proof_instruction_offset, - range_proof_instruction_offset, - )?; - - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - check_program_account(mint_info.owner)?; - let mint_data = &mint_info.data.borrow_mut(); - let mint = PodStateWithExtensions::::unpack(mint_data)?; - - if expected_decimals != mint.base.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - - if mint.get_extension::().is_ok() { - return Err(TokenError::IllegalMintBurnConversion.into()); - } - - if let Ok(extension) = mint.get_extension::() { - if extension.paused.into() { - return Err(TokenError::MintPaused.into()); - } - } - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - if token_account - .get_extension::() - .is_ok() - { - return Err(TokenError::NonTransferable.into()); - } - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - if token_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if token_account.base.mint != *mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - // Wrapped SOL withdrawals are not supported because lamports cannot be - // apparated. - assert!(!token_account.base.is_native()); - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - confidential_transfer_account.valid_as_source()?; - - // Check that the encryption public key associated with the confidential - // extension is consistent with the public key that was actually used to - // generate the zkp. - if confidential_transfer_account.elgamal_pubkey != proof_context.source_pubkey { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - // Prevent unnecessary ciphertext arithmetic syscalls if the withdraw amount is - // zero - if amount > 0 { - confidential_transfer_account.available_balance = ciphertext_arithmetic::subtract_from( - &confidential_transfer_account.available_balance, - amount, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - } - // Check that the final available balance ciphertext is consistent with the - // actual ciphertext for which the zero-knowledge proof was generated for. - if confidential_transfer_account.available_balance != proof_context.remaining_balance_ciphertext - { - return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); - } - - confidential_transfer_account.decryptable_available_balance = new_decryptable_available_balance; - token_account.base.amount = u64::from(token_account.base.amount) - .checked_add(amount) - .ok_or(TokenError::Overflow)? - .into(); - - Ok(()) -} - -/// Processes a [`Transfer`] or [`TransferWithFee`] instruction. -#[allow(clippy::too_many_arguments)] -#[cfg(feature = "zk-ops")] -fn process_transfer( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_source_decryptable_available_balance: DecryptableBalance, - transfer_amount_auditor_ciphertext_lo: &PodElGamalCiphertext, - transfer_amount_auditor_ciphertext_hi: &PodElGamalCiphertext, - equality_proof_instruction_offset: i64, - transfer_amount_ciphertext_validity_proof_instruction_offset: i64, - fee_sigma_proof_instruction_offset: Option, - fee_ciphertext_validity_proof_instruction_offset: Option, - range_proof_instruction_offset: i64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let source_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - - check_program_account(mint_info.owner)?; - let mint_data = mint_info.data.borrow_mut(); - let mint = PodStateWithExtensions::::unpack(&mint_data)?; - - if let Ok(extension) = mint.get_extension::() { - if extension.paused.into() { - return Err(TokenError::MintPaused.into()); - } - } - - let confidential_transfer_mint = mint.get_extension::()?; - - // A `Transfer` instruction must be accompanied by a zero-knowledge proof - // instruction that certify the validity of the transfer amounts. The kind - // of zero-knowledge proof instruction depends on whether a transfer incurs - // a fee or not. - // - If the mint is not extended for fees or the instruction is for a - // self-transfer, then - // transfer fee is not required. - // - If the mint is extended for fees and the instruction is not a - // self-transfer, then - // transfer fee is required. - let authority_info = if mint.get_extension::().is_err() { - // Transfer fee is not required. Decode the zero-knowledge proof as - // `TransferContext`. - // - // The zero-knowledge proof certifies that: - // 1. the transfer amount is encrypted in the correct form - // 2. the source account has enough balance to send the transfer amount - let proof_context = verify_transfer_proof( - account_info_iter, - equality_proof_instruction_offset, - transfer_amount_ciphertext_validity_proof_instruction_offset, - range_proof_instruction_offset, - )?; - - let authority_info = next_account_info(account_info_iter)?; - - // Check that the auditor encryption public key associated wth the confidential - // mint is consistent with what was actually used to generate the zkp. - if !confidential_transfer_mint - .auditor_elgamal_pubkey - .equals(&proof_context.transfer_pubkeys.auditor) - { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - let proof_context_auditor_ciphertext_lo = proof_context - .ciphertext_lo - .try_extract_ciphertext(2) - .map_err(|e| -> TokenError { e.into() })?; - let proof_context_auditor_ciphertext_hi = proof_context - .ciphertext_hi - .try_extract_ciphertext(2) - .map_err(|e| -> TokenError { e.into() })?; - - check_auditor_ciphertext( - transfer_amount_auditor_ciphertext_lo, - transfer_amount_auditor_ciphertext_hi, - &proof_context_auditor_ciphertext_lo, - &proof_context_auditor_ciphertext_hi, - )?; - - process_source_for_transfer( - program_id, - source_account_info, - mint_info, - authority_info, - account_info_iter.as_slice(), - &proof_context, - new_source_decryptable_available_balance, - )?; - - process_destination_for_transfer(destination_account_info, mint_info, &proof_context)?; - - authority_info - } else { - // Transfer fee is required. - let transfer_fee_config = mint.get_extension::()?; - let fee_parameters = transfer_fee_config.get_epoch_fee(Clock::get()?.epoch); - - let fee_sigma_proof_insruction_offset = - fee_sigma_proof_instruction_offset.ok_or(ProgramError::InvalidInstructionData)?; - let fee_ciphertext_validity_proof_insruction_offset = - fee_ciphertext_validity_proof_instruction_offset - .ok_or(ProgramError::InvalidInstructionData)?; - - // Decode the zero-knowledge proof as `TransferWithFeeContext`. - // - // The zero-knowledge proof certifies that: - // 1. the transfer amount is encrypted in the correct form - // 2. the source account has enough balance to send the transfer amount - // 3. the transfer fee is computed correctly and encrypted in the correct form - let proof_context = verify_transfer_with_fee_proof( - account_info_iter, - equality_proof_instruction_offset, - transfer_amount_ciphertext_validity_proof_instruction_offset, - fee_sigma_proof_insruction_offset, - fee_ciphertext_validity_proof_insruction_offset, - range_proof_instruction_offset, - fee_parameters, - )?; - - let authority_info = next_account_info(account_info_iter)?; - - // Check that the encryption public keys associated with the mint confidential - // transfer and confidential transfer fee extensions are consistent with - // the keys that were used to generate the zkp. - if !confidential_transfer_mint - .auditor_elgamal_pubkey - .equals(&proof_context.transfer_with_fee_pubkeys.auditor) - { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - let confidential_transfer_fee_config = - mint.get_extension::()?; - - // Check that the withdraw withheld authority ElGamal public key in the mint is - // consistent with what was used to generate the zkp. - if proof_context - .transfer_with_fee_pubkeys - .withdraw_withheld_authority - != confidential_transfer_fee_config.withdraw_withheld_authority_elgamal_pubkey - { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - let proof_context_auditor_ciphertext_lo = proof_context - .ciphertext_lo - .try_extract_ciphertext(2) - .map_err(TokenError::from)?; - let proof_context_auditor_ciphertext_hi = proof_context - .ciphertext_hi - .try_extract_ciphertext(2) - .map_err(TokenError::from)?; - - check_auditor_ciphertext( - transfer_amount_auditor_ciphertext_lo, - transfer_amount_auditor_ciphertext_hi, - &proof_context_auditor_ciphertext_lo, - &proof_context_auditor_ciphertext_hi, - )?; - - process_source_for_transfer_with_fee( - program_id, - source_account_info, - mint_info, - authority_info, - account_info_iter.as_slice(), - &proof_context, - new_source_decryptable_available_balance, - )?; - - let is_self_transfer = source_account_info.key == destination_account_info.key; - process_destination_for_transfer_with_fee( - destination_account_info, - mint_info, - &proof_context, - is_self_transfer, - )?; - - authority_info - }; - - if let Some(program_id) = transfer_hook::get_program_id(&mint) { - // set transferring flags, scope the borrow to avoid double-borrow during CPI - { - let mut source_account_data = source_account_info.data.borrow_mut(); - let mut source_account = - PodStateWithExtensionsMut::::unpack(&mut source_account_data)?; - transfer_hook::set_transferring(&mut source_account)?; - } - { - let mut destination_account_data = destination_account_info.data.borrow_mut(); - let mut destination_account = - PodStateWithExtensionsMut::::unpack(&mut destination_account_data)?; - transfer_hook::set_transferring(&mut destination_account)?; - } - - // can't doubly-borrow the mint data either - drop(mint_data); - - // Since the amount is unknown during a confidential transfer, pass in - // u64::MAX as a convention. - spl_transfer_hook_interface::onchain::invoke_execute( - &program_id, - source_account_info.clone(), - mint_info.clone(), - destination_account_info.clone(), - authority_info.clone(), - account_info_iter.as_slice(), - u64::MAX, - )?; - - // unset transferring flag - transfer_hook::unset_transferring(source_account_info)?; - transfer_hook::unset_transferring(destination_account_info)?; - } - - Ok(()) -} - -/// Processes the changes for the sending party of a confidential transfer -#[cfg(feature = "zk-ops")] -fn process_source_for_transfer( - program_id: &Pubkey, - source_account_info: &AccountInfo, - mint_info: &AccountInfo, - authority_info: &AccountInfo, - signers: &[AccountInfo], - proof_context: &TransferProofContext, - new_source_decryptable_available_balance: DecryptableBalance, -) -> ProgramResult { - check_program_account(source_account_info.owner)?; - let authority_info_data_len = authority_info.data_len(); - let token_account_data = &mut source_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - if token_account - .get_extension::() - .is_ok() - { - return Err(TokenError::NonTransferable.into()); - } - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - signers, - )?; - - if token_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if token_account.base.mint != *mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - confidential_transfer_account.valid_as_source()?; - - // Check that the source encryption public key is consistent with what was - // actually used to generate the zkp. - if proof_context.transfer_pubkeys.source != confidential_transfer_account.elgamal_pubkey { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - let source_transfer_amount_lo = proof_context - .ciphertext_lo - .try_extract_ciphertext(0) - .map_err(TokenError::from)?; - let source_transfer_amount_hi = proof_context - .ciphertext_hi - .try_extract_ciphertext(0) - .map_err(TokenError::from)?; - - let new_source_available_balance = ciphertext_arithmetic::subtract_with_lo_hi( - &confidential_transfer_account.available_balance, - &source_transfer_amount_lo, - &source_transfer_amount_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - // Check that the computed available balance is consistent with what was - // actually used to generate the zkp on the client side. - if new_source_available_balance != proof_context.new_source_ciphertext { - return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); - } - - confidential_transfer_account.available_balance = new_source_available_balance; - confidential_transfer_account.decryptable_available_balance = - new_source_decryptable_available_balance; - - Ok(()) -} - -#[cfg(feature = "zk-ops")] -fn process_destination_for_transfer( - destination_account_info: &AccountInfo, - mint_info: &AccountInfo, - proof_context: &TransferProofContext, -) -> ProgramResult { - check_program_account(destination_account_info.owner)?; - let destination_token_account_data = &mut destination_account_info.data.borrow_mut(); - let mut destination_token_account = - PodStateWithExtensionsMut::::unpack(destination_token_account_data)?; - - if destination_token_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if destination_token_account.base.mint != *mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - if memo_required(&destination_token_account) { - check_previous_sibling_instruction_is_memo()?; - } - - let destination_confidential_transfer_account = - destination_token_account.get_extension_mut::()?; - destination_confidential_transfer_account.valid_as_destination()?; - - if proof_context.transfer_pubkeys.destination - != destination_confidential_transfer_account.elgamal_pubkey - { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - let destination_ciphertext_lo = proof_context - .ciphertext_lo - .try_extract_ciphertext(1) - .map_err(TokenError::from)?; - let destination_ciphertext_hi = proof_context - .ciphertext_hi - .try_extract_ciphertext(1) - .map_err(TokenError::from)?; - - destination_confidential_transfer_account.pending_balance_lo = ciphertext_arithmetic::add( - &destination_confidential_transfer_account.pending_balance_lo, - &destination_ciphertext_lo, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - destination_confidential_transfer_account.pending_balance_hi = ciphertext_arithmetic::add( - &destination_confidential_transfer_account.pending_balance_hi, - &destination_ciphertext_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - destination_confidential_transfer_account.increment_pending_balance_credit_counter()?; - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -#[cfg(feature = "zk-ops")] -fn process_source_for_transfer_with_fee( - program_id: &Pubkey, - source_account_info: &AccountInfo, - mint_info: &AccountInfo, - authority_info: &AccountInfo, - signers: &[AccountInfo], - proof_context: &TransferWithFeeProofContext, - new_source_decryptable_available_balance: DecryptableBalance, -) -> ProgramResult { - check_program_account(source_account_info.owner)?; - let authority_info_data_len = authority_info.data_len(); - let token_account_data = &mut source_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - if token_account - .get_extension::() - .is_ok() - { - return Err(TokenError::NonTransferable.into()); - } - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - signers, - )?; - - if token_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if token_account.base.mint != *mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - confidential_transfer_account.valid_as_source()?; - - // Check that the source encryption public key is consistent with what was - // actually used to generate the zkp. - if proof_context.transfer_with_fee_pubkeys.source - != confidential_transfer_account.elgamal_pubkey - { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - let source_transfer_amount_lo = proof_context - .ciphertext_lo - .try_extract_ciphertext(0) - .map_err(TokenError::from)?; - let source_transfer_amount_hi = proof_context - .ciphertext_hi - .try_extract_ciphertext(0) - .map_err(TokenError::from)?; - - let new_source_available_balance = ciphertext_arithmetic::subtract_with_lo_hi( - &confidential_transfer_account.available_balance, - &source_transfer_amount_lo, - &source_transfer_amount_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - // Check that the computed available balance is consistent with what was - // actually used to generate the zkp on the client side. - if new_source_available_balance != proof_context.new_source_ciphertext { - return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); - } - - confidential_transfer_account.available_balance = new_source_available_balance; - confidential_transfer_account.decryptable_available_balance = - new_source_decryptable_available_balance; - - Ok(()) -} - -#[cfg(feature = "zk-ops")] -fn process_destination_for_transfer_with_fee( - destination_account_info: &AccountInfo, - mint_info: &AccountInfo, - proof_context: &TransferWithFeeProofContext, - is_self_transfer: bool, -) -> ProgramResult { - check_program_account(destination_account_info.owner)?; - let destination_token_account_data = &mut destination_account_info.data.borrow_mut(); - let mut destination_token_account = - PodStateWithExtensionsMut::::unpack(destination_token_account_data)?; - - if destination_token_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if destination_token_account.base.mint != *mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - if memo_required(&destination_token_account) { - check_previous_sibling_instruction_is_memo()?; - } - - let destination_confidential_transfer_account = - destination_token_account.get_extension_mut::()?; - destination_confidential_transfer_account.valid_as_destination()?; - - if proof_context.transfer_with_fee_pubkeys.destination - != destination_confidential_transfer_account.elgamal_pubkey - { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - - let destination_transfer_amount_lo = proof_context - .ciphertext_lo - .try_extract_ciphertext(1) - .map_err(TokenError::from)?; - let destination_transfer_amount_hi = proof_context - .ciphertext_hi - .try_extract_ciphertext(1) - .map_err(TokenError::from)?; - - destination_confidential_transfer_account.pending_balance_lo = ciphertext_arithmetic::add( - &destination_confidential_transfer_account.pending_balance_lo, - &destination_transfer_amount_lo, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - destination_confidential_transfer_account.pending_balance_hi = ciphertext_arithmetic::add( - &destination_confidential_transfer_account.pending_balance_hi, - &destination_transfer_amount_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - destination_confidential_transfer_account.increment_pending_balance_credit_counter()?; - - // process transfer fee - if !is_self_transfer { - // Decode lo and hi fee amounts encrypted under the destination encryption - // public key - let destination_fee_lo = proof_context - .fee_ciphertext_lo - .try_extract_ciphertext(0) - .map_err(TokenError::from)?; - let destination_fee_hi = proof_context - .fee_ciphertext_hi - .try_extract_ciphertext(0) - .map_err(TokenError::from)?; - - // Subtract the fee amount from the destination pending balance - destination_confidential_transfer_account.pending_balance_lo = - ciphertext_arithmetic::subtract( - &destination_confidential_transfer_account.pending_balance_lo, - &destination_fee_lo, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - destination_confidential_transfer_account.pending_balance_hi = - ciphertext_arithmetic::subtract( - &destination_confidential_transfer_account.pending_balance_hi, - &destination_fee_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - // Decode lo and hi fee amounts encrypted under the withdraw authority - // encryption public key - let withdraw_withheld_authority_fee_lo = proof_context - .fee_ciphertext_lo - .try_extract_ciphertext(1) - .map_err(TokenError::from)?; - let withdraw_withheld_authority_fee_hi = proof_context - .fee_ciphertext_hi - .try_extract_ciphertext(1) - .map_err(TokenError::from)?; - - let destination_confidential_transfer_fee_amount = - destination_token_account.get_extension_mut::()?; - - // Add the fee amount to the destination withheld fee - destination_confidential_transfer_fee_amount.withheld_amount = - ciphertext_arithmetic::add_with_lo_hi( - &destination_confidential_transfer_fee_amount.withheld_amount, - &withdraw_withheld_authority_fee_lo, - &withdraw_withheld_authority_fee_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - } - - Ok(()) -} - -/// Processes an [`ApplyPendingBalance`] instruction. -#[cfg(feature = "zk-ops")] -fn process_apply_pending_balance( - program_id: &Pubkey, - accounts: &[AccountInfo], - ApplyPendingBalanceData { - expected_pending_balance_credit_counter, - new_decryptable_available_balance, - }: &ApplyPendingBalanceData, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - - confidential_transfer_account.available_balance = ciphertext_arithmetic::add_with_lo_hi( - &confidential_transfer_account.available_balance, - &confidential_transfer_account.pending_balance_lo, - &confidential_transfer_account.pending_balance_hi, - ) - .ok_or(TokenError::CiphertextArithmeticFailed)?; - - confidential_transfer_account.actual_pending_balance_credit_counter = - confidential_transfer_account.pending_balance_credit_counter; - confidential_transfer_account.expected_pending_balance_credit_counter = - *expected_pending_balance_credit_counter; - confidential_transfer_account.decryptable_available_balance = - *new_decryptable_available_balance; - confidential_transfer_account.pending_balance_credit_counter = 0.into(); - confidential_transfer_account.pending_balance_lo = EncryptedBalance::zeroed(); - confidential_transfer_account.pending_balance_hi = EncryptedBalance::zeroed(); - - Ok(()) -} - -/// Processes a [`DisableConfidentialCredits`] or [`EnableConfidentialCredits`] -/// instruction. -fn process_allow_confidential_credits( - program_id: &Pubkey, - accounts: &[AccountInfo], - allow_confidential_credits: bool, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - confidential_transfer_account.allow_confidential_credits = allow_confidential_credits.into(); - - Ok(()) -} - -/// Processes an [`DisableNonConfidentialCredits`] or -/// [`EnableNonConfidentialCredits`] instruction. -fn process_allow_non_confidential_credits( - program_id: &Pubkey, - accounts: &[AccountInfo], - allow_non_confidential_credits: bool, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - check_program_account(token_account_info.owner)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; - - Processor::validate_owner( - program_id, - &token_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - let confidential_transfer_account = - token_account.get_extension_mut::()?; - confidential_transfer_account.allow_non_confidential_credits = - allow_non_confidential_credits.into(); - - Ok(()) -} - -#[allow(dead_code)] -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - - match decode_instruction_type(input)? { - ConfidentialTransferInstruction::InitializeMint => { - msg!("ConfidentialTransferInstruction::InitializeMint"); - let data = decode_instruction_data::(input)?; - process_initialize_mint( - accounts, - &data.authority, - data.auto_approve_new_accounts, - &data.auditor_elgamal_pubkey, - ) - } - ConfidentialTransferInstruction::UpdateMint => { - msg!("ConfidentialTransferInstruction::UpdateMint"); - let data = decode_instruction_data::(input)?; - process_update_mint( - accounts, - data.auto_approve_new_accounts, - &data.auditor_elgamal_pubkey, - ) - } - ConfidentialTransferInstruction::ConfigureAccount => { - msg!("ConfidentialTransferInstruction::ConfigureAccount"); - let data = decode_instruction_data::(input)?; - process_configure_account( - program_id, - accounts, - &data.decryptable_zero_balance, - &data.maximum_pending_balance_credit_counter, - ElGamalPubkeySource::ProofInstructionOffset(data.proof_instruction_offset as i64), - ) - } - ConfidentialTransferInstruction::ApproveAccount => { - msg!("ConfidentialTransferInstruction::ApproveAccount"); - process_approve_account(accounts) - } - ConfidentialTransferInstruction::EmptyAccount => { - msg!("ConfidentialTransferInstruction::EmptyAccount"); - let data = decode_instruction_data::(input)?; - process_empty_account(program_id, accounts, data.proof_instruction_offset as i64) - } - ConfidentialTransferInstruction::Deposit => { - msg!("ConfidentialTransferInstruction::Deposit"); - #[cfg(feature = "zk-ops")] - { - let data = decode_instruction_data::(input)?; - process_deposit(program_id, accounts, data.amount.into(), data.decimals) - } - #[cfg(not(feature = "zk-ops"))] - Err(ProgramError::InvalidInstructionData) - } - ConfidentialTransferInstruction::Withdraw => { - msg!("ConfidentialTransferInstruction::Withdraw"); - #[cfg(feature = "zk-ops")] - { - let data = decode_instruction_data::(input)?; - process_withdraw( - program_id, - accounts, - data.amount.into(), - data.decimals, - data.new_decryptable_available_balance, - data.equality_proof_instruction_offset as i64, - data.range_proof_instruction_offset as i64, - ) - } - #[cfg(not(feature = "zk-ops"))] - Err(ProgramError::InvalidInstructionData) - } - ConfidentialTransferInstruction::Transfer => { - msg!("ConfidentialTransferInstruction::Transfer"); - #[cfg(feature = "zk-ops")] - { - let data = decode_instruction_data::(input)?; - process_transfer( - program_id, - accounts, - data.new_source_decryptable_available_balance, - &data.transfer_amount_auditor_ciphertext_lo, - &data.transfer_amount_auditor_ciphertext_hi, - data.equality_proof_instruction_offset as i64, - data.ciphertext_validity_proof_instruction_offset as i64, - None, - None, - data.range_proof_instruction_offset as i64, - ) - } - #[cfg(not(feature = "zk-ops"))] - Err(ProgramError::InvalidInstructionData) - } - ConfidentialTransferInstruction::ApplyPendingBalance => { - msg!("ConfidentialTransferInstruction::ApplyPendingBalance"); - #[cfg(feature = "zk-ops")] - { - process_apply_pending_balance( - program_id, - accounts, - decode_instruction_data::(input)?, - ) - } - #[cfg(not(feature = "zk-ops"))] - { - Err(ProgramError::InvalidInstructionData) - } - } - ConfidentialTransferInstruction::DisableConfidentialCredits => { - msg!("ConfidentialTransferInstruction::DisableConfidentialCredits"); - process_allow_confidential_credits(program_id, accounts, false) - } - ConfidentialTransferInstruction::EnableConfidentialCredits => { - msg!("ConfidentialTransferInstruction::EnableConfidentialCredits"); - process_allow_confidential_credits(program_id, accounts, true) - } - ConfidentialTransferInstruction::DisableNonConfidentialCredits => { - msg!("ConfidentialTransferInstruction::DisableNonConfidentialCredits"); - process_allow_non_confidential_credits(program_id, accounts, false) - } - ConfidentialTransferInstruction::EnableNonConfidentialCredits => { - msg!("ConfidentialTransferInstruction::EnableNonConfidentialCredits"); - process_allow_non_confidential_credits(program_id, accounts, true) - } - ConfidentialTransferInstruction::TransferWithFee => { - msg!("ConfidentialTransferInstruction::TransferWithFee"); - #[cfg(feature = "zk-ops")] - { - let data = decode_instruction_data::(input)?; - process_transfer( - program_id, - accounts, - data.new_source_decryptable_available_balance, - &data.transfer_amount_auditor_ciphertext_lo, - &data.transfer_amount_auditor_ciphertext_hi, - data.equality_proof_instruction_offset as i64, - data.transfer_amount_ciphertext_validity_proof_instruction_offset as i64, - Some(data.fee_sigma_proof_instruction_offset as i64), - Some(data.fee_ciphertext_validity_proof_instruction_offset as i64), - data.range_proof_instruction_offset as i64, - ) - } - #[cfg(not(feature = "zk-ops"))] - Err(ProgramError::InvalidInstructionData) - } - ConfidentialTransferInstruction::ConfigureAccountWithRegistry => { - msg!("ConfidentialTransferInstruction::ConfigureAccountWithRegistry"); - process_configure_account_with_registry(program_id, accounts) - } - } -} diff --git a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs b/token/program-2022/src/extension/confidential_transfer/verify_proof.rs deleted file mode 100644 index 88df4074436..00000000000 --- a/token/program-2022/src/extension/confidential_transfer/verify_proof.rs +++ /dev/null @@ -1,197 +0,0 @@ -use { - crate::{ - error::TokenError, - extension::{confidential_transfer::instruction::*, transfer_fee::TransferFee}, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - program_error::ProgramError, - }, - spl_token_confidential_transfer_proof_extraction::{ - instruction::verify_and_extract_context, transfer::TransferProofContext, - transfer_with_fee::TransferWithFeeProofContext, withdraw::WithdrawProofContext, - }, - std::slice::Iter, -}; - -/// Verify zero-knowledge proofs needed for a [Withdraw] instruction and return -/// the corresponding proof context. -#[cfg(feature = "zk-ops")] -pub fn verify_withdraw_proof( - account_info_iter: &mut Iter, - equality_proof_instruction_offset: i64, - range_proof_instruction_offset: i64, -) -> Result { - let sysvar_account_info = - if equality_proof_instruction_offset != 0 || range_proof_instruction_offset != 0 { - Some(next_account_info(account_info_iter)?) - } else { - None - }; - - let equality_proof_context = verify_and_extract_context::< - CiphertextCommitmentEqualityProofData, - CiphertextCommitmentEqualityProofContext, - >( - account_info_iter, - equality_proof_instruction_offset, - sysvar_account_info, - )?; - - let range_proof_context = - verify_and_extract_context::( - account_info_iter, - range_proof_instruction_offset, - sysvar_account_info, - )?; - - // The `WithdrawProofContext` constructor verifies the consistency of the - // individual proof context and generates a `WithdrawProofContext` struct - // that is used to process the rest of the token-2022 logic. - let transfer_proof_context = - WithdrawProofContext::verify_and_extract(&equality_proof_context, &range_proof_context) - .map_err(|e| -> TokenError { e.into() })?; - - Ok(transfer_proof_context) -} - -/// Verify zero-knowledge proof needed for a [Transfer] instruction without fee -/// and return the corresponding proof context. -#[cfg(feature = "zk-ops")] -pub fn verify_transfer_proof( - account_info_iter: &mut Iter, - equality_proof_instruction_offset: i64, - ciphertext_validity_proof_instruction_offset: i64, - range_proof_instruction_offset: i64, -) -> Result { - let sysvar_account_info = if equality_proof_instruction_offset != 0 - || ciphertext_validity_proof_instruction_offset != 0 - || range_proof_instruction_offset != 0 - { - Some(next_account_info(account_info_iter)?) - } else { - None - }; - - let equality_proof_context = verify_and_extract_context::< - CiphertextCommitmentEqualityProofData, - CiphertextCommitmentEqualityProofContext, - >( - account_info_iter, - equality_proof_instruction_offset, - sysvar_account_info, - )?; - - let ciphertext_validity_proof_context = verify_and_extract_context::< - BatchedGroupedCiphertext3HandlesValidityProofData, - BatchedGroupedCiphertext3HandlesValidityProofContext, - >( - account_info_iter, - ciphertext_validity_proof_instruction_offset, - sysvar_account_info, - )?; - - let range_proof_context = - verify_and_extract_context::( - account_info_iter, - range_proof_instruction_offset, - sysvar_account_info, - )?; - - // The `TransferProofContext` constructor verifies the consistency of the - // individual proof context and generates a `TransferWithFeeProofInfo` struct - // that is used to process the rest of the token-2022 logic. - let transfer_proof_context = TransferProofContext::verify_and_extract( - &equality_proof_context, - &ciphertext_validity_proof_context, - &range_proof_context, - ) - .map_err(|e| -> TokenError { e.into() })?; - - Ok(transfer_proof_context) -} - -/// Verify zero-knowledge proof needed for a [Transfer] instruction with fee and -/// return the corresponding proof context. -#[cfg(feature = "zk-ops")] -#[allow(clippy::too_many_arguments)] -pub fn verify_transfer_with_fee_proof( - account_info_iter: &mut Iter, - equality_proof_instruction_offset: i64, - transfer_amount_ciphertext_validity_proof_instruction_offset: i64, - fee_sigma_proof_instruction_offset: i64, - fee_ciphertext_validity_proof_instruction_offset: i64, - range_proof_instruction_offset: i64, - fee_parameters: &TransferFee, -) -> Result { - let sysvar_account_info = if equality_proof_instruction_offset != 0 - || transfer_amount_ciphertext_validity_proof_instruction_offset != 0 - || fee_sigma_proof_instruction_offset != 0 - || fee_ciphertext_validity_proof_instruction_offset != 0 - || range_proof_instruction_offset != 0 - { - Some(next_account_info(account_info_iter)?) - } else { - None - }; - - let equality_proof_context = verify_and_extract_context::< - CiphertextCommitmentEqualityProofData, - CiphertextCommitmentEqualityProofContext, - >( - account_info_iter, - equality_proof_instruction_offset, - sysvar_account_info, - )?; - - let transfer_amount_ciphertext_validity_proof_context = verify_and_extract_context::< - BatchedGroupedCiphertext3HandlesValidityProofData, - BatchedGroupedCiphertext3HandlesValidityProofContext, - >( - account_info_iter, - transfer_amount_ciphertext_validity_proof_instruction_offset, - sysvar_account_info, - )?; - - let fee_sigma_proof_context = - verify_and_extract_context::( - account_info_iter, - fee_sigma_proof_instruction_offset, - sysvar_account_info, - )?; - - let fee_ciphertext_validity_proof_context = verify_and_extract_context::< - BatchedGroupedCiphertext2HandlesValidityProofData, - BatchedGroupedCiphertext2HandlesValidityProofContext, - >( - account_info_iter, - fee_ciphertext_validity_proof_instruction_offset, - sysvar_account_info, - )?; - - let range_proof_context = - verify_and_extract_context::( - account_info_iter, - range_proof_instruction_offset, - sysvar_account_info, - )?; - - // The `TransferWithFeeProofContext` constructor verifies the consistency of - // the individual proof context and generates a - // `TransferWithFeeProofInfo` struct that is used to process the rest of - // the token-2022 logic. The consistency check includes verifying - // whether the fee-related zkps were generated with respect to the correct fee - // parameter that is stored in the mint extension. - let transfer_with_fee_proof_context = TransferWithFeeProofContext::verify_and_extract( - &equality_proof_context, - &transfer_amount_ciphertext_validity_proof_context, - &fee_sigma_proof_context, - &fee_ciphertext_validity_proof_context, - &range_proof_context, - fee_parameters.transfer_fee_basis_points.into(), - fee_parameters.maximum_fee.into(), - ) - .map_err(|e| -> TokenError { e.into() })?; - - Ok(transfer_with_fee_proof_context) -} diff --git a/token/program-2022/src/extension/confidential_transfer_fee/account_info.rs b/token/program-2022/src/extension/confidential_transfer_fee/account_info.rs deleted file mode 100644 index ee87dd2f5a8..00000000000 --- a/token/program-2022/src/extension/confidential_transfer_fee/account_info.rs +++ /dev/null @@ -1,60 +0,0 @@ -use { - crate::{error::TokenError, extension::confidential_transfer_fee::EncryptedWithheldAmount}, - bytemuck::{Pod, Zeroable}, - solana_zk_sdk::{ - encryption::{ - elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey}, - pedersen::PedersenOpening, - }, - zk_elgamal_proof_program::proof_data::ciphertext_ciphertext_equality::CiphertextCiphertextEqualityProofData, - }, -}; - -/// Confidential transfer fee extension information needed to construct a -/// `WithdrawWithheldTokensFromMint` or `WithdrawWithheldTokensFromAccounts` -/// instruction. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct WithheldTokensInfo { - /// The available balance - pub(crate) withheld_amount: EncryptedWithheldAmount, -} -impl WithheldTokensInfo { - /// Create a `WithheldTokensInfo` from an ElGamal ciphertext. - pub fn new(withheld_amount: &EncryptedWithheldAmount) -> Self { - Self { - withheld_amount: *withheld_amount, - } - } - - /// Create withdraw withheld proof data. - pub fn generate_proof_data( - &self, - withdraw_withheld_authority_elgamal_keypair: &ElGamalKeypair, - destination_elgamal_pubkey: &ElGamalPubkey, - ) -> Result { - let withheld_amount_in_mint: ElGamalCiphertext = self - .withheld_amount - .try_into() - .map_err(|_| TokenError::AccountDecryption)?; - - let decrypted_withheld_amount_in_mint = withheld_amount_in_mint - .decrypt_u32(withdraw_withheld_authority_elgamal_keypair.secret()) - .ok_or(TokenError::AccountDecryption)?; - - let destination_opening = PedersenOpening::new_rand(); - - let destination_ciphertext = destination_elgamal_pubkey - .encrypt_with(decrypted_withheld_amount_in_mint, &destination_opening); - - CiphertextCiphertextEqualityProofData::new( - withdraw_withheld_authority_elgamal_keypair, - destination_elgamal_pubkey, - &withheld_amount_in_mint, - &destination_ciphertext, - &destination_opening, - decrypted_withheld_amount_in_mint, - ) - .map_err(|_| TokenError::ProofGeneration) - } -} diff --git a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs b/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs deleted file mode 100644 index 1d140e5af01..00000000000 --- a/token/program-2022/src/extension/confidential_transfer_fee/instruction.rs +++ /dev/null @@ -1,579 +0,0 @@ -#[cfg(feature = "serde-traits")] -use { - crate::serialization::{aeciphertext_fromstr, elgamalpubkey_fromstr}, - serde::{Deserialize, Serialize}, -}; -use { - crate::{ - check_program_account, - error::TokenError, - extension::confidential_transfer::{ - instruction::CiphertextCiphertextEqualityProofData, DecryptableBalance, - }, - instruction::{encode_instruction, TokenInstruction}, - solana_zk_sdk::{ - encryption::pod::elgamal::PodElGamalPubkey, - zk_elgamal_proof_program::instruction::ProofInstruction, - }, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - sysvar, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation}, - std::convert::TryFrom, -}; - -/// Confidential Transfer extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] -#[repr(u8)] -pub enum ConfidentialTransferFeeInstruction { - /// Initializes confidential transfer fees for a mint. - /// - /// The `ConfidentialTransferFeeInstruction::InitializeConfidentialTransferFeeConfig` - /// instruction requires no signers and MUST be included within the same - /// Transaction as `TokenInstruction::InitializeMint`. Otherwise another - /// party can initialize the configuration. - /// - /// The instruction fails if the `TokenInstruction::InitializeMint` - /// instruction has already executed for the mint. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The SPL Token mint. - /// - /// Data expected by this instruction: - /// `InitializeConfidentialTransferFeeConfigData` - InitializeConfidentialTransferFeeConfig, - - /// Transfer all withheld confidential tokens in the mint to an account. - /// Signed by the mint's withdraw withheld tokens authority. - /// - /// The withheld confidential tokens are aggregated directly into the - /// destination available balance. - /// - /// In order for this instruction to be successfully processed, it must be - /// accompanied by the `VerifyCiphertextCiphertextEquality` instruction - /// of the `zk_elgamal_proof` program in the same transaction or the - /// address of a context state account for the proof must be provided. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The token mint. Must include the `TransferFeeConfig` - /// extension. - /// 1. `[writable]` The fee receiver account. Must include the - /// `TransferFeeAmount` and `ConfidentialTransferAccount` extensions. - /// 2. `[]` Instructions sysvar if `VerifyCiphertextCiphertextEquality` is - /// included in the same transaction or context state account if - /// `VerifyCiphertextCiphertextEquality` is pre-verified into a context - /// state account. - /// 3. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 4. `[signer]` The mint's `withdraw_withheld_authority`. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The token mint. Must include the `TransferFeeConfig` - /// extension. - /// 1. `[writable]` The fee receiver account. Must include the - /// `TransferFeeAmount` and `ConfidentialTransferAccount` extensions. - /// 2. `[]` Instructions sysvar if `VerifyCiphertextCiphertextEquality` is - /// included in the same transaction or context state account if - /// `VerifyCiphertextCiphertextEquality` is pre-verified into a context - /// state account. - /// 3. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 4. `[]` The mint's multisig `withdraw_withheld_authority`. - /// 5. ..`5+M` `[signer]` M signer accounts. - /// - /// Data expected by this instruction: - /// `WithdrawWithheldTokensFromMintData` - WithdrawWithheldTokensFromMint, - - /// Transfer all withheld tokens to an account. Signed by the mint's - /// withdraw withheld tokens authority. This instruction is susceptible - /// to front-running. Use `HarvestWithheldTokensToMint` and - /// `WithdrawWithheldTokensFromMint` as an alternative. - /// - /// The withheld confidential tokens are aggregated directly into the - /// destination available balance. - /// - /// Note on front-running: This instruction requires a zero-knowledge proof - /// verification instruction that is checked with respect to the account - /// state (the currently withheld fees). Suppose that a withdraw - /// withheld authority generates the - /// `WithdrawWithheldTokensFromAccounts` instruction along with a - /// corresponding zero-knowledge proof for a specified set of accounts, - /// and submits it on chain. If the withheld fees at any - /// of the specified accounts change before the - /// `WithdrawWithheldTokensFromAccounts` is executed on chain, the - /// zero-knowledge proof will not verify with respect to the new state, - /// forcing the transaction to fail. - /// - /// If front-running occurs, then users can look up the updated states of - /// the accounts, generate a new zero-knowledge proof and try again. - /// Alternatively, withdraw withheld authority can first move the - /// withheld amount to the mint using `HarvestWithheldTokensToMint` and - /// then move the withheld fees from mint to a specified destination - /// account using `WithdrawWithheldTokensFromMint`. - /// - /// In order for this instruction to be successfully processed, it must be - /// accompanied by the `VerifyWithdrawWithheldTokens` instruction of the - /// `zk_elgamal_proof` program in the same transaction or the address of a - /// context state account for the proof must be provided. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[]` The token mint. Must include the `TransferFeeConfig` - /// extension. - /// 1. `[writable]` The fee receiver account. Must include the - /// `TransferFeeAmount` and `ConfidentialTransferAccount` extensions. - /// 2. `[]` Instructions sysvar if `VerifyCiphertextCiphertextEquality` is - /// included in the same transaction or context state account if - /// `VerifyCiphertextCiphertextEquality` is pre-verified into a context - /// state account. - /// 3. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 4. `[signer]` The mint's `withdraw_withheld_authority`. - /// 5. ..`5+N` `[writable]` The source accounts to withdraw from. - /// - /// * Multisignature owner/delegate - /// 0. `[]` The token mint. Must include the `TransferFeeConfig` - /// extension. - /// 1. `[writable]` The fee receiver account. Must include the - /// `TransferFeeAmount` and `ConfidentialTransferAccount` extensions. - /// 2. `[]` Instructions sysvar if `VerifyCiphertextCiphertextEquality` is - /// included in the same transaction or context state account if - /// `VerifyCiphertextCiphertextEquality` is pre-verified into a context - /// state account. - /// 3. `[]` (Optional) Record account if the accompanying proof is to be - /// read from a record account. - /// 4. `[]` The mint's multisig `withdraw_withheld_authority`. - /// 5. ..`5+M` `[signer]` M signer accounts. - /// 6. `5+M+1..5+M+N` `[writable]` The source accounts to withdraw from. - /// - /// Data expected by this instruction: - /// `WithdrawWithheldTokensFromAccountsData` - WithdrawWithheldTokensFromAccounts, - - /// Permissionless instruction to transfer all withheld confidential tokens - /// to the mint. - /// - /// Succeeds for frozen accounts. - /// - /// Accounts provided should include both the `TransferFeeAmount` and - /// `ConfidentialTransferAccount` extension. If not, the account is skipped. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint. - /// 1. ..`1+N` `[writable]` The source accounts to harvest from. - /// - /// Data expected by this instruction: - /// None - HarvestWithheldTokensToMint, - - /// Configure a confidential transfer fee mint to accept harvested - /// confidential fees. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The token mint. - /// 1. `[signer]` The confidential transfer fee authority. - /// - /// *Multisignature owner/delegate - /// 0. `[writable]` The token mint. - /// 1. `[]` The confidential transfer fee multisig authority, - /// 2. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// None - EnableHarvestToMint, - - /// Configure a confidential transfer fee mint to reject any harvested - /// confidential fees. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The token mint. - /// 1. `[signer]` The confidential transfer fee authority. - /// - /// *Multisignature owner/delegate - /// 0. `[writable]` The token mint. - /// 1. `[]` The confidential transfer fee multisig authority, - /// 2. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - /// - /// Data expected by this instruction: - /// None - DisableHarvestToMint, -} - -/// Data expected by `InitializeConfidentialTransferFeeConfig` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeConfidentialTransferFeeConfigData { - /// confidential transfer fee authority - pub authority: OptionalNonZeroPubkey, - - /// ElGamal public key used to encrypt withheld fees. - #[cfg_attr(feature = "serde-traits", serde(with = "elgamalpubkey_fromstr"))] - pub withdraw_withheld_authority_elgamal_pubkey: PodElGamalPubkey, -} - -/// Data expected by -/// `ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromMint` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct WithdrawWithheldTokensFromMintData { - /// Relative location of the `ProofInstruction::VerifyWithdrawWithheld` - /// instruction to the `WithdrawWithheldTokensFromMint` instruction in - /// the transaction. If the offset is `0`, then use a context state - /// account for the proof. - pub proof_instruction_offset: i8, - /// The new decryptable balance in the destination token account. - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub new_decryptable_available_balance: DecryptableBalance, -} - -/// Data expected by -/// `ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromAccounts` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct WithdrawWithheldTokensFromAccountsData { - /// Number of token accounts harvested - pub num_token_accounts: u8, - /// Relative location of the `ProofInstruction::VerifyWithdrawWithheld` - /// instruction to the `VerifyWithdrawWithheldTokensFromAccounts` - /// instruction in the transaction. If the offset is `0`, then use a - /// context state account for the proof. - pub proof_instruction_offset: i8, - /// The new decryptable balance in the destination token account. - #[cfg_attr(feature = "serde-traits", serde(with = "aeciphertext_fromstr"))] - pub new_decryptable_available_balance: DecryptableBalance, -} - -/// Create a `InitializeConfidentialTransferFeeConfig` instruction -pub fn initialize_confidential_transfer_fee_config( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: Option, - withdraw_withheld_authority_elgamal_pubkey: &PodElGamalPubkey, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferFeeExtension, - ConfidentialTransferFeeInstruction::InitializeConfidentialTransferFeeConfig, - &InitializeConfidentialTransferFeeConfigData { - authority: authority.try_into()?, - withdraw_withheld_authority_elgamal_pubkey: *withdraw_withheld_authority_elgamal_pubkey, - }, - )) -} - -/// Create an inner `WithdrawWithheldTokensFromMint` instruction -/// -/// This instruction is suitable for use with a cross-program `invoke` -pub fn inner_withdraw_withheld_tokens_from_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - new_decryptable_available_balance: &DecryptableBalance, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new(*destination, false), - ]; - - let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferFeeExtension, - ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromMint, - &WithdrawWithheldTokensFromMintData { - proof_instruction_offset, - new_decryptable_available_balance: *new_decryptable_available_balance, - }, - )) -} - -/// Create an `WithdrawWithheldTokensFromMint` instruction -pub fn withdraw_withheld_tokens_from_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - new_decryptable_available_balance: &DecryptableBalance, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - proof_data_location: ProofLocation, -) -> Result, ProgramError> { - let mut instructions = vec![inner_withdraw_withheld_tokens_from_mint( - token_program_id, - mint, - destination, - new_decryptable_available_balance, - authority, - multisig_signers, - proof_data_location, - )?]; - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - proof_data_location - { - // This constructor appends the proof instruction right after the - // `WithdrawWithheldTokensFromMint` instruction. This means that the proof - // instruction offset must be always be 1. To use an arbitrary proof - // instruction offset, use the - // `inner_withdraw_withheld_tokens_from_mint` constructor. - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 1 { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyCiphertextCiphertextEquality - .encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyCiphertextCiphertextEquality - .encode_verify_proof_from_account(None, address, offset), - ), - }; - }; - - Ok(instructions) -} - -/// Create an inner `WithdrawWithheldTokensFromMint` instruction -/// -/// This instruction is suitable for use with a cross-program `invoke` -#[allow(clippy::too_many_arguments)] -pub fn inner_withdraw_withheld_tokens_from_accounts( - token_program_id: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - new_decryptable_available_balance: &DecryptableBalance, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - sources: &[&Pubkey], - proof_data_location: ProofLocation, -) -> Result { - check_program_account(token_program_id)?; - let num_token_accounts = - u8::try_from(sources.len()).map_err(|_| ProgramError::InvalidInstructionData)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new(*destination, false), - ]; - - let proof_instruction_offset = match proof_data_location { - ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) => { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false)); - if let ProofData::RecordAccount(record_address, _) = proof_data { - accounts.push(AccountMeta::new_readonly(*record_address, false)); - } - proof_instruction_offset.into() - } - ProofLocation::ContextStateAccount(context_state_account) => { - accounts.push(AccountMeta::new_readonly(*context_state_account, false)); - 0 - } - }; - - accounts.push(AccountMeta::new_readonly( - *authority, - multisig_signers.is_empty(), - )); - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - for source in sources.iter() { - accounts.push(AccountMeta::new(**source, false)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferFeeExtension, - ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromAccounts, - &WithdrawWithheldTokensFromAccountsData { - proof_instruction_offset, - num_token_accounts, - new_decryptable_available_balance: *new_decryptable_available_balance, - }, - )) -} - -/// Create a `WithdrawWithheldTokensFromAccounts` instruction -#[allow(clippy::too_many_arguments)] -pub fn withdraw_withheld_tokens_from_accounts( - token_program_id: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - new_decryptable_available_balance: &DecryptableBalance, - authority: &Pubkey, - multisig_signers: &[&Pubkey], - sources: &[&Pubkey], - proof_data_location: ProofLocation, -) -> Result, ProgramError> { - let mut instructions = vec![inner_withdraw_withheld_tokens_from_accounts( - token_program_id, - mint, - destination, - new_decryptable_available_balance, - authority, - multisig_signers, - sources, - proof_data_location, - )?]; - - if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) = - proof_data_location - { - // This constructor appends the proof instruction right after the - // `WithdrawWithheldTokensFromAccounts` instruction. This means that the proof - // instruction offset must always be 1. To use an arbitrary proof - // instruction offset, use the - // `inner_withdraw_withheld_tokens_from_accounts` constructor. - let proof_instruction_offset: i8 = proof_instruction_offset.into(); - if proof_instruction_offset != 1 { - return Err(TokenError::InvalidProofInstructionOffset.into()); - } - match proof_data { - ProofData::InstructionData(data) => instructions.push( - ProofInstruction::VerifyCiphertextCiphertextEquality - .encode_verify_proof(None, data), - ), - ProofData::RecordAccount(address, offset) => instructions.push( - ProofInstruction::VerifyCiphertextCiphertextEquality - .encode_verify_proof_from_account(None, address, offset), - ), - }; - }; - - Ok(instructions) -} - -/// Creates a `HarvestWithheldTokensToMint` instruction -pub fn harvest_withheld_tokens_to_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - sources: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![AccountMeta::new(*mint, false)]; - - for source in sources.iter() { - accounts.push(AccountMeta::new(**source, false)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferFeeExtension, - ConfidentialTransferFeeInstruction::HarvestWithheldTokensToMint, - &(), - )) -} - -/// Create an `EnableHarvestToMint` instruction -pub fn enable_harvest_to_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), - ]; - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferFeeExtension, - ConfidentialTransferFeeInstruction::EnableHarvestToMint, - &(), - )) -} - -/// Create a `DisableHarvestToMint` instruction -pub fn disable_harvest_to_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - multisig_signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, multisig_signers.is_empty()), - ]; - - for multisig_signer in multisig_signers.iter() { - accounts.push(AccountMeta::new_readonly(**multisig_signer, true)); - } - - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ConfidentialTransferFeeExtension, - ConfidentialTransferFeeInstruction::DisableHarvestToMint, - &(), - )) -} diff --git a/token/program-2022/src/extension/confidential_transfer_fee/mod.rs b/token/program-2022/src/extension/confidential_transfer_fee/mod.rs deleted file mode 100644 index f48eb2dceb1..00000000000 --- a/token/program-2022/src/extension/confidential_transfer_fee/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -use { - crate::{ - error::TokenError, - extension::{Extension, ExtensionType}, - }, - bytemuck::{Pod, Zeroable}, - solana_program::entrypoint::ProgramResult, - solana_zk_sdk::encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodBool}, - spl_token_confidential_transfer_proof_extraction::encryption::PodFeeCiphertext, -}; - -/// Confidential transfer fee extension instructions -pub mod instruction; - -/// Confidential transfer fee extension processor -pub mod processor; - -/// Confidential Transfer Fee extension account information needed for -/// instructions -#[cfg(not(target_os = "solana"))] -pub mod account_info; - -/// ElGamal ciphertext containing a transfer fee -pub type EncryptedFee = PodFeeCiphertext; -/// ElGamal ciphertext containing a withheld fee in an account -pub type EncryptedWithheldAmount = PodElGamalCiphertext; - -/// Confidential transfer fee extension data for mints -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct ConfidentialTransferFeeConfig { - /// Optional authority to set the withdraw withheld authority ElGamal key - pub authority: OptionalNonZeroPubkey, - - /// Withheld fees from accounts must be encrypted with this ElGamal key. - /// - /// Note that whoever holds the ElGamal private key for this ElGamal public - /// key has the ability to decode any withheld fee amount that are - /// associated with accounts. When combined with the fee parameters, the - /// withheld fee amounts can reveal information about transfer amounts. - pub withdraw_withheld_authority_elgamal_pubkey: PodElGamalPubkey, - - /// If `false`, the harvest of withheld tokens to mint is rejected. - pub harvest_to_mint_enabled: PodBool, - - /// Withheld confidential transfer fee tokens that have been moved to the - /// mint for withdrawal. - pub withheld_amount: EncryptedWithheldAmount, -} - -impl Extension for ConfidentialTransferFeeConfig { - const TYPE: ExtensionType = ExtensionType::ConfidentialTransferFeeConfig; -} - -/// Confidential transfer fee -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct ConfidentialTransferFeeAmount { - /// Amount withheld during confidential transfers, to be harvest to the mint - pub withheld_amount: EncryptedWithheldAmount, -} - -impl Extension for ConfidentialTransferFeeAmount { - const TYPE: ExtensionType = ExtensionType::ConfidentialTransferFeeAmount; -} - -impl ConfidentialTransferFeeAmount { - /// Check if a confidential transfer fee account is in a closable state. - pub fn closable(&self) -> ProgramResult { - if self.withheld_amount == EncryptedWithheldAmount::zeroed() { - Ok(()) - } else { - Err(TokenError::ConfidentialTransferFeeAccountHasWithheldFee.into()) - } - } -} diff --git a/token/program-2022/src/extension/confidential_transfer_fee/processor.rs b/token/program-2022/src/extension/confidential_transfer_fee/processor.rs deleted file mode 100644 index 1d7ec386805..00000000000 --- a/token/program-2022/src/extension/confidential_transfer_fee/processor.rs +++ /dev/null @@ -1,499 +0,0 @@ -// Remove feature once zk ops syscalls are enabled on all networks -#[cfg(feature = "zk-ops")] -use spl_token_confidential_transfer_ciphertext_arithmetic as ciphertext_arithmetic; -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - confidential_transfer::{ - instruction::{ - CiphertextCiphertextEqualityProofContext, CiphertextCiphertextEqualityProofData, - }, - ConfidentialTransferAccount, DecryptableBalance, - }, - confidential_transfer_fee::{ - instruction::{ - ConfidentialTransferFeeInstruction, - InitializeConfidentialTransferFeeConfigData, - WithdrawWithheldTokensFromAccountsData, WithdrawWithheldTokensFromMintData, - }, - ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, - EncryptedWithheldAmount, - }, - transfer_fee::TransferFeeConfig, - BaseStateWithExtensions, BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::{PodAccount, PodMint}, - processor::Processor, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, - }, - bytemuck::Zeroable, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_confidential_transfer_proof_extraction::instruction::verify_and_extract_context, -}; - -/// Processes an [`InitializeConfidentialTransferFeeConfig`] instruction. -fn process_initialize_confidential_transfer_fee_config( - accounts: &[AccountInfo], - authority: &OptionalNonZeroPubkey, - withdraw_withheld_authority_elgamal_pubkey: &PodElGamalPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - let extension = mint.init_extension::(true)?; - extension.authority = *authority; - extension.withdraw_withheld_authority_elgamal_pubkey = - *withdraw_withheld_authority_elgamal_pubkey; - extension.harvest_to_mint_enabled = true.into(); - extension.withheld_amount = EncryptedWithheldAmount::zeroed(); - - Ok(()) -} - -/// Processes a [`WithdrawWithheldTokensFromMint`] instruction. -#[cfg(feature = "zk-ops")] -fn process_withdraw_withheld_tokens_from_mint( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_decryptable_available_balance: &DecryptableBalance, - proof_instruction_offset: i64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - - // zero-knowledge proof certifies that the exact withheld amount is credited to - // the destination account. - let proof_context = verify_and_extract_context::< - CiphertextCiphertextEqualityProofData, - CiphertextCiphertextEqualityProofContext, - >(account_info_iter, proof_instruction_offset, None)?; - - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - // unnecessary check, but helps for clarity - check_program_account(mint_account_info.owner)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - - // mint must be extended for fees - { - let transfer_fee_config = mint.get_extension::()?; - let withdraw_withheld_authority = - Option::::from(transfer_fee_config.withdraw_withheld_authority) - .ok_or(TokenError::NoAuthorityExists)?; - Processor::validate_owner( - program_id, - &withdraw_withheld_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - } // free `transfer_fee_config` to borrow `confidential_transfer_fee_config` as - // mutable - - // mint must also be extended for confidential transfers, but forgo an explicit - // check since it is not possible to initialize a confidential transfer mint - // without it - - let confidential_transfer_fee_config = - mint.get_extension_mut::()?; - - // basic checks for the destination account - must be extended for confidential - // transfers - let mut destination_account_data = destination_account_info.data.borrow_mut(); - let mut destination_account = - PodStateWithExtensionsMut::::unpack(&mut destination_account_data)?; - - if destination_account.base.mint != *mint_account_info.key { - return Err(TokenError::MintMismatch.into()); - } - if destination_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - let destination_confidential_transfer_account = - destination_account.get_extension_mut::()?; - destination_confidential_transfer_account.valid_as_destination()?; - - // The funds are moved from the mint to a destination account. Here, the - // `source` equates to the withdraw withheld authority associated in the - // mint. - - // Check that the withdraw authority ElGamal public key associated with the mint - // is consistent with what was actually used to generate the zkp. - if proof_context.first_pubkey - != confidential_transfer_fee_config.withdraw_withheld_authority_elgamal_pubkey - { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - // Check that the ElGamal public key associated with the destination account is - // consistent with what was actually used to generate the zkp. - if proof_context.second_pubkey != destination_confidential_transfer_account.elgamal_pubkey { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - // Check that the withheld amount ciphertext is consistent with the ciphertext - // data that was actually used to generate the zkp. - if proof_context.first_ciphertext != confidential_transfer_fee_config.withheld_amount { - return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); - } - - // The proof data contains the mint withheld amount encrypted under the - // destination ElGamal pubkey. Add this amount to the available balance. - destination_confidential_transfer_account.available_balance = ciphertext_arithmetic::add( - &destination_confidential_transfer_account.available_balance, - &proof_context.second_ciphertext, - ) - .ok_or(ProgramError::InvalidInstructionData)?; - - destination_confidential_transfer_account.decryptable_available_balance = - *new_decryptable_available_balance; - - // Fee is now withdrawn, so zero out the mint withheld amount. - confidential_transfer_fee_config.withheld_amount = EncryptedWithheldAmount::zeroed(); - - Ok(()) -} - -/// Processes a [`WithdrawWithheldTokensFromAccounts`] instruction. -#[cfg(feature = "zk-ops")] -fn process_withdraw_withheld_tokens_from_accounts( - program_id: &Pubkey, - accounts: &[AccountInfo], - num_token_accounts: u8, - new_decryptable_available_balance: &DecryptableBalance, - proof_instruction_offset: i64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - - // zero-knowledge proof certifies that the exact aggregate withheld amount is - // credited to the destination account. - let proof_context = verify_and_extract_context::< - CiphertextCiphertextEqualityProofData, - CiphertextCiphertextEqualityProofContext, - >(account_info_iter, proof_instruction_offset, None)?; - - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - let account_infos = account_info_iter.as_slice(); - let num_signers = account_infos - .len() - .saturating_sub(num_token_accounts as usize); - - // unnecessary check, but helps for clarity - check_program_account(mint_account_info.owner)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - - // mint must be extended for fees - let transfer_fee_config = mint.get_extension::()?; - let withdraw_withheld_authority = - Option::::from(transfer_fee_config.withdraw_withheld_authority) - .ok_or(TokenError::NoAuthorityExists)?; - Processor::validate_owner( - program_id, - &withdraw_withheld_authority, - authority_info, - authority_info_data_len, - &account_infos[..num_signers], - )?; - - let mut destination_account_data = destination_account_info.data.borrow_mut(); - let mut destination_account = - PodStateWithExtensionsMut::::unpack(&mut destination_account_data)?; - if destination_account.base.mint != *mint_account_info.key { - return Err(TokenError::MintMismatch.into()); - } - if destination_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - // Sum up the withheld amounts in all the accounts. - let mut aggregate_withheld_amount = EncryptedWithheldAmount::zeroed(); - for account_info in &account_infos[num_signers..] { - // self-harvest, can't double-borrow the underlying data - if account_info.key == destination_account_info.key { - let destination_confidential_transfer_fee_amount = destination_account - .get_extension_mut::() - .map_err(|_| TokenError::InvalidState)?; - - aggregate_withheld_amount = ciphertext_arithmetic::add( - &aggregate_withheld_amount, - &destination_confidential_transfer_fee_amount.withheld_amount, - ) - .ok_or(ProgramError::InvalidInstructionData)?; - - destination_confidential_transfer_fee_amount.withheld_amount = - EncryptedWithheldAmount::zeroed(); - } else { - match harvest_from_account(mint_account_info.key, account_info) { - Ok(encrypted_withheld_amount) => { - aggregate_withheld_amount = ciphertext_arithmetic::add( - &aggregate_withheld_amount, - &encrypted_withheld_amount, - ) - .ok_or(ProgramError::InvalidInstructionData)?; - } - Err(e) => { - msg!("Error harvesting from {}: {}", account_info.key, e); - } - } - } - } - - let destination_confidential_transfer_account = - destination_account.get_extension_mut::()?; - destination_confidential_transfer_account.valid_as_destination()?; - - // The funds are moved from the accounts to a destination account. Here, the - // `source` equates to the withdraw withheld authority associated in the - // mint. - - // Checks that the withdraw authority ElGamal public key associated with the - // mint is consistent with what was actually used to generate the zkp. - let confidential_transfer_fee_config = - mint.get_extension_mut::()?; - if proof_context.first_pubkey - != confidential_transfer_fee_config.withdraw_withheld_authority_elgamal_pubkey - { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - // Checks that the ElGamal public key associated with the destination account is - // consistent with what was actually used to generate the zkp. - if proof_context.second_pubkey != destination_confidential_transfer_account.elgamal_pubkey { - return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into()); - } - // Checks that the withheld amount ciphertext is consistent with the ciphertext - // data that was actually used to generate the zkp. - if proof_context.first_ciphertext != aggregate_withheld_amount { - return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); - } - - // The proof data contains the mint withheld amount encrypted under the - // destination ElGamal pubkey. This amount is added to the destination - // available balance. - destination_confidential_transfer_account.available_balance = ciphertext_arithmetic::add( - &destination_confidential_transfer_account.available_balance, - &proof_context.second_ciphertext, - ) - .ok_or(ProgramError::InvalidInstructionData)?; - - destination_confidential_transfer_account.decryptable_available_balance = - *new_decryptable_available_balance; - - Ok(()) -} - -#[cfg(feature = "zk-ops")] -fn harvest_from_account<'b>( - mint_key: &'b Pubkey, - token_account_info: &'b AccountInfo<'_>, -) -> Result { - let mut token_account_data = token_account_info.data.borrow_mut(); - let mut token_account = - PodStateWithExtensionsMut::::unpack(&mut token_account_data) - .map_err(|_| TokenError::InvalidState)?; - if token_account.base.mint != *mint_key { - return Err(TokenError::MintMismatch); - } - check_program_account(token_account_info.owner).map_err(|_| TokenError::InvalidState)?; - - let confidential_transfer_token_account = token_account - .get_extension_mut::() - .map_err(|_| TokenError::InvalidState)?; - - let withheld_amount = confidential_transfer_token_account.withheld_amount; - confidential_transfer_token_account.withheld_amount = EncryptedWithheldAmount::zeroed(); - - Ok(withheld_amount) -} - -/// Process a [`HarvestWithheldTokensToMint`] instruction. -#[cfg(feature = "zk-ops")] -fn process_harvest_withheld_tokens_to_mint(accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let token_account_infos = account_info_iter.as_slice(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - mint.get_extension::()?; - let confidential_transfer_fee_mint = - mint.get_extension_mut::()?; - - let harvest_to_mint_enabled: bool = confidential_transfer_fee_mint - .harvest_to_mint_enabled - .into(); - if !harvest_to_mint_enabled { - return Err(TokenError::HarvestToMintDisabled.into()); - } - - for token_account_info in token_account_infos { - match harvest_from_account(mint_account_info.key, token_account_info) { - Ok(withheld_amount) => { - let new_mint_withheld_amount = ciphertext_arithmetic::add( - &confidential_transfer_fee_mint.withheld_amount, - &withheld_amount, - ) - .ok_or(ProgramError::InvalidInstructionData)?; - - confidential_transfer_fee_mint.withheld_amount = new_mint_withheld_amount; - } - Err(e) => { - msg!("Error harvesting from {}: {}", token_account_info.key, e); - } - } - } - Ok(()) -} - -/// Process a [`EnableHarvestToMint`] instruction. -fn process_enable_harvest_to_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - check_program_account(mint_info.owner)?; - let mint_data = &mut mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; - let confidential_transfer_fee_mint = - mint.get_extension_mut::()?; - - let maybe_confidential_transfer_fee_authority: Option = - confidential_transfer_fee_mint.authority.into(); - let confidential_transfer_fee_authority = - maybe_confidential_transfer_fee_authority.ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &confidential_transfer_fee_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - confidential_transfer_fee_mint.harvest_to_mint_enabled = true.into(); - Ok(()) -} - -/// Process a [`DisableHarvestToMint`] instruction. -fn process_disable_harvest_to_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - check_program_account(mint_info.owner)?; - let mint_data = &mut mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(mint_data)?; - let confidential_transfer_fee_mint = - mint.get_extension_mut::()?; - - let maybe_confidential_transfer_fee_authority: Option = - confidential_transfer_fee_mint.authority.into(); - let confidential_transfer_fee_authority = - maybe_confidential_transfer_fee_authority.ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &confidential_transfer_fee_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - confidential_transfer_fee_mint.harvest_to_mint_enabled = false.into(); - Ok(()) -} - -#[allow(dead_code)] -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - - match decode_instruction_type(input)? { - ConfidentialTransferFeeInstruction::InitializeConfidentialTransferFeeConfig => { - msg!("ConfidentialTransferFeeInstruction::InitializeConfidentialTransferFeeConfig"); - let data = - decode_instruction_data::(input)?; - process_initialize_confidential_transfer_fee_config( - accounts, - &data.authority, - &data.withdraw_withheld_authority_elgamal_pubkey, - ) - } - ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromMint => { - msg!("ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromMint"); - #[cfg(feature = "zk-ops")] - { - let data = decode_instruction_data::(input)?; - process_withdraw_withheld_tokens_from_mint( - program_id, - accounts, - &data.new_decryptable_available_balance, - data.proof_instruction_offset as i64, - ) - } - #[cfg(not(feature = "zk-ops"))] - { - Err(ProgramError::InvalidInstructionData) - } - } - ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromAccounts => { - msg!("ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromAccounts"); - #[cfg(feature = "zk-ops")] - { - let data = - decode_instruction_data::(input)?; - process_withdraw_withheld_tokens_from_accounts( - program_id, - accounts, - data.num_token_accounts, - &data.new_decryptable_available_balance, - data.proof_instruction_offset as i64, - ) - } - #[cfg(not(feature = "zk-ops"))] - { - Err(ProgramError::InvalidInstructionData) - } - } - ConfidentialTransferFeeInstruction::HarvestWithheldTokensToMint => { - msg!("ConfidentialTransferFeeInstruction::HarvestWithheldTokensToMint"); - #[cfg(feature = "zk-ops")] - { - process_harvest_withheld_tokens_to_mint(accounts) - } - #[cfg(not(feature = "zk-ops"))] - { - Err(ProgramError::InvalidInstructionData) - } - } - ConfidentialTransferFeeInstruction::EnableHarvestToMint => { - msg!("ConfidentialTransferFeeInstruction::EnableHarvestToMint"); - process_enable_harvest_to_mint(program_id, accounts) - } - ConfidentialTransferFeeInstruction::DisableHarvestToMint => { - msg!("ConfidentialTransferFeeInstruction::DisableHarvestToMint"); - process_disable_harvest_to_mint(program_id, accounts) - } - } -} diff --git a/token/program-2022/src/extension/cpi_guard/instruction.rs b/token/program-2022/src/extension/cpi_guard/instruction.rs deleted file mode 100644 index cff707e3316..00000000000 --- a/token/program-2022/src/extension/cpi_guard/instruction.rs +++ /dev/null @@ -1,104 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - instruction::{encode_instruction, TokenInstruction}, - }, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, -}; - -/// CPI Guard extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum CpiGuardInstruction { - /// Lock certain token operations from taking place within CPI for this - /// Account, namely: - /// * `Transfer` and `Burn` must go through a delegate. - /// * `CloseAccount` can only return lamports to owner. - /// * `SetAuthority` can only be used to remove an existing close authority. - /// * `Approve` is disallowed entirely. - /// - /// In addition, CPI Guard cannot be enabled or disabled via CPI. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to update. - /// 1. `[signer]` The account's owner. - /// - /// * Multisignature authority - /// 0. `[writable]` The account to update. - /// 1. `[]` The account's multisignature owner. - /// 2. `..2+M` `[signer]` M signer accounts. - Enable, - /// Allow all token operations to happen via CPI as normal. - /// - /// Implicitly initializes the extension in the case where it is not - /// present. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to update. - /// 1. `[signer]` The account's owner. - /// - /// * Multisignature authority - /// 0. `[writable]` The account to update. - /// 1. `[]` The account's multisignature owner. - /// 2. `..2+M` `[signer]` M signer accounts. - Disable, -} - -/// Create an `Enable` instruction -pub fn enable_cpi_guard( - token_program_id: &Pubkey, - account: &Pubkey, - owner: &Pubkey, - signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*account, false), - AccountMeta::new_readonly(*owner, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::CpiGuardExtension, - CpiGuardInstruction::Enable, - &(), - )) -} - -/// Create a `Disable` instruction -pub fn disable_cpi_guard( - token_program_id: &Pubkey, - account: &Pubkey, - owner: &Pubkey, - signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*account, false), - AccountMeta::new_readonly(*owner, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::CpiGuardExtension, - CpiGuardInstruction::Disable, - &(), - )) -} diff --git a/token/program-2022/src/extension/cpi_guard/mod.rs b/token/program-2022/src/extension/cpi_guard/mod.rs deleted file mode 100644 index 7af9bd71153..00000000000 --- a/token/program-2022/src/extension/cpi_guard/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut}, - state::Account, - }, - bytemuck::{Pod, Zeroable}, - solana_program::instruction::{get_stack_height, TRANSACTION_LEVEL_STACK_HEIGHT}, - spl_pod::primitives::PodBool, -}; - -/// CPI Guard extension instructions -pub mod instruction; - -/// CPI Guard extension processor -pub mod processor; - -/// CPI Guard extension for Accounts -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct CpiGuard { - /// Lock privileged token operations from happening via CPI - pub lock_cpi: PodBool, -} -impl Extension for CpiGuard { - const TYPE: ExtensionType = ExtensionType::CpiGuard; -} - -/// Determine if CPI Guard is enabled for this account -pub fn cpi_guard_enabled(account_state: &StateWithExtensionsMut) -> bool { - if let Ok(extension) = account_state.get_extension::() { - return extension.lock_cpi.into(); - } - false -} - -/// Determine if we are in CPI -pub fn in_cpi() -> bool { - get_stack_height() > TRANSACTION_LEVEL_STACK_HEIGHT -} diff --git a/token/program-2022/src/extension/cpi_guard/processor.rs b/token/program-2022/src/extension/cpi_guard/processor.rs deleted file mode 100644 index edaaa42282a..00000000000 --- a/token/program-2022/src/extension/cpi_guard/processor.rs +++ /dev/null @@ -1,74 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - cpi_guard::{in_cpi, instruction::CpiGuardInstruction, CpiGuard}, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::decode_instruction_type, - pod::PodAccount, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - pubkey::Pubkey, - }, -}; - -/// Toggle the `CpiGuard` extension, initializing the extension if not already -/// present. -fn process_toggle_cpi_guard( - program_id: &Pubkey, - accounts: &[AccountInfo], - enable: bool, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut account_data = token_account_info.data.borrow_mut(); - let mut account = PodStateWithExtensionsMut::::unpack(&mut account_data)?; - - Processor::validate_owner( - program_id, - &account.base.owner, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?; - - if in_cpi() { - return Err(TokenError::CpiGuardSettingsLocked.into()); - } - - let extension = if let Ok(extension) = account.get_extension_mut::() { - extension - } else { - account.init_extension::(true)? - }; - extension.lock_cpi = enable.into(); - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - - match decode_instruction_type(input)? { - CpiGuardInstruction::Enable => { - msg!("CpiGuardInstruction::Enable"); - process_toggle_cpi_guard(program_id, accounts, true /* enable */) - } - CpiGuardInstruction::Disable => { - msg!("CpiGuardInstruction::Disable"); - process_toggle_cpi_guard(program_id, accounts, false /* disable */) - } - } -} diff --git a/token/program-2022/src/extension/default_account_state/instruction.rs b/token/program-2022/src/extension/default_account_state/instruction.rs deleted file mode 100644 index 5ecc66052ee..00000000000 --- a/token/program-2022/src/extension/default_account_state/instruction.rs +++ /dev/null @@ -1,127 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, error::TokenError, instruction::TokenInstruction, - state::AccountState, - }, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, - std::convert::TryFrom, -}; - -/// Default Account State extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum DefaultAccountStateInstruction { - /// Initialize a new mint with the default state for new Accounts. - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// - /// Data expected by this instruction: - /// `crate::state::AccountState` - Initialize, - /// Update the default state for new Accounts. Only supported for mints that - /// include the `DefaultAccountState` extension. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[signer]` The mint freeze authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[]` The mint's multisignature freeze authority. - /// 2. `..2+M` `[signer]` M signer accounts. - /// - /// Data expected by this instruction: - /// `crate::state::AccountState` - Update, -} - -/// Utility function for decoding a `DefaultAccountState` instruction and its -/// data -pub fn decode_instruction( - input: &[u8], -) -> Result<(DefaultAccountStateInstruction, AccountState), ProgramError> { - if input.len() != 2 { - return Err(TokenError::InvalidInstruction.into()); - } - Ok(( - DefaultAccountStateInstruction::try_from(input[0]) - .or(Err(TokenError::InvalidInstruction))?, - AccountState::try_from(input[1]).or(Err(TokenError::InvalidInstruction))?, - )) -} - -fn encode_instruction( - token_program_id: &Pubkey, - accounts: Vec, - instruction_type: DefaultAccountStateInstruction, - state: &AccountState, -) -> Instruction { - let mut data = TokenInstruction::DefaultAccountStateExtension.pack(); - data.push(instruction_type.into()); - data.push((*state).into()); - Instruction { - program_id: *token_program_id, - accounts, - data, - } -} - -/// Create an `Initialize` instruction -pub fn initialize_default_account_state( - token_program_id: &Pubkey, - mint: &Pubkey, - state: &AccountState, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - Ok(encode_instruction( - token_program_id, - accounts, - DefaultAccountStateInstruction::Initialize, - state, - )) -} - -/// Create an `Initialize` instruction -pub fn update_default_account_state( - token_program_id: &Pubkey, - mint: &Pubkey, - freeze_authority: &Pubkey, - signers: &[&Pubkey], - state: &AccountState, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*freeze_authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - DefaultAccountStateInstruction::Update, - state, - )) -} diff --git a/token/program-2022/src/extension/default_account_state/mod.rs b/token/program-2022/src/extension/default_account_state/mod.rs deleted file mode 100644 index 939bd16b06f..00000000000 --- a/token/program-2022/src/extension/default_account_state/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, -}; - -/// Default Account state extension instructions -pub mod instruction; - -/// Default Account state extension processor -pub mod processor; - -/// Default Account::state extension data for mints. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct DefaultAccountState { - /// Default Account::state in which new Accounts should be initialized - pub state: PodAccountState, -} -impl Extension for DefaultAccountState { - const TYPE: ExtensionType = ExtensionType::DefaultAccountState; -} - -type PodAccountState = u8; diff --git a/token/program-2022/src/extension/default_account_state/processor.rs b/token/program-2022/src/extension/default_account_state/processor.rs deleted file mode 100644 index f220f386022..00000000000 --- a/token/program-2022/src/extension/default_account_state/processor.rs +++ /dev/null @@ -1,96 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - default_account_state::{ - instruction::{decode_instruction, DefaultAccountStateInstruction}, - DefaultAccountState, - }, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - pod::{PodCOption, PodMint}, - processor::Processor, - state::AccountState, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - pubkey::Pubkey, - }, -}; - -fn check_valid_default_state(state: AccountState) -> ProgramResult { - match state { - AccountState::Uninitialized => Err(TokenError::InvalidState.into()), - _ => Ok(()), - } -} - -fn process_initialize_default_account_state( - accounts: &[AccountInfo], - state: AccountState, -) -> ProgramResult { - check_valid_default_state(state)?; - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - let extension = mint.init_extension::(true)?; - extension.state = state.into(); - Ok(()) -} - -fn process_update_default_account_state( - program_id: &Pubkey, - accounts: &[AccountInfo], - state: AccountState, -) -> ProgramResult { - check_valid_default_state(state)?; - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let freeze_authority_info = next_account_info(account_info_iter)?; - let freeze_authority_info_data_len = freeze_authority_info.data_len(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - - match &mint.base.freeze_authority { - PodCOption { - option: PodCOption::::SOME, - value: freeze_authority, - } => Processor::validate_owner( - program_id, - freeze_authority, - freeze_authority_info, - freeze_authority_info_data_len, - account_info_iter.as_slice(), - ), - _ => Err(TokenError::NoAuthorityExists.into()), - }?; - - let extension = mint.get_extension_mut::()?; - extension.state = state.into(); - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - - let (instruction, state) = decode_instruction(input)?; - match instruction { - DefaultAccountStateInstruction::Initialize => { - msg!("DefaultAccountStateInstruction::Initialize"); - process_initialize_default_account_state(accounts, state) - } - DefaultAccountStateInstruction::Update => { - msg!("DefaultAccountStateInstruction::Update"); - process_update_default_account_state(program_id, accounts, state) - } - } -} diff --git a/token/program-2022/src/extension/group_member_pointer/instruction.rs b/token/program-2022/src/extension/group_member_pointer/instruction.rs deleted file mode 100644 index 5aa006c29c2..00000000000 --- a/token/program-2022/src/extension/group_member_pointer/instruction.rs +++ /dev/null @@ -1,128 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - instruction::{encode_instruction, TokenInstruction}, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - std::convert::TryInto, -}; - -/// Group member pointer extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum GroupMemberPointerInstruction { - /// Initialize a new mint with a group member pointer - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// - /// Data expected by this instruction: - /// `crate::extension::group_member_pointer::instruction::InitializeInstructionData` - Initialize, - /// Update the group member pointer address. Only supported for mints that - /// include the `GroupMemberPointer` extension. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[signer]` The group member pointer authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[]` The group member pointer authority. - /// 2. `..2+M` `[signer]` M signer accounts. - /// - /// Data expected by this instruction: - /// `crate::extension::group_member_pointer::instruction::UpdateInstructionData` - Update, -} - -/// Data expected by `Initialize` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeInstructionData { - /// The public key for the account that can update the group address - pub authority: OptionalNonZeroPubkey, - /// The account address that holds the member - pub member_address: OptionalNonZeroPubkey, -} - -/// Data expected by `Update` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct UpdateInstructionData { - /// The new account address that holds the group - pub member_address: OptionalNonZeroPubkey, -} - -/// Create an `Initialize` instruction -pub fn initialize( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: Option, - member_address: Option, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::GroupMemberPointerExtension, - GroupMemberPointerInstruction::Initialize, - &InitializeInstructionData { - authority: authority.try_into()?, - member_address: member_address.try_into()?, - }, - )) -} - -/// Create an `Update` instruction -pub fn update( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], - member_address: Option, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::GroupMemberPointerExtension, - GroupMemberPointerInstruction::Update, - &UpdateInstructionData { - member_address: member_address.try_into()?, - }, - )) -} diff --git a/token/program-2022/src/extension/group_member_pointer/mod.rs b/token/program-2022/src/extension/group_member_pointer/mod.rs deleted file mode 100644 index a74e4872dd6..00000000000 --- a/token/program-2022/src/extension/group_member_pointer/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -/// Instructions for the `GroupMemberPointer` extension -pub mod instruction; -/// Instruction processor for the `GroupMemberPointer` extension -pub mod processor; - -/// Group member pointer extension data for mints. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct GroupMemberPointer { - /// Authority that can set the member address - pub authority: OptionalNonZeroPubkey, - /// Account address that holds the member - pub member_address: OptionalNonZeroPubkey, -} - -impl Extension for GroupMemberPointer { - const TYPE: ExtensionType = ExtensionType::GroupMemberPointer; -} diff --git a/token/program-2022/src/extension/group_member_pointer/processor.rs b/token/program-2022/src/extension/group_member_pointer/processor.rs deleted file mode 100644 index 30cbab21f3e..00000000000 --- a/token/program-2022/src/extension/group_member_pointer/processor.rs +++ /dev/null @@ -1,103 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - group_member_pointer::{ - instruction::{ - GroupMemberPointerInstruction, InitializeInstructionData, UpdateInstructionData, - }, - GroupMemberPointer, - }, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::PodMint, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -fn process_initialize( - _program_id: &Pubkey, - accounts: &[AccountInfo], - authority: &OptionalNonZeroPubkey, - member_address: &OptionalNonZeroPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - - if Option::::from(*authority).is_none() - && Option::::from(*member_address).is_none() - { - msg!( - "The group member pointer extension requires at least an authority or an address for \ - initialization, neither was provided" - ); - Err(TokenError::InvalidInstruction)?; - } - - let extension = mint.init_extension::(true)?; - extension.authority = *authority; - extension.member_address = *member_address; - Ok(()) -} - -fn process_update( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_member_address: &OptionalNonZeroPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - let authority = - Option::::from(extension.authority).ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &authority, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?; - - extension.member_address = *new_member_address; - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - match decode_instruction_type(input)? { - GroupMemberPointerInstruction::Initialize => { - msg!("GroupMemberPointerInstruction::Initialize"); - let InitializeInstructionData { - authority, - member_address, - } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, authority, member_address) - } - GroupMemberPointerInstruction::Update => { - msg!("GroupMemberPointerInstruction::Update"); - let UpdateInstructionData { member_address } = decode_instruction_data(input)?; - process_update(program_id, accounts, member_address) - } - } -} diff --git a/token/program-2022/src/extension/group_pointer/instruction.rs b/token/program-2022/src/extension/group_pointer/instruction.rs deleted file mode 100644 index 5204b18fc4a..00000000000 --- a/token/program-2022/src/extension/group_pointer/instruction.rs +++ /dev/null @@ -1,128 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - instruction::{encode_instruction, TokenInstruction}, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - std::convert::TryInto, -}; - -/// Group pointer extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum GroupPointerInstruction { - /// Initialize a new mint with a group pointer - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// - /// Data expected by this instruction: - /// `crate::extension::group_pointer::instruction::InitializeInstructionData` - Initialize, - /// Update the group pointer address. Only supported for mints that - /// include the `GroupPointer` extension. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[signer]` The group pointer authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[]` The mint's group pointer authority. - /// 2. `..2+M` `[signer]` M signer accounts. - /// - /// Data expected by this instruction: - /// `crate::extension::group_pointer::instruction::UpdateInstructionData` - Update, -} - -/// Data expected by `Initialize` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeInstructionData { - /// The public key for the account that can update the group address - pub authority: OptionalNonZeroPubkey, - /// The account address that holds the group - pub group_address: OptionalNonZeroPubkey, -} - -/// Data expected by `Update` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct UpdateInstructionData { - /// The new account address that holds the group configurations - pub group_address: OptionalNonZeroPubkey, -} - -/// Create an `Initialize` instruction -pub fn initialize( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: Option, - group_address: Option, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::GroupPointerExtension, - GroupPointerInstruction::Initialize, - &InitializeInstructionData { - authority: authority.try_into()?, - group_address: group_address.try_into()?, - }, - )) -} - -/// Create an `Update` instruction -pub fn update( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], - group_address: Option, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::GroupPointerExtension, - GroupPointerInstruction::Update, - &UpdateInstructionData { - group_address: group_address.try_into()?, - }, - )) -} diff --git a/token/program-2022/src/extension/group_pointer/mod.rs b/token/program-2022/src/extension/group_pointer/mod.rs deleted file mode 100644 index 27344d2d254..00000000000 --- a/token/program-2022/src/extension/group_pointer/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -/// Instructions for the `GroupPointer` extension -pub mod instruction; -/// Instruction processor for the `GroupPointer` extension -pub mod processor; - -/// Group pointer extension data for mints. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct GroupPointer { - /// Authority that can set the group address - pub authority: OptionalNonZeroPubkey, - /// Account address that holds the group - pub group_address: OptionalNonZeroPubkey, -} - -impl Extension for GroupPointer { - const TYPE: ExtensionType = ExtensionType::GroupPointer; -} diff --git a/token/program-2022/src/extension/group_pointer/processor.rs b/token/program-2022/src/extension/group_pointer/processor.rs deleted file mode 100644 index 48cc42f6638..00000000000 --- a/token/program-2022/src/extension/group_pointer/processor.rs +++ /dev/null @@ -1,103 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - group_pointer::{ - instruction::{ - GroupPointerInstruction, InitializeInstructionData, UpdateInstructionData, - }, - GroupPointer, - }, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::PodMint, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -fn process_initialize( - _program_id: &Pubkey, - accounts: &[AccountInfo], - authority: &OptionalNonZeroPubkey, - group_address: &OptionalNonZeroPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - - if Option::::from(*authority).is_none() - && Option::::from(*group_address).is_none() - { - msg!( - "The group pointer extension requires at least an authority or an address for \ - initialization, neither was provided" - ); - Err(TokenError::InvalidInstruction)?; - } - - let extension = mint.init_extension::(true)?; - extension.authority = *authority; - extension.group_address = *group_address; - Ok(()) -} - -fn process_update( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_group_address: &OptionalNonZeroPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - let authority = - Option::::from(extension.authority).ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &authority, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?; - - extension.group_address = *new_group_address; - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - match decode_instruction_type(input)? { - GroupPointerInstruction::Initialize => { - msg!("GroupPointerInstruction::Initialize"); - let InitializeInstructionData { - authority, - group_address, - } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, authority, group_address) - } - GroupPointerInstruction::Update => { - msg!("GroupPointerInstruction::Update"); - let UpdateInstructionData { group_address } = decode_instruction_data(input)?; - process_update(program_id, accounts, group_address) - } - } -} diff --git a/token/program-2022/src/extension/immutable_owner.rs b/token/program-2022/src/extension/immutable_owner.rs deleted file mode 100644 index cc323599490..00000000000 --- a/token/program-2022/src/extension/immutable_owner.rs +++ /dev/null @@ -1,17 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, -}; - -/// Indicates that the Account owner authority cannot be changed -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct ImmutableOwner; - -impl Extension for ImmutableOwner { - const TYPE: ExtensionType = ExtensionType::ImmutableOwner; -} diff --git a/token/program-2022/src/extension/interest_bearing_mint/instruction.rs b/token/program-2022/src/extension/interest_bearing_mint/instruction.rs deleted file mode 100644 index f7b07e24fc8..00000000000 --- a/token/program-2022/src/extension/interest_bearing_mint/instruction.rs +++ /dev/null @@ -1,117 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - extension::interest_bearing_mint::BasisPoints, - instruction::{encode_instruction, TokenInstruction}, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - std::convert::TryInto, -}; - -/// Interesting-bearing mint extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum InterestBearingMintInstruction { - /// Initialize a new mint with interest accrual. - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// - /// Data expected by this instruction: - /// `crate::extension::interest_bearing::instruction::InitializeInstructionData` - Initialize, - /// Update the interest rate. Only supported for mints that include the - /// `InterestBearingConfig` extension. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[signer]` The mint rate authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[]` The mint's multisignature rate authority. - /// 2. `..2+M` `[signer]` M signer accounts. - /// - /// Data expected by this instruction: - /// `crate::extension::interest_bearing::BasisPoints` - UpdateRate, -} - -/// Data expected by `InterestBearing::Initialize` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeInstructionData { - /// The public key for the account that can update the rate - pub rate_authority: OptionalNonZeroPubkey, - /// The initial interest rate - pub rate: BasisPoints, -} - -/// Create an `Initialize` instruction -pub fn initialize( - token_program_id: &Pubkey, - mint: &Pubkey, - rate_authority: Option, - rate: i16, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::InterestBearingMintExtension, - InterestBearingMintInstruction::Initialize, - &InitializeInstructionData { - rate_authority: rate_authority.try_into()?, - rate: rate.into(), - }, - )) -} - -/// Create an `UpdateRate` instruction -pub fn update_rate( - token_program_id: &Pubkey, - mint: &Pubkey, - rate_authority: &Pubkey, - signers: &[&Pubkey], - rate: i16, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*rate_authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::InterestBearingMintExtension, - InterestBearingMintInstruction::UpdateRate, - &BasisPoints::from(rate), - )) -} diff --git a/token/program-2022/src/extension/interest_bearing_mint/mod.rs b/token/program-2022/src/extension/interest_bearing_mint/mod.rs deleted file mode 100644 index 29fb5e250df..00000000000 --- a/token/program-2022/src/extension/interest_bearing_mint/mod.rs +++ /dev/null @@ -1,464 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - extension::{Extension, ExtensionType}, - trim_ui_amount_string, - }, - bytemuck::{Pod, Zeroable}, - solana_program::program_error::ProgramError, - spl_pod::{ - optional_keys::OptionalNonZeroPubkey, - primitives::{PodI16, PodI64}, - }, - std::convert::TryInto, -}; - -/// Interest-bearing mint extension instructions -pub mod instruction; - -/// Interest-bearing mint extension processor -pub mod processor; - -/// Annual interest rate, expressed as basis points -pub type BasisPoints = PodI16; -const ONE_IN_BASIS_POINTS: f64 = 10_000.; -const SECONDS_PER_YEAR: f64 = 60. * 60. * 24. * 365.24; - -/// `UnixTimestamp` expressed with an alignment-independent type -pub type UnixTimestamp = PodI64; - -/// Interest-bearing extension data for mints -/// -/// Tokens accrue interest at an annual rate expressed by `current_rate`, -/// compounded continuously, so APY will be higher than the published interest -/// rate. -/// -/// To support changing the rate, the config also maintains state for the -/// previous rate. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct InterestBearingConfig { - /// Authority that can set the interest rate and authority - pub rate_authority: OptionalNonZeroPubkey, - /// Timestamp of initialization, from which to base interest calculations - pub initialization_timestamp: UnixTimestamp, - /// Average rate from initialization until the last time it was updated - pub pre_update_average_rate: BasisPoints, - /// Timestamp of the last update, used to calculate the total amount accrued - pub last_update_timestamp: UnixTimestamp, - /// Current rate, since the last update - pub current_rate: BasisPoints, -} -impl InterestBearingConfig { - fn pre_update_timespan(&self) -> Option { - i64::from(self.last_update_timestamp).checked_sub(self.initialization_timestamp.into()) - } - - fn pre_update_exp(&self) -> Option { - let numerator = (i16::from(self.pre_update_average_rate) as i128) - .checked_mul(self.pre_update_timespan()? as i128)? as f64; - let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS; - Some(exponent.exp()) - } - - fn post_update_timespan(&self, unix_timestamp: i64) -> Option { - unix_timestamp.checked_sub(self.last_update_timestamp.into()) - } - - fn post_update_exp(&self, unix_timestamp: i64) -> Option { - let numerator = (i16::from(self.current_rate) as i128) - .checked_mul(self.post_update_timespan(unix_timestamp)? as i128)? - as f64; - let exponent = numerator / SECONDS_PER_YEAR / ONE_IN_BASIS_POINTS; - Some(exponent.exp()) - } - - fn total_scale(&self, decimals: u8, unix_timestamp: i64) -> Option { - Some( - self.pre_update_exp()? * self.post_update_exp(unix_timestamp)? - / 10_f64.powi(decimals as i32), - ) - } - - /// Convert a raw amount to its UI representation using the given decimals - /// field. Excess zeroes or unneeded decimal point are trimmed. - pub fn amount_to_ui_amount( - &self, - amount: u64, - decimals: u8, - unix_timestamp: i64, - ) -> Option { - let scaled_amount_with_interest = - (amount as f64) * self.total_scale(decimals, unix_timestamp)?; - let ui_amount = format!("{scaled_amount_with_interest:.*}", decimals as usize); - Some(trim_ui_amount_string(ui_amount, decimals)) - } - - /// Try to convert a UI representation of a token amount to its raw amount - /// using the given decimals field - pub fn try_ui_amount_into_amount( - &self, - ui_amount: &str, - decimals: u8, - unix_timestamp: i64, - ) -> Result { - let scaled_amount = ui_amount - .parse::() - .map_err(|_| ProgramError::InvalidArgument)?; - let amount = scaled_amount - / self - .total_scale(decimals, unix_timestamp) - .ok_or(ProgramError::InvalidArgument)?; - if amount > (u64::MAX as f64) || amount < (u64::MIN as f64) || amount.is_nan() { - Err(ProgramError::InvalidArgument) - } else { - // this is important, if you round earlier, you'll get wrong "inf" - // answers - Ok(amount.round() as u64) - } - } - - /// The new average rate is the time-weighted average of the current rate - /// and average rate, solving for r such that: - /// - /// ```text - /// exp(r_1 * t_1) * exp(r_2 * t_2) = exp(r * (t_1 + t_2)) - /// - /// r_1 * t_1 + r_2 * t_2 = r * (t_1 + t_2) - /// - /// r = (r_1 * t_1 + r_2 * t_2) / (t_1 + t_2) - /// ``` - pub fn time_weighted_average_rate(&self, current_timestamp: i64) -> Option { - let initialization_timestamp = i64::from(self.initialization_timestamp) as i128; - let last_update_timestamp = i64::from(self.last_update_timestamp) as i128; - - let r_1 = i16::from(self.pre_update_average_rate) as i128; - let t_1 = last_update_timestamp.checked_sub(initialization_timestamp)?; - let r_2 = i16::from(self.current_rate) as i128; - let t_2 = (current_timestamp as i128).checked_sub(last_update_timestamp)?; - let total_timespan = t_1.checked_add(t_2)?; - let average_rate = if total_timespan == 0 { - // happens in testing situations, just use the new rate since the earlier - // one was never practically used - r_2 - } else { - r_1.checked_mul(t_1)? - .checked_add(r_2.checked_mul(t_2)?)? - .checked_div(total_timespan)? - }; - average_rate.try_into().ok() - } -} -impl Extension for InterestBearingConfig { - const TYPE: ExtensionType = ExtensionType::InterestBearingConfig; -} - -#[cfg(test)] -mod tests { - use {super::*, proptest::prelude::*}; - - const INT_SECONDS_PER_YEAR: i64 = 6 * 6 * 24 * 36524; - const TEST_DECIMALS: u8 = 2; - - #[test] - fn seconds_per_year() { - assert_eq!(SECONDS_PER_YEAR, 31_556_736.); - assert_eq!(INT_SECONDS_PER_YEAR, 31_556_736); - } - - #[test] - fn specific_amount_to_ui_amount() { - const ONE: u64 = 1_000_000_000_000_000_000; - // constant 5% - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: 500.into(), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: 500.into(), - }; - // 1 year at 5% gives a total of exp(0.05) = 1.0512710963760241 - let ui_amount = config - .amount_to_ui_amount(ONE, 18, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(ui_amount, "1.051271096376024117"); - // with 1 decimal place - let ui_amount = config - .amount_to_ui_amount(ONE, 19, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(ui_amount, "0.1051271096376024117"); - // with 10 decimal places - let ui_amount = config - .amount_to_ui_amount(ONE, 28, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(ui_amount, "0.0000000001051271096376024175"); // different digits at the end! - - // huge amount with 10 decimal places - let ui_amount = config - .amount_to_ui_amount(10_000_000_000, 10, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(ui_amount, "1.0512710964"); - - // negative - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: PodI16::from(-500), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: PodI16::from(-500), - }; - // 1 year at -5% gives a total of exp(-0.05) = 0.951229424500714 - let ui_amount = config - .amount_to_ui_amount(ONE, 18, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(ui_amount, "0.951229424500713905"); - - // net out - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: PodI16::from(-500), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: PodI16::from(500), - }; - // 1 year at -5% and 1 year at 5% gives a total of 1 - let ui_amount = config - .amount_to_ui_amount(1, 0, INT_SECONDS_PER_YEAR * 2) - .unwrap(); - assert_eq!(ui_amount, "1"); - - // huge values - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: PodI16::from(500), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: PodI16::from(500), - }; - let ui_amount = config - .amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 2) - .unwrap(); - assert_eq!(ui_amount, "20386805083448098816"); - let ui_amount = config - .amount_to_ui_amount(u64::MAX, 0, INT_SECONDS_PER_YEAR * 10_000) - .unwrap(); - // there's an underflow risk, but it works! - assert_eq!(ui_amount, "258917064265813826192025834755112557504850551118283225815045099303279643822914042296793377611277551888244755303462190670431480816358154467489350925148558569427069926786360814068189956495940285398273555561779717914539956777398245259214848"); - } - - #[test] - fn specific_ui_amount_to_amount() { - // constant 5% - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: 500.into(), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: 500.into(), - }; - // 1 year at 5% gives a total of exp(0.05) = 1.0512710963760241 - let amount = config - .try_ui_amount_into_amount("1.0512710963760241", 0, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(1, amount); - // with 1 decimal place - let amount = config - .try_ui_amount_into_amount("0.10512710963760241", 1, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(amount, 1); - // with 10 decimal places - let amount = config - .try_ui_amount_into_amount("0.00000000010512710963760242", 10, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(amount, 1); - - // huge amount with 10 decimal places - let amount = config - .try_ui_amount_into_amount("1.0512710963760241", 10, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(amount, 10_000_000_000); - - // negative - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: PodI16::from(-500), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: PodI16::from(-500), - }; - // 1 year at -5% gives a total of exp(-0.05) = 0.951229424500714 - let amount = config - .try_ui_amount_into_amount("0.951229424500714", 0, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(amount, 1); - - // net out - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: PodI16::from(-500), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: PodI16::from(500), - }; - // 1 year at -5% and 1 year at 5% gives a total of 1 - let amount = config - .try_ui_amount_into_amount("1", 0, INT_SECONDS_PER_YEAR * 2) - .unwrap(); - assert_eq!(amount, 1); - - // huge values - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: PodI16::from(500), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: PodI16::from(500), - }; - let amount = config - .try_ui_amount_into_amount("20386805083448100000", 0, INT_SECONDS_PER_YEAR * 2) - .unwrap(); - assert_eq!(amount, u64::MAX); - let amount = config - .try_ui_amount_into_amount("258917064265813830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 0, INT_SECONDS_PER_YEAR * 10_000) - .unwrap(); - assert_eq!(amount, u64::MAX); - // scientific notation "e" - let amount = config - .try_ui_amount_into_amount("2.5891706426581383e236", 0, INT_SECONDS_PER_YEAR * 10_000) - .unwrap(); - assert_eq!(amount, u64::MAX); - // scientific notation "E" - let amount = config - .try_ui_amount_into_amount("2.5891706426581383E236", 0, INT_SECONDS_PER_YEAR * 10_000) - .unwrap(); - assert_eq!(amount, u64::MAX); - - // overflow u64 fail - assert_eq!( - Err(ProgramError::InvalidArgument), - config.try_ui_amount_into_amount("20386805083448200001", 0, INT_SECONDS_PER_YEAR) - ); - - for fail_ui_amount in ["-0.0000000000000000000001", "inf", "-inf", "NaN"] { - assert_eq!( - Err(ProgramError::InvalidArgument), - config.try_ui_amount_into_amount(fail_ui_amount, 0, INT_SECONDS_PER_YEAR) - ); - } - } - - #[test] - fn specific_amount_to_ui_amount_no_interest() { - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: 0.into(), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: 0.into(), - }; - for (amount, expected) in [(23, "0.23"), (110, "1.1"), (4200, "42"), (0, "0")] { - let ui_amount = config - .amount_to_ui_amount(amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(ui_amount, expected); - } - } - - #[test] - fn specific_ui_amount_to_amount_no_interest() { - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: 0.into(), - pre_update_average_rate: 0.into(), - last_update_timestamp: INT_SECONDS_PER_YEAR.into(), - current_rate: 0.into(), - }; - for (ui_amount, expected) in [ - ("0.23", 23), - ("0.20", 20), - ("0.2000", 20), - (".2", 20), - ("1.1", 110), - ("1.10", 110), - ("42", 4200), - ("42.", 4200), - ("0", 0), - ] { - let amount = config - .try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(expected, amount); - } - - // this is invalid with normal mints, but rounding for this mint makes it ok - let amount = config - .try_ui_amount_into_amount("0.111", TEST_DECIMALS, INT_SECONDS_PER_YEAR) - .unwrap(); - assert_eq!(11, amount); - - // fail if invalid ui_amount passed in - for ui_amount in ["", ".", "0.t"] { - assert_eq!( - Err(ProgramError::InvalidArgument), - config.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, INT_SECONDS_PER_YEAR), - ); - } - } - - prop_compose! { - /// Three values in ascending order - fn low_middle_high() - (middle in 1..i64::MAX - 1) - (low in 0..=middle, middle in Just(middle), high in middle..=i64::MAX) - -> (i64, i64, i64) { - (low, middle, high) - } - } - - proptest! { - #[test] - fn time_weighted_average_calc( - current_rate in i16::MIN..i16::MAX, - pre_update_average_rate in i16::MIN..i16::MAX, - (initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(), - ) { - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: initialization_timestamp.into(), - pre_update_average_rate: pre_update_average_rate.into(), - last_update_timestamp: last_update_timestamp.into(), - current_rate: current_rate.into(), - }; - let new_rate = config.time_weighted_average_rate(current_timestamp).unwrap(); - if pre_update_average_rate <= current_rate { - assert!(pre_update_average_rate <= new_rate); - assert!(new_rate <= current_rate); - } else { - assert!(current_rate <= new_rate); - assert!(new_rate <= pre_update_average_rate); - } - } - - #[test] - fn amount_to_ui_amount( - current_rate in i16::MIN..i16::MAX, - pre_update_average_rate in i16::MIN..i16::MAX, - (initialization_timestamp, last_update_timestamp, current_timestamp) in low_middle_high(), - amount in 0..=u64::MAX, - decimals in 0u8..20u8, - ) { - let config = InterestBearingConfig { - rate_authority: OptionalNonZeroPubkey::default(), - initialization_timestamp: initialization_timestamp.into(), - pre_update_average_rate: pre_update_average_rate.into(), - last_update_timestamp: last_update_timestamp.into(), - current_rate: current_rate.into(), - }; - let ui_amount = config.amount_to_ui_amount(amount, decimals, current_timestamp); - assert!(ui_amount.is_some()); - } - } -} diff --git a/token/program-2022/src/extension/interest_bearing_mint/processor.rs b/token/program-2022/src/extension/interest_bearing_mint/processor.rs deleted file mode 100644 index cf5c6c6a9b1..00000000000 --- a/token/program-2022/src/extension/interest_bearing_mint/processor.rs +++ /dev/null @@ -1,107 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - interest_bearing_mint::{ - instruction::{InitializeInstructionData, InterestBearingMintInstruction}, - BasisPoints, InterestBearingConfig, - }, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::PodMint, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - clock::Clock, - entrypoint::ProgramResult, - msg, - pubkey::Pubkey, - sysvar::Sysvar, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -fn process_initialize( - _program_id: &Pubkey, - accounts: &[AccountInfo], - rate_authority: &OptionalNonZeroPubkey, - rate: &BasisPoints, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - - let clock = Clock::get()?; - let extension = mint.init_extension::(true)?; - extension.rate_authority = *rate_authority; - extension.initialization_timestamp = clock.unix_timestamp.into(); - extension.last_update_timestamp = clock.unix_timestamp.into(); - // There is no validation on the rate, since ridiculous values are *technically* - // possible! - extension.pre_update_average_rate = *rate; - extension.current_rate = *rate; - Ok(()) -} - -fn process_update_rate( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_rate: &BasisPoints, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - let rate_authority = - Option::::from(extension.rate_authority).ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &rate_authority, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?; - - let clock = Clock::get()?; - let new_average_rate = extension - .time_weighted_average_rate(clock.unix_timestamp) - .ok_or(TokenError::Overflow)?; - extension.pre_update_average_rate = new_average_rate.into(); - extension.last_update_timestamp = clock.unix_timestamp.into(); - // There is no validation on the rate, since ridiculous values are *technically* - // possible! - extension.current_rate = *new_rate; - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - match decode_instruction_type(input)? { - InterestBearingMintInstruction::Initialize => { - msg!("InterestBearingMintInstruction::Initialize"); - let InitializeInstructionData { - rate_authority, - rate, - } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, rate_authority, rate) - } - InterestBearingMintInstruction::UpdateRate => { - msg!("InterestBearingMintInstruction::UpdateRate"); - let new_rate = decode_instruction_data(input)?; - process_update_rate(program_id, accounts, new_rate) - } - } -} diff --git a/token/program-2022/src/extension/memo_transfer/instruction.rs b/token/program-2022/src/extension/memo_transfer/instruction.rs deleted file mode 100644 index 48c3d78adab..00000000000 --- a/token/program-2022/src/extension/memo_transfer/instruction.rs +++ /dev/null @@ -1,98 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - instruction::{encode_instruction, TokenInstruction}, - }, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, -}; - -/// Required Memo Transfers extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum RequiredMemoTransfersInstruction { - /// Require memos for transfers into this Account. Adds the `MemoTransfer` - /// extension to the Account, if it doesn't already exist. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to update. - /// 1. `[signer]` The account's owner. - /// - /// * Multisignature authority - /// 0. `[writable]` The account to update. - /// 1. `[]` The account's multisignature owner. - /// 2. `..2+M` `[signer]` M signer accounts. - Enable, - /// Stop requiring memos for transfers into this Account. - /// - /// Implicitly initializes the extension in the case where it is not - /// present. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to update. - /// 1. `[signer]` The account's owner. - /// - /// * Multisignature authority - /// 0. `[writable]` The account to update. - /// 1. `[]` The account's multisignature owner. - /// 2. `..2+M` `[signer]` M signer accounts. - Disable, -} - -/// Create an `Enable` instruction -pub fn enable_required_transfer_memos( - token_program_id: &Pubkey, - account: &Pubkey, - owner: &Pubkey, - signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*account, false), - AccountMeta::new_readonly(*owner, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::MemoTransferExtension, - RequiredMemoTransfersInstruction::Enable, - &(), - )) -} - -/// Create a `Disable` instruction -pub fn disable_required_transfer_memos( - token_program_id: &Pubkey, - account: &Pubkey, - owner: &Pubkey, - signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*account, false), - AccountMeta::new_readonly(*owner, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::MemoTransferExtension, - RequiredMemoTransfersInstruction::Disable, - &(), - )) -} diff --git a/token/program-2022/src/extension/memo_transfer/mod.rs b/token/program-2022/src/extension/memo_transfer/mod.rs deleted file mode 100644 index ab1565db568..00000000000 --- a/token/program-2022/src/extension/memo_transfer/mod.rs +++ /dev/null @@ -1,55 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - error::TokenError, - extension::{BaseState, BaseStateWithExtensions, Extension, ExtensionType}, - }, - bytemuck::{Pod, Zeroable}, - solana_program::{ - instruction::get_processed_sibling_instruction, program_error::ProgramError, pubkey::Pubkey, - }, - spl_pod::primitives::PodBool, -}; - -/// Memo Transfer extension instructions -pub mod instruction; - -/// Memo Transfer extension processor -pub mod processor; - -/// Memo Transfer extension for Accounts -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct MemoTransfer { - /// Require transfers into this account to be accompanied by a memo - pub require_incoming_transfer_memos: PodBool, -} -impl Extension for MemoTransfer { - const TYPE: ExtensionType = ExtensionType::MemoTransfer; -} - -/// Determine if a memo is required for transfers into this account -pub fn memo_required, S: BaseState>(account_state: &BSE) -> bool { - if let Ok(extension) = account_state.get_extension::() { - return extension.require_incoming_transfer_memos.into(); - } - false -} - -/// Check if the previous sibling instruction is a memo -pub fn check_previous_sibling_instruction_is_memo() -> Result<(), ProgramError> { - let is_memo_program = |program_id: &Pubkey| -> bool { - program_id == &spl_memo::id() || program_id == &spl_memo::v1::id() - }; - let previous_instruction = get_processed_sibling_instruction(0); - match previous_instruction { - Some(instruction) if is_memo_program(&instruction.program_id) => {} - _ => { - return Err(TokenError::NoMemo.into()); - } - } - Ok(()) -} diff --git a/token/program-2022/src/extension/memo_transfer/processor.rs b/token/program-2022/src/extension/memo_transfer/processor.rs deleted file mode 100644 index 3d1a1de658f..00000000000 --- a/token/program-2022/src/extension/memo_transfer/processor.rs +++ /dev/null @@ -1,69 +0,0 @@ -use { - crate::{ - check_program_account, - extension::{ - memo_transfer::{instruction::RequiredMemoTransfersInstruction, MemoTransfer}, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::decode_instruction_type, - pod::PodAccount, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - pubkey::Pubkey, - }, -}; - -/// Toggle the `RequiredMemoTransfers` extension, initializing the extension if -/// not already present. -fn process_toggle_required_memo_transfers( - program_id: &Pubkey, - accounts: &[AccountInfo], - enable: bool, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut account_data = token_account_info.data.borrow_mut(); - let mut account = PodStateWithExtensionsMut::::unpack(&mut account_data)?; - - Processor::validate_owner( - program_id, - &account.base.owner, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?; - - let extension = if let Ok(extension) = account.get_extension_mut::() { - extension - } else { - account.init_extension::(true)? - }; - extension.require_incoming_transfer_memos = enable.into(); - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - - match decode_instruction_type(input)? { - RequiredMemoTransfersInstruction::Enable => { - msg!("RequiredMemoTransfersInstruction::Enable"); - process_toggle_required_memo_transfers(program_id, accounts, true /* enable */) - } - RequiredMemoTransfersInstruction::Disable => { - msg!("RequiredMemoTransfersInstruction::Disable"); - process_toggle_required_memo_transfers(program_id, accounts, false /* disable */) - } - } -} diff --git a/token/program-2022/src/extension/metadata_pointer/instruction.rs b/token/program-2022/src/extension/metadata_pointer/instruction.rs deleted file mode 100644 index 47d326869c9..00000000000 --- a/token/program-2022/src/extension/metadata_pointer/instruction.rs +++ /dev/null @@ -1,128 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - instruction::{encode_instruction, TokenInstruction}, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - std::convert::TryInto, -}; - -/// Metadata pointer extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum MetadataPointerInstruction { - /// Initialize a new mint with a metadata pointer - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// - /// Data expected by this instruction: - /// `crate::extension::metadata_pointer::instruction::InitializeInstructionData` - Initialize, - /// Update the metadata pointer address. Only supported for mints that - /// include the `MetadataPointer` extension. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[signer]` The metadata pointer authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[]` The mint's metadata pointer authority. - /// 2. `..2+M` `[signer]` M signer accounts. - /// - /// Data expected by this instruction: - /// `crate::extension::metadata_pointer::instruction::UpdateInstructionData` - Update, -} - -/// Data expected by `Initialize` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeInstructionData { - /// The public key for the account that can update the metadata address - pub authority: OptionalNonZeroPubkey, - /// The account address that holds the metadata - pub metadata_address: OptionalNonZeroPubkey, -} - -/// Data expected by `Update` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct UpdateInstructionData { - /// The new account address that holds the metadata - pub metadata_address: OptionalNonZeroPubkey, -} - -/// Create an `Initialize` instruction -pub fn initialize( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: Option, - metadata_address: Option, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::MetadataPointerExtension, - MetadataPointerInstruction::Initialize, - &InitializeInstructionData { - authority: authority.try_into()?, - metadata_address: metadata_address.try_into()?, - }, - )) -} - -/// Create an `Update` instruction -pub fn update( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], - metadata_address: Option, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::MetadataPointerExtension, - MetadataPointerInstruction::Update, - &UpdateInstructionData { - metadata_address: metadata_address.try_into()?, - }, - )) -} diff --git a/token/program-2022/src/extension/metadata_pointer/mod.rs b/token/program-2022/src/extension/metadata_pointer/mod.rs deleted file mode 100644 index 639d92df406..00000000000 --- a/token/program-2022/src/extension/metadata_pointer/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -/// Instructions for the `MetadataPointer` extension -pub mod instruction; -/// Instruction processor for the `MetadataPointer` extension -pub mod processor; - -/// Metadata pointer extension data for mints. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct MetadataPointer { - /// Authority that can set the metadata address - pub authority: OptionalNonZeroPubkey, - /// Account address that holds the metadata - pub metadata_address: OptionalNonZeroPubkey, -} - -impl Extension for MetadataPointer { - const TYPE: ExtensionType = ExtensionType::MetadataPointer; -} diff --git a/token/program-2022/src/extension/metadata_pointer/processor.rs b/token/program-2022/src/extension/metadata_pointer/processor.rs deleted file mode 100644 index 23350372fdf..00000000000 --- a/token/program-2022/src/extension/metadata_pointer/processor.rs +++ /dev/null @@ -1,100 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - metadata_pointer::{ - instruction::{ - InitializeInstructionData, MetadataPointerInstruction, UpdateInstructionData, - }, - MetadataPointer, - }, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::PodMint, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -fn process_initialize( - _program_id: &Pubkey, - accounts: &[AccountInfo], - authority: &OptionalNonZeroPubkey, - metadata_address: &OptionalNonZeroPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - - let extension = mint.init_extension::(true)?; - extension.authority = *authority; - - if Option::::from(*authority).is_none() - && Option::::from(*metadata_address).is_none() - { - msg!("The metadata pointer extension requires at least an authority or an address for initialization, neither was provided"); - Err(TokenError::InvalidInstruction)?; - } - extension.metadata_address = *metadata_address; - Ok(()) -} - -fn process_update( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_metadata_address: &OptionalNonZeroPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - let authority = - Option::::from(extension.authority).ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &authority, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?; - - extension.metadata_address = *new_metadata_address; - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - match decode_instruction_type(input)? { - MetadataPointerInstruction::Initialize => { - msg!("MetadataPointerInstruction::Initialize"); - let InitializeInstructionData { - authority, - metadata_address, - } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, authority, metadata_address) - } - MetadataPointerInstruction::Update => { - msg!("MetadataPointerInstruction::Update"); - let UpdateInstructionData { metadata_address } = decode_instruction_data(input)?; - process_update(program_id, accounts, metadata_address) - } - } -} diff --git a/token/program-2022/src/extension/mint_close_authority.rs b/token/program-2022/src/extension/mint_close_authority.rs deleted file mode 100644 index 7321b7f6004..00000000000 --- a/token/program-2022/src/extension/mint_close_authority.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -/// Close authority extension data for mints. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct MintCloseAuthority { - /// Optional authority to close the mint - pub close_authority: OptionalNonZeroPubkey, -} -impl Extension for MintCloseAuthority { - const TYPE: ExtensionType = ExtensionType::MintCloseAuthority; -} diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs deleted file mode 100644 index e68d5b4a618..00000000000 --- a/token/program-2022/src/extension/mod.rs +++ /dev/null @@ -1,3131 +0,0 @@ -//! Extensions available to token mints and accounts - -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - error::TokenError, - extension::{ - confidential_mint_burn::ConfidentialMintBurn, - confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, - confidential_transfer_fee::{ - ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, - }, - cpi_guard::CpiGuard, - default_account_state::DefaultAccountState, - group_member_pointer::GroupMemberPointer, - group_pointer::GroupPointer, - immutable_owner::ImmutableOwner, - interest_bearing_mint::InterestBearingConfig, - memo_transfer::MemoTransfer, - metadata_pointer::MetadataPointer, - mint_close_authority::MintCloseAuthority, - non_transferable::{NonTransferable, NonTransferableAccount}, - pausable::{PausableAccount, PausableConfig}, - permanent_delegate::PermanentDelegate, - scaled_ui_amount::ScaledUiAmountConfig, - transfer_fee::{TransferFeeAmount, TransferFeeConfig}, - transfer_hook::{TransferHook, TransferHookAccount}, - }, - pod::{PodAccount, PodMint}, - state::{Account, Mint, Multisig, PackedSizeOf}, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - account_info::AccountInfo, - program_error::ProgramError, - program_pack::{IsInitialized, Pack}, - }, - spl_pod::{ - bytemuck::{pod_from_bytes, pod_from_bytes_mut, pod_get_packed_len}, - primitives::PodU16, - }, - spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, - spl_type_length_value::variable_len_pack::VariableLenPack, - std::{ - cmp::Ordering, - convert::{TryFrom, TryInto}, - mem::size_of, - }, -}; - -/// Confidential Transfer extension -pub mod confidential_transfer; -/// Confidential Transfer Fee extension -pub mod confidential_transfer_fee; -/// CPI Guard extension -pub mod cpi_guard; -/// Default Account State extension -pub mod default_account_state; -/// Group Member Pointer extension -pub mod group_member_pointer; -/// Group Pointer extension -pub mod group_pointer; -/// Immutable Owner extension -pub mod immutable_owner; -/// Interest-Bearing Mint extension -pub mod interest_bearing_mint; -/// Memo Transfer extension -pub mod memo_transfer; -/// Metadata Pointer extension -pub mod metadata_pointer; -/// Mint Close Authority extension -pub mod mint_close_authority; -/// Non Transferable extension -pub mod non_transferable; -/// Pausable extension -pub mod pausable; -/// Permanent Delegate extension -pub mod permanent_delegate; -/// Utility to reallocate token accounts -pub mod reallocate; -/// Scaled UI Amount extension -pub mod scaled_ui_amount; -/// Token-group extension -pub mod token_group; -/// Token-metadata extension -pub mod token_metadata; -/// Transfer Fee extension -pub mod transfer_fee; -/// Transfer Hook extension -pub mod transfer_hook; - -/// Confidential mint-burn extension -pub mod confidential_mint_burn; - -/// Length in TLV structure -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct Length(PodU16); -impl From for usize { - fn from(n: Length) -> Self { - Self::from(u16::from(n.0)) - } -} -impl TryFrom for Length { - type Error = ProgramError; - fn try_from(n: usize) -> Result { - u16::try_from(n) - .map(|v| Self(PodU16::from(v))) - .map_err(|_| ProgramError::AccountDataTooSmall) - } -} - -/// Helper function to get the current `TlvIndices` from the current spot -fn get_tlv_indices(type_start: usize) -> TlvIndices { - let length_start = type_start.saturating_add(size_of::()); - let value_start = length_start.saturating_add(pod_get_packed_len::()); - TlvIndices { - type_start, - length_start, - value_start, - } -} - -/// Helper function to tack on the size of an extension bytes if an account with -/// extensions is exactly the size of a multisig -const fn adjust_len_for_multisig(account_len: usize) -> usize { - if account_len == Multisig::LEN { - account_len.saturating_add(size_of::()) - } else { - account_len - } -} - -/// Helper function to calculate exactly how many bytes a value will take up, -/// given the value's length -const fn add_type_and_length_to_len(value_len: usize) -> usize { - value_len - .saturating_add(size_of::()) - .saturating_add(pod_get_packed_len::()) -} - -/// Helper struct for returning the indices of the type, length, and value in -/// a TLV entry -#[derive(Debug)] -struct TlvIndices { - pub type_start: usize, - pub length_start: usize, - pub value_start: usize, -} -fn get_extension_indices( - tlv_data: &[u8], - init: bool, -) -> Result { - let mut start_index = 0; - let v_account_type = V::TYPE.get_account_type(); - while start_index < tlv_data.len() { - let tlv_indices = get_tlv_indices(start_index); - if tlv_data.len() < tlv_indices.value_start { - return Err(ProgramError::InvalidAccountData); - } - let extension_type = - ExtensionType::try_from(&tlv_data[tlv_indices.type_start..tlv_indices.length_start])?; - let account_type = extension_type.get_account_type(); - if extension_type == V::TYPE { - // found an instance of the extension that we're initializing, return! - return Ok(tlv_indices); - // got to an empty spot, init here, or error if we're searching, since - // nothing is written after an Uninitialized spot - } else if extension_type == ExtensionType::Uninitialized { - if init { - return Ok(tlv_indices); - } else { - return Err(TokenError::ExtensionNotFound.into()); - } - } else if v_account_type != account_type { - return Err(TokenError::ExtensionTypeMismatch.into()); - } else { - let length = pod_from_bytes::( - &tlv_data[tlv_indices.length_start..tlv_indices.value_start], - )?; - let value_end_index = tlv_indices.value_start.saturating_add(usize::from(*length)); - start_index = value_end_index; - } - } - Err(ProgramError::InvalidAccountData) -} - -/// Basic information about the TLV buffer, collected from iterating through all -/// entries -#[derive(Debug, PartialEq)] -struct TlvDataInfo { - /// The extension types written in the TLV buffer - extension_types: Vec, - /// The total number bytes allocated for all TLV entries. - /// - /// Each TLV entry's allocated bytes comprises two bytes for the `type`, two - /// bytes for the `length`, and `length` number of bytes for the `value`. - used_len: usize, -} - -/// Fetches basic information about the TLV buffer by iterating through all -/// TLV entries. -fn get_tlv_data_info(tlv_data: &[u8]) -> Result { - let mut extension_types = vec![]; - let mut start_index = 0; - while start_index < tlv_data.len() { - let tlv_indices = get_tlv_indices(start_index); - if tlv_data.len() < tlv_indices.length_start { - // There aren't enough bytes to store the next type, which means we - // got to the end. The last byte could be used during a realloc! - return Ok(TlvDataInfo { - extension_types, - used_len: tlv_indices.type_start, - }); - } - let extension_type = - ExtensionType::try_from(&tlv_data[tlv_indices.type_start..tlv_indices.length_start])?; - if extension_type == ExtensionType::Uninitialized { - return Ok(TlvDataInfo { - extension_types, - used_len: tlv_indices.type_start, - }); - } else { - if tlv_data.len() < tlv_indices.value_start { - // not enough bytes to store the length, malformed - return Err(ProgramError::InvalidAccountData); - } - extension_types.push(extension_type); - let length = pod_from_bytes::( - &tlv_data[tlv_indices.length_start..tlv_indices.value_start], - )?; - - let value_end_index = tlv_indices.value_start.saturating_add(usize::from(*length)); - if value_end_index > tlv_data.len() { - // value blows past the size of the slice, malformed - return Err(ProgramError::InvalidAccountData); - } - start_index = value_end_index; - } - } - Ok(TlvDataInfo { - extension_types, - used_len: start_index, - }) -} - -fn get_first_extension_type(tlv_data: &[u8]) -> Result, ProgramError> { - if tlv_data.is_empty() { - Ok(None) - } else { - let tlv_indices = get_tlv_indices(0); - if tlv_data.len() <= tlv_indices.length_start { - return Ok(None); - } - let extension_type = - ExtensionType::try_from(&tlv_data[tlv_indices.type_start..tlv_indices.length_start])?; - if extension_type == ExtensionType::Uninitialized { - Ok(None) - } else { - Ok(Some(extension_type)) - } - } -} - -fn check_min_len_and_not_multisig(input: &[u8], minimum_len: usize) -> Result<(), ProgramError> { - if input.len() == Multisig::LEN || input.len() < minimum_len { - Err(ProgramError::InvalidAccountData) - } else { - Ok(()) - } -} - -fn check_account_type(account_type: AccountType) -> Result<(), ProgramError> { - if account_type != S::ACCOUNT_TYPE { - Err(ProgramError::InvalidAccountData) - } else { - Ok(()) - } -} - -/// Any account with extensions must be at least `Account::LEN`. Both mints and -/// accounts can have extensions -/// A mint with extensions that takes it past 165 could be indiscernible from an -/// Account with an extension, even if we add the account type. For example, -/// let's say we have: -/// -/// ```text -/// Account: 165 bytes... + [2, 0, 3, 0, 100, ....] -/// ^ ^ ^ ^ -/// acct type extension length data... -/// -/// Mint: 82 bytes... + 83 bytes of other extension data -/// + [2, 0, 3, 0, 100, ....] -/// (data in extension just happens to look like this) -/// ``` -/// -/// With this approach, we only start writing the TLV data after `Account::LEN`, -/// which means we always know that the account type is going to be right after -/// that. We do a special case checking for a Multisig length, because those -/// aren't extensible under any circumstances. -const BASE_ACCOUNT_LENGTH: usize = Account::LEN; -/// Helper that tacks on the `AccountType` length, which gives the minimum for -/// any account with extensions -const BASE_ACCOUNT_AND_TYPE_LENGTH: usize = BASE_ACCOUNT_LENGTH + size_of::(); - -fn type_and_tlv_indices( - rest_input: &[u8], -) -> Result, ProgramError> { - if rest_input.is_empty() { - Ok(None) - } else { - let account_type_index = BASE_ACCOUNT_LENGTH.saturating_sub(S::SIZE_OF); - // check padding is all zeroes - let tlv_start_index = account_type_index.saturating_add(size_of::()); - if rest_input.len() <= tlv_start_index { - return Err(ProgramError::InvalidAccountData); - } - if rest_input[..account_type_index] != vec![0; account_type_index] { - Err(ProgramError::InvalidAccountData) - } else { - Ok(Some((account_type_index, tlv_start_index))) - } - } -} - -/// Checks a base buffer to verify if it is an Account without having to -/// completely deserialize it -fn is_initialized_account(input: &[u8]) -> Result { - const ACCOUNT_INITIALIZED_INDEX: usize = 108; // See state.rs#L99 - - if input.len() != BASE_ACCOUNT_LENGTH { - return Err(ProgramError::InvalidAccountData); - } - Ok(input[ACCOUNT_INITIALIZED_INDEX] != 0) -} - -fn get_extension_bytes(tlv_data: &[u8]) -> Result<&[u8], ProgramError> { - if V::TYPE.get_account_type() != S::ACCOUNT_TYPE { - return Err(ProgramError::InvalidAccountData); - } - let TlvIndices { - type_start: _, - length_start, - value_start, - } = get_extension_indices::(tlv_data, false)?; - // get_extension_indices has checked that tlv_data is long enough to include - // these indices - let length = pod_from_bytes::(&tlv_data[length_start..value_start])?; - let value_end = value_start.saturating_add(usize::from(*length)); - if tlv_data.len() < value_end { - return Err(ProgramError::InvalidAccountData); - } - Ok(&tlv_data[value_start..value_end]) -} - -fn get_extension_bytes_mut( - tlv_data: &mut [u8], -) -> Result<&mut [u8], ProgramError> { - if V::TYPE.get_account_type() != S::ACCOUNT_TYPE { - return Err(ProgramError::InvalidAccountData); - } - let TlvIndices { - type_start: _, - length_start, - value_start, - } = get_extension_indices::(tlv_data, false)?; - // get_extension_indices has checked that tlv_data is long enough to include - // these indices - let length = pod_from_bytes::(&tlv_data[length_start..value_start])?; - let value_end = value_start.saturating_add(usize::from(*length)); - if tlv_data.len() < value_end { - return Err(ProgramError::InvalidAccountData); - } - Ok(&mut tlv_data[value_start..value_end]) -} - -/// Calculate the new expected size if the state allocates the given number -/// of bytes for the given extension type. -/// -/// Provides the correct answer regardless if the extension is already present -/// in the TLV data. -fn try_get_new_account_len_for_extension_len( - tlv_data: &[u8], - new_extension_len: usize, -) -> Result { - // get the new length used by the extension - let new_extension_tlv_len = add_type_and_length_to_len(new_extension_len); - let tlv_info = get_tlv_data_info(tlv_data)?; - // If we're adding an extension, then we must have at least BASE_ACCOUNT_LENGTH - // and account type - let current_len = tlv_info - .used_len - .saturating_add(BASE_ACCOUNT_AND_TYPE_LENGTH); - // get the current length used by the extension - let current_extension_len = get_extension_bytes::(tlv_data) - .map(|x| add_type_and_length_to_len(x.len())) - .unwrap_or(0); - let new_len = current_len - .saturating_sub(current_extension_len) - .saturating_add(new_extension_tlv_len); - Ok(adjust_len_for_multisig(new_len)) -} - -/// Trait for base state with extension -pub trait BaseStateWithExtensions { - /// Get the buffer containing all extension data - fn get_tlv_data(&self) -> &[u8]; - - /// Fetch the bytes for a TLV entry - fn get_extension_bytes(&self) -> Result<&[u8], ProgramError> { - get_extension_bytes::(self.get_tlv_data()) - } - - /// Unpack a portion of the TLV data as the desired type - fn get_extension(&self) -> Result<&V, ProgramError> { - pod_from_bytes::(self.get_extension_bytes::()?) - } - - /// Unpacks a portion of the TLV data as the desired variable-length type - fn get_variable_len_extension( - &self, - ) -> Result { - let data = get_extension_bytes::(self.get_tlv_data())?; - V::unpack_from_slice(data) - } - - /// Iterates through the TLV entries, returning only the types - fn get_extension_types(&self) -> Result, ProgramError> { - get_tlv_data_info(self.get_tlv_data()).map(|x| x.extension_types) - } - - /// Get just the first extension type, useful to track mixed initialization - fn get_first_extension_type(&self) -> Result, ProgramError> { - get_first_extension_type(self.get_tlv_data()) - } - - /// Get the total number of bytes used by TLV entries and the base type - fn try_get_account_len(&self) -> Result { - let tlv_info = get_tlv_data_info(self.get_tlv_data())?; - if tlv_info.extension_types.is_empty() { - Ok(S::SIZE_OF) - } else { - let total_len = tlv_info - .used_len - .saturating_add(BASE_ACCOUNT_AND_TYPE_LENGTH); - Ok(adjust_len_for_multisig(total_len)) - } - } - /// Calculate the new expected size if the state allocates the given - /// fixed-length extension instance. - /// If the state already has the extension, the resulting account length - /// will be unchanged. - fn try_get_new_account_len(&self) -> Result { - try_get_new_account_len_for_extension_len::( - self.get_tlv_data(), - pod_get_packed_len::(), - ) - } - - /// Calculate the new expected size if the state allocates the given - /// variable-length extension instance. - fn try_get_new_account_len_for_variable_len_extension( - &self, - new_extension: &V, - ) -> Result { - try_get_new_account_len_for_extension_len::( - self.get_tlv_data(), - new_extension.get_packed_len()?, - ) - } -} - -/// Encapsulates owned immutable base state data (mint or account) with possible -/// extensions -#[derive(Clone, Debug, PartialEq)] -pub struct StateWithExtensionsOwned { - /// Unpacked base data - pub base: S, - /// Raw TLV data, deserialized on demand - tlv_data: Vec, -} -impl StateWithExtensionsOwned { - /// Unpack base state, leaving the extension data as a slice - /// - /// Fails if the base state is not initialized. - pub fn unpack(mut input: Vec) -> Result { - check_min_len_and_not_multisig(&input, S::SIZE_OF)?; - let mut rest = input.split_off(S::SIZE_OF); - let base = S::unpack(&input)?; - if let Some((account_type_index, tlv_start_index)) = type_and_tlv_indices::(&rest)? { - // type_and_tlv_indices() checks that returned indexes are within range - let account_type = AccountType::try_from(rest[account_type_index]) - .map_err(|_| ProgramError::InvalidAccountData)?; - check_account_type::(account_type)?; - let tlv_data = rest.split_off(tlv_start_index); - Ok(Self { base, tlv_data }) - } else { - Ok(Self { - base, - tlv_data: vec![], - }) - } - } -} - -impl BaseStateWithExtensions for StateWithExtensionsOwned { - fn get_tlv_data(&self) -> &[u8] { - &self.tlv_data - } -} - -/// Encapsulates immutable base state data (mint or account) with possible -/// extensions -#[derive(Debug, PartialEq)] -pub struct StateWithExtensions<'data, S: BaseState + Pack> { - /// Unpacked base data - pub base: S, - /// Slice of data containing all TLV data, deserialized on demand - tlv_data: &'data [u8], -} -impl<'data, S: BaseState + Pack> StateWithExtensions<'data, S> { - /// Unpack base state, leaving the extension data as a slice - /// - /// Fails if the base state is not initialized. - pub fn unpack(input: &'data [u8]) -> Result { - check_min_len_and_not_multisig(input, S::SIZE_OF)?; - let (base_data, rest) = input.split_at(S::SIZE_OF); - let base = S::unpack(base_data)?; - let tlv_data = unpack_tlv_data::(rest)?; - Ok(Self { base, tlv_data }) - } -} -impl<'a, S: BaseState + Pack> BaseStateWithExtensions for StateWithExtensions<'a, S> { - fn get_tlv_data(&self) -> &[u8] { - self.tlv_data - } -} - -/// Encapsulates immutable base state data (mint or account) with possible -/// extensions, where the base state is Pod for zero-copy serde. -#[derive(Debug, PartialEq)] -pub struct PodStateWithExtensions<'data, S: BaseState + Pod> { - /// Unpacked base data - pub base: &'data S, - /// Slice of data containing all TLV data, deserialized on demand - tlv_data: &'data [u8], -} -impl<'data, S: BaseState + Pod> PodStateWithExtensions<'data, S> { - /// Unpack base state, leaving the extension data as a slice - /// - /// Fails if the base state is not initialized. - pub fn unpack(input: &'data [u8]) -> Result { - check_min_len_and_not_multisig(input, S::SIZE_OF)?; - let (base_data, rest) = input.split_at(S::SIZE_OF); - let base = pod_from_bytes::(base_data)?; - if !base.is_initialized() { - Err(ProgramError::UninitializedAccount) - } else { - let tlv_data = unpack_tlv_data::(rest)?; - Ok(Self { base, tlv_data }) - } - } -} -impl<'a, S: BaseState + Pod> BaseStateWithExtensions for PodStateWithExtensions<'a, S> { - fn get_tlv_data(&self) -> &[u8] { - self.tlv_data - } -} - -/// Trait for mutable base state with extension -pub trait BaseStateWithExtensionsMut: BaseStateWithExtensions { - /// Get the underlying TLV data as mutable - fn get_tlv_data_mut(&mut self) -> &mut [u8]; - - /// Get the underlying account type as mutable - fn get_account_type_mut(&mut self) -> &mut [u8]; - - /// Unpack a portion of the TLV data as the base mutable bytes - fn get_extension_bytes_mut(&mut self) -> Result<&mut [u8], ProgramError> { - get_extension_bytes_mut::(self.get_tlv_data_mut()) - } - - /// Unpack a portion of the TLV data as the desired type that allows - /// modifying the type - fn get_extension_mut(&mut self) -> Result<&mut V, ProgramError> { - pod_from_bytes_mut::(self.get_extension_bytes_mut::()?) - } - - /// Packs a variable-length extension into its appropriate data segment. - /// Fails if space hasn't already been allocated for the given extension - fn pack_variable_len_extension( - &mut self, - extension: &V, - ) -> Result<(), ProgramError> { - let data = self.get_extension_bytes_mut::()?; - // NOTE: Do *not* use `pack`, since the length check will cause - // reallocations to smaller sizes to fail - extension.pack_into_slice(data) - } - - /// Packs the default extension data into an open slot if not already found - /// in the data buffer. If extension is already found in the buffer, it - /// overwrites the existing extension with the default state if - /// `overwrite` is set. If extension found, but `overwrite` is not set, - /// it returns error. - fn init_extension( - &mut self, - overwrite: bool, - ) -> Result<&mut V, ProgramError> { - let length = pod_get_packed_len::(); - let buffer = self.alloc::(length, overwrite)?; - let extension_ref = pod_from_bytes_mut::(buffer)?; - *extension_ref = V::default(); - Ok(extension_ref) - } - - /// Reallocate and overwrite the TLV entry for the given variable-length - /// extension. - /// - /// Returns an error if the extension is not present, or if there is not - /// enough space in the buffer. - fn realloc_variable_len_extension( - &mut self, - new_extension: &V, - ) -> Result<(), ProgramError> { - let data = self.realloc::(new_extension.get_packed_len()?)?; - new_extension.pack_into_slice(data) - } - - /// Reallocate the TLV entry for the given extension to the given number of - /// bytes. - /// - /// If the new length is smaller, it will compact the rest of the buffer and - /// zero out the difference at the end. If it's larger, it will move the - /// rest of the buffer data and zero out the new data. - /// - /// Returns an error if the extension is not present, or if this is not - /// enough space in the buffer. - fn realloc( - &mut self, - length: usize, - ) -> Result<&mut [u8], ProgramError> { - let tlv_data = self.get_tlv_data_mut(); - let TlvIndices { - type_start: _, - length_start, - value_start, - } = get_extension_indices::(tlv_data, false)?; - let tlv_len = get_tlv_data_info(tlv_data).map(|x| x.used_len)?; - let data_len = tlv_data.len(); - - let length_ref = pod_from_bytes_mut::(&mut tlv_data[length_start..value_start])?; - let old_length = usize::from(*length_ref); - - // Length check to avoid a panic later in `copy_within` - if old_length < length { - let new_tlv_len = tlv_len.saturating_add(length.saturating_sub(old_length)); - if new_tlv_len > data_len { - return Err(ProgramError::InvalidAccountData); - } - } - - // write new length after the check, to avoid getting into a bad situation - // if trying to recover from an error - *length_ref = Length::try_from(length)?; - - let old_value_end = value_start.saturating_add(old_length); - let new_value_end = value_start.saturating_add(length); - tlv_data.copy_within(old_value_end..tlv_len, new_value_end); - match old_length.cmp(&length) { - Ordering::Greater => { - // realloc to smaller, zero out the end - let new_tlv_len = tlv_len.saturating_sub(old_length.saturating_sub(length)); - tlv_data[new_tlv_len..tlv_len].fill(0); - } - Ordering::Less => { - // realloc to bigger, zero out the new bytes - tlv_data[old_value_end..new_value_end].fill(0); - } - Ordering::Equal => {} // nothing needed! - } - - Ok(&mut tlv_data[value_start..new_value_end]) - } - - /// Allocate the given number of bytes for the given variable-length - /// extension and write its contents into the TLV buffer. - /// - /// This can only be used for variable-sized types, such as `String` or - /// `Vec`. `Pod` types must use `init_extension` - fn init_variable_len_extension( - &mut self, - extension: &V, - overwrite: bool, - ) -> Result<(), ProgramError> { - let data = self.alloc::(extension.get_packed_len()?, overwrite)?; - extension.pack_into_slice(data) - } - - /// Allocate some space for the extension in the TLV data - fn alloc( - &mut self, - length: usize, - overwrite: bool, - ) -> Result<&mut [u8], ProgramError> { - if V::TYPE.get_account_type() != S::ACCOUNT_TYPE { - return Err(ProgramError::InvalidAccountData); - } - let tlv_data = self.get_tlv_data_mut(); - let TlvIndices { - type_start, - length_start, - value_start, - } = get_extension_indices::(tlv_data, true)?; - - if tlv_data[type_start..].len() < add_type_and_length_to_len(length) { - return Err(ProgramError::InvalidAccountData); - } - let extension_type = ExtensionType::try_from(&tlv_data[type_start..length_start])?; - - if extension_type == ExtensionType::Uninitialized || overwrite { - // write extension type - let extension_type_array: [u8; 2] = V::TYPE.into(); - let extension_type_ref = &mut tlv_data[type_start..length_start]; - extension_type_ref.copy_from_slice(&extension_type_array); - // write length - let length_ref = - pod_from_bytes_mut::(&mut tlv_data[length_start..value_start])?; - - // check that the length is the same if we're doing an alloc - // with overwrite, otherwise a realloc should be done - if overwrite && extension_type == V::TYPE && usize::from(*length_ref) != length { - return Err(TokenError::InvalidLengthForAlloc.into()); - } - - *length_ref = Length::try_from(length)?; - - let value_end = value_start.saturating_add(length); - Ok(&mut tlv_data[value_start..value_end]) - } else { - // extension is already initialized, but no overwrite permission - Err(TokenError::ExtensionAlreadyInitialized.into()) - } - } - - /// If `extension_type` is an Account-associated `ExtensionType` that - /// requires initialization on `InitializeAccount`, this method packs - /// the default relevant `Extension` of an `ExtensionType` into an open - /// slot if not already found in the data buffer, otherwise overwrites - /// the existing extension with the default state. For all other - /// `ExtensionType`s, this is a no-op. - fn init_account_extension_from_type( - &mut self, - extension_type: ExtensionType, - ) -> Result<(), ProgramError> { - if extension_type.get_account_type() != AccountType::Account { - return Ok(()); - } - match extension_type { - ExtensionType::TransferFeeAmount => { - self.init_extension::(true).map(|_| ()) - } - ExtensionType::ImmutableOwner => { - self.init_extension::(true).map(|_| ()) - } - ExtensionType::NonTransferableAccount => self - .init_extension::(true) - .map(|_| ()), - ExtensionType::TransferHookAccount => { - self.init_extension::(true).map(|_| ()) - } - // ConfidentialTransfers are currently opt-in only, so this is a no-op for extra safety - // on InitializeAccount - ExtensionType::ConfidentialTransferAccount => Ok(()), - ExtensionType::PausableAccount => { - self.init_extension::(true).map(|_| ()) - } - #[cfg(test)] - ExtensionType::AccountPaddingTest => { - self.init_extension::(true).map(|_| ()) - } - _ => unreachable!(), - } - } - - /// Write the account type into the buffer, done during the base - /// state initialization - /// Noops if there is no room for an extension in the account, needed for - /// pure base mints / accounts. - fn init_account_type(&mut self) -> Result<(), ProgramError> { - let first_extension_type = self.get_first_extension_type()?; - let account_type = self.get_account_type_mut(); - if !account_type.is_empty() { - if let Some(extension_type) = first_extension_type { - let account_type = extension_type.get_account_type(); - if account_type != S::ACCOUNT_TYPE { - return Err(TokenError::ExtensionBaseMismatch.into()); - } - } - account_type[0] = S::ACCOUNT_TYPE.into(); - } - Ok(()) - } - - /// Check that the account type on the account (if initialized) matches the - /// account type for any extensions initialized on the TLV data - fn check_account_type_matches_extension_type(&self) -> Result<(), ProgramError> { - if let Some(extension_type) = self.get_first_extension_type()? { - let account_type = extension_type.get_account_type(); - if account_type != S::ACCOUNT_TYPE { - return Err(TokenError::ExtensionBaseMismatch.into()); - } - } - Ok(()) - } -} - -/// Encapsulates mutable base state data (mint or account) with possible -/// extensions -#[derive(Debug, PartialEq)] -pub struct StateWithExtensionsMut<'data, S: BaseState> { - /// Unpacked base data - pub base: S, - /// Raw base data - base_data: &'data mut [u8], - /// Writable account type - account_type: &'data mut [u8], - /// Slice of data containing all TLV data, deserialized on demand - tlv_data: &'data mut [u8], -} -impl<'data, S: BaseState + Pack> StateWithExtensionsMut<'data, S> { - /// Unpack base state, leaving the extension data as a mutable slice - /// - /// Fails if the base state is not initialized. - pub fn unpack(input: &'data mut [u8]) -> Result { - check_min_len_and_not_multisig(input, S::SIZE_OF)?; - let (base_data, rest) = input.split_at_mut(S::SIZE_OF); - let base = S::unpack(base_data)?; - let (account_type, tlv_data) = unpack_type_and_tlv_data_mut::(rest)?; - Ok(Self { - base, - base_data, - account_type, - tlv_data, - }) - } - - /// Unpack an uninitialized base state, leaving the extension data as a - /// mutable slice - /// - /// Fails if the base state has already been initialized. - pub fn unpack_uninitialized(input: &'data mut [u8]) -> Result { - check_min_len_and_not_multisig(input, S::SIZE_OF)?; - let (base_data, rest) = input.split_at_mut(S::SIZE_OF); - let base = S::unpack_unchecked(base_data)?; - if base.is_initialized() { - return Err(TokenError::AlreadyInUse.into()); - } - let (account_type, tlv_data) = unpack_uninitialized_type_and_tlv_data_mut::(rest)?; - let state = Self { - base, - base_data, - account_type, - tlv_data, - }; - state.check_account_type_matches_extension_type()?; - Ok(state) - } - - /// Packs base state data into the base data portion - pub fn pack_base(&mut self) { - S::pack_into_slice(&self.base, self.base_data); - } -} -impl<'a, S: BaseState> BaseStateWithExtensions for StateWithExtensionsMut<'a, S> { - fn get_tlv_data(&self) -> &[u8] { - self.tlv_data - } -} -impl<'a, S: BaseState> BaseStateWithExtensionsMut for StateWithExtensionsMut<'a, S> { - fn get_tlv_data_mut(&mut self) -> &mut [u8] { - self.tlv_data - } - fn get_account_type_mut(&mut self) -> &mut [u8] { - self.account_type - } -} - -/// Encapsulates mutable base state data (mint or account) with possible -/// extensions, where the base state is Pod for zero-copy serde. -#[derive(Debug, PartialEq)] -pub struct PodStateWithExtensionsMut<'data, S: BaseState> { - /// Unpacked base data - pub base: &'data mut S, - /// Writable account type - account_type: &'data mut [u8], - /// Slice of data containing all TLV data, deserialized on demand - tlv_data: &'data mut [u8], -} -impl<'data, S: BaseState + Pod> PodStateWithExtensionsMut<'data, S> { - /// Unpack base state, leaving the extension data as a mutable slice - /// - /// Fails if the base state is not initialized. - pub fn unpack(input: &'data mut [u8]) -> Result { - check_min_len_and_not_multisig(input, S::SIZE_OF)?; - let (base_data, rest) = input.split_at_mut(S::SIZE_OF); - let base = pod_from_bytes_mut::(base_data)?; - if !base.is_initialized() { - Err(ProgramError::UninitializedAccount) - } else { - let (account_type, tlv_data) = unpack_type_and_tlv_data_mut::(rest)?; - Ok(Self { - base, - account_type, - tlv_data, - }) - } - } - - /// Unpack an uninitialized base state, leaving the extension data as a - /// mutable slice - /// - /// Fails if the base state has already been initialized. - pub fn unpack_uninitialized(input: &'data mut [u8]) -> Result { - check_min_len_and_not_multisig(input, S::SIZE_OF)?; - let (base_data, rest) = input.split_at_mut(S::SIZE_OF); - let base = pod_from_bytes_mut::(base_data)?; - if base.is_initialized() { - return Err(TokenError::AlreadyInUse.into()); - } - let (account_type, tlv_data) = unpack_uninitialized_type_and_tlv_data_mut::(rest)?; - let state = Self { - base, - account_type, - tlv_data, - }; - state.check_account_type_matches_extension_type()?; - Ok(state) - } -} - -impl<'a, S: BaseState> BaseStateWithExtensions for PodStateWithExtensionsMut<'a, S> { - fn get_tlv_data(&self) -> &[u8] { - self.tlv_data - } -} -impl<'a, S: BaseState> BaseStateWithExtensionsMut for PodStateWithExtensionsMut<'a, S> { - fn get_tlv_data_mut(&mut self) -> &mut [u8] { - self.tlv_data - } - fn get_account_type_mut(&mut self) -> &mut [u8] { - self.account_type - } -} - -fn unpack_tlv_data(rest: &[u8]) -> Result<&[u8], ProgramError> { - if let Some((account_type_index, tlv_start_index)) = type_and_tlv_indices::(rest)? { - // type_and_tlv_indices() checks that returned indexes are within range - let account_type = AccountType::try_from(rest[account_type_index]) - .map_err(|_| ProgramError::InvalidAccountData)?; - check_account_type::(account_type)?; - Ok(&rest[tlv_start_index..]) - } else { - Ok(&[]) - } -} - -fn unpack_type_and_tlv_data_with_check_mut< - S: BaseState, - F: Fn(AccountType) -> Result<(), ProgramError>, ->( - rest: &mut [u8], - check_fn: F, -) -> Result<(&mut [u8], &mut [u8]), ProgramError> { - if let Some((account_type_index, tlv_start_index)) = type_and_tlv_indices::(rest)? { - // type_and_tlv_indices() checks that returned indexes are within range - let account_type = AccountType::try_from(rest[account_type_index]) - .map_err(|_| ProgramError::InvalidAccountData)?; - check_fn(account_type)?; - let (account_type, tlv_data) = rest.split_at_mut(tlv_start_index); - Ok(( - &mut account_type[account_type_index..tlv_start_index], - tlv_data, - )) - } else { - Ok((&mut [], &mut [])) - } -} - -fn unpack_type_and_tlv_data_mut( - rest: &mut [u8], -) -> Result<(&mut [u8], &mut [u8]), ProgramError> { - unpack_type_and_tlv_data_with_check_mut::(rest, check_account_type::) -} - -fn unpack_uninitialized_type_and_tlv_data_mut( - rest: &mut [u8], -) -> Result<(&mut [u8], &mut [u8]), ProgramError> { - unpack_type_and_tlv_data_with_check_mut::(rest, |account_type| { - if account_type != AccountType::Uninitialized { - Err(ProgramError::InvalidAccountData) - } else { - Ok(()) - } - }) -} - -/// If `AccountType` is uninitialized, set it to the `BaseState`'s -/// `ACCOUNT_TYPE`; if `AccountType` is already set, check is set correctly for -/// `BaseState`. This method assumes that the `base_data` has already been -/// packed with data of the desired type. -pub fn set_account_type(input: &mut [u8]) -> Result<(), ProgramError> { - check_min_len_and_not_multisig(input, S::SIZE_OF)?; - let (base_data, rest) = input.split_at_mut(S::SIZE_OF); - if S::ACCOUNT_TYPE == AccountType::Account && !is_initialized_account(base_data)? { - return Err(ProgramError::InvalidAccountData); - } - if let Some((account_type_index, _tlv_start_index)) = type_and_tlv_indices::(rest)? { - let mut account_type = AccountType::try_from(rest[account_type_index]) - .map_err(|_| ProgramError::InvalidAccountData)?; - if account_type == AccountType::Uninitialized { - rest[account_type_index] = S::ACCOUNT_TYPE.into(); - account_type = S::ACCOUNT_TYPE; - } - check_account_type::(account_type)?; - Ok(()) - } else { - Err(ProgramError::InvalidAccountData) - } -} - -/// Different kinds of accounts. Note that `Mint`, `Account`, and `Multisig` -/// types are determined exclusively by the size of the account, and are not -/// included in the account data. `AccountType` is only included if extensions -/// have been initialized. -#[repr(u8)] -#[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] -pub enum AccountType { - /// Marker for 0 data - Uninitialized, - /// Mint account with additional extensions - Mint, - /// Token holding account with additional extensions - Account, -} -impl Default for AccountType { - fn default() -> Self { - Self::Uninitialized - } -} - -/// Extensions that can be applied to mints or accounts. Mint extensions must -/// only be applied to mint accounts, and account extensions must only be -/// applied to token holding accounts. -#[repr(u16)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] -pub enum ExtensionType { - /// Used as padding if the account size would otherwise be 355, same as a - /// multisig - Uninitialized, - /// Includes transfer fee rate info and accompanying authorities to withdraw - /// and set the fee - TransferFeeConfig, - /// Includes withheld transfer fees - TransferFeeAmount, - /// Includes an optional mint close authority - MintCloseAuthority, - /// Auditor configuration for confidential transfers - ConfidentialTransferMint, - /// State for confidential transfers - ConfidentialTransferAccount, - /// Specifies the default Account::state for new Accounts - DefaultAccountState, - /// Indicates that the Account owner authority cannot be changed - ImmutableOwner, - /// Require inbound transfers to have memo - MemoTransfer, - /// Indicates that the tokens from this mint can't be transferred - NonTransferable, - /// Tokens accrue interest over time, - InterestBearingConfig, - /// Locks privileged token operations from happening via CPI - CpiGuard, - /// Includes an optional permanent delegate - PermanentDelegate, - /// Indicates that the tokens in this account belong to a non-transferable - /// mint - NonTransferableAccount, - /// Mint requires a CPI to a program implementing the "transfer hook" - /// interface - TransferHook, - /// Indicates that the tokens in this account belong to a mint with a - /// transfer hook - TransferHookAccount, - /// Includes encrypted withheld fees and the encryption public that they are - /// encrypted under - ConfidentialTransferFeeConfig, - /// Includes confidential withheld transfer fees - ConfidentialTransferFeeAmount, - /// Mint contains a pointer to another account (or the same account) that - /// holds metadata - MetadataPointer, - /// Mint contains token-metadata - TokenMetadata, - /// Mint contains a pointer to another account (or the same account) that - /// holds group configurations - GroupPointer, - /// Mint contains token group configurations - TokenGroup, - /// Mint contains a pointer to another account (or the same account) that - /// holds group member configurations - GroupMemberPointer, - /// Mint contains token group member configurations - TokenGroupMember, - /// Mint allowing the minting and burning of confidential tokens - ConfidentialMintBurn, - /// Tokens whose UI amount is scaled by a given amount - ScaledUiAmount, - /// Tokens where minting / burning / transferring can be paused - Pausable, - /// Indicates that the account belongs to a pausable mint - PausableAccount, - - /// Test variable-length mint extension - #[cfg(test)] - VariableLenMintTest = u16::MAX - 2, - /// Padding extension used to make an account exactly Multisig::LEN, used - /// for testing - #[cfg(test)] - AccountPaddingTest, - /// Padding extension used to make a mint exactly Multisig::LEN, used for - /// testing - #[cfg(test)] - MintPaddingTest, -} -impl TryFrom<&[u8]> for ExtensionType { - type Error = ProgramError; - fn try_from(a: &[u8]) -> Result { - Self::try_from(u16::from_le_bytes( - a.try_into().map_err(|_| ProgramError::InvalidAccountData)?, - )) - .map_err(|_| ProgramError::InvalidAccountData) - } -} -impl From for [u8; 2] { - fn from(a: ExtensionType) -> Self { - u16::from(a).to_le_bytes() - } -} -impl ExtensionType { - /// Returns true if the given extension type is sized - /// - /// Most extension types should be sized, so any variable-length extension - /// types should be added here by hand - const fn sized(&self) -> bool { - match self { - ExtensionType::TokenMetadata => false, - #[cfg(test)] - ExtensionType::VariableLenMintTest => false, - _ => true, - } - } - - /// Get the data length of the type associated with the enum - /// - /// Fails if the extension type has a variable length - fn try_get_type_len(&self) -> Result { - if !self.sized() { - return Err(ProgramError::InvalidArgument); - } - Ok(match self { - ExtensionType::Uninitialized => 0, - ExtensionType::TransferFeeConfig => pod_get_packed_len::(), - ExtensionType::TransferFeeAmount => pod_get_packed_len::(), - ExtensionType::MintCloseAuthority => pod_get_packed_len::(), - ExtensionType::ImmutableOwner => pod_get_packed_len::(), - ExtensionType::ConfidentialTransferMint => { - pod_get_packed_len::() - } - ExtensionType::ConfidentialTransferAccount => { - pod_get_packed_len::() - } - ExtensionType::DefaultAccountState => pod_get_packed_len::(), - ExtensionType::MemoTransfer => pod_get_packed_len::(), - ExtensionType::NonTransferable => pod_get_packed_len::(), - ExtensionType::InterestBearingConfig => pod_get_packed_len::(), - ExtensionType::CpiGuard => pod_get_packed_len::(), - ExtensionType::PermanentDelegate => pod_get_packed_len::(), - ExtensionType::NonTransferableAccount => pod_get_packed_len::(), - ExtensionType::TransferHook => pod_get_packed_len::(), - ExtensionType::TransferHookAccount => pod_get_packed_len::(), - ExtensionType::ConfidentialTransferFeeConfig => { - pod_get_packed_len::() - } - ExtensionType::ConfidentialTransferFeeAmount => { - pod_get_packed_len::() - } - ExtensionType::MetadataPointer => pod_get_packed_len::(), - ExtensionType::TokenMetadata => unreachable!(), - ExtensionType::GroupPointer => pod_get_packed_len::(), - ExtensionType::TokenGroup => pod_get_packed_len::(), - ExtensionType::GroupMemberPointer => pod_get_packed_len::(), - ExtensionType::TokenGroupMember => pod_get_packed_len::(), - ExtensionType::ConfidentialMintBurn => pod_get_packed_len::(), - ExtensionType::ScaledUiAmount => pod_get_packed_len::(), - ExtensionType::Pausable => pod_get_packed_len::(), - ExtensionType::PausableAccount => pod_get_packed_len::(), - #[cfg(test)] - ExtensionType::AccountPaddingTest => pod_get_packed_len::(), - #[cfg(test)] - ExtensionType::MintPaddingTest => pod_get_packed_len::(), - #[cfg(test)] - ExtensionType::VariableLenMintTest => unreachable!(), - }) - } - - /// Get the TLV length for an `ExtensionType` - /// - /// Fails if the extension type has a variable length - fn try_get_tlv_len(&self) -> Result { - Ok(add_type_and_length_to_len(self.try_get_type_len()?)) - } - - /// Get the TLV length for a set of `ExtensionType`s - /// - /// Fails if any of the extension types has a variable length - fn try_get_total_tlv_len(extension_types: &[Self]) -> Result { - // dedupe extensions - let mut extensions = vec![]; - for extension_type in extension_types { - if !extensions.contains(&extension_type) { - extensions.push(extension_type); - } - } - extensions.iter().map(|e| e.try_get_tlv_len()).sum() - } - - /// Get the required account data length for the given `ExtensionType`s - /// - /// Fails if any of the extension types has a variable length - pub fn try_calculate_account_len( - extension_types: &[Self], - ) -> Result { - if extension_types.is_empty() { - Ok(S::SIZE_OF) - } else { - let extension_size = Self::try_get_total_tlv_len(extension_types)?; - let total_len = extension_size.saturating_add(BASE_ACCOUNT_AND_TYPE_LENGTH); - Ok(adjust_len_for_multisig(total_len)) - } - } - - /// Get the associated account type - pub fn get_account_type(&self) -> AccountType { - match self { - ExtensionType::Uninitialized => AccountType::Uninitialized, - ExtensionType::TransferFeeConfig - | ExtensionType::MintCloseAuthority - | ExtensionType::ConfidentialTransferMint - | ExtensionType::DefaultAccountState - | ExtensionType::NonTransferable - | ExtensionType::InterestBearingConfig - | ExtensionType::PermanentDelegate - | ExtensionType::TransferHook - | ExtensionType::ConfidentialTransferFeeConfig - | ExtensionType::MetadataPointer - | ExtensionType::TokenMetadata - | ExtensionType::GroupPointer - | ExtensionType::TokenGroup - | ExtensionType::GroupMemberPointer - | ExtensionType::ConfidentialMintBurn - | ExtensionType::TokenGroupMember - | ExtensionType::ScaledUiAmount - | ExtensionType::Pausable => AccountType::Mint, - ExtensionType::ImmutableOwner - | ExtensionType::TransferFeeAmount - | ExtensionType::ConfidentialTransferAccount - | ExtensionType::MemoTransfer - | ExtensionType::NonTransferableAccount - | ExtensionType::TransferHookAccount - | ExtensionType::CpiGuard - | ExtensionType::ConfidentialTransferFeeAmount - | ExtensionType::PausableAccount => AccountType::Account, - #[cfg(test)] - ExtensionType::VariableLenMintTest => AccountType::Mint, - #[cfg(test)] - ExtensionType::AccountPaddingTest => AccountType::Account, - #[cfg(test)] - ExtensionType::MintPaddingTest => AccountType::Mint, - } - } - - /// Based on a set of `AccountType::Mint` `ExtensionType`s, get the list of - /// `AccountType::Account` `ExtensionType`s required on `InitializeAccount` - pub fn get_required_init_account_extensions(mint_extension_types: &[Self]) -> Vec { - let mut account_extension_types = vec![]; - for extension_type in mint_extension_types { - match extension_type { - ExtensionType::TransferFeeConfig => { - account_extension_types.push(ExtensionType::TransferFeeAmount); - } - ExtensionType::NonTransferable => { - account_extension_types.push(ExtensionType::NonTransferableAccount); - account_extension_types.push(ExtensionType::ImmutableOwner); - } - ExtensionType::TransferHook => { - account_extension_types.push(ExtensionType::TransferHookAccount); - } - ExtensionType::Pausable => { - account_extension_types.push(ExtensionType::PausableAccount); - } - #[cfg(test)] - ExtensionType::MintPaddingTest => { - account_extension_types.push(ExtensionType::AccountPaddingTest); - } - _ => {} - } - } - account_extension_types - } - - /// Check for invalid combination of mint extensions - pub fn check_for_invalid_mint_extension_combinations( - mint_extension_types: &[Self], - ) -> Result<(), TokenError> { - let mut transfer_fee_config = false; - let mut confidential_transfer_mint = false; - let mut confidential_transfer_fee_config = false; - let mut confidential_mint_burn = false; - let mut interest_bearing = false; - let mut scaled_ui_amount = false; - - for extension_type in mint_extension_types { - match extension_type { - ExtensionType::TransferFeeConfig => transfer_fee_config = true, - ExtensionType::ConfidentialTransferMint => confidential_transfer_mint = true, - ExtensionType::ConfidentialTransferFeeConfig => { - confidential_transfer_fee_config = true - } - ExtensionType::ConfidentialMintBurn => confidential_mint_burn = true, - ExtensionType::InterestBearingConfig => interest_bearing = true, - ExtensionType::ScaledUiAmount => scaled_ui_amount = true, - _ => (), - } - } - - if confidential_transfer_fee_config && !(transfer_fee_config && confidential_transfer_mint) - { - return Err(TokenError::InvalidExtensionCombination); - } - - if transfer_fee_config && confidential_transfer_mint && !confidential_transfer_fee_config { - return Err(TokenError::InvalidExtensionCombination); - } - - if confidential_mint_burn && !confidential_transfer_mint { - return Err(TokenError::InvalidExtensionCombination); - } - - if scaled_ui_amount && interest_bearing { - return Err(TokenError::InvalidExtensionCombination); - } - - Ok(()) - } -} - -/// Trait for base states, specifying the associated enum -pub trait BaseState: PackedSizeOf + IsInitialized { - /// Associated extension type enum, checked at the start of TLV entries - const ACCOUNT_TYPE: AccountType; -} -impl BaseState for Account { - const ACCOUNT_TYPE: AccountType = AccountType::Account; -} -impl BaseState for Mint { - const ACCOUNT_TYPE: AccountType = AccountType::Mint; -} -impl BaseState for PodAccount { - const ACCOUNT_TYPE: AccountType = AccountType::Account; -} -impl BaseState for PodMint { - const ACCOUNT_TYPE: AccountType = AccountType::Mint; -} - -/// Trait to be implemented by all extension states, specifying which extension -/// and account type they are associated with -pub trait Extension { - /// Associated extension type enum, checked at the start of TLV entries - const TYPE: ExtensionType; -} - -/// Padding a mint account to be exactly `Multisig::LEN`. -/// We need to pad 185 bytes, since `Multisig::LEN = 355`, `Account::LEN = 165`, -/// `size_of::() = 1`, `size_of::() = 2`, -/// `size_of::() = 2`. -/// -/// ``` -/// assert_eq!(355 - 165 - 1 - 2 - 2, 185); -/// ``` -#[cfg(test)] -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct MintPaddingTest { - /// Largest value under 185 that implements Pod - pub padding1: [u8; 128], - /// Largest value under 57 that implements Pod - pub padding2: [u8; 48], - /// Exact value needed to finish the padding - pub padding3: [u8; 9], -} -#[cfg(test)] -impl Extension for MintPaddingTest { - const TYPE: ExtensionType = ExtensionType::MintPaddingTest; -} -#[cfg(test)] -impl Default for MintPaddingTest { - fn default() -> Self { - Self { - padding1: [1; 128], - padding2: [2; 48], - padding3: [3; 9], - } - } -} -/// Account version of the `MintPadding` -#[cfg(test)] -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct AccountPaddingTest(MintPaddingTest); -#[cfg(test)] -impl Extension for AccountPaddingTest { - const TYPE: ExtensionType = ExtensionType::AccountPaddingTest; -} - -/// Packs a fixed-length extension into a TLV space -/// -/// This function reallocates the account as needed to accommodate for the -/// change in space. -/// -/// If the extension already exists, it will overwrite the existing extension -/// if `overwrite` is `true`, otherwise it will return an error. -/// -/// If the extension does not exist, it will reallocate the account and write -/// the extension into the TLV buffer. -/// -/// NOTE: Since this function deals with fixed-size extensions, it does not -/// handle _decreasing_ the size of an account's data buffer, like the function -/// `alloc_and_serialize_variable_len_extension` does. -pub(crate) fn alloc_and_serialize( - account_info: &AccountInfo, - new_extension: &V, - overwrite: bool, -) -> Result<(), ProgramError> { - let previous_account_len = account_info.try_data_len()?; - let new_account_len = { - let data = account_info.try_borrow_data()?; - let state = PodStateWithExtensions::::unpack(&data)?; - state.try_get_new_account_len::()? - }; - - // Realloc the account first, if needed - if new_account_len > previous_account_len { - account_info.realloc(new_account_len, false)?; - } - let mut buffer = account_info.try_borrow_mut_data()?; - if previous_account_len <= BASE_ACCOUNT_LENGTH { - set_account_type::(*buffer)?; - } - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer)?; - - // Write the extension - let extension = state.init_extension::(overwrite)?; - *extension = *new_extension; - - Ok(()) -} - -/// Packs a variable-length extension into a TLV space -/// -/// This function reallocates the account as needed to accommodate for the -/// change in space, then reallocates in the TLV buffer, and finally writes the -/// bytes. -/// -/// NOTE: Unlike the `reallocate` instruction, this function will reduce the -/// size of an account if it has too many bytes allocated for the given value. -pub(crate) fn alloc_and_serialize_variable_len_extension< - S: BaseState + Pod, - V: Extension + VariableLenPack, ->( - account_info: &AccountInfo, - new_extension: &V, - overwrite: bool, -) -> Result<(), ProgramError> { - let previous_account_len = account_info.try_data_len()?; - let (new_account_len, extension_already_exists) = { - let data = account_info.try_borrow_data()?; - let state = PodStateWithExtensions::::unpack(&data)?; - let new_account_len = - state.try_get_new_account_len_for_variable_len_extension(new_extension)?; - let extension_already_exists = state.get_extension_bytes::().is_ok(); - (new_account_len, extension_already_exists) - }; - - if extension_already_exists && !overwrite { - return Err(TokenError::ExtensionAlreadyInitialized.into()); - } - - if previous_account_len < new_account_len { - // account size increased, so realloc the account, then the TLV entry, then - // write data - account_info.realloc(new_account_len, false)?; - let mut buffer = account_info.try_borrow_mut_data()?; - if extension_already_exists { - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer)?; - state.realloc_variable_len_extension(new_extension)?; - } else { - if previous_account_len <= BASE_ACCOUNT_LENGTH { - set_account_type::(*buffer)?; - } - // now alloc in the TLV buffer and write the data - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer)?; - state.init_variable_len_extension(new_extension, false)?; - } - } else { - // do it backwards otherwise, write the state, realloc TLV, then the account - let mut buffer = account_info.try_borrow_mut_data()?; - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer)?; - if extension_already_exists { - state.realloc_variable_len_extension(new_extension)?; - } else { - // this situation can happen if we have an overallocated buffer - state.init_variable_len_extension(new_extension, false)?; - } - - let removed_bytes = previous_account_len - .checked_sub(new_account_len) - .ok_or(ProgramError::AccountDataTooSmall)?; - if removed_bytes > 0 { - // this is probably fine, but be safe and avoid invalidating references - drop(buffer); - account_info.realloc(new_account_len, false)?; - } - } - Ok(()) -} - -#[cfg(test)] -mod test { - use { - super::*, - crate::{ - pod::test::{TEST_POD_ACCOUNT, TEST_POD_MINT}, - state::test::{TEST_ACCOUNT_SLICE, TEST_MINT_SLICE}, - }, - bytemuck::Pod, - solana_program::{ - account_info::{Account as GetAccount, IntoAccountInfo}, - clock::Epoch, - entrypoint::MAX_PERMITTED_DATA_INCREASE, - pubkey::Pubkey, - }, - spl_pod::{ - bytemuck::pod_bytes_of, - optional_keys::OptionalNonZeroPubkey, - primitives::{PodBool, PodU64}, - }, - transfer_fee::test::test_transfer_fee_config, - }; - - /// Test fixed-length struct - #[repr(C)] - #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] - struct FixedLenMintTest { - data: [u8; 8], - } - impl Extension for FixedLenMintTest { - const TYPE: ExtensionType = ExtensionType::MintPaddingTest; - } - - /// Test variable-length struct - #[derive(Clone, Debug, PartialEq)] - struct VariableLenMintTest { - data: Vec, - } - impl Extension for VariableLenMintTest { - const TYPE: ExtensionType = ExtensionType::VariableLenMintTest; - } - impl VariableLenPack for VariableLenMintTest { - fn pack_into_slice(&self, dst: &mut [u8]) -> Result<(), ProgramError> { - let data_start = size_of::(); - let end = data_start + self.data.len(); - if dst.len() < end { - Err(ProgramError::InvalidAccountData) - } else { - dst[..data_start].copy_from_slice(&self.data.len().to_le_bytes()); - dst[data_start..end].copy_from_slice(&self.data); - Ok(()) - } - } - fn unpack_from_slice(src: &[u8]) -> Result { - let data_start = size_of::(); - let length = u64::from_le_bytes(src[..data_start].try_into().unwrap()) as usize; - if src[data_start..data_start + length].len() != length { - return Err(ProgramError::InvalidAccountData); - } - let data = Vec::from(&src[data_start..data_start + length]); - Ok(Self { data }) - } - fn get_packed_len(&self) -> Result { - Ok(size_of::().saturating_add(self.data.len())) - } - } - - const MINT_WITH_EXTENSION: &[u8] = &[ - 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 42, 0, 0, 0, 0, 0, 0, 0, 7, 1, 1, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // base mint - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // padding - 1, // account type - 3, 0, // extension type - 32, 0, // length - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, // data - ]; - - const ACCOUNT_WITH_EXTENSION: &[u8] = &[ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, // mint - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, // owner - 3, 0, 0, 0, 0, 0, 0, 0, // amount - 1, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, - 4, 4, 4, 4, 4, 4, // delegate - 2, // account state - 1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, // is native - 6, 0, 0, 0, 0, 0, 0, 0, // delegated amount - 1, 0, 0, 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 7, 7, 7, // close authority - 2, // account type - 15, 0, // extension type - 1, 0, // length - 1, // data - ]; - - #[test] - fn unpack_opaque_buffer() { - // Mint - let state = PodStateWithExtensions::::unpack(MINT_WITH_EXTENSION).unwrap(); - assert_eq!(state.base, &TEST_POD_MINT); - let extension = state.get_extension::().unwrap(); - let close_authority = - OptionalNonZeroPubkey::try_from(Some(Pubkey::new_from_array([1; 32]))).unwrap(); - assert_eq!(extension.close_authority, close_authority); - assert_eq!( - state.get_extension::(), - Err(ProgramError::InvalidAccountData) - ); - assert_eq!( - PodStateWithExtensions::::unpack(MINT_WITH_EXTENSION), - Err(ProgramError::UninitializedAccount) - ); - - let state = PodStateWithExtensions::::unpack(TEST_MINT_SLICE).unwrap(); - assert_eq!(state.base, &TEST_POD_MINT); - - let mut test_mint = TEST_MINT_SLICE.to_vec(); - let state = PodStateWithExtensionsMut::::unpack(&mut test_mint).unwrap(); - assert_eq!(state.base, &TEST_POD_MINT); - - // Account - let state = PodStateWithExtensions::::unpack(ACCOUNT_WITH_EXTENSION).unwrap(); - assert_eq!(state.base, &TEST_POD_ACCOUNT); - let extension = state.get_extension::().unwrap(); - let transferring = PodBool::from(true); - assert_eq!(extension.transferring, transferring); - assert_eq!( - PodStateWithExtensions::::unpack(ACCOUNT_WITH_EXTENSION), - Err(ProgramError::InvalidAccountData) - ); - - let state = PodStateWithExtensions::::unpack(TEST_ACCOUNT_SLICE).unwrap(); - assert_eq!(state.base, &TEST_POD_ACCOUNT); - - let mut test_account = TEST_ACCOUNT_SLICE.to_vec(); - let state = PodStateWithExtensionsMut::::unpack(&mut test_account).unwrap(); - assert_eq!(state.base, &TEST_POD_ACCOUNT); - } - - #[test] - fn mint_fail_unpack_opaque_buffer() { - // input buffer too small - let mut buffer = vec![0, 3]; - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::InvalidAccountData) - ); - assert_eq!( - PodStateWithExtensionsMut::::unpack(&mut buffer), - Err(ProgramError::InvalidAccountData) - ); - assert_eq!( - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer), - Err(ProgramError::InvalidAccountData) - ); - - // tweak the account type - let mut buffer = MINT_WITH_EXTENSION.to_vec(); - buffer[BASE_ACCOUNT_LENGTH] = 3; - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::InvalidAccountData) - ); - - // clear the mint initialized byte - let mut buffer = MINT_WITH_EXTENSION.to_vec(); - buffer[45] = 0; - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::UninitializedAccount) - ); - - // tweak the padding - let mut buffer = MINT_WITH_EXTENSION.to_vec(); - buffer[PodMint::SIZE_OF] = 100; - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::InvalidAccountData) - ); - - // tweak the extension type - let mut buffer = MINT_WITH_EXTENSION.to_vec(); - buffer[BASE_ACCOUNT_LENGTH + 1] = 2; - let state = PodStateWithExtensions::::unpack(&buffer).unwrap(); - assert_eq!( - state.get_extension::(), - Err(ProgramError::Custom( - TokenError::ExtensionTypeMismatch as u32 - )) - ); - - // tweak the length, too big - let mut buffer = MINT_WITH_EXTENSION.to_vec(); - buffer[BASE_ACCOUNT_LENGTH + 3] = 100; - let state = PodStateWithExtensions::::unpack(&buffer).unwrap(); - assert_eq!( - state.get_extension::(), - Err(ProgramError::InvalidAccountData) - ); - - // tweak the length, too small - let mut buffer = MINT_WITH_EXTENSION.to_vec(); - buffer[BASE_ACCOUNT_LENGTH + 3] = 10; - let state = PodStateWithExtensions::::unpack(&buffer).unwrap(); - assert_eq!( - state.get_extension::(), - Err(ProgramError::InvalidAccountData) - ); - - // data buffer is too small - let buffer = &MINT_WITH_EXTENSION[..MINT_WITH_EXTENSION.len() - 1]; - let state = PodStateWithExtensions::::unpack(buffer).unwrap(); - assert_eq!( - state.get_extension::(), - Err(ProgramError::InvalidAccountData) - ); - } - - #[test] - fn account_fail_unpack_opaque_buffer() { - // input buffer too small - let mut buffer = vec![0, 3]; - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::InvalidAccountData) - ); - assert_eq!( - PodStateWithExtensionsMut::::unpack(&mut buffer), - Err(ProgramError::InvalidAccountData) - ); - assert_eq!( - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer), - Err(ProgramError::InvalidAccountData) - ); - - // input buffer invalid - // all 5's - not a valid `AccountState` - let mut buffer = vec![5; BASE_ACCOUNT_LENGTH]; - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::UninitializedAccount) - ); - assert_eq!( - PodStateWithExtensionsMut::::unpack(&mut buffer), - Err(ProgramError::UninitializedAccount) - ); - - // tweak the account type - let mut buffer = ACCOUNT_WITH_EXTENSION.to_vec(); - buffer[BASE_ACCOUNT_LENGTH] = 3; - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::InvalidAccountData) - ); - - // clear the state byte - let mut buffer = ACCOUNT_WITH_EXTENSION.to_vec(); - buffer[108] = 0; - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::UninitializedAccount) - ); - - // tweak the extension type - let mut buffer = ACCOUNT_WITH_EXTENSION.to_vec(); - buffer[BASE_ACCOUNT_LENGTH + 1] = 12; - let state = PodStateWithExtensions::::unpack(&buffer).unwrap(); - assert_eq!( - state.get_extension::(), - Err(ProgramError::Custom( - TokenError::ExtensionTypeMismatch as u32 - )) - ); - - // tweak the length, too big - let mut buffer = ACCOUNT_WITH_EXTENSION.to_vec(); - buffer[BASE_ACCOUNT_LENGTH + 3] = 100; - let state = PodStateWithExtensions::::unpack(&buffer).unwrap(); - assert_eq!( - state.get_extension::(), - Err(ProgramError::InvalidAccountData) - ); - - // tweak the length, too small - let mut buffer = ACCOUNT_WITH_EXTENSION.to_vec(); - buffer[BASE_ACCOUNT_LENGTH + 3] = 10; - let state = PodStateWithExtensions::::unpack(&buffer).unwrap(); - assert_eq!( - state.get_extension::(), - Err(ProgramError::InvalidAccountData) - ); - - // data buffer is too small - let buffer = &ACCOUNT_WITH_EXTENSION[..ACCOUNT_WITH_EXTENSION.len() - 1]; - let state = PodStateWithExtensions::::unpack(buffer).unwrap(); - assert_eq!( - state.get_extension::(), - Err(ProgramError::InvalidAccountData) - ); - } - - #[test] - fn get_extension_types_with_opaque_buffer() { - // incorrect due to the length - assert_eq!( - get_tlv_data_info(&[1, 0, 1, 1]).unwrap_err(), - ProgramError::InvalidAccountData, - ); - // incorrect due to the huge enum number - assert_eq!( - get_tlv_data_info(&[0, 1, 0, 0]).unwrap_err(), - ProgramError::InvalidAccountData, - ); - // correct due to the good enum number and zero length - assert_eq!( - get_tlv_data_info(&[1, 0, 0, 0]).unwrap(), - TlvDataInfo { - extension_types: vec![ExtensionType::try_from(1).unwrap()], - used_len: add_type_and_length_to_len(0), - } - ); - // correct since it's just uninitialized data at the end - assert_eq!( - get_tlv_data_info(&[0, 0]).unwrap(), - TlvDataInfo { - extension_types: vec![], - used_len: 0 - } - ); - } - - #[test] - fn mint_with_extension_pack_unpack() { - let mint_size = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::MintCloseAuthority, - ExtensionType::TransferFeeConfig, - ]) - .unwrap(); - let mut buffer = vec![0; mint_size]; - - // fail unpack - assert_eq!( - PodStateWithExtensionsMut::::unpack(&mut buffer), - Err(ProgramError::UninitializedAccount), - ); - - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - // fail init account extension - assert_eq!( - state.init_extension::(true), - Err(ProgramError::InvalidAccountData), - ); - - // success write extension - let close_authority = - OptionalNonZeroPubkey::try_from(Some(Pubkey::new_from_array([1; 32]))).unwrap(); - let extension = state.init_extension::(true).unwrap(); - extension.close_authority = close_authority; - assert_eq!( - &state.get_extension_types().unwrap(), - &[ExtensionType::MintCloseAuthority] - ); - - // fail init extension when already initialized - assert_eq!( - state.init_extension::(false), - Err(ProgramError::Custom( - TokenError::ExtensionAlreadyInitialized as u32 - )) - ); - - // fail unpack as account, a mint extension was written - assert_eq!( - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer), - Err(ProgramError::Custom( - TokenError::ExtensionBaseMismatch as u32 - )) - ); - - // fail unpack again, still no base data - assert_eq!( - PodStateWithExtensionsMut::::unpack(&mut buffer.clone()), - Err(ProgramError::UninitializedAccount), - ); - - // write base mint - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - state.init_account_type().unwrap(); - - // check raw buffer - let mut expect = TEST_MINT_SLICE.to_vec(); - expect.extend_from_slice(&[0; BASE_ACCOUNT_LENGTH - PodMint::SIZE_OF]); // padding - expect.push(AccountType::Mint.into()); - expect.extend_from_slice(&(ExtensionType::MintCloseAuthority as u16).to_le_bytes()); - expect - .extend_from_slice(&(pod_get_packed_len::() as u16).to_le_bytes()); - expect.extend_from_slice(&[1; 32]); // data - expect.extend_from_slice(&[0; size_of::()]); - expect.extend_from_slice(&[0; size_of::()]); - expect.extend_from_slice(&[0; size_of::()]); - assert_eq!(expect, buffer); - - // unpack uninitialized will now fail because the PodMint is now initialized - assert_eq!( - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer.clone()), - Err(TokenError::AlreadyInUse.into()), - ); - - // check unpacking - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - - // update base - *state.base = TEST_POD_MINT; - state.base.supply = (u64::from(state.base.supply) + 100).into(); - - // check unpacking - let unpacked_extension = state.get_extension_mut::().unwrap(); - assert_eq!(*unpacked_extension, MintCloseAuthority { close_authority }); - - // update extension - let close_authority = OptionalNonZeroPubkey::try_from(None).unwrap(); - unpacked_extension.close_authority = close_authority; - - // check updates are propagated - let base = *state.base; - let state = PodStateWithExtensions::::unpack(&buffer).unwrap(); - assert_eq!(state.base, &base); - let unpacked_extension = state.get_extension::().unwrap(); - assert_eq!(*unpacked_extension, MintCloseAuthority { close_authority }); - - // check raw buffer - let mut expect = vec![]; - expect.extend_from_slice(bytemuck::bytes_of(&base)); - expect.extend_from_slice(&[0; BASE_ACCOUNT_LENGTH - PodMint::SIZE_OF]); // padding - expect.push(AccountType::Mint.into()); - expect.extend_from_slice(&(ExtensionType::MintCloseAuthority as u16).to_le_bytes()); - expect - .extend_from_slice(&(pod_get_packed_len::() as u16).to_le_bytes()); - expect.extend_from_slice(&[0; 32]); - expect.extend_from_slice(&[0; size_of::()]); - expect.extend_from_slice(&[0; size_of::()]); - expect.extend_from_slice(&[0; size_of::()]); - assert_eq!(expect, buffer); - - // fail unpack as an account - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::UninitializedAccount), - ); - - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - // init one more extension - let mint_transfer_fee = test_transfer_fee_config(); - let new_extension = state.init_extension::(true).unwrap(); - new_extension.transfer_fee_config_authority = - mint_transfer_fee.transfer_fee_config_authority; - new_extension.withdraw_withheld_authority = mint_transfer_fee.withdraw_withheld_authority; - new_extension.withheld_amount = mint_transfer_fee.withheld_amount; - new_extension.older_transfer_fee = mint_transfer_fee.older_transfer_fee; - new_extension.newer_transfer_fee = mint_transfer_fee.newer_transfer_fee; - - assert_eq!( - &state.get_extension_types().unwrap(), - &[ - ExtensionType::MintCloseAuthority, - ExtensionType::TransferFeeConfig - ] - ); - - // check raw buffer - let mut expect = vec![]; - expect.extend_from_slice(pod_bytes_of(&base)); - expect.extend_from_slice(&[0; BASE_ACCOUNT_LENGTH - PodMint::SIZE_OF]); // padding - expect.push(AccountType::Mint.into()); - expect.extend_from_slice(&(ExtensionType::MintCloseAuthority as u16).to_le_bytes()); - expect - .extend_from_slice(&(pod_get_packed_len::() as u16).to_le_bytes()); - expect.extend_from_slice(&[0; 32]); // data - expect.extend_from_slice(&(ExtensionType::TransferFeeConfig as u16).to_le_bytes()); - expect.extend_from_slice(&(pod_get_packed_len::() as u16).to_le_bytes()); - expect.extend_from_slice(pod_bytes_of(&mint_transfer_fee)); - assert_eq!(expect, buffer); - - // fail to init one more extension that does not fit - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - assert_eq!( - state.init_extension::(true), - Err(ProgramError::InvalidAccountData), - ); - } - - #[test] - fn mint_extension_any_order() { - let mint_size = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::MintCloseAuthority, - ExtensionType::TransferFeeConfig, - ]) - .unwrap(); - let mut buffer = vec![0; mint_size]; - - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - // write extensions - let close_authority = - OptionalNonZeroPubkey::try_from(Some(Pubkey::new_from_array([1; 32]))).unwrap(); - let extension = state.init_extension::(true).unwrap(); - extension.close_authority = close_authority; - - let mint_transfer_fee = test_transfer_fee_config(); - let extension = state.init_extension::(true).unwrap(); - extension.transfer_fee_config_authority = mint_transfer_fee.transfer_fee_config_authority; - extension.withdraw_withheld_authority = mint_transfer_fee.withdraw_withheld_authority; - extension.withheld_amount = mint_transfer_fee.withheld_amount; - extension.older_transfer_fee = mint_transfer_fee.older_transfer_fee; - extension.newer_transfer_fee = mint_transfer_fee.newer_transfer_fee; - - assert_eq!( - &state.get_extension_types().unwrap(), - &[ - ExtensionType::MintCloseAuthority, - ExtensionType::TransferFeeConfig - ] - ); - - // write base mint - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - state.init_account_type().unwrap(); - - let mut other_buffer = vec![0; mint_size]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut other_buffer).unwrap(); - - // write base mint - *state.base = TEST_POD_MINT; - state.init_account_type().unwrap(); - - // write extensions in a different order - let mint_transfer_fee = test_transfer_fee_config(); - let extension = state.init_extension::(true).unwrap(); - extension.transfer_fee_config_authority = mint_transfer_fee.transfer_fee_config_authority; - extension.withdraw_withheld_authority = mint_transfer_fee.withdraw_withheld_authority; - extension.withheld_amount = mint_transfer_fee.withheld_amount; - extension.older_transfer_fee = mint_transfer_fee.older_transfer_fee; - extension.newer_transfer_fee = mint_transfer_fee.newer_transfer_fee; - - let close_authority = - OptionalNonZeroPubkey::try_from(Some(Pubkey::new_from_array([1; 32]))).unwrap(); - let extension = state.init_extension::(true).unwrap(); - extension.close_authority = close_authority; - - assert_eq!( - &state.get_extension_types().unwrap(), - &[ - ExtensionType::TransferFeeConfig, - ExtensionType::MintCloseAuthority - ] - ); - - // buffers are NOT the same because written in a different order - assert_ne!(buffer, other_buffer); - let state = PodStateWithExtensions::::unpack(&buffer).unwrap(); - let other_state = PodStateWithExtensions::::unpack(&other_buffer).unwrap(); - - // BUT mint and extensions are the same - assert_eq!( - state.get_extension::().unwrap(), - other_state.get_extension::().unwrap() - ); - assert_eq!( - state.get_extension::().unwrap(), - other_state.get_extension::().unwrap() - ); - assert_eq!(state.base, other_state.base); - } - - #[test] - fn mint_with_multisig_len() { - let mut buffer = vec![0; Multisig::LEN]; - assert_eq!( - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer), - Err(ProgramError::InvalidAccountData), - ); - let mint_size = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MintPaddingTest]) - .unwrap(); - assert_eq!(mint_size, Multisig::LEN + size_of::()); - let mut buffer = vec![0; mint_size]; - - // write base mint - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - state.init_account_type().unwrap(); - - // write padding - let extension = state.init_extension::(true).unwrap(); - extension.padding1 = [1; 128]; - extension.padding2 = [1; 48]; - extension.padding3 = [1; 9]; - - assert_eq!( - &state.get_extension_types().unwrap(), - &[ExtensionType::MintPaddingTest] - ); - - // check raw buffer - let mut expect = TEST_MINT_SLICE.to_vec(); - expect.extend_from_slice(&[0; BASE_ACCOUNT_LENGTH - PodMint::SIZE_OF]); // padding - expect.push(AccountType::Mint.into()); - expect.extend_from_slice(&(ExtensionType::MintPaddingTest as u16).to_le_bytes()); - expect.extend_from_slice(&(pod_get_packed_len::() as u16).to_le_bytes()); - expect.extend_from_slice(&vec![1; pod_get_packed_len::()]); - expect.extend_from_slice(&(ExtensionType::Uninitialized as u16).to_le_bytes()); - assert_eq!(expect, buffer); - } - - #[test] - fn account_with_extension_pack_unpack() { - let account_size = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::TransferFeeAmount, - ]) - .unwrap(); - let mut buffer = vec![0; account_size]; - - // fail unpack - assert_eq!( - PodStateWithExtensionsMut::::unpack(&mut buffer), - Err(ProgramError::UninitializedAccount), - ); - - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - // fail init mint extension - assert_eq!( - state.init_extension::(true), - Err(ProgramError::InvalidAccountData), - ); - // success write extension - let withheld_amount = PodU64::from(u64::MAX); - let extension = state.init_extension::(true).unwrap(); - extension.withheld_amount = withheld_amount; - - assert_eq!( - &state.get_extension_types().unwrap(), - &[ExtensionType::TransferFeeAmount] - ); - - // fail unpack again, still no base data - assert_eq!( - PodStateWithExtensionsMut::::unpack(&mut buffer.clone()), - Err(ProgramError::UninitializedAccount), - ); - - // write base account - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_ACCOUNT; - state.init_account_type().unwrap(); - let base = *state.base; - - // check raw buffer - let mut expect = TEST_ACCOUNT_SLICE.to_vec(); - expect.push(AccountType::Account.into()); - expect.extend_from_slice(&(ExtensionType::TransferFeeAmount as u16).to_le_bytes()); - expect.extend_from_slice(&(pod_get_packed_len::() as u16).to_le_bytes()); - expect.extend_from_slice(&u64::from(withheld_amount).to_le_bytes()); - assert_eq!(expect, buffer); - - // check unpacking - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - assert_eq!(state.base, &base); - assert_eq!( - &state.get_extension_types().unwrap(), - &[ExtensionType::TransferFeeAmount] - ); - - // update base - *state.base = TEST_POD_ACCOUNT; - state.base.amount = (u64::from(state.base.amount) + 100).into(); - - // check unpacking - let unpacked_extension = state.get_extension_mut::().unwrap(); - assert_eq!(*unpacked_extension, TransferFeeAmount { withheld_amount }); - - // update extension - let withheld_amount = PodU64::from(u32::MAX as u64); - unpacked_extension.withheld_amount = withheld_amount; - - // check updates are propagated - let base = *state.base; - let state = PodStateWithExtensions::::unpack(&buffer).unwrap(); - assert_eq!(state.base, &base); - let unpacked_extension = state.get_extension::().unwrap(); - assert_eq!(*unpacked_extension, TransferFeeAmount { withheld_amount }); - - // check raw buffer - let mut expect = vec![]; - expect.extend_from_slice(pod_bytes_of(&base)); - expect.push(AccountType::Account.into()); - expect.extend_from_slice(&(ExtensionType::TransferFeeAmount as u16).to_le_bytes()); - expect.extend_from_slice(&(pod_get_packed_len::() as u16).to_le_bytes()); - expect.extend_from_slice(&u64::from(withheld_amount).to_le_bytes()); - assert_eq!(expect, buffer); - - // fail unpack as a mint - assert_eq!( - PodStateWithExtensions::::unpack(&buffer), - Err(ProgramError::InvalidAccountData), - ); - } - - #[test] - fn account_with_multisig_len() { - let mut buffer = vec![0; Multisig::LEN]; - assert_eq!( - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer), - Err(ProgramError::InvalidAccountData), - ); - let account_size = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::AccountPaddingTest, - ]) - .unwrap(); - assert_eq!(account_size, Multisig::LEN + size_of::()); - let mut buffer = vec![0; account_size]; - - // write base account - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_ACCOUNT; - state.init_account_type().unwrap(); - - // write padding - let extension = state.init_extension::(true).unwrap(); - extension.0.padding1 = [2; 128]; - extension.0.padding2 = [2; 48]; - extension.0.padding3 = [2; 9]; - - assert_eq!( - &state.get_extension_types().unwrap(), - &[ExtensionType::AccountPaddingTest] - ); - - // check raw buffer - let mut expect = TEST_ACCOUNT_SLICE.to_vec(); - expect.push(AccountType::Account.into()); - expect.extend_from_slice(&(ExtensionType::AccountPaddingTest as u16).to_le_bytes()); - expect - .extend_from_slice(&(pod_get_packed_len::() as u16).to_le_bytes()); - expect.extend_from_slice(&vec![2; pod_get_packed_len::()]); - expect.extend_from_slice(&(ExtensionType::Uninitialized as u16).to_le_bytes()); - assert_eq!(expect, buffer); - } - - #[test] - fn test_set_account_type() { - // account with buffer big enough for AccountType and Extension - let mut buffer = TEST_ACCOUNT_SLICE.to_vec(); - let needed_len = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::ImmutableOwner, - ]) - .unwrap() - - buffer.len(); - buffer.append(&mut vec![0; needed_len]); - let err = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - set_account_type::(&mut buffer).unwrap(); - // unpack is viable after manual set_account_type - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - assert_eq!(state.base, &TEST_POD_ACCOUNT); - assert_eq!(state.account_type[0], AccountType::Account as u8); - state.init_extension::(true).unwrap(); // just confirming initialization works - - // account with buffer big enough for AccountType only - let mut buffer = TEST_ACCOUNT_SLICE.to_vec(); - buffer.append(&mut vec![0; 2]); - let err = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - set_account_type::(&mut buffer).unwrap(); - // unpack is viable after manual set_account_type - let state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - assert_eq!(state.base, &TEST_POD_ACCOUNT); - assert_eq!(state.account_type[0], AccountType::Account as u8); - - // account with AccountType already set => noop - let mut buffer = TEST_ACCOUNT_SLICE.to_vec(); - buffer.append(&mut vec![2, 0]); - let _ = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - set_account_type::(&mut buffer).unwrap(); - let state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - assert_eq!(state.base, &TEST_POD_ACCOUNT); - assert_eq!(state.account_type[0], AccountType::Account as u8); - - // account with wrong AccountType fails - let mut buffer = TEST_ACCOUNT_SLICE.to_vec(); - buffer.append(&mut vec![1, 0]); - let err = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - let err = set_account_type::(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - - // mint with buffer big enough for AccountType and Extension - let mut buffer = TEST_MINT_SLICE.to_vec(); - let needed_len = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::MintCloseAuthority, - ]) - .unwrap() - - buffer.len(); - buffer.append(&mut vec![0; needed_len]); - let err = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - set_account_type::(&mut buffer).unwrap(); - // unpack is viable after manual set_account_type - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - assert_eq!(state.base, &TEST_POD_MINT); - assert_eq!(state.account_type[0], AccountType::Mint as u8); - state.init_extension::(true).unwrap(); - - // mint with buffer big enough for AccountType only - let mut buffer = TEST_MINT_SLICE.to_vec(); - buffer.append(&mut vec![0; PodAccount::SIZE_OF - PodMint::SIZE_OF]); - buffer.append(&mut vec![0; 2]); - let err = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - set_account_type::(&mut buffer).unwrap(); - // unpack is viable after manual set_account_type - let state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - assert_eq!(state.base, &TEST_POD_MINT); - assert_eq!(state.account_type[0], AccountType::Mint as u8); - - // mint with AccountType already set => noop - let mut buffer = TEST_MINT_SLICE.to_vec(); - buffer.append(&mut vec![0; PodAccount::SIZE_OF - PodMint::SIZE_OF]); - buffer.append(&mut vec![1, 0]); - set_account_type::(&mut buffer).unwrap(); - let state = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap(); - assert_eq!(state.base, &TEST_POD_MINT); - assert_eq!(state.account_type[0], AccountType::Mint as u8); - - // mint with wrong AccountType fails - let mut buffer = TEST_MINT_SLICE.to_vec(); - buffer.append(&mut vec![0; PodAccount::SIZE_OF - PodMint::SIZE_OF]); - buffer.append(&mut vec![2, 0]); - let err = PodStateWithExtensionsMut::::unpack(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - let err = set_account_type::(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - } - - #[test] - fn test_set_account_type_wrongly() { - // try to set PodAccount account_type to PodMint - let mut buffer = TEST_ACCOUNT_SLICE.to_vec(); - buffer.append(&mut vec![0; 2]); - let err = set_account_type::(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - - // try to set PodMint account_type to PodAccount - let mut buffer = TEST_MINT_SLICE.to_vec(); - buffer.append(&mut vec![0; PodAccount::SIZE_OF - PodMint::SIZE_OF]); - buffer.append(&mut vec![0; 2]); - let err = set_account_type::(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - } - - #[test] - fn test_get_required_init_account_extensions() { - // Some mint extensions with no required account extensions - let mint_extensions = vec![ - ExtensionType::MintCloseAuthority, - ExtensionType::Uninitialized, - ]; - assert_eq!( - ExtensionType::get_required_init_account_extensions(&mint_extensions), - vec![] - ); - - // One mint extension with required account extension, one without - let mint_extensions = vec![ - ExtensionType::TransferFeeConfig, - ExtensionType::MintCloseAuthority, - ]; - assert_eq!( - ExtensionType::get_required_init_account_extensions(&mint_extensions), - vec![ExtensionType::TransferFeeAmount] - ); - - // Some mint extensions both with required account extensions - let mint_extensions = vec![ - ExtensionType::TransferFeeConfig, - ExtensionType::MintPaddingTest, - ]; - assert_eq!( - ExtensionType::get_required_init_account_extensions(&mint_extensions), - vec![ - ExtensionType::TransferFeeAmount, - ExtensionType::AccountPaddingTest - ] - ); - - // Demonstrate that method does not dedupe inputs or outputs - let mint_extensions = vec![ - ExtensionType::TransferFeeConfig, - ExtensionType::TransferFeeConfig, - ]; - assert_eq!( - ExtensionType::get_required_init_account_extensions(&mint_extensions), - vec![ - ExtensionType::TransferFeeAmount, - ExtensionType::TransferFeeAmount - ] - ); - } - - #[test] - fn mint_without_extensions() { - let space = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let mut buffer = vec![0; space]; - assert_eq!( - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer), - Err(ProgramError::InvalidAccountData), - ); - - // write base account - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - state.init_account_type().unwrap(); - - // fail init extension - assert_eq!( - state.init_extension::(true), - Err(ProgramError::InvalidAccountData), - ); - - assert_eq!(TEST_MINT_SLICE, buffer); - } - - #[test] - fn test_init_nonzero_default() { - let mint_size = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MintPaddingTest]) - .unwrap(); - let mut buffer = vec![0; mint_size]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - state.init_account_type().unwrap(); - let extension = state.init_extension::(true).unwrap(); - assert_eq!(extension.padding1, [1; 128]); - assert_eq!(extension.padding2, [2; 48]); - assert_eq!(extension.padding3, [3; 9]); - } - - #[test] - fn test_init_buffer_too_small() { - let mint_size = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::MintCloseAuthority, - ]) - .unwrap(); - let mut buffer = vec![0; mint_size - 1]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - let err = state - .init_extension::(true) - .unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - - state.tlv_data[0] = 3; - state.tlv_data[2] = 32; - let err = state.get_extension_mut::().unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - - let mut buffer = vec![0; PodMint::SIZE_OF + 2]; - let err = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - - // OK since there are two bytes for the type, which is `Uninitialized` - let mut buffer = vec![0; BASE_ACCOUNT_LENGTH + 3]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - let err = state.get_extension_mut::().unwrap_err(); - assert_eq!(err, ProgramError::InvalidAccountData); - - assert_eq!(state.get_extension_types().unwrap(), vec![]); - - // OK, there aren't two bytes for the type, but that's fine - let mut buffer = vec![0; BASE_ACCOUNT_LENGTH + 2]; - let state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - assert_eq!(state.get_extension_types().unwrap(), []); - } - - #[test] - fn test_extension_with_no_data() { - let account_size = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::ImmutableOwner, - ]) - .unwrap(); - let mut buffer = vec![0; account_size]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_ACCOUNT; - state.init_account_type().unwrap(); - - let err = state.get_extension::().unwrap_err(); - assert_eq!( - err, - ProgramError::Custom(TokenError::ExtensionNotFound as u32) - ); - - state.init_extension::(true).unwrap(); - assert_eq!( - get_first_extension_type(state.tlv_data).unwrap(), - Some(ExtensionType::ImmutableOwner) - ); - assert_eq!( - get_tlv_data_info(state.tlv_data).unwrap(), - TlvDataInfo { - extension_types: vec![ExtensionType::ImmutableOwner], - used_len: add_type_and_length_to_len(0) - } - ); - } - - #[test] - fn fail_account_len_with_metadata() { - assert_eq!( - ExtensionType::try_calculate_account_len::(&[ - ExtensionType::MintCloseAuthority, - ExtensionType::VariableLenMintTest, - ExtensionType::TransferFeeConfig, - ]) - .unwrap_err(), - ProgramError::InvalidArgument - ); - } - - #[test] - fn alloc() { - let variable_len = VariableLenMintTest { data: vec![1] }; - let alloc_size = variable_len.get_packed_len().unwrap(); - let account_size = - BASE_ACCOUNT_LENGTH + size_of::() + add_type_and_length_to_len(alloc_size); - let mut buffer = vec![0; account_size]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - state - .init_variable_len_extension(&variable_len, false) - .unwrap(); - - // can't double alloc - assert_eq!( - state - .init_variable_len_extension(&variable_len, false) - .unwrap_err(), - TokenError::ExtensionAlreadyInitialized.into() - ); - - // unless overwrite is set - state - .init_variable_len_extension(&variable_len, true) - .unwrap(); - - // can't change the size during overwrite though - assert_eq!( - state - .init_variable_len_extension(&VariableLenMintTest { data: vec![] }, true) - .unwrap_err(), - TokenError::InvalidLengthForAlloc.into() - ); - - // try to write too far, fail earlier - assert_eq!( - state - .init_variable_len_extension(&VariableLenMintTest { data: vec![1, 2] }, true) - .unwrap_err(), - ProgramError::InvalidAccountData - ); - } - - #[test] - fn realloc() { - let small_variable_len = VariableLenMintTest { - data: vec![1, 2, 3], - }; - let base_variable_len = VariableLenMintTest { - data: vec![1, 2, 3, 4], - }; - let big_variable_len = VariableLenMintTest { - data: vec![1, 2, 3, 4, 5], - }; - let too_big_variable_len = VariableLenMintTest { - data: vec![1, 2, 3, 4, 5, 6], - }; - let account_size = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MetadataPointer]) - .unwrap() - + add_type_and_length_to_len(big_variable_len.get_packed_len().unwrap()); - let mut buffer = vec![0; account_size]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - - // alloc both types - state - .init_variable_len_extension(&base_variable_len, false) - .unwrap(); - let max_pubkey = - OptionalNonZeroPubkey::try_from(Some(Pubkey::new_from_array([255; 32]))).unwrap(); - let extension = state.init_extension::(false).unwrap(); - extension.authority = max_pubkey; - extension.metadata_address = max_pubkey; - - // realloc first entry to larger - state - .realloc_variable_len_extension(&big_variable_len) - .unwrap(); - let extension = state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(extension, big_variable_len); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, max_pubkey); - assert_eq!(extension.metadata_address, max_pubkey); - - // realloc to smaller - state - .realloc_variable_len_extension(&small_variable_len) - .unwrap(); - let extension = state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(extension, small_variable_len); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, max_pubkey); - assert_eq!(extension.metadata_address, max_pubkey); - let diff = big_variable_len.get_packed_len().unwrap() - - small_variable_len.get_packed_len().unwrap(); - assert_eq!(&buffer[account_size - diff..account_size], vec![0; diff]); - - // unpack again since we dropped the last `state` - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - // realloc too much, fails - assert_eq!( - state - .realloc_variable_len_extension(&too_big_variable_len) - .unwrap_err(), - ProgramError::InvalidAccountData, - ); - } - - #[test] - fn account_len() { - let small_variable_len = VariableLenMintTest { - data: vec![20, 30, 40], - }; - let variable_len = VariableLenMintTest { - data: vec![20, 30, 40, 50], - }; - let big_variable_len = VariableLenMintTest { - data: vec![20, 30, 40, 50, 60], - }; - let value_len = variable_len.get_packed_len().unwrap(); - let account_size = - BASE_ACCOUNT_LENGTH + size_of::() + add_type_and_length_to_len(value_len); - let mut buffer = vec![0; account_size]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - - // With a new extension, new length must include padding, 1 byte for - // account type, 2 bytes for type, 2 for length - let current_len = state.try_get_account_len().unwrap(); - assert_eq!(current_len, PodMint::SIZE_OF); - let new_len = state - .try_get_new_account_len_for_variable_len_extension::( - &variable_len, - ) - .unwrap(); - assert_eq!( - new_len, - BASE_ACCOUNT_AND_TYPE_LENGTH.saturating_add(add_type_and_length_to_len(value_len)) - ); - - state - .init_variable_len_extension::(&variable_len, false) - .unwrap(); - let current_len = state.try_get_account_len().unwrap(); - assert_eq!(current_len, new_len); - - // Reduce the extension size - let new_len = state - .try_get_new_account_len_for_variable_len_extension::( - &small_variable_len, - ) - .unwrap(); - assert_eq!(current_len.checked_sub(new_len).unwrap(), 1); - - // Increase the extension size - let new_len = state - .try_get_new_account_len_for_variable_len_extension::( - &big_variable_len, - ) - .unwrap(); - assert_eq!(new_len.checked_sub(current_len).unwrap(), 1); - - // Maintain the extension size - let new_len = state - .try_get_new_account_len_for_variable_len_extension::( - &variable_len, - ) - .unwrap(); - assert_eq!(new_len, current_len); - } - - /// Test helper for mimicking the data layout an on-chain `AccountInfo`, - /// which permits "reallocs" as the Solana runtime does it - struct SolanaAccountData { - data: Vec, - lamports: u64, - owner: Pubkey, - } - impl SolanaAccountData { - /// Create a new fake solana account data. The underlying vector is - /// overallocated to mimic the runtime - fn new(account_data: &[u8]) -> Self { - let mut data = vec![]; - data.extend_from_slice(&(account_data.len() as u64).to_le_bytes()); - data.extend_from_slice(account_data); - data.extend_from_slice(&[0; MAX_PERMITTED_DATA_INCREASE]); - Self { - data, - lamports: 10, - owner: Pubkey::new_unique(), - } - } - - /// Data lops off the first 8 bytes, since those store the size of the - /// account for the Solana runtime - fn data(&self) -> &[u8] { - let start = size_of::(); - let len = self.len(); - &self.data[start..start + len] - } - - /// Gets the runtime length of the account data - fn len(&self) -> usize { - self.data - .get(..size_of::()) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .unwrap() as usize - } - } - impl GetAccount for SolanaAccountData { - fn get(&mut self) -> (&mut u64, &mut [u8], &Pubkey, bool, Epoch) { - // need to pull out the data here to avoid a double-mutable borrow - let start = size_of::(); - let len = self.len(); - ( - &mut self.lamports, - &mut self.data[start..start + len], - &self.owner, - false, - Epoch::default(), - ) - } - } - - #[test] - fn alloc_new_fixed_len_tlv_in_account_info_from_base_size() { - let fixed_len = FixedLenMintTest { - data: [1, 2, 3, 4, 5, 6, 7, 8], - }; - let value_len = pod_get_packed_len::(); - let base_account_size = PodMint::SIZE_OF; - let mut buffer = vec![0; base_account_size]; - let state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - - let mut data = SolanaAccountData::new(&buffer); - let key = Pubkey::new_unique(); - let account_info = (&key, &mut data).into_account_info(); - - alloc_and_serialize::(&account_info, &fixed_len, false).unwrap(); - let new_account_len = BASE_ACCOUNT_AND_TYPE_LENGTH + add_type_and_length_to_len(value_len); - assert_eq!(data.len(), new_account_len); - let state = PodStateWithExtensions::::unpack(data.data()).unwrap(); - assert_eq!( - state.get_extension::().unwrap(), - &fixed_len, - ); - - // alloc again succeeds with "overwrite" - let account_info = (&key, &mut data).into_account_info(); - alloc_and_serialize::(&account_info, &fixed_len, true).unwrap(); - - // alloc again fails without "overwrite" - let account_info = (&key, &mut data).into_account_info(); - assert_eq!( - alloc_and_serialize::(&account_info, &fixed_len, false).unwrap_err(), - TokenError::ExtensionAlreadyInitialized.into() - ); - } - - #[test] - fn alloc_new_variable_len_tlv_in_account_info_from_base_size() { - let variable_len = VariableLenMintTest { data: vec![20, 99] }; - let value_len = variable_len.get_packed_len().unwrap(); - let base_account_size = PodMint::SIZE_OF; - let mut buffer = vec![0; base_account_size]; - let state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - - let mut data = SolanaAccountData::new(&buffer); - let key = Pubkey::new_unique(); - let account_info = (&key, &mut data).into_account_info(); - - alloc_and_serialize_variable_len_extension::( - &account_info, - &variable_len, - false, - ) - .unwrap(); - let new_account_len = BASE_ACCOUNT_AND_TYPE_LENGTH + add_type_and_length_to_len(value_len); - assert_eq!(data.len(), new_account_len); - let state = PodStateWithExtensions::::unpack(data.data()).unwrap(); - assert_eq!( - state - .get_variable_len_extension::() - .unwrap(), - variable_len - ); - - // alloc again succeeds with "overwrite" - let account_info = (&key, &mut data).into_account_info(); - alloc_and_serialize_variable_len_extension::( - &account_info, - &variable_len, - true, - ) - .unwrap(); - - // alloc again fails without "overwrite" - let account_info = (&key, &mut data).into_account_info(); - assert_eq!( - alloc_and_serialize_variable_len_extension::( - &account_info, - &variable_len, - false, - ) - .unwrap_err(), - TokenError::ExtensionAlreadyInitialized.into() - ); - } - - #[test] - fn alloc_new_fixed_len_tlv_in_account_info_from_extended_size() { - let fixed_len = FixedLenMintTest { - data: [1, 2, 3, 4, 5, 6, 7, 8], - }; - let value_len = pod_get_packed_len::(); - let account_size = - ExtensionType::try_calculate_account_len::(&[ExtensionType::GroupPointer]) - .unwrap() - + add_type_and_length_to_len(value_len); - let mut buffer = vec![0; account_size]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - state.init_account_type().unwrap(); - - let test_key = - OptionalNonZeroPubkey::try_from(Some(Pubkey::new_from_array([20; 32]))).unwrap(); - let extension = state.init_extension::(false).unwrap(); - extension.authority = test_key; - extension.group_address = test_key; - - let mut data = SolanaAccountData::new(&buffer); - let key = Pubkey::new_unique(); - let account_info = (&key, &mut data).into_account_info(); - - alloc_and_serialize::(&account_info, &fixed_len, false).unwrap(); - let new_account_len = BASE_ACCOUNT_AND_TYPE_LENGTH - + add_type_and_length_to_len(value_len) - + add_type_and_length_to_len(size_of::()); - assert_eq!(data.len(), new_account_len); - let state = PodStateWithExtensions::::unpack(data.data()).unwrap(); - assert_eq!( - state.get_extension::().unwrap(), - &fixed_len, - ); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, test_key); - assert_eq!(extension.group_address, test_key); - - // alloc again succeeds with "overwrite" - let account_info = (&key, &mut data).into_account_info(); - alloc_and_serialize::(&account_info, &fixed_len, true).unwrap(); - - // alloc again fails without "overwrite" - let account_info = (&key, &mut data).into_account_info(); - assert_eq!( - alloc_and_serialize::(&account_info, &fixed_len, false).unwrap_err(), - TokenError::ExtensionAlreadyInitialized.into() - ); - } - - #[test] - fn alloc_new_variable_len_tlv_in_account_info_from_extended_size() { - let variable_len = VariableLenMintTest { data: vec![42, 6] }; - let value_len = variable_len.get_packed_len().unwrap(); - let account_size = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MetadataPointer]) - .unwrap() - + add_type_and_length_to_len(value_len); - let mut buffer = vec![0; account_size]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - state.init_account_type().unwrap(); - - let test_key = - OptionalNonZeroPubkey::try_from(Some(Pubkey::new_from_array([20; 32]))).unwrap(); - let extension = state.init_extension::(false).unwrap(); - extension.authority = test_key; - extension.metadata_address = test_key; - - let mut data = SolanaAccountData::new(&buffer); - let key = Pubkey::new_unique(); - let account_info = (&key, &mut data).into_account_info(); - - alloc_and_serialize_variable_len_extension::( - &account_info, - &variable_len, - false, - ) - .unwrap(); - let new_account_len = BASE_ACCOUNT_AND_TYPE_LENGTH - + add_type_and_length_to_len(value_len) - + add_type_and_length_to_len(size_of::()); - assert_eq!(data.len(), new_account_len); - let state = PodStateWithExtensions::::unpack(data.data()).unwrap(); - assert_eq!( - state - .get_variable_len_extension::() - .unwrap(), - variable_len - ); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, test_key); - assert_eq!(extension.metadata_address, test_key); - - // alloc again succeeds with "overwrite" - let account_info = (&key, &mut data).into_account_info(); - alloc_and_serialize_variable_len_extension::( - &account_info, - &variable_len, - true, - ) - .unwrap(); - - // alloc again fails without "overwrite" - let account_info = (&key, &mut data).into_account_info(); - assert_eq!( - alloc_and_serialize_variable_len_extension::( - &account_info, - &variable_len, - false, - ) - .unwrap_err(), - TokenError::ExtensionAlreadyInitialized.into() - ); - } - - #[test] - fn realloc_variable_len_tlv_in_account_info() { - let variable_len = VariableLenMintTest { - data: vec![1, 2, 3, 4, 5], - }; - let alloc_size = variable_len.get_packed_len().unwrap(); - let account_size = - ExtensionType::try_calculate_account_len::(&[ExtensionType::MetadataPointer]) - .unwrap() - + add_type_and_length_to_len(alloc_size); - let mut buffer = vec![0; account_size]; - let mut state = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut buffer).unwrap(); - *state.base = TEST_POD_MINT; - state.init_account_type().unwrap(); - - // alloc both types - state - .init_variable_len_extension(&variable_len, false) - .unwrap(); - let max_pubkey = - OptionalNonZeroPubkey::try_from(Some(Pubkey::new_from_array([255; 32]))).unwrap(); - let extension = state.init_extension::(false).unwrap(); - extension.authority = max_pubkey; - extension.metadata_address = max_pubkey; - - // reallocate to smaller, make sure existing extension is fine - let mut data = SolanaAccountData::new(&buffer); - let key = Pubkey::new_unique(); - let account_info = (&key, &mut data).into_account_info(); - let variable_len = VariableLenMintTest { data: vec![1, 2] }; - alloc_and_serialize_variable_len_extension::( - &account_info, - &variable_len, - true, - ) - .unwrap(); - - let state = PodStateWithExtensions::::unpack(data.data()).unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, max_pubkey); - assert_eq!(extension.metadata_address, max_pubkey); - let extension = state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(extension, variable_len); - assert_eq!(data.len(), state.try_get_account_len().unwrap()); - - // reallocate to larger - let account_info = (&key, &mut data).into_account_info(); - let variable_len = VariableLenMintTest { - data: vec![1, 2, 3, 4, 5, 6, 7], - }; - alloc_and_serialize_variable_len_extension::( - &account_info, - &variable_len, - true, - ) - .unwrap(); - - let state = PodStateWithExtensions::::unpack(data.data()).unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, max_pubkey); - assert_eq!(extension.metadata_address, max_pubkey); - let extension = state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(extension, variable_len); - assert_eq!(data.len(), state.try_get_account_len().unwrap()); - - // reallocate to same - let account_info = (&key, &mut data).into_account_info(); - let variable_len = VariableLenMintTest { - data: vec![7, 6, 5, 4, 3, 2, 1], - }; - alloc_and_serialize_variable_len_extension::( - &account_info, - &variable_len, - true, - ) - .unwrap(); - - let state = PodStateWithExtensions::::unpack(data.data()).unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(extension.authority, max_pubkey); - assert_eq!(extension.metadata_address, max_pubkey); - let extension = state - .get_variable_len_extension::() - .unwrap(); - assert_eq!(extension, variable_len); - assert_eq!(data.len(), state.try_get_account_len().unwrap()); - } -} diff --git a/token/program-2022/src/extension/non_transferable.rs b/token/program-2022/src/extension/non_transferable.rs deleted file mode 100644 index f78b86142ad..00000000000 --- a/token/program-2022/src/extension/non_transferable.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, -}; - -/// Indicates that the tokens from this mint can't be transferred -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct NonTransferable; - -/// Indicates that the tokens from this account belong to a non-transferable -/// mint -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct NonTransferableAccount; - -impl Extension for NonTransferable { - const TYPE: ExtensionType = ExtensionType::NonTransferable; -} - -impl Extension for NonTransferableAccount { - const TYPE: ExtensionType = ExtensionType::NonTransferableAccount; -} diff --git a/token/program-2022/src/extension/pausable/instruction.rs b/token/program-2022/src/extension/pausable/instruction.rs deleted file mode 100644 index 0cc7c973997..00000000000 --- a/token/program-2022/src/extension/pausable/instruction.rs +++ /dev/null @@ -1,136 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - instruction::{encode_instruction, TokenInstruction}, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, -}; - -/// Pausable extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum PausableInstruction { - /// Initialize the pausable extension for the given mint account - /// - /// Fails if the account has already been initialized, so must be called - /// before `InitializeMint`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint account to initialize. - /// - /// Data expected by this instruction: - /// `crate::extension::pausable::instruction::InitializeInstructionData` - Initialize, - /// Pause minting, burning, and transferring for the mint. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to update. - /// 1. `[signer]` The mint's pause authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint to update. - /// 1. `[]` The mint's multisignature pause authority. - /// 2. `..2+M` `[signer]` M signer accounts. - Pause, - /// Resume minting, burning, and transferring for the mint. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to update. - /// 1. `[signer]` The mint's pause authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint to update. - /// 1. `[]` The mint's multisignature pause authority. - /// 2. `..2+M` `[signer]` M signer accounts. - Resume, -} - -/// Data expected by `PausableInstruction::Initialize` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeInstructionData { - /// The public key for the account that can pause the mint - pub authority: Pubkey, -} - -/// Create an `Initialize` instruction -pub fn initialize( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::PausableExtension, - PausableInstruction::Initialize, - &InitializeInstructionData { - authority: *authority, - }, - )) -} - -/// Create a `Pause` instruction -pub fn pause( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::PausableExtension, - PausableInstruction::Pause, - &(), - )) -} - -/// Create a `Resume` instruction -pub fn resume( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::PausableExtension, - PausableInstruction::Resume, - &(), - )) -} diff --git a/token/program-2022/src/extension/pausable/mod.rs b/token/program-2022/src/extension/pausable/mod.rs deleted file mode 100644 index 82a3878abe4..00000000000 --- a/token/program-2022/src/extension/pausable/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, - spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodBool}, -}; - -/// Instruction types for the pausable extension -pub mod instruction; -/// Instruction processor for the pausable extension -pub mod processor; - -/// Indicates that the tokens from this mint can be paused -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(C)] -pub struct PausableConfig { - /// Authority that can pause or resume activity on the mint - pub authority: OptionalNonZeroPubkey, - /// Whether minting / transferring / burning tokens is paused - pub paused: PodBool, -} - -/// Indicates that the tokens from this account belong to a pausable mint -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PausableAccount; - -impl Extension for PausableConfig { - const TYPE: ExtensionType = ExtensionType::Pausable; -} - -impl Extension for PausableAccount { - const TYPE: ExtensionType = ExtensionType::PausableAccount; -} diff --git a/token/program-2022/src/extension/pausable/processor.rs b/token/program-2022/src/extension/pausable/processor.rs deleted file mode 100644 index 5f47982e114..00000000000 --- a/token/program-2022/src/extension/pausable/processor.rs +++ /dev/null @@ -1,91 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - pausable::{ - instruction::{InitializeInstructionData, PausableInstruction}, - PausableConfig, - }, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::PodMint, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - pubkey::Pubkey, - }, -}; - -fn process_initialize( - _program_id: &Pubkey, - accounts: &[AccountInfo], - authority: &Pubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - - let extension = mint.init_extension::(true)?; - extension.authority = Some(*authority).try_into()?; - - Ok(()) -} - -/// Pause or resume minting / burning / transferring on the mint -fn process_toggle_pause( - program_id: &Pubkey, - accounts: &[AccountInfo], - pause: bool, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - let maybe_authority: Option = extension.authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - - Processor::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - extension.paused = pause.into(); - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - - match decode_instruction_type(input)? { - PausableInstruction::Initialize => { - msg!("PausableInstruction::Initialize"); - let InitializeInstructionData { authority } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, authority) - } - PausableInstruction::Pause => { - msg!("PausableInstruction::Pause"); - process_toggle_pause(program_id, accounts, true /* pause */) - } - PausableInstruction::Resume => { - msg!("PausableInstruction::Resume"); - process_toggle_pause(program_id, accounts, false /* resume */) - } - } -} diff --git a/token/program-2022/src/extension/permanent_delegate.rs b/token/program-2022/src/extension/permanent_delegate.rs deleted file mode 100644 index cab3862a46a..00000000000 --- a/token/program-2022/src/extension/permanent_delegate.rs +++ /dev/null @@ -1,32 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{BaseState, BaseStateWithExtensions, Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, - solana_program::pubkey::Pubkey, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -/// Permanent delegate extension data for mints. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct PermanentDelegate { - /// Optional permanent delegate for transferring or burning tokens - pub delegate: OptionalNonZeroPubkey, -} -impl Extension for PermanentDelegate { - const TYPE: ExtensionType = ExtensionType::PermanentDelegate; -} - -/// Attempts to get the permanent delegate from the TLV data, returning None -/// if the extension is not found -pub fn get_permanent_delegate>( - state: &BSE, -) -> Option { - state - .get_extension::() - .ok() - .and_then(|e| Option::::from(e.delegate)) -} diff --git a/token/program-2022/src/extension/reallocate.rs b/token/program-2022/src/extension/reallocate.rs deleted file mode 100644 index c21ada497d4..00000000000 --- a/token/program-2022/src/extension/reallocate.rs +++ /dev/null @@ -1,116 +0,0 @@ -use { - crate::{ - error::TokenError, - extension::{ - set_account_type, AccountType, BaseStateWithExtensions, ExtensionType, - StateWithExtensions, StateWithExtensionsMut, - }, - processor::Processor, - state::Account, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program::invoke, - program_option::COption, - pubkey::Pubkey, - system_instruction, - sysvar::{rent::Rent, Sysvar}, - }, -}; - -/// Processes a [Reallocate](enum.TokenInstruction.html) instruction -pub fn process_reallocate( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_extension_types: Vec, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let payer_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - // check that account is the right type and validate owner - let (mut current_extension_types, native_token_amount) = { - let token_account = token_account_info.data.borrow(); - let account = StateWithExtensions::::unpack(&token_account)?; - Processor::validate_owner( - program_id, - &account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - let native_token_amount = account.base.is_native().then_some(account.base.amount); - (account.get_extension_types()?, native_token_amount) - }; - - // check that all desired extensions are for the right account type - if new_extension_types - .iter() - .any(|extension_type| extension_type.get_account_type() != AccountType::Account) - { - return Err(TokenError::InvalidState.into()); - } - // ExtensionType::try_calculate_account_len() dedupes types, so just a dumb - // concatenation is fine here - current_extension_types.extend_from_slice(&new_extension_types); - let needed_account_len = - ExtensionType::try_calculate_account_len::(¤t_extension_types)?; - - // if account is already large enough, return early - if token_account_info.data_len() >= needed_account_len { - return Ok(()); - } - - // reallocate - msg!( - "account needs realloc, +{:?} bytes", - needed_account_len - token_account_info.data_len() - ); - token_account_info.realloc(needed_account_len, false)?; - - // if additional lamports needed to remain rent-exempt, transfer them - let rent = Rent::get()?; - let new_rent_exempt_reserve = rent.minimum_balance(needed_account_len); - - let current_lamport_reserve = token_account_info - .lamports() - .checked_sub(native_token_amount.unwrap_or(0)) - .ok_or(TokenError::Overflow)?; - let lamports_diff = new_rent_exempt_reserve.saturating_sub(current_lamport_reserve); - if lamports_diff > 0 { - invoke( - &system_instruction::transfer(payer_info.key, token_account_info.key, lamports_diff), - &[ - payer_info.clone(), - token_account_info.clone(), - system_program_info.clone(), - ], - )?; - } - - // set account_type, if needed - let mut token_account_data = token_account_info.data.borrow_mut(); - set_account_type::(&mut token_account_data)?; - - // sync the rent exempt reserve for native accounts - if let Some(native_token_amount) = native_token_amount { - let mut token_account = StateWithExtensionsMut::::unpack(&mut token_account_data)?; - // sanity check that there are enough lamports to cover the token amount - // and the rent exempt reserve - let minimum_lamports = new_rent_exempt_reserve - .checked_add(native_token_amount) - .ok_or(TokenError::Overflow)?; - if token_account_info.lamports() < minimum_lamports { - return Err(TokenError::InvalidState.into()); - } - token_account.base.is_native = COption::Some(new_rent_exempt_reserve); - token_account.pack_base(); - } - - Ok(()) -} diff --git a/token/program-2022/src/extension/scaled_ui_amount/instruction.rs b/token/program-2022/src/extension/scaled_ui_amount/instruction.rs deleted file mode 100644 index cb939f6a675..00000000000 --- a/token/program-2022/src/extension/scaled_ui_amount/instruction.rs +++ /dev/null @@ -1,143 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - extension::scaled_ui_amount::{PodF64, UnixTimestamp}, - instruction::{encode_instruction, TokenInstruction}, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - std::convert::TryInto, -}; - -/// Interesting-bearing mint extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum ScaledUiAmountMintInstruction { - /// Initialize a new mint with scaled UI amounts. - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// Fails if the multiplier is less than or equal to 0 or if it's - /// [subnormal](https://en.wikipedia.org/wiki/Subnormal_number). - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// - /// Data expected by this instruction: - /// `crate::extension::scaled_ui_amount::instruction::InitializeInstructionData` - Initialize, - /// Update the multiplier. Only supported for mints that include the - /// `ScaledUiAmount` extension. - /// - /// Fails if the multiplier is less than or equal to 0 or if it's - /// [subnormal](https://en.wikipedia.org/wiki/Subnormal_number). - /// - /// The authority provides a new multiplier and a unix timestamp on which - /// it should take effect. If the timestamp is before the current time, - /// immediately sets the multiplier. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[signer]` The multiplier authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[]` The mint's multisignature multiplier authority. - /// 2. `..2+M` `[signer]` M signer accounts. - /// - /// Data expected by this instruction: - /// `crate::extension::scaled_ui_amount::instruction::UpdateMultiplierInstructionData` - UpdateMultiplier, -} - -/// Data expected by `ScaledUiAmountMint::Initialize` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeInstructionData { - /// The public key for the account that can update the multiplier - pub authority: OptionalNonZeroPubkey, - /// The initial multiplier - pub multiplier: PodF64, -} - -/// Data expected by `ScaledUiAmountMint::UpdateMultiplier` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct UpdateMultiplierInstructionData { - /// The new multiplier - pub multiplier: PodF64, - /// Timestamp at which the new multiplier will take effect - pub effective_timestamp: UnixTimestamp, -} - -/// Create an `Initialize` instruction -pub fn initialize( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: Option, - multiplier: f64, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ScaledUiAmountExtension, - ScaledUiAmountMintInstruction::Initialize, - &InitializeInstructionData { - authority: authority.try_into()?, - multiplier: multiplier.into(), - }, - )) -} - -/// Create an `UpdateMultiplier` instruction -pub fn update_multiplier( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], - multiplier: f64, - effective_timestamp: i64, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::ScaledUiAmountExtension, - ScaledUiAmountMintInstruction::UpdateMultiplier, - &UpdateMultiplierInstructionData { - effective_timestamp: effective_timestamp.into(), - multiplier: multiplier.into(), - }, - )) -} diff --git a/token/program-2022/src/extension/scaled_ui_amount/mod.rs b/token/program-2022/src/extension/scaled_ui_amount/mod.rs deleted file mode 100644 index 80b635f207e..00000000000 --- a/token/program-2022/src/extension/scaled_ui_amount/mod.rs +++ /dev/null @@ -1,345 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - extension::{Extension, ExtensionType}, - trim_ui_amount_string, - }, - bytemuck::{Pod, Zeroable}, - solana_program::program_error::ProgramError, - spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodI64}, -}; - -/// Scaled UI amount extension instructions -pub mod instruction; - -/// Scaled UI amount extension processor -pub mod processor; - -/// `UnixTimestamp` expressed with an alignment-independent type -pub type UnixTimestamp = PodI64; - -/// `f64` type that can be used in `Pod`s -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(from = "f64", into = "f64"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PodF64(pub [u8; 8]); -impl PodF64 { - fn from_primitive(n: f64) -> Self { - Self(n.to_le_bytes()) - } -} -impl From for PodF64 { - fn from(n: f64) -> Self { - Self::from_primitive(n) - } -} -impl From for f64 { - fn from(pod: PodF64) -> Self { - Self::from_le_bytes(pod.0) - } -} - -/// Scaled UI amount extension data for mints -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct ScaledUiAmountConfig { - /// Authority that can set the scaling amount and authority - pub authority: OptionalNonZeroPubkey, - /// Amount to multiply raw amounts by, outside of the decimal - pub multiplier: PodF64, - /// Unix timestamp at which `new_multiplier` comes into effective - pub new_multiplier_effective_timestamp: UnixTimestamp, - /// Next multiplier, once `new_multiplier_effective_timestamp` is reached - pub new_multiplier: PodF64, -} -impl ScaledUiAmountConfig { - fn total_multiplier(&self, decimals: u8, unix_timestamp: i64) -> f64 { - let multiplier = if unix_timestamp >= self.new_multiplier_effective_timestamp.into() { - self.new_multiplier - } else { - self.multiplier - }; - f64::from(multiplier) / 10_f64.powi(decimals as i32) - } - - /// Convert a raw amount to its UI representation using the given decimals - /// field. Excess zeroes or unneeded decimal point are trimmed. - pub fn amount_to_ui_amount( - &self, - amount: u64, - decimals: u8, - unix_timestamp: i64, - ) -> Option { - let scaled_amount = (amount as f64) * self.total_multiplier(decimals, unix_timestamp); - let ui_amount = format!("{scaled_amount:.*}", decimals as usize); - Some(trim_ui_amount_string(ui_amount, decimals)) - } - - /// Try to convert a UI representation of a token amount to its raw amount - /// using the given decimals field - pub fn try_ui_amount_into_amount( - &self, - ui_amount: &str, - decimals: u8, - unix_timestamp: i64, - ) -> Result { - let scaled_amount = ui_amount - .parse::() - .map_err(|_| ProgramError::InvalidArgument)?; - let amount = scaled_amount / self.total_multiplier(decimals, unix_timestamp); - if amount > (u64::MAX as f64) || amount < (u64::MIN as f64) || amount.is_nan() { - Err(ProgramError::InvalidArgument) - } else { - // this is important, if you round earlier, you'll get wrong "inf" - // answers - Ok(amount.round() as u64) - } - } -} -impl Extension for ScaledUiAmountConfig { - const TYPE: ExtensionType = ExtensionType::ScaledUiAmount; -} - -#[cfg(test)] -mod tests { - use {super::*, proptest::prelude::*}; - - const TEST_DECIMALS: u8 = 2; - - #[test] - fn multiplier_choice() { - let multiplier = 5.0; - let new_multiplier = 10.0; - let new_multiplier_effective_timestamp = 1; - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: PodF64::from(multiplier), - new_multiplier: PodF64::from(new_multiplier), - new_multiplier_effective_timestamp: UnixTimestamp::from( - new_multiplier_effective_timestamp, - ), - }; - assert_eq!( - config.total_multiplier(0, new_multiplier_effective_timestamp), - new_multiplier - ); - assert_eq!( - config.total_multiplier(0, new_multiplier_effective_timestamp - 1), - multiplier - ); - assert_eq!(config.total_multiplier(0, 0), multiplier); - assert_eq!(config.total_multiplier(0, i64::MIN), multiplier); - assert_eq!(config.total_multiplier(0, i64::MAX), new_multiplier); - } - - #[test] - fn specific_amount_to_ui_amount() { - // 5x - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: PodF64::from(5.0), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - let ui_amount = config.amount_to_ui_amount(1, 0, 0).unwrap(); - assert_eq!(ui_amount, "5"); - // with 1 decimal place - let ui_amount = config.amount_to_ui_amount(1, 1, 0).unwrap(); - assert_eq!(ui_amount, "0.5"); - // with 10 decimal places - let ui_amount = config.amount_to_ui_amount(1, 10, 0).unwrap(); - assert_eq!(ui_amount, "0.0000000005"); - - // huge amount with 10 decimal places - let ui_amount = config.amount_to_ui_amount(10_000_000_000, 10, 0).unwrap(); - assert_eq!(ui_amount, "5"); - - // huge values - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: PodF64::from(f64::MAX), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - let ui_amount = config.amount_to_ui_amount(u64::MAX, 0, 0).unwrap(); - assert_eq!(ui_amount, "inf"); - } - - #[test] - fn specific_ui_amount_to_amount() { - // constant 5x - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: 5.0.into(), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - let amount = config.try_ui_amount_into_amount("5.0", 0, 0).unwrap(); - assert_eq!(1, amount); - // with 1 decimal place - let amount = config - .try_ui_amount_into_amount("0.500000000", 1, 0) - .unwrap(); - assert_eq!(amount, 1); - // with 10 decimal places - let amount = config - .try_ui_amount_into_amount("0.00000000050000000000000000", 10, 0) - .unwrap(); - assert_eq!(amount, 1); - - // huge amount with 10 decimal places - let amount = config - .try_ui_amount_into_amount("5.0000000000000000", 10, 0) - .unwrap(); - assert_eq!(amount, 10_000_000_000); - - // huge values - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: 5.0.into(), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - let amount = config - .try_ui_amount_into_amount("92233720368547758075", 0, 0) - .unwrap(); - assert_eq!(amount, u64::MAX); - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: f64::MAX.into(), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - // scientific notation "e" - let amount = config - .try_ui_amount_into_amount("1.7976931348623157e308", 0, 0) - .unwrap(); - assert_eq!(amount, 1); - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: 9.745314011399998e288.into(), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - let amount = config - .try_ui_amount_into_amount("1.7976931348623157e308", 0, 0) - .unwrap(); - assert_eq!(amount, u64::MAX); - // scientific notation "E" - let amount = config - .try_ui_amount_into_amount("1.7976931348623157E308", 0, 0) - .unwrap(); - assert_eq!(amount, u64::MAX); - - // this is unfortunate, but underflows can happen due to floats - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: 1.0.into(), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - assert_eq!( - u64::MAX, - config - .try_ui_amount_into_amount("18446744073709551616", 0, 0) - .unwrap() // u64::MAX + 1 - ); - - // overflow u64 fail - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: 0.1.into(), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - assert_eq!( - Err(ProgramError::InvalidArgument), - config.try_ui_amount_into_amount("18446744073709551615", 0, 0) // u64::MAX + 1 - ); - - for fail_ui_amount in ["-0.0000000000000000000001", "inf", "-inf", "NaN"] { - assert_eq!( - Err(ProgramError::InvalidArgument), - config.try_ui_amount_into_amount(fail_ui_amount, 0, 0) - ); - } - } - - #[test] - fn specific_amount_to_ui_amount_no_scale() { - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: 1.0.into(), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - for (amount, expected) in [(23, "0.23"), (110, "1.1"), (4200, "42"), (0, "0")] { - let ui_amount = config - .amount_to_ui_amount(amount, TEST_DECIMALS, 0) - .unwrap(); - assert_eq!(ui_amount, expected); - } - } - - #[test] - fn specific_ui_amount_to_amount_no_scale() { - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: 1.0.into(), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - for (ui_amount, expected) in [ - ("0.23", 23), - ("0.20", 20), - ("0.2000", 20), - (".2", 20), - ("1.1", 110), - ("1.10", 110), - ("42", 4200), - ("42.", 4200), - ("0", 0), - ] { - let amount = config - .try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, 0) - .unwrap(); - assert_eq!(expected, amount); - } - - // this is invalid with normal mints, but rounding for this mint makes it ok - let amount = config - .try_ui_amount_into_amount("0.111", TEST_DECIMALS, 0) - .unwrap(); - assert_eq!(11, amount); - - // fail if invalid ui_amount passed in - for ui_amount in ["", ".", "0.t"] { - assert_eq!( - Err(ProgramError::InvalidArgument), - config.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, 0), - ); - } - } - - proptest! { - #[test] - fn amount_to_ui_amount( - scale in 0f64..=f64::MAX, - amount in 0..=u64::MAX, - decimals in 0u8..20u8, - ) { - let config = ScaledUiAmountConfig { - authority: OptionalNonZeroPubkey::default(), - multiplier: scale.into(), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - let ui_amount = config.amount_to_ui_amount(amount, decimals, 0); - assert!(ui_amount.is_some()); - } - } -} diff --git a/token/program-2022/src/extension/scaled_ui_amount/processor.rs b/token/program-2022/src/extension/scaled_ui_amount/processor.rs deleted file mode 100644 index 1199a64c804..00000000000 --- a/token/program-2022/src/extension/scaled_ui_amount/processor.rs +++ /dev/null @@ -1,126 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - scaled_ui_amount::{ - instruction::{ - InitializeInstructionData, ScaledUiAmountMintInstruction, - UpdateMultiplierInstructionData, - }, - PodF64, ScaledUiAmountConfig, UnixTimestamp, - }, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::PodMint, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - clock::Clock, - entrypoint::ProgramResult, - msg, - pubkey::Pubkey, - sysvar::Sysvar, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -fn try_validate_multiplier(multiplier: &PodF64) -> ProgramResult { - let float_multiplier = f64::from(*multiplier); - if float_multiplier.is_sign_positive() && float_multiplier.is_normal() { - Ok(()) - } else { - Err(TokenError::InvalidScale.into()) - } -} - -fn process_initialize( - _program_id: &Pubkey, - accounts: &[AccountInfo], - authority: &OptionalNonZeroPubkey, - multiplier: &PodF64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - - let extension = mint.init_extension::(true)?; - extension.authority = *authority; - try_validate_multiplier(multiplier)?; - extension.multiplier = *multiplier; - extension.new_multiplier_effective_timestamp = 0.into(); - extension.new_multiplier = *multiplier; - Ok(()) -} - -fn process_update_multiplier( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_multiplier: &PodF64, - effective_timestamp: &UnixTimestamp, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - let authority = - Option::::from(extension.authority).ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &authority, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?; - - try_validate_multiplier(new_multiplier)?; - let clock = Clock::get()?; - extension.new_multiplier = *new_multiplier; - let int_effective_timestamp = i64::from(*effective_timestamp); - // just floor it to 0 - if int_effective_timestamp < 0 { - extension.new_multiplier_effective_timestamp = 0.into(); - } else { - extension.new_multiplier_effective_timestamp = *effective_timestamp; - } - // if the new effective timestamp has already passed, also set the old - // multiplier, just to be clear - if clock.unix_timestamp >= int_effective_timestamp { - extension.multiplier = *new_multiplier; - } - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - match decode_instruction_type(input)? { - ScaledUiAmountMintInstruction::Initialize => { - msg!("ScaledUiAmountMintInstruction::Initialize"); - let InitializeInstructionData { - authority, - multiplier, - } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, authority, multiplier) - } - ScaledUiAmountMintInstruction::UpdateMultiplier => { - msg!("ScaledUiAmountMintInstruction::UpdateScale"); - let UpdateMultiplierInstructionData { - effective_timestamp, - multiplier, - } = decode_instruction_data(input)?; - process_update_multiplier(program_id, accounts, multiplier, effective_timestamp) - } - } -} diff --git a/token/program-2022/src/extension/token_group/mod.rs b/token/program-2022/src/extension/token_group/mod.rs deleted file mode 100644 index 32bb723a72d..00000000000 --- a/token/program-2022/src/extension/token_group/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -use { - crate::extension::{Extension, ExtensionType}, - spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, -}; - -/// Instruction processor for the `TokenGroup` extension -pub mod processor; - -impl Extension for TokenGroup { - const TYPE: ExtensionType = ExtensionType::TokenGroup; -} - -impl Extension for TokenGroupMember { - const TYPE: ExtensionType = ExtensionType::TokenGroupMember; -} diff --git a/token/program-2022/src/extension/token_group/processor.rs b/token/program-2022/src/extension/token_group/processor.rs deleted file mode 100644 index 4fa35253541..00000000000 --- a/token/program-2022/src/extension/token_group/processor.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Token-group processor - -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - alloc_and_serialize, group_member_pointer::GroupMemberPointer, - group_pointer::GroupPointer, BaseStateWithExtensions, BaseStateWithExtensionsMut, - PodStateWithExtensions, PodStateWithExtensionsMut, - }, - pod::{PodCOption, PodMint}, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_group_interface::{ - error::TokenGroupError, - instruction::{ - InitializeGroup, TokenGroupInstruction, UpdateGroupAuthority, UpdateGroupMaxSize, - }, - state::{TokenGroup, TokenGroupMember}, - }, -}; - -fn check_update_authority( - update_authority_info: &AccountInfo, - expected_update_authority: &OptionalNonZeroPubkey, -) -> Result<(), ProgramError> { - if !update_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - let update_authority = Option::::from(*expected_update_authority) - .ok_or(TokenGroupError::ImmutableGroup)?; - if update_authority != *update_authority_info.key { - return Err(TokenGroupError::IncorrectUpdateAuthority.into()); - } - Ok(()) -} - -/// Processes a [`InitializeGroup`](enum.TokenGroupInstruction.html) -/// instruction. -pub fn process_initialize_group( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: InitializeGroup, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let group_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let mint_authority_info = next_account_info(account_info_iter)?; - - // check that the mint and group accounts are the same, since the group - // extension should only describe itself - if group_info.key != mint_info.key { - msg!("Group configurations for a mint must be initialized in the mint itself."); - return Err(TokenError::MintMismatch.into()); - } - - // scope the mint authority check, since the mint is in the same account! - { - // This check isn't really needed since we'll be writing into the account, - // but auditors like it - check_program_account(mint_info.owner)?; - let mint_data = mint_info.try_borrow_data()?; - let mint = PodStateWithExtensions::::unpack(&mint_data)?; - - if !mint_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - if mint.base.mint_authority != PodCOption::some(*mint_authority_info.key) { - return Err(TokenGroupError::IncorrectMintAuthority.into()); - } - - if mint.get_extension::().is_err() { - msg!( - "A mint with group configurations must have the group-pointer extension \ - initialized" - ); - return Err(TokenError::InvalidExtensionCombination.into()); - } - } - - // Allocate a TLV entry for the space and write it in - // Assumes that there's enough SOL for the new rent-exemption - let group = TokenGroup::new(mint_info.key, data.update_authority, data.max_size.into()); - alloc_and_serialize::(group_info, &group, false)?; - - Ok(()) -} - -/// Processes an -/// [`UpdateGroupMaxSize`](enum.TokenGroupInstruction.html) -/// instruction -pub fn process_update_group_max_size( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: UpdateGroupMaxSize, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let group_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - let mut buffer = group_info.try_borrow_mut_data()?; - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer)?; - let group = state.get_extension_mut::()?; - - check_update_authority(update_authority_info, &group.update_authority)?; - - group.update_max_size(data.max_size.into())?; - - Ok(()) -} - -/// Processes an -/// [`UpdateGroupAuthority`](enum.TokenGroupInstruction.html) -/// instruction -pub fn process_update_group_authority( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: UpdateGroupAuthority, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let group_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - let mut buffer = group_info.try_borrow_mut_data()?; - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer)?; - let group = state.get_extension_mut::()?; - - check_update_authority(update_authority_info, &group.update_authority)?; - - group.update_authority = data.new_authority; - - Ok(()) -} - -/// Processes an [`InitializeMember`](enum.TokenGroupInstruction.html) -/// instruction -pub fn process_initialize_member(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let member_info = next_account_info(account_info_iter)?; - let member_mint_info = next_account_info(account_info_iter)?; - let member_mint_authority_info = next_account_info(account_info_iter)?; - let group_info = next_account_info(account_info_iter)?; - let group_update_authority_info = next_account_info(account_info_iter)?; - - // check that the mint and member accounts are the same, since the member - // extension should only describe itself - if member_info.key != member_mint_info.key { - msg!("Group member configurations for a mint must be initialized in the mint itself."); - return Err(TokenError::MintMismatch.into()); - } - - // scope the mint authority check, since the mint is in the same account! - { - // This check isn't really needed since we'll be writing into the account, - // but auditors like it - check_program_account(member_mint_info.owner)?; - let member_mint_data = member_mint_info.try_borrow_data()?; - let member_mint = PodStateWithExtensions::::unpack(&member_mint_data)?; - - if !member_mint_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - if member_mint.base.mint_authority != PodCOption::some(*member_mint_authority_info.key) { - return Err(TokenGroupError::IncorrectMintAuthority.into()); - } - - if member_mint.get_extension::().is_err() { - msg!( - "A mint with group member configurations must have the group-member-pointer \ - extension initialized" - ); - return Err(TokenError::InvalidExtensionCombination.into()); - } - } - - // Make sure the member mint is not the same as the group mint - if member_info.key == group_info.key { - return Err(TokenGroupError::MemberAccountIsGroupAccount.into()); - } - - // Increment the size of the group - let mut buffer = group_info.try_borrow_mut_data()?; - let mut state = PodStateWithExtensionsMut::::unpack(&mut buffer)?; - let group = state.get_extension_mut::()?; - - check_update_authority(group_update_authority_info, &group.update_authority)?; - let member_number = group.increment_size()?; - - // Allocate a TLV entry for the space and write it in - let member = TokenGroupMember::new(member_mint_info.key, group_info.key, member_number); - alloc_and_serialize::(member_info, &member, false)?; - - Ok(()) -} - -/// Processes an [`Instruction`](enum.Instruction.html). -pub fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction: TokenGroupInstruction, -) -> ProgramResult { - match instruction { - TokenGroupInstruction::InitializeGroup(data) => { - msg!("TokenGroupInstruction: InitializeGroup"); - process_initialize_group(program_id, accounts, data) - } - TokenGroupInstruction::UpdateGroupMaxSize(data) => { - msg!("TokenGroupInstruction: UpdateGroupMaxSize"); - process_update_group_max_size(program_id, accounts, data) - } - TokenGroupInstruction::UpdateGroupAuthority(data) => { - msg!("TokenGroupInstruction: UpdateGroupAuthority"); - process_update_group_authority(program_id, accounts, data) - } - TokenGroupInstruction::InitializeMember(_) => { - msg!("TokenGroupInstruction: InitializeMember"); - process_initialize_member(program_id, accounts) - } - } -} diff --git a/token/program-2022/src/extension/token_metadata/mod.rs b/token/program-2022/src/extension/token_metadata/mod.rs deleted file mode 100644 index 5fb94c42bcf..00000000000 --- a/token/program-2022/src/extension/token_metadata/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -use { - crate::extension::{Extension, ExtensionType}, - spl_token_metadata_interface::state::TokenMetadata, -}; - -/// Instruction processor for the `TokenMetadata` extension -pub mod processor; - -impl Extension for TokenMetadata { - const TYPE: ExtensionType = ExtensionType::TokenMetadata; -} diff --git a/token/program-2022/src/extension/token_metadata/processor.rs b/token/program-2022/src/extension/token_metadata/processor.rs deleted file mode 100644 index 55237014c2c..00000000000 --- a/token/program-2022/src/extension/token_metadata/processor.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! Token-metadata processor - -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - alloc_and_serialize_variable_len_extension, metadata_pointer::MetadataPointer, - BaseStateWithExtensions, PodStateWithExtensions, - }, - pod::{PodCOption, PodMint}, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program::set_return_data, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_metadata_interface::{ - error::TokenMetadataError, - instruction::{ - Emit, Initialize, RemoveKey, TokenMetadataInstruction, UpdateAuthority, UpdateField, - }, - state::TokenMetadata, - }, -}; - -fn check_update_authority( - update_authority_info: &AccountInfo, - expected_update_authority: &OptionalNonZeroPubkey, -) -> Result<(), ProgramError> { - if !update_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - let update_authority = Option::::from(*expected_update_authority) - .ok_or(TokenMetadataError::ImmutableMetadata)?; - if update_authority != *update_authority_info.key { - return Err(TokenMetadataError::IncorrectUpdateAuthority.into()); - } - Ok(()) -} - -/// Processes a [`Initialize`](enum.TokenMetadataInstruction.html) instruction. -pub fn process_initialize( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: Initialize, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let metadata_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let mint_authority_info = next_account_info(account_info_iter)?; - - // check that the mint and metadata accounts are the same, since the metadata - // extension should only describe itself - if metadata_info.key != mint_info.key { - msg!("Metadata for a mint must be initialized in the mint itself."); - return Err(TokenError::MintMismatch.into()); - } - - // scope the mint authority check, since the mint is in the same account! - { - // This check isn't really needed since we'll be writing into the account, - // but auditors like it - check_program_account(mint_info.owner)?; - let mint_data = mint_info.try_borrow_data()?; - let mint = PodStateWithExtensions::::unpack(&mint_data)?; - - if !mint_authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - if mint.base.mint_authority != PodCOption::some(*mint_authority_info.key) { - return Err(TokenMetadataError::IncorrectMintAuthority.into()); - } - - if mint.get_extension::().is_err() { - msg!("A mint with metadata must have the metadata-pointer extension initialized"); - return Err(TokenError::InvalidExtensionCombination.into()); - } - } - - // Create the token metadata - let update_authority = OptionalNonZeroPubkey::try_from(Some(*update_authority_info.key))?; - let token_metadata = TokenMetadata { - name: data.name, - symbol: data.symbol, - uri: data.uri, - update_authority, - mint: *mint_info.key, - ..Default::default() - }; - - // allocate a TLV entry for the space and write it in, assumes that there's - // enough SOL for the new rent-exemption - alloc_and_serialize_variable_len_extension::( - metadata_info, - &token_metadata, - false, - )?; - - Ok(()) -} - -/// Processes an [`UpdateField`](enum.TokenMetadataInstruction.html) -/// instruction. -pub fn process_update_field( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: UpdateField, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let metadata_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - // deserialize the metadata, but scope the data borrow since we'll probably - // realloc the account - let mut token_metadata = { - let buffer = metadata_info.try_borrow_data()?; - let mint = PodStateWithExtensions::::unpack(&buffer)?; - mint.get_variable_len_extension::()? - }; - - check_update_authority(update_authority_info, &token_metadata.update_authority)?; - - // Update the field - token_metadata.update(data.field, data.value); - - // Update / realloc the account - alloc_and_serialize_variable_len_extension::(metadata_info, &token_metadata, true)?; - - Ok(()) -} - -/// Processes a [`RemoveKey`](enum.TokenMetadataInstruction.html) instruction. -pub fn process_remove_key( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: RemoveKey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let metadata_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - // deserialize the metadata, but scope the data borrow since we'll probably - // realloc the account - let mut token_metadata = { - let buffer = metadata_info.try_borrow_data()?; - let mint = PodStateWithExtensions::::unpack(&buffer)?; - mint.get_variable_len_extension::()? - }; - - check_update_authority(update_authority_info, &token_metadata.update_authority)?; - if !token_metadata.remove_key(&data.key) && !data.idempotent { - return Err(TokenMetadataError::KeyNotFound.into()); - } - alloc_and_serialize_variable_len_extension::(metadata_info, &token_metadata, true)?; - Ok(()) -} - -/// Processes a [`UpdateAuthority`](enum.TokenMetadataInstruction.html) -/// instruction. -pub fn process_update_authority( - _program_id: &Pubkey, - accounts: &[AccountInfo], - data: UpdateAuthority, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let metadata_info = next_account_info(account_info_iter)?; - let update_authority_info = next_account_info(account_info_iter)?; - - // deserialize the metadata, but scope the data borrow since we'll write - // to the account later - let mut token_metadata = { - let buffer = metadata_info.try_borrow_data()?; - let mint = PodStateWithExtensions::::unpack(&buffer)?; - mint.get_variable_len_extension::()? - }; - - check_update_authority(update_authority_info, &token_metadata.update_authority)?; - token_metadata.update_authority = data.new_authority; - // Update the account, no realloc needed! - alloc_and_serialize_variable_len_extension::(metadata_info, &token_metadata, true)?; - - Ok(()) -} - -/// Processes an [`Emit`](enum.TokenMetadataInstruction.html) instruction. -pub fn process_emit(program_id: &Pubkey, accounts: &[AccountInfo], data: Emit) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let metadata_info = next_account_info(account_info_iter)?; - - if metadata_info.owner != program_id { - return Err(ProgramError::IllegalOwner); - } - - let buffer = metadata_info.try_borrow_data()?; - let state = PodStateWithExtensions::::unpack(&buffer)?; - let metadata_bytes = state.get_extension_bytes::()?; - - if let Some(range) = TokenMetadata::get_slice(metadata_bytes, data.start, data.end) { - set_return_data(range); - } - Ok(()) -} - -/// Processes an [`Instruction`](enum.Instruction.html). -pub fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction: TokenMetadataInstruction, -) -> ProgramResult { - match instruction { - TokenMetadataInstruction::Initialize(data) => { - msg!("TokenMetadataInstruction: Initialize"); - process_initialize(program_id, accounts, data) - } - TokenMetadataInstruction::UpdateField(data) => { - msg!("TokenMetadataInstruction: UpdateField"); - process_update_field(program_id, accounts, data) - } - TokenMetadataInstruction::RemoveKey(data) => { - msg!("TokenMetadataInstruction: RemoveKey"); - process_remove_key(program_id, accounts, data) - } - TokenMetadataInstruction::UpdateAuthority(data) => { - msg!("TokenMetadataInstruction: UpdateAuthority"); - process_update_authority(program_id, accounts, data) - } - TokenMetadataInstruction::Emit(data) => { - msg!("TokenMetadataInstruction: Emit"); - process_emit(program_id, accounts, data) - } - } -} diff --git a/token/program-2022/src/extension/transfer_fee/instruction.rs b/token/program-2022/src/extension/transfer_fee/instruction.rs deleted file mode 100644 index 2f25e19f353..00000000000 --- a/token/program-2022/src/extension/transfer_fee/instruction.rs +++ /dev/null @@ -1,500 +0,0 @@ -#[cfg(feature = "serde-traits")] -use { - crate::serialization::coption_fromstr, - serde::{Deserialize, Serialize}, -}; -use { - crate::{check_program_account, error::TokenError, instruction::TokenInstruction}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - program_option::COption, - pubkey::Pubkey, - }, - std::convert::TryFrom, -}; - -/// Transfer Fee extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr( - feature = "serde-traits", - serde(rename_all = "camelCase", rename_all_fields = "camelCase") -)] -#[derive(Clone, Copy, Debug, PartialEq)] -#[repr(u8)] -pub enum TransferFeeInstruction { - /// Initialize the transfer fee on a new mint. - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - InitializeTransferFeeConfig { - /// Pubkey that may update the fees - #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] - transfer_fee_config_authority: COption, - /// Withdraw instructions must be signed by this key - #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] - withdraw_withheld_authority: COption, - /// Amount of transfer collected as fees, expressed as basis points of - /// the transfer amount - transfer_fee_basis_points: u16, - /// Maximum fee assessed on transfers - maximum_fee: u64, - }, - /// Transfer, providing expected mint information and fees - /// - /// This instruction succeeds if the mint has no configured transfer fee - /// and the provided fee is 0. This allows applications to use - /// `TransferCheckedWithFee` with any mint. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The source account. May include the - /// `TransferFeeAmount` extension. - /// 1. `[]` The token mint. May include the `TransferFeeConfig` extension. - /// 2. `[writable]` The destination account. May include the - /// `TransferFeeAmount` extension. - /// 3. `[signer]` The source account's owner/delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The source account. - /// 1. `[]` The token mint. - /// 2. `[writable]` The destination account. - /// 3. `[]` The source account's multisignature owner/delegate. - /// 4. `..4+M` `[signer]` M signer accounts. - TransferCheckedWithFee { - /// The amount of tokens to transfer. - amount: u64, - /// Expected number of base 10 digits to the right of the decimal place. - decimals: u8, - /// Expected fee assessed on this transfer, calculated off-chain based - /// on the `transfer_fee_basis_points` and `maximum_fee` of the mint. - /// May be 0 for a mint without a configured transfer fee. - fee: u64, - }, - /// Transfer all withheld tokens in the mint to an account. Signed by the - /// mint's withdraw withheld tokens authority. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The token mint. Must include the `TransferFeeConfig` - /// extension. - /// 1. `[writable]` The fee receiver account. Must include the - /// `TransferFeeAmount` extension associated with the provided mint. - /// 2. `[signer]` The mint's `withdraw_withheld_authority`. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The token mint. - /// 1. `[writable]` The destination account. - /// 2. `[]` The mint's multisig `withdraw_withheld_authority`. - /// 3. `..3+M `[signer]` M signer accounts. - WithdrawWithheldTokensFromMint, - /// Transfer all withheld tokens to an account. Signed by the mint's - /// withdraw withheld tokens authority. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[]` The token mint. Must include the `TransferFeeConfig` - /// extension. - /// 1. `[writable]` The fee receiver account. Must include the - /// `TransferFeeAmount` extension and be associated with the provided - /// mint. - /// 2. `[signer]` The mint's `withdraw_withheld_authority`. - /// 3. `..3+N` `[writable]` The source accounts to withdraw from. - /// - /// * Multisignature owner/delegate - /// 0. `[]` The token mint. - /// 1. `[writable]` The destination account. - /// 2. `[]` The mint's multisig `withdraw_withheld_authority`. - /// 3. `..3+M` `[signer]` M signer accounts. - /// 4. `3+M+1..3+M+N` `[writable]` The source accounts to withdraw from. - WithdrawWithheldTokensFromAccounts { - /// Number of token accounts harvested - num_token_accounts: u8, - }, - /// Permissionless instruction to transfer all withheld tokens to the mint. - /// - /// Succeeds for frozen accounts. - /// - /// Accounts provided should include the `TransferFeeAmount` extension. If - /// not, the account is skipped. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint. - /// 1. `..1+N` `[writable]` The source accounts to harvest from. - HarvestWithheldTokensToMint, - /// Set transfer fee. Only supported for mints that include the - /// `TransferFeeConfig` extension. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[signer]` The mint's fee account owner. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[]` The mint's multisignature fee account owner. - /// 2. `..2+M` `[signer]` M signer accounts. - SetTransferFee { - /// Amount of transfer collected as fees, expressed as basis points of - /// the transfer amount - transfer_fee_basis_points: u16, - /// Maximum fee assessed on transfers - maximum_fee: u64, - }, -} -impl TransferFeeInstruction { - /// Unpacks a byte buffer into a `TransferFeeInstruction` - pub fn unpack(input: &[u8]) -> Result { - use TokenError::InvalidInstruction; - - let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?; - Ok(match tag { - 0 => { - let (transfer_fee_config_authority, rest) = - TokenInstruction::unpack_pubkey_option(rest)?; - let (withdraw_withheld_authority, rest) = - TokenInstruction::unpack_pubkey_option(rest)?; - let (transfer_fee_basis_points, rest) = TokenInstruction::unpack_u16(rest)?; - let (maximum_fee, _) = TokenInstruction::unpack_u64(rest)?; - Self::InitializeTransferFeeConfig { - transfer_fee_config_authority, - withdraw_withheld_authority, - transfer_fee_basis_points, - maximum_fee, - } - } - 1 => { - let (amount, decimals, rest) = TokenInstruction::unpack_amount_decimals(rest)?; - let (fee, _) = TokenInstruction::unpack_u64(rest)?; - Self::TransferCheckedWithFee { - amount, - decimals, - fee, - } - } - 2 => Self::WithdrawWithheldTokensFromMint, - 3 => { - let (&num_token_accounts, _) = rest.split_first().ok_or(InvalidInstruction)?; - Self::WithdrawWithheldTokensFromAccounts { num_token_accounts } - } - 4 => Self::HarvestWithheldTokensToMint, - 5 => { - let (transfer_fee_basis_points, rest) = TokenInstruction::unpack_u16(rest)?; - let (maximum_fee, _) = TokenInstruction::unpack_u64(rest)?; - Self::SetTransferFee { - transfer_fee_basis_points, - maximum_fee, - } - } - _ => return Err(TokenError::InvalidInstruction.into()), - }) - } - - /// Packs a `TransferFeeInstruction` into a byte buffer. - pub fn pack(&self, buffer: &mut Vec) { - match *self { - Self::InitializeTransferFeeConfig { - ref transfer_fee_config_authority, - ref withdraw_withheld_authority, - transfer_fee_basis_points, - maximum_fee, - } => { - buffer.push(0); - TokenInstruction::pack_pubkey_option(transfer_fee_config_authority, buffer); - TokenInstruction::pack_pubkey_option(withdraw_withheld_authority, buffer); - buffer.extend_from_slice(&transfer_fee_basis_points.to_le_bytes()); - buffer.extend_from_slice(&maximum_fee.to_le_bytes()); - } - Self::TransferCheckedWithFee { - amount, - decimals, - fee, - } => { - buffer.push(1); - buffer.extend_from_slice(&amount.to_le_bytes()); - buffer.extend_from_slice(&decimals.to_le_bytes()); - buffer.extend_from_slice(&fee.to_le_bytes()); - } - Self::WithdrawWithheldTokensFromMint => { - buffer.push(2); - } - Self::WithdrawWithheldTokensFromAccounts { num_token_accounts } => { - buffer.push(3); - buffer.push(num_token_accounts); - } - Self::HarvestWithheldTokensToMint => { - buffer.push(4); - } - Self::SetTransferFee { - transfer_fee_basis_points, - maximum_fee, - } => { - buffer.push(5); - buffer.extend_from_slice(&transfer_fee_basis_points.to_le_bytes()); - buffer.extend_from_slice(&maximum_fee.to_le_bytes()); - } - } - } -} - -fn encode_instruction_data(transfer_fee_instruction: TransferFeeInstruction) -> Vec { - let mut data = TokenInstruction::TransferFeeExtension.pack(); - transfer_fee_instruction.pack(&mut data); - data -} - -/// Create a `InitializeTransferFeeConfig` instruction -pub fn initialize_transfer_fee_config( - token_program_id: &Pubkey, - mint: &Pubkey, - transfer_fee_config_authority: Option<&Pubkey>, - withdraw_withheld_authority: Option<&Pubkey>, - transfer_fee_basis_points: u16, - maximum_fee: u64, -) -> Result { - check_program_account(token_program_id)?; - let transfer_fee_config_authority = transfer_fee_config_authority.cloned().into(); - let withdraw_withheld_authority = withdraw_withheld_authority.cloned().into(); - let data = encode_instruction_data(TransferFeeInstruction::InitializeTransferFeeConfig { - transfer_fee_config_authority, - withdraw_withheld_authority, - transfer_fee_basis_points, - maximum_fee, - }); - - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new(*mint, false)], - data, - }) -} - -/// Create a `TransferCheckedWithFee` instruction -#[allow(clippy::too_many_arguments)] -pub fn transfer_checked_with_fee( - token_program_id: &Pubkey, - source: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], - amount: u64, - decimals: u8, - fee: u64, -) -> Result { - check_program_account(token_program_id)?; - let data = encode_instruction_data(TransferFeeInstruction::TransferCheckedWithFee { - amount, - decimals, - fee, - }); - - let mut accounts = Vec::with_capacity(4 + signers.len()); - accounts.push(AccountMeta::new(*source, false)); - accounts.push(AccountMeta::new_readonly(*mint, false)); - accounts.push(AccountMeta::new(*destination, false)); - accounts.push(AccountMeta::new_readonly(*authority, signers.is_empty())); - for signer in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `WithdrawWithheldTokensFromMint` instruction -pub fn withdraw_withheld_tokens_from_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = Vec::with_capacity(3 + signers.len()); - accounts.push(AccountMeta::new(*mint, false)); - accounts.push(AccountMeta::new(*destination, false)); - accounts.push(AccountMeta::new_readonly(*authority, signers.is_empty())); - for signer in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data: encode_instruction_data(TransferFeeInstruction::WithdrawWithheldTokensFromMint), - }) -} - -/// Creates a `WithdrawWithheldTokensFromAccounts` instruction -pub fn withdraw_withheld_tokens_from_accounts( - token_program_id: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], - sources: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let num_token_accounts = - u8::try_from(sources.len()).map_err(|_| ProgramError::InvalidInstructionData)?; - let mut accounts = Vec::with_capacity(3 + signers.len() + sources.len()); - accounts.push(AccountMeta::new_readonly(*mint, false)); - accounts.push(AccountMeta::new(*destination, false)); - accounts.push(AccountMeta::new_readonly(*authority, signers.is_empty())); - for signer in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer, true)); - } - for source in sources.iter() { - accounts.push(AccountMeta::new(**source, false)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data: encode_instruction_data(TransferFeeInstruction::WithdrawWithheldTokensFromAccounts { - num_token_accounts, - }), - }) -} - -/// Creates a `HarvestWithheldTokensToMint` instruction -pub fn harvest_withheld_tokens_to_mint( - token_program_id: &Pubkey, - mint: &Pubkey, - sources: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = Vec::with_capacity(1 + sources.len()); - accounts.push(AccountMeta::new(*mint, false)); - for source in sources.iter() { - accounts.push(AccountMeta::new(**source, false)); - } - Ok(Instruction { - program_id: *token_program_id, - accounts, - data: encode_instruction_data(TransferFeeInstruction::HarvestWithheldTokensToMint), - }) -} - -/// Creates a `SetTransferFee` instruction -pub fn set_transfer_fee( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], - transfer_fee_basis_points: u16, - maximum_fee: u64, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = Vec::with_capacity(2 + signers.len()); - accounts.push(AccountMeta::new(*mint, false)); - accounts.push(AccountMeta::new_readonly(*authority, signers.is_empty())); - for signer in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data: encode_instruction_data(TransferFeeInstruction::SetTransferFee { - transfer_fee_basis_points, - maximum_fee, - }), - }) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_instruction_packing() { - let check = TransferFeeInstruction::InitializeTransferFeeConfig { - transfer_fee_config_authority: COption::Some(Pubkey::new_from_array([11u8; 32])), - withdraw_withheld_authority: COption::None, - transfer_fee_basis_points: 111, - maximum_fee: u64::MAX, - }; - let mut packed = vec![]; - check.pack(&mut packed); - let mut expect = vec![0, 1]; - expect.extend_from_slice(&[11u8; 32]); - expect.extend_from_slice(&[0]); - expect.extend_from_slice(&111u16.to_le_bytes()); - expect.extend_from_slice(&u64::MAX.to_le_bytes()); - assert_eq!(packed, expect); - let unpacked = TransferFeeInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TransferFeeInstruction::TransferCheckedWithFee { - amount: 24, - decimals: 24, - fee: 23, - }; - let mut packed = vec![]; - check.pack(&mut packed); - let mut expect = vec![1]; - expect.extend_from_slice(&24u64.to_le_bytes()); - expect.extend_from_slice(&[24u8]); - expect.extend_from_slice(&23u64.to_le_bytes()); - assert_eq!(packed, expect); - let unpacked = TransferFeeInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TransferFeeInstruction::WithdrawWithheldTokensFromMint; - let mut packed = vec![]; - check.pack(&mut packed); - let expect = [2]; - assert_eq!(packed, expect); - let unpacked = TransferFeeInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let num_token_accounts = 255; - let check = - TransferFeeInstruction::WithdrawWithheldTokensFromAccounts { num_token_accounts }; - let mut packed = vec![]; - check.pack(&mut packed); - let expect = [3, num_token_accounts]; - assert_eq!(packed, expect); - let unpacked = TransferFeeInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TransferFeeInstruction::HarvestWithheldTokensToMint; - let mut packed = vec![]; - check.pack(&mut packed); - let expect = [4]; - assert_eq!(packed, expect); - let unpacked = TransferFeeInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TransferFeeInstruction::SetTransferFee { - transfer_fee_basis_points: u16::MAX, - maximum_fee: u64::MAX, - }; - let mut packed = vec![]; - check.pack(&mut packed); - let mut expect = vec![5]; - expect.extend_from_slice(&u16::MAX.to_le_bytes()); - expect.extend_from_slice(&u64::MAX.to_le_bytes()); - assert_eq!(packed, expect); - let unpacked = TransferFeeInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - } -} diff --git a/token/program-2022/src/extension/transfer_fee/mod.rs b/token/program-2022/src/extension/transfer_fee/mod.rs deleted file mode 100644 index ed1dec473d4..00000000000 --- a/token/program-2022/src/extension/transfer_fee/mod.rs +++ /dev/null @@ -1,475 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - error::TokenError, - extension::{Extension, ExtensionType}, - }, - bytemuck::{Pod, Zeroable}, - solana_program::{clock::Epoch, entrypoint::ProgramResult}, - spl_pod::{ - optional_keys::OptionalNonZeroPubkey, - primitives::{PodU16, PodU64}, - }, - std::{ - cmp, - convert::{TryFrom, TryInto}, - }, -}; - -/// Transfer fee extension instructions -pub mod instruction; - -/// Transfer fee extension processor -pub mod processor; - -/// Maximum possible fee in basis points is `100%`, aka 10,000 basis points -pub const MAX_FEE_BASIS_POINTS: u16 = 10_000; -const ONE_IN_BASIS_POINTS: u128 = MAX_FEE_BASIS_POINTS as u128; - -/// Transfer fee information -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct TransferFee { - /// First epoch where the transfer fee takes effect - pub epoch: PodU64, // Epoch, - /// Maximum fee assessed on transfers, expressed as an amount of tokens - pub maximum_fee: PodU64, - /// Amount of transfer collected as fees, expressed as basis points of the - /// transfer amount (increments of `0.01%`) - pub transfer_fee_basis_points: PodU16, -} -impl TransferFee { - /// Calculate ceiling-division - /// - /// Ceiling-division - /// `ceil[ numerator / denominator ]` - /// can be represented as a floor-division - /// `floor[ (numerator + denominator - 1) / denominator]` - fn ceil_div(numerator: u128, denominator: u128) -> Option { - numerator - .checked_add(denominator)? - .checked_sub(1)? - .checked_div(denominator) - } - - /// Calculate the transfer fee - pub fn calculate_fee(&self, pre_fee_amount: u64) -> Option { - let transfer_fee_basis_points = u16::from(self.transfer_fee_basis_points) as u128; - if transfer_fee_basis_points == 0 || pre_fee_amount == 0 { - Some(0) - } else { - let numerator = (pre_fee_amount as u128).checked_mul(transfer_fee_basis_points)?; - let raw_fee = Self::ceil_div(numerator, ONE_IN_BASIS_POINTS)? - .try_into() // guaranteed to be okay - .ok()?; - - Some(cmp::min(raw_fee, u64::from(self.maximum_fee))) - } - } - - /// Calculate the gross transfer amount after deducting fees - pub fn calculate_post_fee_amount(&self, pre_fee_amount: u64) -> Option { - pre_fee_amount.checked_sub(self.calculate_fee(pre_fee_amount)?) - } - - /// Calculate the transfer amount that will result in a specified net - /// transfer amount. - /// - /// The original transfer amount may not always be unique due to rounding. - /// In this case, the smaller amount will be chosen. - /// e.g. Both transfer amount 10, 11 with `10%` fee rate results in net - /// transfer amount of 9. In this case, 10 will be chosen. - /// e.g. Fee rate is `100%`. In this case, 0 will be chosen. - /// - /// The original transfer amount may not always exist on large net transfer - /// amounts due to overflow. In this case, `None` is returned. - /// e.g. The net fee amount is `u64::MAX` with a positive fee rate. - pub fn calculate_pre_fee_amount(&self, post_fee_amount: u64) -> Option { - let maximum_fee = u64::from(self.maximum_fee); - let transfer_fee_basis_points = u16::from(self.transfer_fee_basis_points) as u128; - match (transfer_fee_basis_points, post_fee_amount) { - // no fee, same amount - (0, _) => Some(post_fee_amount), - // 0 zero out, 0 in - (_, 0) => Some(0), - // 100%, cap at max fee - (ONE_IN_BASIS_POINTS, _) => maximum_fee.checked_add(post_fee_amount), - _ => { - let numerator = (post_fee_amount as u128).checked_mul(ONE_IN_BASIS_POINTS)?; - let denominator = ONE_IN_BASIS_POINTS.checked_sub(transfer_fee_basis_points)?; - let raw_pre_fee_amount = Self::ceil_div(numerator, denominator)?; - - if raw_pre_fee_amount.checked_sub(post_fee_amount as u128)? >= maximum_fee as u128 { - post_fee_amount.checked_add(maximum_fee) - } else { - // should return `None` if `pre_fee_amount` overflows - u64::try_from(raw_pre_fee_amount).ok() - } - } - } - } - - /// Calculate the fee that would produce the given output - /// - /// Note: this function is not an exact inverse operation of - /// `calculate_fee`. Meaning, it is not the case that: - /// - /// `calculate_fee(x) == calculate_inverse_fee(x - calculate_fee(x))` - /// - /// Only the following relationship holds: - /// - /// `calculate_fee(x) >= calculate_inverse_fee(x - calculate_fee(x))` - pub fn calculate_inverse_fee(&self, post_fee_amount: u64) -> Option { - let pre_fee_amount = self.calculate_pre_fee_amount(post_fee_amount)?; - self.calculate_fee(pre_fee_amount) - } -} - -/// Transfer fee extension data for mints. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct TransferFeeConfig { - /// Optional authority to set the fee - pub transfer_fee_config_authority: OptionalNonZeroPubkey, - /// Withdraw from mint instructions must be signed by this key - pub withdraw_withheld_authority: OptionalNonZeroPubkey, - /// Withheld transfer fee tokens that have been moved to the mint for - /// withdrawal - pub withheld_amount: PodU64, - /// Older transfer fee, used if `current epoch < new_transfer_fee.epoch` - pub older_transfer_fee: TransferFee, - /// Newer transfer fee, used if `current epoch >= new_transfer_fee.epoch` - pub newer_transfer_fee: TransferFee, -} -impl TransferFeeConfig { - /// Get the fee for the given epoch - pub fn get_epoch_fee(&self, epoch: Epoch) -> &TransferFee { - if epoch >= self.newer_transfer_fee.epoch.into() { - &self.newer_transfer_fee - } else { - &self.older_transfer_fee - } - } - /// Calculate the fee for the given epoch and input amount - pub fn calculate_epoch_fee(&self, epoch: Epoch, pre_fee_amount: u64) -> Option { - self.get_epoch_fee(epoch).calculate_fee(pre_fee_amount) - } - /// Calculate the fee for the given epoch and output amount - pub fn calculate_inverse_epoch_fee(&self, epoch: Epoch, post_fee_amount: u64) -> Option { - self.get_epoch_fee(epoch) - .calculate_inverse_fee(post_fee_amount) - } -} -impl Extension for TransferFeeConfig { - const TYPE: ExtensionType = ExtensionType::TransferFeeConfig; -} - -/// Transfer fee extension data for accounts. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct TransferFeeAmount { - /// Amount withheld during transfers, to be harvested to the mint - pub withheld_amount: PodU64, -} -impl TransferFeeAmount { - /// Check if the extension is in a closable state - pub fn closable(&self) -> ProgramResult { - if self.withheld_amount == 0.into() { - Ok(()) - } else { - Err(TokenError::AccountHasWithheldTransferFees.into()) - } - } -} -impl Extension for TransferFeeAmount { - const TYPE: ExtensionType = ExtensionType::TransferFeeAmount; -} - -#[cfg(test)] -pub(crate) mod test { - use {super::*, proptest::prelude::*, solana_program::pubkey::Pubkey, std::convert::TryFrom}; - - const NEWER_EPOCH: u64 = 100; - const OLDER_EPOCH: u64 = 1; - - pub(crate) fn test_transfer_fee_config() -> TransferFeeConfig { - TransferFeeConfig { - transfer_fee_config_authority: OptionalNonZeroPubkey::try_from(Some( - Pubkey::new_from_array([10; 32]), - )) - .unwrap(), - withdraw_withheld_authority: OptionalNonZeroPubkey::try_from(Some( - Pubkey::new_from_array([11; 32]), - )) - .unwrap(), - withheld_amount: PodU64::from(u64::MAX), - older_transfer_fee: TransferFee { - epoch: PodU64::from(OLDER_EPOCH), - maximum_fee: PodU64::from(10), - transfer_fee_basis_points: PodU16::from(100), - }, - newer_transfer_fee: TransferFee { - epoch: PodU64::from(NEWER_EPOCH), - maximum_fee: PodU64::from(5_000), - transfer_fee_basis_points: PodU16::from(1), - }, - } - } - - #[test] - fn epoch_fee() { - let transfer_fee_config = test_transfer_fee_config(); - // during epoch 100 and after, use newer transfer fee - assert_eq!( - transfer_fee_config.get_epoch_fee(NEWER_EPOCH).epoch, - NEWER_EPOCH.into() - ); - assert_eq!( - transfer_fee_config.get_epoch_fee(NEWER_EPOCH + 1).epoch, - NEWER_EPOCH.into() - ); - assert_eq!( - transfer_fee_config.get_epoch_fee(u64::MAX).epoch, - NEWER_EPOCH.into() - ); - // before that, use older transfer fee - assert_eq!( - transfer_fee_config.get_epoch_fee(NEWER_EPOCH - 1).epoch, - OLDER_EPOCH.into() - ); - assert_eq!( - transfer_fee_config.get_epoch_fee(OLDER_EPOCH).epoch, - OLDER_EPOCH.into() - ); - assert_eq!( - transfer_fee_config.get_epoch_fee(OLDER_EPOCH + 1).epoch, - OLDER_EPOCH.into() - ); - } - - #[test] - fn calculate_fee_max() { - let one = u64::try_from(ONE_IN_BASIS_POINTS).unwrap(); - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(5_000), - transfer_fee_basis_points: PodU16::from(1), - }; - let maximum_fee = u64::from(transfer_fee.maximum_fee); - // hit maximum fee - assert_eq!(maximum_fee, transfer_fee.calculate_fee(u64::MAX).unwrap()); - // at exactly the max - assert_eq!( - maximum_fee, - transfer_fee.calculate_fee(maximum_fee * one).unwrap() - ); - // one token above, normally rounds up, but we're at the max - assert_eq!( - maximum_fee, - transfer_fee.calculate_fee(maximum_fee * one + 1).unwrap() - ); - // one token below, rounds up to the max - assert_eq!( - maximum_fee, - transfer_fee.calculate_fee(maximum_fee * one - 1).unwrap() - ); - } - - #[test] - fn calculate_fee_min() { - let one = u64::try_from(ONE_IN_BASIS_POINTS).unwrap(); - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(5_000), - transfer_fee_basis_points: PodU16::from(1), - }; - let minimum_fee = 1; - // hit minimum fee even with 1 token - assert_eq!(minimum_fee, transfer_fee.calculate_fee(1).unwrap()); - // still minimum at 2 tokens - assert_eq!(minimum_fee, transfer_fee.calculate_fee(2).unwrap()); - // still minimum at 10_000 tokens - assert_eq!(minimum_fee, transfer_fee.calculate_fee(one).unwrap()); - // 2 token fee at 10_001 - assert_eq!( - minimum_fee + 1, - transfer_fee.calculate_fee(one + 1).unwrap() - ); - // zero is always zero - assert_eq!(0, transfer_fee.calculate_fee(0).unwrap()); - } - - #[test] - fn calculate_fee_zero() { - let one = u64::try_from(ONE_IN_BASIS_POINTS).unwrap(); - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(u64::MAX), - transfer_fee_basis_points: PodU16::from(0), - }; - // always zero fee - assert_eq!(0, transfer_fee.calculate_fee(0).unwrap()); - assert_eq!(0, transfer_fee.calculate_fee(u64::MAX).unwrap()); - assert_eq!(0, transfer_fee.calculate_fee(1).unwrap()); - assert_eq!(0, transfer_fee.calculate_fee(one).unwrap()); - - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(0), - transfer_fee_basis_points: PodU16::from(MAX_FEE_BASIS_POINTS), - }; - // always zero fee - assert_eq!(0, transfer_fee.calculate_fee(0).unwrap()); - assert_eq!(0, transfer_fee.calculate_fee(u64::MAX).unwrap()); - assert_eq!(0, transfer_fee.calculate_fee(1).unwrap()); - assert_eq!(0, transfer_fee.calculate_fee(one).unwrap()); - } - - #[test] - fn calculate_fee_exact_out_max() { - let one = u64::try_from(ONE_IN_BASIS_POINTS).unwrap(); - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(5_000), - transfer_fee_basis_points: PodU16::from(1), - }; - let maximum_fee = u64::from(transfer_fee.maximum_fee); - // hit maximum fee - assert_eq!( - maximum_fee, - transfer_fee - .calculate_inverse_fee(u64::MAX - maximum_fee) - .unwrap() - ); - // at exactly the max - assert_eq!( - maximum_fee, - transfer_fee - .calculate_inverse_fee(maximum_fee * one - maximum_fee) - .unwrap() - ); - // one token above, normally rounds up, but we're at the max - assert_eq!( - maximum_fee, - transfer_fee - .calculate_inverse_fee(maximum_fee * one - maximum_fee + 1) - .unwrap() - ); - // one token below, rounds up to the max - assert_eq!( - maximum_fee, - transfer_fee - .calculate_inverse_fee(maximum_fee * one - maximum_fee - 1) - .unwrap() - ); - } - - #[test] - fn calculate_pre_fee_amount_edge_cases() { - let maximum_fee = 5_000; - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(maximum_fee), - transfer_fee_basis_points: PodU16::from(u16::try_from(ONE_IN_BASIS_POINTS).unwrap()), - }; - - // 0 zero out, 0 in - assert_eq!(0, transfer_fee.calculate_pre_fee_amount(0).unwrap()); - - // cap at max fee - assert_eq!( - 1 + maximum_fee, - transfer_fee.calculate_pre_fee_amount(1).unwrap() - ); - - // no fee same amount - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(maximum_fee), - transfer_fee_basis_points: PodU16::from(0), - }; - assert_eq!(1, transfer_fee.calculate_pre_fee_amount(1).unwrap()); - } - - #[test] - fn calculate_fee_exact_out_min() { - let one = u64::try_from(ONE_IN_BASIS_POINTS).unwrap(); - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(5_000), - transfer_fee_basis_points: PodU16::from(1), - }; - let minimum_fee = 1; - // hit minimum fee even with 1 token - assert_eq!(minimum_fee, transfer_fee.calculate_inverse_fee(1).unwrap()); - // still minimum at 2 tokens - assert_eq!(minimum_fee, transfer_fee.calculate_inverse_fee(2).unwrap()); - // still minimum at 9_999 tokens - assert_eq!( - minimum_fee, - transfer_fee.calculate_inverse_fee(one - 1).unwrap() - ); - // 2 token fee at 10_000 - assert_eq!( - minimum_fee + 1, - transfer_fee.calculate_inverse_fee(one).unwrap() - ); - // zero is zero token - assert_eq!(0, transfer_fee.calculate_inverse_fee(0).unwrap()); - } - - proptest! { - #[test] - fn round_trip_fee_calculation( - transfer_fee_basis_points in 0u16..MAX_FEE_BASIS_POINTS, - maximum_fee in u64::MIN..=u64::MAX, - amount_in in 0..=u64::MAX - ) { - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(maximum_fee), - transfer_fee_basis_points: PodU16::from(transfer_fee_basis_points), - }; - let fee = transfer_fee.calculate_fee(amount_in).unwrap(); - let amount_out = amount_in.checked_sub(fee).unwrap(); - let fee_exact_out = transfer_fee.calculate_inverse_fee(amount_out).unwrap(); - let diff = if fee > fee_exact_out { - fee - fee_exact_out - } else { - fee_exact_out - fee - }; - // We lose precision with every division by 10000, so for huge amounts, - // the difference can be in the hundreds. This comes out to less than - // 1 / 10^15 - let one = MAX_FEE_BASIS_POINTS as u64; - let precision = amount_in / one / one / one; - assert!(diff < precision, "diff is {} for precision {}", diff, precision); - } - } - - proptest! { - #[test] - fn inverse_fee_relationship( - transfer_fee_basis_points in 0u16..MAX_FEE_BASIS_POINTS, - maximum_fee in u64::MIN..=u64::MAX, - amount_in in 0..=u64::MAX - ) { - let transfer_fee = TransferFee { - epoch: PodU64::from(0), - maximum_fee: PodU64::from(maximum_fee), - transfer_fee_basis_points: PodU16::from(transfer_fee_basis_points), - }; - let fee = transfer_fee.calculate_fee(amount_in).unwrap(); - let amount_out = amount_in.checked_sub(fee).unwrap(); - let fee_exact_out = transfer_fee.calculate_inverse_fee(amount_out).unwrap(); - assert!(fee >= fee_exact_out); - } - } -} diff --git a/token/program-2022/src/extension/transfer_fee/processor.rs b/token/program-2022/src/extension/transfer_fee/processor.rs deleted file mode 100644 index 0e158f850a0..00000000000 --- a/token/program-2022/src/extension/transfer_fee/processor.rs +++ /dev/null @@ -1,326 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - transfer_fee::{ - instruction::TransferFeeInstruction, TransferFee, TransferFeeAmount, - TransferFeeConfig, MAX_FEE_BASIS_POINTS, - }, - BaseStateWithExtensions, BaseStateWithExtensionsMut, PodStateWithExtensions, - PodStateWithExtensionsMut, - }, - pod::{PodAccount, PodMint}, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - clock::Clock, - entrypoint::ProgramResult, - msg, - program_option::COption, - pubkey::Pubkey, - sysvar::Sysvar, - }, - std::convert::TryInto, -}; - -fn process_initialize_transfer_fee_config( - accounts: &[AccountInfo], - transfer_fee_config_authority: COption, - withdraw_withheld_authority: COption, - transfer_fee_basis_points: u16, - maximum_fee: u64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - let extension = mint.init_extension::(true)?; - extension.transfer_fee_config_authority = transfer_fee_config_authority.try_into()?; - extension.withdraw_withheld_authority = withdraw_withheld_authority.try_into()?; - extension.withheld_amount = 0u64.into(); - - if transfer_fee_basis_points > MAX_FEE_BASIS_POINTS { - return Err(TokenError::TransferFeeExceedsMaximum.into()); - } - // To be safe, set newer and older transfer fees to the same thing on init, - // but only newer will actually be used - let epoch = Clock::get()?.epoch; - let transfer_fee = TransferFee { - epoch: epoch.into(), - transfer_fee_basis_points: transfer_fee_basis_points.into(), - maximum_fee: maximum_fee.into(), - }; - extension.older_transfer_fee = transfer_fee; - extension.newer_transfer_fee = transfer_fee; - - Ok(()) -} - -fn process_set_transfer_fee( - program_id: &Pubkey, - accounts: &[AccountInfo], - transfer_fee_basis_points: u16, - maximum_fee: u64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - - let transfer_fee_config_authority = - Option::::from(extension.transfer_fee_config_authority) - .ok_or(TokenError::NoAuthorityExists)?; - Processor::validate_owner( - program_id, - &transfer_fee_config_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - if transfer_fee_basis_points > MAX_FEE_BASIS_POINTS { - return Err(TokenError::TransferFeeExceedsMaximum.into()); - } - - // When setting the transfer fee, we have two situations: - // * newer transfer fee epoch <= current epoch: newer transfer fee is the active - // one, so overwrite older transfer fee with newer, then overwrite newer - // transfer fee - // * newer transfer fee epoch >= next epoch: it was never used, so just - // overwrite next transfer fee - let epoch = Clock::get()?.epoch; - if u64::from(extension.newer_transfer_fee.epoch) <= epoch { - extension.older_transfer_fee = extension.newer_transfer_fee; - } - // set two epochs ahead to avoid rug pulls at the end of an epoch - let newer_fee_start_epoch = epoch.saturating_add(2); - let transfer_fee = TransferFee { - epoch: newer_fee_start_epoch.into(), - transfer_fee_basis_points: transfer_fee_basis_points.into(), - maximum_fee: maximum_fee.into(), - }; - extension.newer_transfer_fee = transfer_fee; - - Ok(()) -} - -fn process_withdraw_withheld_tokens_from_mint( - program_id: &Pubkey, - accounts: &[AccountInfo], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - // unnecessary check, but helps for clarity - check_program_account(mint_account_info.owner)?; - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - - let withdraw_withheld_authority = Option::::from(extension.withdraw_withheld_authority) - .ok_or(TokenError::NoAuthorityExists)?; - Processor::validate_owner( - program_id, - &withdraw_withheld_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - let mut destination_account_data = destination_account_info.data.borrow_mut(); - let destination_account = - PodStateWithExtensionsMut::::unpack(&mut destination_account_data)?; - if destination_account.base.mint != *mint_account_info.key { - return Err(TokenError::MintMismatch.into()); - } - if destination_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - let withheld_amount = u64::from(extension.withheld_amount); - extension.withheld_amount = 0.into(); - destination_account.base.amount = u64::from(destination_account.base.amount) - .checked_add(withheld_amount) - .ok_or(TokenError::Overflow)? - .into(); - - Ok(()) -} - -fn harvest_from_account<'b>( - mint_key: &'b Pubkey, - token_account_info: &'b AccountInfo<'_>, -) -> Result { - let mut token_account_data = token_account_info.data.borrow_mut(); - let mut token_account = - PodStateWithExtensionsMut::::unpack(&mut token_account_data) - .map_err(|_| TokenError::InvalidState)?; - if token_account.base.mint != *mint_key { - return Err(TokenError::MintMismatch); - } - check_program_account(token_account_info.owner).map_err(|_| TokenError::InvalidState)?; - let token_account_extension = token_account - .get_extension_mut::() - .map_err(|_| TokenError::InvalidState)?; - let account_withheld_amount = u64::from(token_account_extension.withheld_amount); - token_account_extension.withheld_amount = 0.into(); - Ok(account_withheld_amount) -} - -fn process_harvest_withheld_tokens_to_mint(accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let token_account_infos = account_info_iter.as_slice(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let mint_extension = mint.get_extension_mut::()?; - - for token_account_info in token_account_infos { - match harvest_from_account(mint_account_info.key, token_account_info) { - Ok(amount) => { - let mint_withheld_amount = u64::from(mint_extension.withheld_amount); - mint_extension.withheld_amount = mint_withheld_amount - .checked_add(amount) - .ok_or(TokenError::Overflow)? - .into(); - } - Err(e) => { - msg!("Error harvesting from {}: {}", token_account_info.key, e); - } - } - } - Ok(()) -} - -fn process_withdraw_withheld_tokens_from_accounts( - program_id: &Pubkey, - accounts: &[AccountInfo], - num_token_accounts: u8, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - let account_infos = account_info_iter.as_slice(); - let num_signers = account_infos - .len() - .saturating_sub(num_token_accounts as usize); - - // unnecessary check, but helps for clarity - check_program_account(mint_account_info.owner)?; - - let mint_data = mint_account_info.data.borrow(); - let mint = PodStateWithExtensions::::unpack(&mint_data)?; - let extension = mint.get_extension::()?; - - let withdraw_withheld_authority = Option::::from(extension.withdraw_withheld_authority) - .ok_or(TokenError::NoAuthorityExists)?; - Processor::validate_owner( - program_id, - &withdraw_withheld_authority, - authority_info, - authority_info_data_len, - &account_infos[..num_signers], - )?; - - let mut destination_account_data = destination_account_info.data.borrow_mut(); - let mut destination_account = - PodStateWithExtensionsMut::::unpack(&mut destination_account_data)?; - if destination_account.base.mint != *mint_account_info.key { - return Err(TokenError::MintMismatch.into()); - } - if destination_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - for account_info in &account_infos[num_signers..] { - // self-harvest, can't double-borrow the underlying data - if account_info.key == destination_account_info.key { - let token_account_extension = destination_account - .get_extension_mut::() - .map_err(|_| TokenError::InvalidState)?; - let account_withheld_amount = u64::from(token_account_extension.withheld_amount); - token_account_extension.withheld_amount = 0.into(); - destination_account.base.amount = u64::from(destination_account.base.amount) - .checked_add(account_withheld_amount) - .ok_or(TokenError::Overflow)? - .into(); - } else { - match harvest_from_account(mint_account_info.key, account_info) { - Ok(amount) => { - destination_account.base.amount = u64::from(destination_account.base.amount) - .checked_add(amount) - .ok_or(TokenError::Overflow)? - .into(); - } - Err(e) => { - msg!("Error harvesting from {}: {}", account_info.key, e); - } - } - } - } - - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - let instruction = TransferFeeInstruction::unpack(input)?; - check_program_account(program_id)?; - - match instruction { - TransferFeeInstruction::InitializeTransferFeeConfig { - transfer_fee_config_authority, - withdraw_withheld_authority, - transfer_fee_basis_points, - maximum_fee, - } => process_initialize_transfer_fee_config( - accounts, - transfer_fee_config_authority, - withdraw_withheld_authority, - transfer_fee_basis_points, - maximum_fee, - ), - TransferFeeInstruction::TransferCheckedWithFee { - amount, - decimals, - fee, - } => { - msg!("TransferFeeInstruction: TransferCheckedWithFee"); - Processor::process_transfer(program_id, accounts, amount, Some(decimals), Some(fee)) - } - TransferFeeInstruction::WithdrawWithheldTokensFromMint => { - msg!("TransferFeeInstruction: WithdrawWithheldTokensFromMint"); - process_withdraw_withheld_tokens_from_mint(program_id, accounts) - } - TransferFeeInstruction::WithdrawWithheldTokensFromAccounts { num_token_accounts } => { - msg!("TransferFeeInstruction: WithdrawWithheldTokensFromAccounts"); - process_withdraw_withheld_tokens_from_accounts(program_id, accounts, num_token_accounts) - } - TransferFeeInstruction::HarvestWithheldTokensToMint => { - msg!("TransferFeeInstruction: HarvestWithheldTokensToMint"); - process_harvest_withheld_tokens_to_mint(accounts) - } - TransferFeeInstruction::SetTransferFee { - transfer_fee_basis_points, - maximum_fee, - } => { - msg!("TransferFeeInstruction: SetTransferFee"); - process_set_transfer_fee(program_id, accounts, transfer_fee_basis_points, maximum_fee) - } - } -} diff --git a/token/program-2022/src/extension/transfer_hook/instruction.rs b/token/program-2022/src/extension/transfer_hook/instruction.rs deleted file mode 100644 index 86c36c056cb..00000000000 --- a/token/program-2022/src/extension/transfer_hook/instruction.rs +++ /dev/null @@ -1,128 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - instruction::{encode_instruction, TokenInstruction}, - }, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, - std::convert::TryInto, -}; - -/// Transfer hook extension instructions -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -pub enum TransferHookInstruction { - /// Initialize a new mint with a transfer hook program. - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// - /// Data expected by this instruction: - /// `crate::extension::transfer_hook::instruction::InitializeInstructionData` - Initialize, - /// Update the transfer hook program id. Only supported for mints that - /// include the `TransferHook` extension. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[signer]` The transfer hook authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[]` The mint's transfer hook authority. - /// 2. `..2+M` `[signer]` M signer accounts. - /// - /// Data expected by this instruction: - /// `crate::extension::transfer_hook::UpdateInstructionData` - Update, -} - -/// Data expected by `Initialize` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct InitializeInstructionData { - /// The public key for the account that can update the program id - pub authority: OptionalNonZeroPubkey, - /// The program id that performs logic during transfers - pub program_id: OptionalNonZeroPubkey, -} - -/// Data expected by `Update` -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Pod, Zeroable)] -#[repr(C)] -pub struct UpdateInstructionData { - /// The program id that performs logic during transfers - pub program_id: OptionalNonZeroPubkey, -} - -/// Create an `Initialize` instruction -pub fn initialize( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: Option, - transfer_hook_program_id: Option, -) -> Result { - check_program_account(token_program_id)?; - let accounts = vec![AccountMeta::new(*mint, false)]; - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::TransferHookExtension, - TransferHookInstruction::Initialize, - &InitializeInstructionData { - authority: authority.try_into()?, - program_id: transfer_hook_program_id.try_into()?, - }, - )) -} - -/// Create an `Update` instruction -pub fn update( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], - transfer_hook_program_id: Option, -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::TransferHookExtension, - TransferHookInstruction::Update, - &UpdateInstructionData { - program_id: transfer_hook_program_id.try_into()?, - }, - )) -} diff --git a/token/program-2022/src/extension/transfer_hook/mod.rs b/token/program-2022/src/extension/transfer_hook/mod.rs deleted file mode 100644 index 5c1fcad2749..00000000000 --- a/token/program-2022/src/extension/transfer_hook/mod.rs +++ /dev/null @@ -1,80 +0,0 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - extension::{ - BaseState, BaseStateWithExtensions, BaseStateWithExtensionsMut, Extension, - ExtensionType, PodStateWithExtensionsMut, - }, - pod::PodAccount, - }, - bytemuck::{Pod, Zeroable}, - solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}, - spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodBool}, -}; - -/// Instructions for the `TransferHook` extension -pub mod instruction; -/// Instruction processor for the `TransferHook` extension -pub mod processor; - -/// Transfer hook extension data for mints. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct TransferHook { - /// Authority that can set the transfer hook program id - pub authority: OptionalNonZeroPubkey, - /// Program that authorizes the transfer - pub program_id: OptionalNonZeroPubkey, -} - -/// Indicates that the tokens from this account belong to a mint with a transfer -/// hook -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct TransferHookAccount { - /// Flag to indicate that the account is in the middle of a transfer - pub transferring: PodBool, -} - -impl Extension for TransferHook { - const TYPE: ExtensionType = ExtensionType::TransferHook; -} - -impl Extension for TransferHookAccount { - const TYPE: ExtensionType = ExtensionType::TransferHookAccount; -} - -/// Attempts to get the transfer hook program id from the TLV data, returning -/// None if the extension is not found -pub fn get_program_id>( - state: &BSE, -) -> Option { - state - .get_extension::() - .ok() - .and_then(|e| Option::::from(e.program_id)) -} - -/// Helper function to set the transferring flag before calling into transfer -/// hook -pub fn set_transferring, S: BaseState>( - account: &mut BSE, -) -> Result<(), ProgramError> { - let account_extension = account.get_extension_mut::()?; - account_extension.transferring = true.into(); - Ok(()) -} - -/// Helper function to unset the transferring flag after a transfer -pub fn unset_transferring(account_info: &AccountInfo) -> Result<(), ProgramError> { - let mut account_data = account_info.data.borrow_mut(); - let mut account = PodStateWithExtensionsMut::::unpack(&mut account_data)?; - let account_extension = account.get_extension_mut::()?; - account_extension.transferring = false.into(); - Ok(()) -} diff --git a/token/program-2022/src/extension/transfer_hook/processor.rs b/token/program-2022/src/extension/transfer_hook/processor.rs deleted file mode 100644 index 4eb63c120f6..00000000000 --- a/token/program-2022/src/extension/transfer_hook/processor.rs +++ /dev/null @@ -1,111 +0,0 @@ -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - transfer_hook::{ - instruction::{ - InitializeInstructionData, TransferHookInstruction, UpdateInstructionData, - }, - TransferHook, - }, - BaseStateWithExtensionsMut, PodStateWithExtensionsMut, - }, - instruction::{decode_instruction_data, decode_instruction_type}, - pod::PodMint, - processor::Processor, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program_error::ProgramError, - pubkey::Pubkey, - }, - spl_pod::optional_keys::OptionalNonZeroPubkey, -}; - -fn process_initialize( - program_id: &Pubkey, - accounts: &[AccountInfo], - authority: &OptionalNonZeroPubkey, - transfer_hook_program_id: &OptionalNonZeroPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - - let extension = mint.init_extension::(true)?; - extension.authority = *authority; - - if let Some(transfer_hook_program_id) = Option::::from(*transfer_hook_program_id) { - if transfer_hook_program_id == *program_id { - return Err(ProgramError::IncorrectProgramId); - } - } else if Option::::from(*authority).is_none() { - msg!("The transfer hook extension requires at least an authority or a program id for initialization, neither was provided"); - Err(TokenError::InvalidInstruction)?; - } - extension.program_id = *transfer_hook_program_id; - Ok(()) -} - -fn process_update( - program_id: &Pubkey, - accounts: &[AccountInfo], - new_program_id: &OptionalNonZeroPubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - let authority = - Option::::from(extension.authority).ok_or(TokenError::NoAuthorityExists)?; - - Processor::validate_owner( - program_id, - &authority, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?; - - if let Some(new_program_id) = Option::::from(*new_program_id) { - if new_program_id == *program_id { - return Err(ProgramError::IncorrectProgramId); - } - } - - extension.program_id = *new_program_id; - Ok(()) -} - -pub(crate) fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - check_program_account(program_id)?; - match decode_instruction_type(input)? { - TransferHookInstruction::Initialize => { - msg!("TransferHookInstruction::Initialize"); - let InitializeInstructionData { - authority, - program_id: transfer_hook_program_id, - } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, authority, transfer_hook_program_id) - } - TransferHookInstruction::Update => { - msg!("TransferHookInstruction::Update"); - let UpdateInstructionData { - program_id: transfer_hook_program_id, - } = decode_instruction_data(input)?; - process_update(program_id, accounts, transfer_hook_program_id) - } - } -} diff --git a/token/program-2022/src/generic_token_account.rs b/token/program-2022/src/generic_token_account.rs deleted file mode 100644 index a58b56375fc..00000000000 --- a/token/program-2022/src/generic_token_account.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Generic Token Account, copied from `spl_token::state` -// Remove all of this and use spl-token's version once token 3.4.0 is released -use { - crate::state::AccountState, - solana_program::pubkey::{Pubkey, PUBKEY_BYTES}, -}; - -const SPL_TOKEN_ACCOUNT_MINT_OFFSET: usize = 0; -const SPL_TOKEN_ACCOUNT_OWNER_OFFSET: usize = 32; - -/// A trait for token Account structs to enable efficiently unpacking various -/// fields without unpacking the complete state. -pub trait GenericTokenAccount { - /// Check if the account data is a valid token account - fn valid_account_data(account_data: &[u8]) -> bool; - - /// Call after account length has already been verified to unpack the - /// account owner - fn unpack_account_owner_unchecked(account_data: &[u8]) -> &Pubkey { - Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_OWNER_OFFSET) - } - - /// Call after account length has already been verified to unpack the - /// account mint - fn unpack_account_mint_unchecked(account_data: &[u8]) -> &Pubkey { - Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_MINT_OFFSET) - } - - /// Call after account length has already been verified to unpack a Pubkey - /// at the specified offset. Panics if `account_data.len()` is less than - /// `PUBKEY_BYTES` - fn unpack_pubkey_unchecked(account_data: &[u8], offset: usize) -> &Pubkey { - bytemuck::from_bytes(&account_data[offset..offset + PUBKEY_BYTES]) - } - - /// Unpacks an account's owner from opaque account data. - fn unpack_account_owner(account_data: &[u8]) -> Option<&Pubkey> { - if Self::valid_account_data(account_data) { - Some(Self::unpack_account_owner_unchecked(account_data)) - } else { - None - } - } - - /// Unpacks an account's mint from opaque account data. - fn unpack_account_mint(account_data: &[u8]) -> Option<&Pubkey> { - if Self::valid_account_data(account_data) { - Some(Self::unpack_account_mint_unchecked(account_data)) - } else { - None - } - } -} - -/// The offset of state field in Account's C representation -pub const ACCOUNT_INITIALIZED_INDEX: usize = 108; - -/// Check if the account data buffer represents an initialized account. -/// This is checking the `state` (`AccountState`) field of an Account object. -pub fn is_initialized_account(account_data: &[u8]) -> bool { - *account_data - .get(ACCOUNT_INITIALIZED_INDEX) - .unwrap_or(&(AccountState::Uninitialized as u8)) - != AccountState::Uninitialized as u8 -} diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs deleted file mode 100644 index 974fc12c7b4..00000000000 --- a/token/program-2022/src/instruction.rs +++ /dev/null @@ -1,2829 +0,0 @@ -//! Instruction types - -// Needed to avoid deprecation warning when generating serde implementation for -// TokenInstruction -#![allow(deprecated)] - -#[cfg(feature = "serde-traits")] -use { - crate::serialization::coption_fromstr, - serde::{Deserialize, Serialize}, - serde_with::{As, DisplayFromStr}, -}; -use { - crate::{ - check_program_account, check_spl_token_program_account, error::TokenError, - extension::ExtensionType, - }, - bytemuck::Pod, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - program_option::COption, - pubkey::{Pubkey, PUBKEY_BYTES}, - system_program, sysvar, - }, - spl_pod::bytemuck::{pod_from_bytes, pod_get_packed_len}, - std::{ - convert::{TryFrom, TryInto}, - mem::size_of, - }, -}; - -/// Minimum number of multisignature signers (min N) -pub const MIN_SIGNERS: usize = 1; -/// Maximum number of multisignature signers (max N) -pub const MAX_SIGNERS: usize = 11; -/// Serialized length of a `u16`, for unpacking -const U16_BYTES: usize = 2; -/// Serialized length of a `u64`, for unpacking -const U64_BYTES: usize = 8; - -/// Instructions supported by the token program. -#[repr(C)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr( - feature = "serde-traits", - serde(rename_all_fields = "camelCase", rename_all = "camelCase") -)] -#[derive(Clone, Debug, PartialEq)] -pub enum TokenInstruction<'a> { - // 0 - /// Initializes a new mint and optionally deposits all the newly minted - /// tokens in an account. - /// - /// The `InitializeMint` instruction requires no signers and MUST be - /// included within the same Transaction as the system program's - /// `CreateAccount` instruction that creates the account being initialized. - /// Otherwise another party can acquire ownership of the uninitialized - /// account. - /// - /// All extensions must be initialized before calling this instruction. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// 1. `[]` Rent sysvar - InitializeMint { - /// Number of base 10 digits to the right of the decimal place. - decimals: u8, - /// The authority/multisignature to mint tokens. - #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] - mint_authority: Pubkey, - /// The freeze authority/multisignature of the mint. - #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] - freeze_authority: COption, - }, - /// Initializes a new account to hold tokens. If this account is associated - /// with the native mint then the token balance of the initialized account - /// will be equal to the amount of SOL in the account. If this account is - /// associated with another mint, that mint must be initialized before this - /// command can succeed. - /// - /// The `InitializeAccount` instruction requires no signers and MUST be - /// included within the same Transaction as the system program's - /// `CreateAccount` instruction that creates the account being initialized. - /// Otherwise another party can acquire ownership of the uninitialized - /// account. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to initialize. - /// 1. `[]` The mint this account will be associated with. - /// 2. `[]` The new account's owner/multisignature. - /// 3. `[]` Rent sysvar - InitializeAccount, - /// Initializes a multisignature account with N provided signers. - /// - /// Multisignature accounts can used in place of any single owner/delegate - /// accounts in any token instruction that require an owner/delegate to be - /// present. The variant field represents the number of signers (M) - /// required to validate this multisignature account. - /// - /// The `InitializeMultisig` instruction requires no signers and MUST be - /// included within the same Transaction as the system program's - /// `CreateAccount` instruction that creates the account being initialized. - /// Otherwise another party can acquire ownership of the uninitialized - /// account. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The multisignature account to initialize. - /// 1. `[]` Rent sysvar - /// 2. ..`2+N`. `[]` The signer accounts, must equal to N where `1 <= N <= - /// 11`. - InitializeMultisig { - /// The number of signers (M) required to validate this multisignature - /// account. - m: u8, - }, - /// NOTE This instruction is deprecated in favor of `TransferChecked` or - /// `TransferCheckedWithFee` - /// - /// Transfers tokens from one account to another either directly or via a - /// delegate. If this account is associated with the native mint then equal - /// amounts of SOL and Tokens will be transferred to the destination - /// account. - /// - /// If either account contains an `TransferFeeAmount` extension, this will - /// fail. Mints with the `TransferFeeConfig` extension are required in - /// order to assess the fee. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The source account. - /// 1. `[writable]` The destination account. - /// 2. `[signer]` The source account's owner/delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The source account. - /// 1. `[writable]` The destination account. - /// 2. `[]` The source account's multisignature owner/delegate. - /// 3. ..`3+M` `[signer]` M signer accounts. - #[deprecated( - since = "4.0.0", - note = "please use `TransferChecked` or `TransferCheckedWithFee` instead" - )] - Transfer { - /// The amount of tokens to transfer. - amount: u64, - }, - /// Approves a delegate. A delegate is given the authority over tokens on - /// behalf of the source account's owner. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The source account. - /// 1. `[]` The delegate. - /// 2. `[signer]` The source account owner. - /// - /// * Multisignature owner - /// 0. `[writable]` The source account. - /// 1. `[]` The delegate. - /// 2. `[]` The source account's multisignature owner. - /// 3. ..`3+M` `[signer]` M signer accounts - Approve { - /// The amount of tokens the delegate is approved for. - amount: u64, - }, - // 5 - /// Revokes the delegate's authority. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The source account. - /// 1. `[signer]` The source account owner or current delegate. - /// - /// * Multisignature owner - /// 0. `[writable]` The source account. - /// 1. `[]` The source account's multisignature owner or current delegate. - /// 2. ..`2+M` `[signer]` M signer accounts - Revoke, - /// Sets a new authority of a mint or account. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint or account to change the authority of. - /// 1. `[signer]` The current authority of the mint or account. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint or account to change the authority of. - /// 1. `[]` The mint's or account's current multisignature authority. - /// 2. ..`2+M` `[signer]` M signer accounts - SetAuthority { - /// The type of authority to update. - authority_type: AuthorityType, - /// The new authority - #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] - new_authority: COption, - }, - /// Mints new tokens to an account. The native mint does not support - /// minting. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[writable]` The account to mint tokens to. - /// 2. `[signer]` The mint's minting authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[writable]` The account to mint tokens to. - /// 2. `[]` The mint's multisignature mint-tokens authority. - /// 3. ..`3+M` `[signer]` M signer accounts. - MintTo { - /// The amount of new tokens to mint. - amount: u64, - }, - /// Burns tokens by removing them from an account. `Burn` does not support - /// accounts associated with the native mint, use `CloseAccount` instead. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The account to burn from. - /// 1. `[writable]` The token mint. - /// 2. `[signer]` The account's owner/delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The account to burn from. - /// 1. `[writable]` The token mint. - /// 2. `[]` The account's multisignature owner/delegate. - /// 3. ..`3+M` `[signer]` M signer accounts. - Burn { - /// The amount of tokens to burn. - amount: u64, - }, - /// Close an account by transferring all its SOL to the destination account. - /// Non-native accounts may only be closed if its token amount is zero. - /// - /// Accounts with the `TransferFeeAmount` extension may only be closed if - /// the withheld amount is zero. - /// - /// Accounts with the `ConfidentialTransfer` extension may only be closed if - /// the pending and available balance ciphertexts are empty. Use - /// `ConfidentialTransferInstruction::ApplyPendingBalance` and - /// `ConfidentialTransferInstruction::EmptyAccount` to empty these - /// ciphertexts. - /// - /// Accounts with the `ConfidentialTransferFee` extension may only be closed - /// if the withheld amount ciphertext is empty. Use - /// `ConfidentialTransferFeeInstruction::HarvestWithheldTokensToMint` to - /// empty this ciphertext. - /// - /// Mints may be closed if they have the `MintCloseAuthority` extension and - /// their token supply is zero - /// - /// Accounts - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The account to close. - /// 1. `[writable]` The destination account. - /// 2. `[signer]` The account's owner. - /// - /// * Multisignature owner - /// 0. `[writable]` The account to close. - /// 1. `[writable]` The destination account. - /// 2. `[]` The account's multisignature owner. - /// 3. ..`3+M` `[signer]` M signer accounts. - CloseAccount, - // 10 - /// Freeze an Initialized account using the Mint's `freeze_authority` (if - /// set). - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The account to freeze. - /// 1. `[]` The token mint. - /// 2. `[signer]` The mint freeze authority. - /// - /// * Multisignature owner - /// 0. `[writable]` The account to freeze. - /// 1. `[]` The token mint. - /// 2. `[]` The mint's multisignature freeze authority. - /// 3. ..`3+M` `[signer]` M signer accounts. - FreezeAccount, - /// Thaw a Frozen account using the Mint's `freeze_authority` (if set). - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The account to freeze. - /// 1. `[]` The token mint. - /// 2. `[signer]` The mint freeze authority. - /// - /// * Multisignature owner - /// 0. `[writable]` The account to freeze. - /// 1. `[]` The token mint. - /// 2. `[]` The mint's multisignature freeze authority. - /// 3. ..`3+M` `[signer]` M signer accounts. - ThawAccount, - - /// Transfers tokens from one account to another either directly or via a - /// delegate. If this account is associated with the native mint then equal - /// amounts of SOL and Tokens will be transferred to the destination - /// account. - /// - /// This instruction differs from `Transfer` in that the token mint and - /// decimals value is checked by the caller. This may be useful when - /// creating transactions offline or within a hardware wallet. - /// - /// If either account contains an `TransferFeeAmount` extension, the fee is - /// withheld in the destination account. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The source account. - /// 1. `[]` The token mint. - /// 2. `[writable]` The destination account. - /// 3. `[signer]` The source account's owner/delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The source account. - /// 1. `[]` The token mint. - /// 2. `[writable]` The destination account. - /// 3. `[]` The source account's multisignature owner/delegate. - /// 4. ..`4+M` `[signer]` M signer accounts. - TransferChecked { - /// The amount of tokens to transfer. - amount: u64, - /// Expected number of base 10 digits to the right of the decimal place. - decimals: u8, - }, - /// Approves a delegate. A delegate is given the authority over tokens on - /// behalf of the source account's owner. - /// - /// This instruction differs from `Approve` in that the token mint and - /// decimals value is checked by the caller. This may be useful when - /// creating transactions offline or within a hardware wallet. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The source account. - /// 1. `[]` The token mint. - /// 2. `[]` The delegate. - /// 3. `[signer]` The source account owner. - /// - /// * Multisignature owner - /// 0. `[writable]` The source account. - /// 1. `[]` The token mint. - /// 2. `[]` The delegate. - /// 3. `[]` The source account's multisignature owner. - /// 4. ..`4+M` `[signer]` M signer accounts - ApproveChecked { - /// The amount of tokens the delegate is approved for. - amount: u64, - /// Expected number of base 10 digits to the right of the decimal place. - decimals: u8, - }, - /// Mints new tokens to an account. The native mint does not support - /// minting. - /// - /// This instruction differs from `MintTo` in that the decimals value is - /// checked by the caller. This may be useful when creating transactions - /// offline or within a hardware wallet. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[writable]` The account to mint tokens to. - /// 2. `[signer]` The mint's minting authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[writable]` The account to mint tokens to. - /// 2. `[]` The mint's multisignature mint-tokens authority. - /// 3. ..`3+M` `[signer]` M signer accounts. - MintToChecked { - /// The amount of new tokens to mint. - amount: u64, - /// Expected number of base 10 digits to the right of the decimal place. - decimals: u8, - }, - // 15 - /// Burns tokens by removing them from an account. `BurnChecked` does not - /// support accounts associated with the native mint, use `CloseAccount` - /// instead. - /// - /// This instruction differs from `Burn` in that the decimals value is - /// checked by the caller. This may be useful when creating transactions - /// offline or within a hardware wallet. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The account to burn from. - /// 1. `[writable]` The token mint. - /// 2. `[signer]` The account's owner/delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The account to burn from. - /// 1. `[writable]` The token mint. - /// 2. `[]` The account's multisignature owner/delegate. - /// 3. ..`3+M` `[signer]` M signer accounts. - BurnChecked { - /// The amount of tokens to burn. - amount: u64, - /// Expected number of base 10 digits to the right of the decimal place. - decimals: u8, - }, - /// Like `InitializeAccount`, but the owner pubkey is passed via instruction - /// data rather than the accounts list. This variant may be preferable - /// when using Cross Program Invocation from an instruction that does - /// not need the owner's `AccountInfo` otherwise. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to initialize. - /// 1. `[]` The mint this account will be associated with. - /// 2. `[]` Rent sysvar - InitializeAccount2 { - /// The new account's owner/multisignature. - #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] - owner: Pubkey, - }, - /// Given a wrapped / native token account (a token account containing SOL) - /// updates its amount field based on the account's underlying `lamports`. - /// This is useful if a non-wrapped SOL account uses - /// `system_instruction::transfer` to move lamports to a wrapped token - /// account, and needs to have its token `amount` field updated. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The native token account to sync with its underlying - /// lamports. - SyncNative, - /// Like `InitializeAccount2`, but does not require the Rent sysvar to be - /// provided - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to initialize. - /// 1. `[]` The mint this account will be associated with. - InitializeAccount3 { - /// The new account's owner/multisignature. - #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] - owner: Pubkey, - }, - /// Like `InitializeMultisig`, but does not require the Rent sysvar to be - /// provided - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The multisignature account to initialize. - /// 1. ..`1+N`. `[]` The signer accounts, must equal to N where `1 <= N <= - /// 11`. - InitializeMultisig2 { - /// The number of signers (M) required to validate this multisignature - /// account. - m: u8, - }, - // 20 - /// Like `InitializeMint`, but does not require the Rent sysvar to be - /// provided - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - InitializeMint2 { - /// Number of base 10 digits to the right of the decimal place. - decimals: u8, - /// The authority/multisignature to mint tokens. - #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] - mint_authority: Pubkey, - /// The freeze authority/multisignature of the mint. - #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] - freeze_authority: COption, - }, - /// Gets the required size of an account for the given mint as a - /// little-endian `u64`. - /// - /// Return data can be fetched using `sol_get_return_data` and deserializing - /// the return data as a little-endian `u64`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[]` The mint to calculate for - GetAccountDataSize { - /// Additional extension types to include in the returned account size - extension_types: Vec, - }, - /// Initialize the Immutable Owner extension for the given token account - /// - /// Fails if the account has already been initialized, so must be called - /// before `InitializeAccount`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to initialize. - /// - /// Data expected by this instruction: - /// None - InitializeImmutableOwner, - /// Convert an Amount of tokens to a `UiAmount` string, using the given - /// mint. - /// - /// Fails on an invalid mint. - /// - /// Return data can be fetched using `sol_get_return_data` and deserialized - /// with `String::from_utf8`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[]` The mint to calculate for - AmountToUiAmount { - /// The amount of tokens to convert. - amount: u64, - }, - /// Convert a `UiAmount` of tokens to a little-endian `u64` raw Amount, - /// using the given mint. - /// - /// Return data can be fetched using `sol_get_return_data` and deserializing - /// the return data as a little-endian `u64`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[]` The mint to calculate for - UiAmountToAmount { - /// The `ui_amount` of tokens to convert. - ui_amount: &'a str, - }, - // 25 - /// Initialize the close account authority on a new mint. - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - InitializeMintCloseAuthority { - /// Authority that must sign the `CloseAccount` instruction on a mint - #[cfg_attr(feature = "serde-traits", serde(with = "coption_fromstr"))] - close_authority: COption, - }, - /// The common instruction prefix for Transfer Fee extension instructions. - /// - /// See `extension::transfer_fee::instruction::TransferFeeInstruction` for - /// further details about the extended instructions that share this - /// instruction prefix - TransferFeeExtension, - /// The common instruction prefix for Confidential Transfer extension - /// instructions. - /// - /// See `extension::confidential_transfer::instruction::ConfidentialTransferInstruction` for - /// further details about the extended instructions that share this - /// instruction prefix - ConfidentialTransferExtension, - /// The common instruction prefix for Default Account State extension - /// instructions. - /// - /// See `extension::default_account_state::instruction::DefaultAccountStateInstruction` for - /// further details about the extended instructions that share this - /// instruction prefix - DefaultAccountStateExtension, - /// Check to see if a token account is large enough for a list of - /// `ExtensionTypes`, and if not, use reallocation to increase the data - /// size. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The account to reallocate. - /// 1. `[signer, writable]` The payer account to fund reallocation - /// 2. `[]` System program for reallocation funding - /// 3. `[signer]` The account's owner. - /// - /// * Multisignature owner - /// 0. `[writable]` The account to reallocate. - /// 1. `[signer, writable]` The payer account to fund reallocation - /// 2. `[]` System program for reallocation funding - /// 3. `[]` The account's multisignature owner/delegate. - /// 4. ..`4+M` `[signer]` M signer accounts. - Reallocate { - /// New extension types to include in the reallocated account - extension_types: Vec, - }, - // 30 - /// The common instruction prefix for Memo Transfer account extension - /// instructions. - /// - /// See `extension::memo_transfer::instruction::RequiredMemoTransfersInstruction` for - /// further details about the extended instructions that share this - /// instruction prefix - MemoTransferExtension, - /// Creates the native mint. - /// - /// This instruction only needs to be invoked once after deployment and is - /// permissionless, Wrapped SOL (`native_mint::id()`) will not be - /// available until this instruction is successfully executed. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writeable,signer]` Funding account (must be a system account) - /// 1. `[writable]` The native mint address - /// 2. `[]` System program for mint account funding - CreateNativeMint, - /// Initialize the non transferable extension for the given mint account - /// - /// Fails if the account has already been initialized, so must be called - /// before `InitializeMint`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint account to initialize. - /// - /// Data expected by this instruction: - /// None - InitializeNonTransferableMint, - /// The common instruction prefix for Interest Bearing extension - /// instructions. - /// - /// See `extension::interest_bearing_mint::instruction::InterestBearingMintInstruction` for - /// further details about the extended instructions that share this - /// instruction prefix - InterestBearingMintExtension, - /// The common instruction prefix for CPI Guard account extension - /// instructions. - /// - /// See `extension::cpi_guard::instruction::CpiGuardInstruction` for - /// further details about the extended instructions that share this - /// instruction prefix - CpiGuardExtension, - // 35 - /// Initialize the permanent delegate on a new mint. - /// - /// Fails if the mint has already been initialized, so must be called before - /// `InitializeMint`. - /// - /// The mint must have exactly enough space allocated for the base mint (82 - /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, - /// then space required for this extension, plus any others. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// - /// Data expected by this instruction: - /// Pubkey for the permanent delegate - InitializePermanentDelegate { - /// Authority that may sign for `Transfer`s and `Burn`s on any account - #[cfg_attr(feature = "serde-traits", serde(with = "As::"))] - delegate: Pubkey, - }, - /// The common instruction prefix for transfer hook extension instructions. - /// - /// See `extension::transfer_hook::instruction::TransferHookInstruction` - /// for further details about the extended instructions that share this - /// instruction prefix - TransferHookExtension, - /// The common instruction prefix for the confidential transfer fee - /// extension instructions. - /// - /// See `extension::confidential_transfer_fee::instruction::ConfidentialTransferFeeInstruction` - /// for further details about the extended instructions that share this - /// instruction prefix - ConfidentialTransferFeeExtension, - /// This instruction is to be used to rescue SOL sent to any `TokenProgram` - /// owned account by sending them to any other account, leaving behind only - /// lamports for rent exemption. - /// - /// 0. `[writable]` Source Account owned by the token program - /// 1. `[writable]` Destination account - /// 2. `[signer]` Authority - /// 3. ..`3+M` `[signer]` M signer accounts. - WithdrawExcessLamports, - /// The common instruction prefix for metadata pointer extension - /// instructions. - /// - /// See `extension::metadata_pointer::instruction::MetadataPointerInstruction` - /// for further details about the extended instructions that share this - /// instruction prefix - MetadataPointerExtension, - // 40 - /// The common instruction prefix for group pointer extension instructions. - /// - /// See `extension::group_pointer::instruction::GroupPointerInstruction` - /// for further details about the extended instructions that share this - /// instruction prefix - GroupPointerExtension, - /// The common instruction prefix for group member pointer extension - /// instructions. - /// - /// See `extension::group_member_pointer::instruction::GroupMemberPointerInstruction` - /// for further details about the extended instructions that share this - /// instruction prefix - GroupMemberPointerExtension, - /// Instruction prefix for instructions to the confidential-mint-burn - /// extension - ConfidentialMintBurnExtension, - /// Instruction prefix for instructions to the scaled ui amount - /// extension - ScaledUiAmountExtension, - /// Instruction prefix for instructions to the pausable extension - PausableExtension, -} -impl<'a> TokenInstruction<'a> { - /// Unpacks a byte buffer into a - /// [`TokenInstruction`](enum.TokenInstruction.html). - pub fn unpack(input: &'a [u8]) -> Result { - use TokenError::InvalidInstruction; - - let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?; - Ok(match tag { - 0 => { - let (&decimals, rest) = rest.split_first().ok_or(InvalidInstruction)?; - let (mint_authority, rest) = Self::unpack_pubkey(rest)?; - let (freeze_authority, _rest) = Self::unpack_pubkey_option(rest)?; - Self::InitializeMint { - mint_authority, - freeze_authority, - decimals, - } - } - 1 => Self::InitializeAccount, - 2 => { - let &m = rest.first().ok_or(InvalidInstruction)?; - Self::InitializeMultisig { m } - } - 3 | 4 | 7 | 8 => { - let amount = rest - .get(..U64_BYTES) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(InvalidInstruction)?; - match tag { - #[allow(deprecated)] - 3 => Self::Transfer { amount }, - 4 => Self::Approve { amount }, - 7 => Self::MintTo { amount }, - 8 => Self::Burn { amount }, - _ => unreachable!(), - } - } - 5 => Self::Revoke, - 6 => { - let (authority_type, rest) = rest - .split_first() - .ok_or_else(|| ProgramError::from(InvalidInstruction)) - .and_then(|(&t, rest)| Ok((AuthorityType::from(t)?, rest)))?; - let (new_authority, _rest) = Self::unpack_pubkey_option(rest)?; - - Self::SetAuthority { - authority_type, - new_authority, - } - } - 9 => Self::CloseAccount, - 10 => Self::FreezeAccount, - 11 => Self::ThawAccount, - 12 => { - let (amount, decimals, _rest) = Self::unpack_amount_decimals(rest)?; - Self::TransferChecked { amount, decimals } - } - 13 => { - let (amount, decimals, _rest) = Self::unpack_amount_decimals(rest)?; - Self::ApproveChecked { amount, decimals } - } - 14 => { - let (amount, decimals, _rest) = Self::unpack_amount_decimals(rest)?; - Self::MintToChecked { amount, decimals } - } - 15 => { - let (amount, decimals, _rest) = Self::unpack_amount_decimals(rest)?; - Self::BurnChecked { amount, decimals } - } - 16 => { - let (owner, _rest) = Self::unpack_pubkey(rest)?; - Self::InitializeAccount2 { owner } - } - 17 => Self::SyncNative, - 18 => { - let (owner, _rest) = Self::unpack_pubkey(rest)?; - Self::InitializeAccount3 { owner } - } - 19 => { - let &m = rest.first().ok_or(InvalidInstruction)?; - Self::InitializeMultisig2 { m } - } - 20 => { - let (&decimals, rest) = rest.split_first().ok_or(InvalidInstruction)?; - let (mint_authority, rest) = Self::unpack_pubkey(rest)?; - let (freeze_authority, _rest) = Self::unpack_pubkey_option(rest)?; - Self::InitializeMint2 { - mint_authority, - freeze_authority, - decimals, - } - } - 21 => { - let mut extension_types = vec![]; - for chunk in rest.chunks(size_of::()) { - extension_types.push(chunk.try_into()?); - } - Self::GetAccountDataSize { extension_types } - } - 22 => Self::InitializeImmutableOwner, - 23 => { - let (amount, _rest) = Self::unpack_u64(rest)?; - Self::AmountToUiAmount { amount } - } - 24 => { - let ui_amount = std::str::from_utf8(rest).map_err(|_| InvalidInstruction)?; - Self::UiAmountToAmount { ui_amount } - } - 25 => { - let (close_authority, _rest) = Self::unpack_pubkey_option(rest)?; - Self::InitializeMintCloseAuthority { close_authority } - } - 26 => Self::TransferFeeExtension, - 27 => Self::ConfidentialTransferExtension, - 28 => Self::DefaultAccountStateExtension, - 29 => { - let mut extension_types = vec![]; - for chunk in rest.chunks(size_of::()) { - extension_types.push(chunk.try_into()?); - } - Self::Reallocate { extension_types } - } - 30 => Self::MemoTransferExtension, - 31 => Self::CreateNativeMint, - 32 => Self::InitializeNonTransferableMint, - 33 => Self::InterestBearingMintExtension, - 34 => Self::CpiGuardExtension, - 35 => { - let (delegate, _rest) = Self::unpack_pubkey(rest)?; - Self::InitializePermanentDelegate { delegate } - } - 36 => Self::TransferHookExtension, - 37 => Self::ConfidentialTransferFeeExtension, - 38 => Self::WithdrawExcessLamports, - 39 => Self::MetadataPointerExtension, - 40 => Self::GroupPointerExtension, - 41 => Self::GroupMemberPointerExtension, - 42 => Self::ConfidentialMintBurnExtension, - 43 => Self::ScaledUiAmountExtension, - 44 => Self::PausableExtension, - _ => return Err(TokenError::InvalidInstruction.into()), - }) - } - - /// Packs a [`TokenInstruction`](enum.TokenInstruction.html) into a byte - /// buffer. - pub fn pack(&self) -> Vec { - let mut buf = Vec::with_capacity(size_of::()); - match self { - &Self::InitializeMint { - ref mint_authority, - ref freeze_authority, - decimals, - } => { - buf.push(0); - buf.push(decimals); - buf.extend_from_slice(mint_authority.as_ref()); - Self::pack_pubkey_option(freeze_authority, &mut buf); - } - Self::InitializeAccount => buf.push(1), - &Self::InitializeMultisig { m } => { - buf.push(2); - buf.push(m); - } - #[allow(deprecated)] - &Self::Transfer { amount } => { - buf.push(3); - buf.extend_from_slice(&amount.to_le_bytes()); - } - &Self::Approve { amount } => { - buf.push(4); - buf.extend_from_slice(&amount.to_le_bytes()); - } - &Self::MintTo { amount } => { - buf.push(7); - buf.extend_from_slice(&amount.to_le_bytes()); - } - &Self::Burn { amount } => { - buf.push(8); - buf.extend_from_slice(&amount.to_le_bytes()); - } - Self::Revoke => buf.push(5), - Self::SetAuthority { - authority_type, - ref new_authority, - } => { - buf.push(6); - buf.push(authority_type.into()); - Self::pack_pubkey_option(new_authority, &mut buf); - } - Self::CloseAccount => buf.push(9), - Self::FreezeAccount => buf.push(10), - Self::ThawAccount => buf.push(11), - &Self::TransferChecked { amount, decimals } => { - buf.push(12); - buf.extend_from_slice(&amount.to_le_bytes()); - buf.push(decimals); - } - &Self::ApproveChecked { amount, decimals } => { - buf.push(13); - buf.extend_from_slice(&amount.to_le_bytes()); - buf.push(decimals); - } - &Self::MintToChecked { amount, decimals } => { - buf.push(14); - buf.extend_from_slice(&amount.to_le_bytes()); - buf.push(decimals); - } - &Self::BurnChecked { amount, decimals } => { - buf.push(15); - buf.extend_from_slice(&amount.to_le_bytes()); - buf.push(decimals); - } - &Self::InitializeAccount2 { owner } => { - buf.push(16); - buf.extend_from_slice(owner.as_ref()); - } - &Self::SyncNative => { - buf.push(17); - } - &Self::InitializeAccount3 { owner } => { - buf.push(18); - buf.extend_from_slice(owner.as_ref()); - } - &Self::InitializeMultisig2 { m } => { - buf.push(19); - buf.push(m); - } - &Self::InitializeMint2 { - ref mint_authority, - ref freeze_authority, - decimals, - } => { - buf.push(20); - buf.push(decimals); - buf.extend_from_slice(mint_authority.as_ref()); - Self::pack_pubkey_option(freeze_authority, &mut buf); - } - Self::GetAccountDataSize { extension_types } => { - buf.push(21); - for extension_type in extension_types { - buf.extend_from_slice(&<[u8; 2]>::from(*extension_type)); - } - } - &Self::InitializeImmutableOwner => { - buf.push(22); - } - &Self::AmountToUiAmount { amount } => { - buf.push(23); - buf.extend_from_slice(&amount.to_le_bytes()); - } - Self::UiAmountToAmount { ui_amount } => { - buf.push(24); - buf.extend_from_slice(ui_amount.as_bytes()); - } - Self::InitializeMintCloseAuthority { close_authority } => { - buf.push(25); - Self::pack_pubkey_option(close_authority, &mut buf); - } - Self::TransferFeeExtension => { - buf.push(26); - } - &Self::ConfidentialTransferExtension => { - buf.push(27); - } - &Self::DefaultAccountStateExtension => { - buf.push(28); - } - Self::Reallocate { extension_types } => { - buf.push(29); - for extension_type in extension_types { - buf.extend_from_slice(&<[u8; 2]>::from(*extension_type)); - } - } - &Self::MemoTransferExtension => { - buf.push(30); - } - &Self::CreateNativeMint => { - buf.push(31); - } - &Self::InitializeNonTransferableMint => { - buf.push(32); - } - &Self::InterestBearingMintExtension => { - buf.push(33); - } - &Self::CpiGuardExtension => { - buf.push(34); - } - Self::InitializePermanentDelegate { delegate } => { - buf.push(35); - buf.extend_from_slice(delegate.as_ref()); - } - &Self::TransferHookExtension => { - buf.push(36); - } - &Self::ConfidentialTransferFeeExtension => { - buf.push(37); - } - &Self::WithdrawExcessLamports => { - buf.push(38); - } - &Self::MetadataPointerExtension => { - buf.push(39); - } - &Self::GroupPointerExtension => { - buf.push(40); - } - &Self::GroupMemberPointerExtension => { - buf.push(41); - } - &Self::ConfidentialMintBurnExtension => { - buf.push(42); - } - &Self::ScaledUiAmountExtension => { - buf.push(43); - } - &Self::PausableExtension => { - buf.push(44); - } - }; - buf - } - - pub(crate) fn unpack_pubkey(input: &[u8]) -> Result<(Pubkey, &[u8]), ProgramError> { - let pk = input - .get(..PUBKEY_BYTES) - .and_then(|x| Pubkey::try_from(x).ok()) - .ok_or(TokenError::InvalidInstruction)?; - Ok((pk, &input[PUBKEY_BYTES..])) - } - - pub(crate) fn unpack_pubkey_option( - input: &[u8], - ) -> Result<(COption, &[u8]), ProgramError> { - match input.split_first() { - Option::Some((&0, rest)) => Ok((COption::None, rest)), - Option::Some((&1, rest)) => { - let (pk, rest) = Self::unpack_pubkey(rest)?; - Ok((COption::Some(pk), rest)) - } - _ => Err(TokenError::InvalidInstruction.into()), - } - } - - pub(crate) fn pack_pubkey_option(value: &COption, buf: &mut Vec) { - match *value { - COption::Some(ref key) => { - buf.push(1); - buf.extend_from_slice(&key.to_bytes()); - } - COption::None => buf.push(0), - } - } - - pub(crate) fn unpack_u16(input: &[u8]) -> Result<(u16, &[u8]), ProgramError> { - let value = input - .get(..U16_BYTES) - .and_then(|slice| slice.try_into().ok()) - .map(u16::from_le_bytes) - .ok_or(TokenError::InvalidInstruction)?; - Ok((value, &input[U16_BYTES..])) - } - - pub(crate) fn unpack_u64(input: &[u8]) -> Result<(u64, &[u8]), ProgramError> { - let value = input - .get(..U64_BYTES) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(TokenError::InvalidInstruction)?; - Ok((value, &input[U64_BYTES..])) - } - - pub(crate) fn unpack_amount_decimals(input: &[u8]) -> Result<(u64, u8, &[u8]), ProgramError> { - let (amount, rest) = Self::unpack_u64(input)?; - let (&decimals, rest) = rest.split_first().ok_or(TokenError::InvalidInstruction)?; - Ok((amount, decimals, rest)) - } -} - -/// Specifies the authority type for `SetAuthority` instructions -#[repr(u8)] -#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] -#[derive(Clone, Debug, PartialEq)] -pub enum AuthorityType { - /// Authority to mint new tokens - MintTokens, - /// Authority to freeze any account associated with the Mint - FreezeAccount, - /// Owner of a given token account - AccountOwner, - /// Authority to close a token account - CloseAccount, - /// Authority to set the transfer fee - TransferFeeConfig, - /// Authority to withdraw withheld tokens from a mint - WithheldWithdraw, - /// Authority to close a mint account - CloseMint, - /// Authority to set the interest rate - InterestRate, - /// Authority to transfer or burn any tokens for a mint - PermanentDelegate, - /// Authority to update confidential transfer mint and approve accounts for - /// confidential transfers - ConfidentialTransferMint, - /// Authority to set the transfer hook program id - TransferHookProgramId, - /// Authority to set the withdraw withheld authority encryption key - ConfidentialTransferFeeConfig, - /// Authority to set the metadata address - MetadataPointer, - /// Authority to set the group address - GroupPointer, - /// Authority to set the group member address - GroupMemberPointer, - /// Authority to set the UI amount scale - ScaledUiAmount, - /// Authority to pause or resume minting / transferring / burning - Pause, -} - -impl AuthorityType { - fn into(&self) -> u8 { - match self { - AuthorityType::MintTokens => 0, - AuthorityType::FreezeAccount => 1, - AuthorityType::AccountOwner => 2, - AuthorityType::CloseAccount => 3, - AuthorityType::TransferFeeConfig => 4, - AuthorityType::WithheldWithdraw => 5, - AuthorityType::CloseMint => 6, - AuthorityType::InterestRate => 7, - AuthorityType::PermanentDelegate => 8, - AuthorityType::ConfidentialTransferMint => 9, - AuthorityType::TransferHookProgramId => 10, - AuthorityType::ConfidentialTransferFeeConfig => 11, - AuthorityType::MetadataPointer => 12, - AuthorityType::GroupPointer => 13, - AuthorityType::GroupMemberPointer => 14, - AuthorityType::ScaledUiAmount => 15, - AuthorityType::Pause => 16, - } - } - - pub(crate) fn from(index: u8) -> Result { - match index { - 0 => Ok(AuthorityType::MintTokens), - 1 => Ok(AuthorityType::FreezeAccount), - 2 => Ok(AuthorityType::AccountOwner), - 3 => Ok(AuthorityType::CloseAccount), - 4 => Ok(AuthorityType::TransferFeeConfig), - 5 => Ok(AuthorityType::WithheldWithdraw), - 6 => Ok(AuthorityType::CloseMint), - 7 => Ok(AuthorityType::InterestRate), - 8 => Ok(AuthorityType::PermanentDelegate), - 9 => Ok(AuthorityType::ConfidentialTransferMint), - 10 => Ok(AuthorityType::TransferHookProgramId), - 11 => Ok(AuthorityType::ConfidentialTransferFeeConfig), - 12 => Ok(AuthorityType::MetadataPointer), - 13 => Ok(AuthorityType::GroupPointer), - 14 => Ok(AuthorityType::GroupMemberPointer), - 15 => Ok(AuthorityType::ScaledUiAmount), - 16 => Ok(AuthorityType::Pause), - _ => Err(TokenError::InvalidInstruction.into()), - } - } -} - -/// Creates a `InitializeMint` instruction. -pub fn initialize_mint( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - mint_authority_pubkey: &Pubkey, - freeze_authority_pubkey: Option<&Pubkey>, - decimals: u8, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let freeze_authority = freeze_authority_pubkey.cloned().into(); - let data = TokenInstruction::InitializeMint { - mint_authority: *mint_authority_pubkey, - freeze_authority, - decimals, - } - .pack(); - - let accounts = vec![ - AccountMeta::new(*mint_pubkey, false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - ]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeMint2` instruction. -pub fn initialize_mint2( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - mint_authority_pubkey: &Pubkey, - freeze_authority_pubkey: Option<&Pubkey>, - decimals: u8, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let freeze_authority = freeze_authority_pubkey.cloned().into(); - let data = TokenInstruction::InitializeMint2 { - mint_authority: *mint_authority_pubkey, - freeze_authority, - decimals, - } - .pack(); - - let accounts = vec![AccountMeta::new(*mint_pubkey, false)]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeAccount` instruction. -pub fn initialize_account( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::InitializeAccount.pack(); - - let accounts = vec![ - AccountMeta::new(*account_pubkey, false), - AccountMeta::new_readonly(*mint_pubkey, false), - AccountMeta::new_readonly(*owner_pubkey, false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - ]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeAccount2` instruction. -pub fn initialize_account2( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::InitializeAccount2 { - owner: *owner_pubkey, - } - .pack(); - - let accounts = vec![ - AccountMeta::new(*account_pubkey, false), - AccountMeta::new_readonly(*mint_pubkey, false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - ]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeAccount3` instruction. -pub fn initialize_account3( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::InitializeAccount3 { - owner: *owner_pubkey, - } - .pack(); - - let accounts = vec![ - AccountMeta::new(*account_pubkey, false), - AccountMeta::new_readonly(*mint_pubkey, false), - ]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeMultisig` instruction. -pub fn initialize_multisig( - token_program_id: &Pubkey, - multisig_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - m: u8, -) -> Result { - check_spl_token_program_account(token_program_id)?; - if !is_valid_signer_index(m as usize) - || !is_valid_signer_index(signer_pubkeys.len()) - || m as usize > signer_pubkeys.len() - { - return Err(ProgramError::MissingRequiredSignature); - } - let data = TokenInstruction::InitializeMultisig { m }.pack(); - - let mut accounts = Vec::with_capacity(1 + 1 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*multisig_pubkey, false)); - accounts.push(AccountMeta::new_readonly(sysvar::rent::id(), false)); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, false)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeMultisig2` instruction. -pub fn initialize_multisig2( - token_program_id: &Pubkey, - multisig_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - m: u8, -) -> Result { - check_spl_token_program_account(token_program_id)?; - if !is_valid_signer_index(m as usize) - || !is_valid_signer_index(signer_pubkeys.len()) - || m as usize > signer_pubkeys.len() - { - return Err(ProgramError::MissingRequiredSignature); - } - let data = TokenInstruction::InitializeMultisig2 { m }.pack(); - - let mut accounts = Vec::with_capacity(1 + 1 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*multisig_pubkey, false)); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, false)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `Transfer` instruction. -#[deprecated( - since = "4.0.0", - note = "please use `transfer_checked` or `transfer_checked_with_fee` instead" -)] -pub fn transfer( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, -) -> Result { - check_spl_token_program_account(token_program_id)?; - #[allow(deprecated)] - let data = TokenInstruction::Transfer { amount }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new(*destination_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *authority_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates an `Approve` instruction. -pub fn approve( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - delegate_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::Approve { amount }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*delegate_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `Revoke` instruction. -pub fn revoke( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::Revoke.pack(); - - let mut accounts = Vec::with_capacity(2 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `SetAuthority` instruction. -pub fn set_authority( - token_program_id: &Pubkey, - owned_pubkey: &Pubkey, - new_authority_pubkey: Option<&Pubkey>, - authority_type: AuthorityType, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_spl_token_program_account(token_program_id)?; - let new_authority = new_authority_pubkey.cloned().into(); - let data = TokenInstruction::SetAuthority { - authority_type, - new_authority, - } - .pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*owned_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `MintTo` instruction. -pub fn mint_to( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - account_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::MintTo { amount }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*mint_pubkey, false)); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `Burn` instruction. -pub fn burn( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::Burn { amount }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *authority_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `CloseAccount` instruction. -pub fn close_account( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::CloseAccount.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new(*destination_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `FreezeAccount` instruction. -pub fn freeze_account( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::FreezeAccount.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `ThawAccount` instruction. -pub fn thaw_account( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::ThawAccount.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `TransferChecked` instruction. -#[allow(clippy::too_many_arguments)] -pub fn transfer_checked( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::TransferChecked { amount, decimals }.pack(); - - let mut accounts = Vec::with_capacity(4 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*mint_pubkey, false)); - accounts.push(AccountMeta::new(*destination_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *authority_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates an `ApproveChecked` instruction. -#[allow(clippy::too_many_arguments)] -pub fn approve_checked( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - delegate_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::ApproveChecked { amount, decimals }.pack(); - - let mut accounts = Vec::with_capacity(4 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*delegate_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `MintToChecked` instruction. -pub fn mint_to_checked( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - account_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::MintToChecked { amount, decimals }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*mint_pubkey, false)); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `BurnChecked` instruction. -pub fn burn_checked( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, -) -> Result { - check_spl_token_program_account(token_program_id)?; - let data = TokenInstruction::BurnChecked { amount, decimals }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *authority_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `SyncNative` instruction -pub fn sync_native( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, -) -> Result { - check_spl_token_program_account(token_program_id)?; - - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new(*account_pubkey, false)], - data: TokenInstruction::SyncNative.pack(), - }) -} - -/// Creates a `GetAccountDataSize` instruction -pub fn get_account_data_size( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - extension_types: &[ExtensionType], -) -> Result { - check_spl_token_program_account(token_program_id)?; - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new_readonly(*mint_pubkey, false)], - data: TokenInstruction::GetAccountDataSize { - extension_types: extension_types.to_vec(), - } - .pack(), - }) -} - -/// Creates an `InitializeMintCloseAuthority` instruction -pub fn initialize_mint_close_authority( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - close_authority: Option<&Pubkey>, -) -> Result { - check_program_account(token_program_id)?; - let close_authority = close_authority.cloned().into(); - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new(*mint_pubkey, false)], - data: TokenInstruction::InitializeMintCloseAuthority { close_authority }.pack(), - }) -} - -/// Create an `InitializeImmutableOwner` instruction -pub fn initialize_immutable_owner( - token_program_id: &Pubkey, - token_account: &Pubkey, -) -> Result { - check_spl_token_program_account(token_program_id)?; - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new(*token_account, false)], - data: TokenInstruction::InitializeImmutableOwner.pack(), - }) -} - -/// Creates an `AmountToUiAmount` instruction -pub fn amount_to_ui_amount( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - amount: u64, -) -> Result { - check_spl_token_program_account(token_program_id)?; - - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new_readonly(*mint_pubkey, false)], - data: TokenInstruction::AmountToUiAmount { amount }.pack(), - }) -} - -/// Creates a `UiAmountToAmount` instruction -pub fn ui_amount_to_amount( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - ui_amount: &str, -) -> Result { - check_spl_token_program_account(token_program_id)?; - - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new_readonly(*mint_pubkey, false)], - data: TokenInstruction::UiAmountToAmount { ui_amount }.pack(), - }) -} - -/// Creates a `Reallocate` instruction -pub fn reallocate( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - payer: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - extension_types: &[ExtensionType], -) -> Result { - check_program_account(token_program_id)?; - - let mut accounts = Vec::with_capacity(4 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new(*payer, true)); - accounts.push(AccountMeta::new_readonly(system_program::id(), false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data: TokenInstruction::Reallocate { - extension_types: extension_types.to_vec(), - } - .pack(), - }) -} - -/// Creates a `CreateNativeMint` instruction -pub fn create_native_mint( - token_program_id: &Pubkey, - payer: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![ - AccountMeta::new(*payer, true), - AccountMeta::new(crate::native_mint::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - ], - data: TokenInstruction::CreateNativeMint.pack(), - }) -} - -/// Creates an `InitializeNonTransferableMint` instruction -pub fn initialize_non_transferable_mint( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new(*mint_pubkey, false)], - data: TokenInstruction::InitializeNonTransferableMint.pack(), - }) -} - -/// Creates an `InitializePermanentDelegate` instruction -pub fn initialize_permanent_delegate( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - delegate: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new(*mint_pubkey, false)], - data: TokenInstruction::InitializePermanentDelegate { - delegate: *delegate, - } - .pack(), - }) -} - -/// Utility function that checks index is between `MIN_SIGNERS` and -/// `MAX_SIGNERS` -pub fn is_valid_signer_index(index: usize) -> bool { - (MIN_SIGNERS..=MAX_SIGNERS).contains(&index) -} - -/// Utility function for decoding just the instruction type -pub fn decode_instruction_type>(input: &[u8]) -> Result { - if input.is_empty() { - Err(ProgramError::InvalidInstructionData) - } else { - T::try_from(input[0]).map_err(|_| TokenError::InvalidInstruction.into()) - } -} - -/// Utility function for decoding instruction data -/// -/// Note: This function expects the entire instruction input, including the -/// instruction type as the first byte. This makes the code concise and safe -/// at the expense of clarity, allowing flows such as: -/// -/// ``` -/// use spl_token_2022::instruction::{decode_instruction_data, decode_instruction_type}; -/// use num_enum::TryFromPrimitive; -/// use bytemuck::{Pod, Zeroable}; -/// -/// #[repr(u8)] -/// #[derive(Clone, Copy, TryFromPrimitive)] -/// enum InstructionType { -/// First -/// } -/// #[derive(Pod, Zeroable, Copy, Clone)] -/// #[repr(transparent)] -/// struct FirstData { -/// a: u8, -/// } -/// let input = [0, 1]; -/// match decode_instruction_type(&input).unwrap() { -/// InstructionType::First => { -/// let FirstData { a } = decode_instruction_data(&input).unwrap(); -/// assert_eq!(*a, 1); -/// } -/// } -/// ``` -pub fn decode_instruction_data(input_with_type: &[u8]) -> Result<&T, ProgramError> { - if input_with_type.len() != pod_get_packed_len::().saturating_add(1) { - Err(ProgramError::InvalidInstructionData) - } else { - pod_from_bytes(&input_with_type[1..]) - } -} - -/// Utility function for encoding instruction data -pub(crate) fn encode_instruction, D: Pod>( - token_program_id: &Pubkey, - accounts: Vec, - token_instruction_type: TokenInstruction, - instruction_type: T, - instruction_data: &D, -) -> Instruction { - let mut data = token_instruction_type.pack(); - data.push(T::into(instruction_type)); - data.extend_from_slice(bytemuck::bytes_of(instruction_data)); - Instruction { - program_id: *token_program_id, - accounts, - data, - } -} - -/// Creates a `WithdrawExcessLamports` Instruction -pub fn withdraw_excess_lamports( - token_program_id: &Pubkey, - source_account: &Pubkey, - destination_account: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - - let mut accounts = vec![ - AccountMeta::new(*source_account, false), - AccountMeta::new(*destination_account, false), - AccountMeta::new_readonly(*authority, signers.is_empty()), - ]; - - for signer in signers { - accounts.push(AccountMeta::new_readonly(**signer, true)) - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data: TokenInstruction::WithdrawExcessLamports.pack(), - }) -} - -#[cfg(test)] -mod test { - use {super::*, crate::pod_instruction::*, proptest::prelude::*}; - - #[test] - fn test_initialize_mint_packing() { - let decimals = 2; - let mint_authority = Pubkey::new_from_array([1u8; 32]); - let freeze_authority = COption::None; - let check = TokenInstruction::InitializeMint { - decimals, - mint_authority, - freeze_authority, - }; - let packed = check.pack(); - let mut expect = Vec::from([0u8, 2]); - expect.extend_from_slice(&[1u8; 32]); - expect.extend_from_slice(&[0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::InitializeMint); - let (pod, pod_freeze_authority) = - decode_instruction_data_with_coption_pubkey::(&packed).unwrap(); - assert_eq!(pod.decimals, decimals); - assert_eq!(pod.mint_authority, mint_authority); - assert_eq!(pod_freeze_authority, freeze_authority.into()); - - let mint_authority = Pubkey::new_from_array([2u8; 32]); - let freeze_authority = COption::Some(Pubkey::new_from_array([3u8; 32])); - let check = TokenInstruction::InitializeMint { - decimals, - mint_authority, - freeze_authority, - }; - let packed = check.pack(); - let mut expect = vec![0u8, 2]; - expect.extend_from_slice(&[2u8; 32]); - expect.extend_from_slice(&[1]); - expect.extend_from_slice(&[3u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::InitializeMint); - let (pod, pod_freeze_authority) = - decode_instruction_data_with_coption_pubkey::(&packed).unwrap(); - assert_eq!(pod.decimals, decimals); - assert_eq!(pod.mint_authority, mint_authority); - assert_eq!(pod_freeze_authority, freeze_authority.into()); - } - - #[test] - fn test_initialize_account_packing() { - let check = TokenInstruction::InitializeAccount; - let packed = check.pack(); - let expect = Vec::from([1u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::InitializeAccount); - } - - #[test] - fn test_initialize_multisig_packing() { - let m = 1; - let check = TokenInstruction::InitializeMultisig { m }; - let packed = check.pack(); - let expect = Vec::from([2u8, 1]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::InitializeMultisig); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.m, m); - } - - #[test] - fn test_transfer_packing() { - let amount = 1; - #[allow(deprecated)] - let check = TokenInstruction::Transfer { amount }; - let packed = check.pack(); - let expect = Vec::from([3u8, 1, 0, 0, 0, 0, 0, 0, 0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::Transfer); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.amount, amount.into()); - } - - #[test] - fn test_approve_packing() { - let amount = 1; - let check = TokenInstruction::Approve { amount }; - let packed = check.pack(); - let expect = Vec::from([4u8, 1, 0, 0, 0, 0, 0, 0, 0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::Approve); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.amount, amount.into()); - } - - #[test] - fn test_revoke_packing() { - let check = TokenInstruction::Revoke; - let packed = check.pack(); - let expect = Vec::from([5u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::Revoke); - } - - #[test] - fn test_set_authority_packing() { - let authority_type = AuthorityType::FreezeAccount; - let new_authority = COption::Some(Pubkey::new_from_array([4u8; 32])); - let check = TokenInstruction::SetAuthority { - authority_type: authority_type.clone(), - new_authority, - }; - let packed = check.pack(); - let mut expect = Vec::from([6u8, 1]); - expect.extend_from_slice(&[1]); - expect.extend_from_slice(&[4u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::SetAuthority); - let (pod, pod_new_authority) = - decode_instruction_data_with_coption_pubkey::(&packed).unwrap(); - assert_eq!( - AuthorityType::from(pod.authority_type).unwrap(), - authority_type - ); - assert_eq!(pod_new_authority, new_authority.into()); - } - - #[test] - fn test_mint_to_packing() { - let amount = 1; - let check = TokenInstruction::MintTo { amount }; - let packed = check.pack(); - let expect = Vec::from([7u8, 1, 0, 0, 0, 0, 0, 0, 0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::MintTo); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.amount, amount.into()); - } - - #[test] - fn test_burn_packing() { - let amount = 1; - let check = TokenInstruction::Burn { amount }; - let packed = check.pack(); - let expect = Vec::from([8u8, 1, 0, 0, 0, 0, 0, 0, 0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::Burn); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.amount, amount.into()); - } - - #[test] - fn test_close_account_packing() { - let check = TokenInstruction::CloseAccount; - let packed = check.pack(); - let expect = Vec::from([9u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::CloseAccount); - } - - #[test] - fn test_freeze_account_packing() { - let check = TokenInstruction::FreezeAccount; - let packed = check.pack(); - let expect = Vec::from([10u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::FreezeAccount); - } - - #[test] - fn test_thaw_account_packing() { - let check = TokenInstruction::ThawAccount; - let packed = check.pack(); - let expect = Vec::from([11u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::ThawAccount); - } - - #[test] - fn test_transfer_checked_packing() { - let amount = 1; - let decimals = 2; - let check = TokenInstruction::TransferChecked { amount, decimals }; - let packed = check.pack(); - let expect = Vec::from([12u8, 1, 0, 0, 0, 0, 0, 0, 0, 2]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::TransferChecked); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.amount, amount.into()); - assert_eq!(pod.decimals, decimals); - } - - #[test] - fn test_approve_checked_packing() { - let amount = 1; - let decimals = 2; - - let check = TokenInstruction::ApproveChecked { amount, decimals }; - let packed = check.pack(); - let expect = Vec::from([13u8, 1, 0, 0, 0, 0, 0, 0, 0, 2]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::ApproveChecked); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.amount, amount.into()); - assert_eq!(pod.decimals, decimals); - } - - #[test] - fn test_mint_to_checked_packing() { - let amount = 1; - let decimals = 2; - let check = TokenInstruction::MintToChecked { amount, decimals }; - let packed = check.pack(); - let expect = Vec::from([14u8, 1, 0, 0, 0, 0, 0, 0, 0, 2]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::MintToChecked); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.amount, amount.into()); - assert_eq!(pod.decimals, decimals); - } - - #[test] - fn test_burn_checked_packing() { - let amount = 1; - let decimals = 2; - let check = TokenInstruction::BurnChecked { amount, decimals }; - let packed = check.pack(); - let expect = Vec::from([15u8, 1, 0, 0, 0, 0, 0, 0, 0, 2]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::BurnChecked); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.amount, amount.into()); - assert_eq!(pod.decimals, decimals); - } - - #[test] - fn test_initialize_account2_packing() { - let owner = Pubkey::new_from_array([2u8; 32]); - let check = TokenInstruction::InitializeAccount2 { owner }; - let packed = check.pack(); - let mut expect = vec![16u8]; - expect.extend_from_slice(&[2u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::InitializeAccount2); - let pod_owner = decode_instruction_data::(&packed).unwrap(); - assert_eq!(*pod_owner, owner); - } - - #[test] - fn test_sync_native_packing() { - let check = TokenInstruction::SyncNative; - let packed = check.pack(); - let expect = vec![17u8]; - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::SyncNative); - } - - #[test] - fn test_initialize_account3_packing() { - let owner = Pubkey::new_from_array([2u8; 32]); - let check = TokenInstruction::InitializeAccount3 { owner }; - let packed = check.pack(); - let mut expect = vec![18u8]; - expect.extend_from_slice(&[2u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::InitializeAccount3); - let pod_owner = decode_instruction_data::(&packed).unwrap(); - assert_eq!(*pod_owner, owner); - } - - #[test] - fn test_initialize_multisig2_packing() { - let m = 1; - let check = TokenInstruction::InitializeMultisig2 { m }; - let packed = check.pack(); - let expect = Vec::from([19u8, 1]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::InitializeMultisig2); - let pod = decode_instruction_data::(&packed).unwrap(); - assert_eq!(pod.m, m); - } - - #[test] - fn test_initialize_mint2_packing() { - let decimals = 2; - let mint_authority = Pubkey::new_from_array([1u8; 32]); - let freeze_authority = COption::None; - let check = TokenInstruction::InitializeMint2 { - decimals, - mint_authority, - freeze_authority, - }; - let packed = check.pack(); - let mut expect = Vec::from([20u8, 2]); - expect.extend_from_slice(&[1u8; 32]); - expect.extend_from_slice(&[0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::InitializeMint2); - let (pod, pod_freeze_authority) = - decode_instruction_data_with_coption_pubkey::(&packed).unwrap(); - assert_eq!(pod.decimals, decimals); - assert_eq!(pod.mint_authority, mint_authority); - assert_eq!(pod_freeze_authority, freeze_authority.into()); - - let decimals = 2; - let mint_authority = Pubkey::new_from_array([2u8; 32]); - let freeze_authority = COption::Some(Pubkey::new_from_array([3u8; 32])); - let check = TokenInstruction::InitializeMint2 { - decimals, - mint_authority, - freeze_authority, - }; - let packed = check.pack(); - let mut expect = vec![20u8, 2]; - expect.extend_from_slice(&[2u8; 32]); - expect.extend_from_slice(&[1]); - expect.extend_from_slice(&[3u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::InitializeMint2); - let (pod, pod_freeze_authority) = - decode_instruction_data_with_coption_pubkey::(&packed).unwrap(); - assert_eq!(pod.decimals, decimals); - assert_eq!(pod.mint_authority, mint_authority); - assert_eq!(pod_freeze_authority, freeze_authority.into()); - } - - #[test] - fn test_get_account_data_size_packing() { - let extension_types = vec![]; - let check = TokenInstruction::GetAccountDataSize { - extension_types: extension_types.clone(), - }; - let packed = check.pack(); - let expect = [21u8]; - assert_eq!(packed, &[21u8]); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::GetAccountDataSize); - let pod_extension_types = packed[1..] - .chunks(std::mem::size_of::()) - .map(ExtensionType::try_from) - .collect::, _>>() - .unwrap(); - assert_eq!(pod_extension_types, extension_types); - - let extension_types = vec![ - ExtensionType::TransferFeeConfig, - ExtensionType::TransferFeeAmount, - ]; - let check = TokenInstruction::GetAccountDataSize { - extension_types: extension_types.clone(), - }; - let packed = check.pack(); - let expect = [21u8, 1, 0, 2, 0]; - assert_eq!(packed, &[21u8, 1, 0, 2, 0]); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::GetAccountDataSize); - let pod_extension_types = packed[1..] - .chunks(std::mem::size_of::()) - .map(ExtensionType::try_from) - .collect::, _>>() - .unwrap(); - assert_eq!(pod_extension_types, extension_types); - } - - #[test] - fn test_amount_to_ui_amount_packing() { - let amount = 42; - let check = TokenInstruction::AmountToUiAmount { amount }; - let packed = check.pack(); - let expect = vec![23u8, 42, 0, 0, 0, 0, 0, 0, 0]; - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::AmountToUiAmount); - let data = decode_instruction_data::(&packed).unwrap(); - assert_eq!(data.amount, amount.into()); - } - - #[test] - fn test_ui_amount_to_amount_packing() { - let ui_amount = "0.42"; - let check = TokenInstruction::UiAmountToAmount { ui_amount }; - let packed = check.pack(); - let expect = vec![24u8, 48, 46, 52, 50]; - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::UiAmountToAmount); - let pod_ui_amount = std::str::from_utf8(&packed[1..]).unwrap(); - assert_eq!(pod_ui_amount, ui_amount); - } - - #[test] - fn test_initialize_mint_close_authority_packing() { - let close_authority = COption::Some(Pubkey::new_from_array([10u8; 32])); - let check = TokenInstruction::InitializeMintCloseAuthority { close_authority }; - let packed = check.pack(); - let mut expect = vec![25u8, 1]; - expect.extend_from_slice(&[10u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!( - instruction_type, - PodTokenInstruction::InitializeMintCloseAuthority - ); - let (_, pod_close_authority) = - decode_instruction_data_with_coption_pubkey::<()>(&packed).unwrap(); - assert_eq!(pod_close_authority, close_authority.into()); - } - - #[test] - fn test_create_native_mint_packing() { - let check = TokenInstruction::CreateNativeMint; - let packed = check.pack(); - let expect = vec![31u8]; - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!(instruction_type, PodTokenInstruction::CreateNativeMint); - } - - #[test] - fn test_initialize_permanent_delegate_packing() { - let delegate = Pubkey::new_from_array([11u8; 32]); - let check = TokenInstruction::InitializePermanentDelegate { delegate }; - let packed = check.pack(); - let mut expect = vec![35u8]; - expect.extend_from_slice(&[11u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let instruction_type = decode_instruction_type::(&packed).unwrap(); - assert_eq!( - instruction_type, - PodTokenInstruction::InitializePermanentDelegate - ); - let pod_delegate = decode_instruction_data::(&packed).unwrap(); - assert_eq!(*pod_delegate, delegate); - } - - macro_rules! test_instruction { - ($a:ident($($b:tt)*)) => { - let instruction_v3 = spl_token::instruction::$a($($b)*).unwrap(); - let instruction_2022 = $a($($b)*).unwrap(); - assert_eq!(instruction_v3, instruction_2022); - } - } - - #[test] - fn test_v3_compatibility() { - let token_program_id = spl_token::id(); - let mint_pubkey = Pubkey::new_unique(); - let mint_authority_pubkey = Pubkey::new_unique(); - let freeze_authority_pubkey = Pubkey::new_unique(); - let decimals = 9u8; - - let account_pubkey = Pubkey::new_unique(); - let owner_pubkey = Pubkey::new_unique(); - - let multisig_pubkey = Pubkey::new_unique(); - let signer_pubkeys_vec = vec![Pubkey::new_unique(); MAX_SIGNERS]; - let signer_pubkeys = signer_pubkeys_vec.iter().collect::>(); - let m = 10u8; - - let source_pubkey = Pubkey::new_unique(); - let destination_pubkey = Pubkey::new_unique(); - let authority_pubkey = Pubkey::new_unique(); - let amount = 1_000_000_000_000; - - let delegate_pubkey = Pubkey::new_unique(); - let owned_pubkey = Pubkey::new_unique(); - let new_authority_pubkey = Pubkey::new_unique(); - - let ui_amount = "100000.00"; - - test_instruction!(initialize_mint( - &token_program_id, - &mint_pubkey, - &mint_authority_pubkey, - None, - decimals, - )); - test_instruction!(initialize_mint2( - &token_program_id, - &mint_pubkey, - &mint_authority_pubkey, - Some(&freeze_authority_pubkey), - decimals, - )); - - test_instruction!(initialize_account( - &token_program_id, - &account_pubkey, - &mint_pubkey, - &owner_pubkey, - )); - test_instruction!(initialize_account2( - &token_program_id, - &account_pubkey, - &mint_pubkey, - &owner_pubkey, - )); - test_instruction!(initialize_account3( - &token_program_id, - &account_pubkey, - &mint_pubkey, - &owner_pubkey, - )); - test_instruction!(initialize_multisig( - &token_program_id, - &multisig_pubkey, - &signer_pubkeys, - m, - )); - test_instruction!(initialize_multisig2( - &token_program_id, - &multisig_pubkey, - &signer_pubkeys, - m, - )); - #[allow(deprecated)] - { - test_instruction!(transfer( - &token_program_id, - &source_pubkey, - &destination_pubkey, - &authority_pubkey, - &signer_pubkeys, - amount - )); - } - test_instruction!(transfer_checked( - &token_program_id, - &source_pubkey, - &mint_pubkey, - &destination_pubkey, - &authority_pubkey, - &signer_pubkeys, - amount, - decimals, - )); - test_instruction!(approve( - &token_program_id, - &source_pubkey, - &delegate_pubkey, - &owner_pubkey, - &signer_pubkeys, - amount - )); - test_instruction!(approve_checked( - &token_program_id, - &source_pubkey, - &mint_pubkey, - &delegate_pubkey, - &owner_pubkey, - &signer_pubkeys, - amount, - decimals - )); - test_instruction!(revoke( - &token_program_id, - &source_pubkey, - &owner_pubkey, - &signer_pubkeys, - )); - - // set_authority - { - let instruction_v3 = spl_token::instruction::set_authority( - &token_program_id, - &owned_pubkey, - Some(&new_authority_pubkey), - spl_token::instruction::AuthorityType::AccountOwner, - &owner_pubkey, - &signer_pubkeys, - ) - .unwrap(); - let instruction_2022 = set_authority( - &token_program_id, - &owned_pubkey, - Some(&new_authority_pubkey), - AuthorityType::AccountOwner, - &owner_pubkey, - &signer_pubkeys, - ) - .unwrap(); - assert_eq!(instruction_v3, instruction_2022); - } - - test_instruction!(mint_to( - &token_program_id, - &mint_pubkey, - &account_pubkey, - &owner_pubkey, - &signer_pubkeys, - amount, - )); - test_instruction!(mint_to_checked( - &token_program_id, - &mint_pubkey, - &account_pubkey, - &owner_pubkey, - &signer_pubkeys, - amount, - decimals, - )); - test_instruction!(burn( - &token_program_id, - &account_pubkey, - &mint_pubkey, - &authority_pubkey, - &signer_pubkeys, - amount, - )); - test_instruction!(burn_checked( - &token_program_id, - &account_pubkey, - &mint_pubkey, - &authority_pubkey, - &signer_pubkeys, - amount, - decimals, - )); - test_instruction!(close_account( - &token_program_id, - &account_pubkey, - &destination_pubkey, - &owner_pubkey, - &signer_pubkeys, - )); - test_instruction!(freeze_account( - &token_program_id, - &account_pubkey, - &mint_pubkey, - &owner_pubkey, - &signer_pubkeys, - )); - test_instruction!(thaw_account( - &token_program_id, - &account_pubkey, - &mint_pubkey, - &owner_pubkey, - &signer_pubkeys, - )); - test_instruction!(sync_native(&token_program_id, &account_pubkey,)); - - // get_account_data_size - { - let instruction_v3 = - spl_token::instruction::get_account_data_size(&token_program_id, &mint_pubkey) - .unwrap(); - let instruction_2022 = - get_account_data_size(&token_program_id, &mint_pubkey, &[]).unwrap(); - assert_eq!(instruction_v3, instruction_2022); - } - - test_instruction!(initialize_immutable_owner( - &token_program_id, - &account_pubkey, - )); - - test_instruction!(amount_to_ui_amount(&token_program_id, &mint_pubkey, amount,)); - - test_instruction!(ui_amount_to_amount( - &token_program_id, - &mint_pubkey, - ui_amount, - )); - } - - proptest! { - #![proptest_config(ProptestConfig::with_cases(1024))] - #[test] - fn test_instruction_unpack_proptest( - data in prop::collection::vec(any::(), 0..255) - ) { - let _no_panic = TokenInstruction::unpack(&data); - } - } -} diff --git a/token/program-2022/src/lib.rs b/token/program-2022/src/lib.rs deleted file mode 100644 index 93d2e058b6f..00000000000 --- a/token/program-2022/src/lib.rs +++ /dev/null @@ -1,167 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -//! An ERC20-like Token program for the Solana blockchain - -pub mod error; -pub mod extension; -pub mod generic_token_account; -pub mod instruction; -pub mod native_mint; -pub mod offchain; -pub mod onchain; -pub mod pod; -pub mod pod_instruction; -pub mod processor; -#[cfg(feature = "serde-traits")] -pub mod serialization; -pub mod state; - -#[cfg(not(feature = "no-entrypoint"))] -mod entrypoint; - -// Export current sdk types for downstream users building with a different sdk -// version -use { - error::TokenError, - solana_program::{ - entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey, system_program, - }, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalCiphertext, -}; -pub use {solana_program, solana_zk_sdk}; - -/// Convert the UI representation of a token amount (using the decimals field -/// defined in its mint) to the raw amount -pub fn ui_amount_to_amount(ui_amount: f64, decimals: u8) -> u64 { - (ui_amount * 10_usize.pow(decimals as u32) as f64) as u64 -} - -/// Convert a raw amount to its UI representation (using the decimals field -/// defined in its mint) -pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> f64 { - amount as f64 / 10_usize.pow(decimals as u32) as f64 -} - -/// Convert a raw amount to its UI representation (using the decimals field -/// defined in its mint) -pub fn amount_to_ui_amount_string(amount: u64, decimals: u8) -> String { - let decimals = decimals as usize; - if decimals > 0 { - // Left-pad zeros to decimals + 1, so we at least have an integer zero - let mut s = format!("{:01$}", amount, decimals + 1); - // Add the decimal point (Sorry, "," locales!) - s.insert(s.len() - decimals, '.'); - s - } else { - amount.to_string() - } -} - -/// Convert a raw amount to its UI representation using the given decimals field -/// Excess zeroes or unneeded decimal point are trimmed. -pub fn amount_to_ui_amount_string_trimmed(amount: u64, decimals: u8) -> String { - let s = amount_to_ui_amount_string(amount, decimals); - trim_ui_amount_string(s, decimals) -} - -/// Trims a string number by removing excess zeroes or unneeded decimal point -fn trim_ui_amount_string(mut ui_amount: String, decimals: u8) -> String { - if decimals > 0 { - let zeros_trimmed = ui_amount.trim_end_matches('0'); - ui_amount = zeros_trimmed.trim_end_matches('.').to_string(); - } - ui_amount -} - -/// Try to convert a UI representation of a token amount to its raw amount using -/// the given decimals field -pub fn try_ui_amount_into_amount(ui_amount: String, decimals: u8) -> Result { - let decimals = decimals as usize; - let mut parts = ui_amount.split('.'); - // splitting a string, even an empty one, will always yield an iterator of at - // least length == 1 - let mut amount_str = parts.next().unwrap().to_string(); - let after_decimal = parts.next().unwrap_or(""); - let after_decimal = after_decimal.trim_end_matches('0'); - if (amount_str.is_empty() && after_decimal.is_empty()) - || parts.next().is_some() - || after_decimal.len() > decimals - { - return Err(ProgramError::InvalidArgument); - } - - amount_str.push_str(after_decimal); - for _ in 0..decimals.saturating_sub(after_decimal.len()) { - amount_str.push('0'); - } - amount_str - .parse::() - .map_err(|_| ProgramError::InvalidArgument) -} - -solana_program::declare_id!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); - -/// Checks that the supplied program ID is correct for spl-token-2022 -pub fn check_program_account(spl_token_program_id: &Pubkey) -> ProgramResult { - if spl_token_program_id != &id() { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} - -/// Checks that the supplied program ID is correct for spl-token or -/// spl-token-2022 -pub fn check_spl_token_program_account(spl_token_program_id: &Pubkey) -> ProgramResult { - if spl_token_program_id != &id() && spl_token_program_id != &spl_token::id() { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} - -/// Checks that the supplied program ID is correct for the ZK ElGamal proof -/// program -pub fn check_zk_elgamal_proof_program_account( - zk_elgamal_proof_program_id: &Pubkey, -) -> ProgramResult { - if zk_elgamal_proof_program_id != &solana_zk_sdk::zk_elgamal_proof_program::id() { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} - -/// Checks if the supplied program ID is that of the system program -pub fn check_system_program_account(system_program_id: &Pubkey) -> ProgramResult { - if system_program_id != &system_program::id() { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} - -/// Checks if the supplied program ID is that of the ElGamal registry program -pub(crate) fn check_elgamal_registry_program_account( - elgamal_registry_account_program_id: &Pubkey, -) -> ProgramResult { - if elgamal_registry_account_program_id != &spl_elgamal_registry::id() { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} - -/// Check instruction data and proof data auditor ciphertext consistency -#[cfg(feature = "zk-ops")] -pub(crate) fn check_auditor_ciphertext( - instruction_data_auditor_ciphertext_lo: &PodElGamalCiphertext, - instruction_data_auditor_ciphertext_hi: &PodElGamalCiphertext, - proof_context_auditor_ciphertext_lo: &PodElGamalCiphertext, - proof_context_auditor_ciphertext_hi: &PodElGamalCiphertext, -) -> ProgramResult { - if instruction_data_auditor_ciphertext_lo != proof_context_auditor_ciphertext_lo { - return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); - } - if instruction_data_auditor_ciphertext_hi != proof_context_auditor_ciphertext_hi { - return Err(TokenError::ConfidentialTransferBalanceMismatch.into()); - } - Ok(()) -} diff --git a/token/program-2022/src/native_mint.rs b/token/program-2022/src/native_mint.rs deleted file mode 100644 index 490e029a3be..00000000000 --- a/token/program-2022/src/native_mint.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! The Mint that represents the native token - -/// There are `10^9` lamports in one SOL -pub const DECIMALS: u8 = 9; - -// The Mint for native SOL Token accounts -solana_program::declare_id!("9pan9bMn5HatX4EJdBwg9VgCa7Uz5HL8N1m5D3NdXejP"); - -/// Seed for the native mint's program-derived address -pub const PROGRAM_ADDRESS_SEEDS: &[&[u8]] = &["native-mint".as_bytes(), &[255]]; - -#[cfg(test)] -mod tests { - use { - super::*, - solana_program::{native_token::*, pubkey::Pubkey}, - }; - - #[test] - fn test_decimals() { - assert!( - (lamports_to_sol(42) - crate::amount_to_ui_amount(42, DECIMALS)).abs() < f64::EPSILON - ); - assert_eq!( - sol_to_lamports(42.), - crate::ui_amount_to_amount(42., DECIMALS) - ); - } - - #[test] - fn expected_native_mint_id() { - let native_mint_id = - Pubkey::create_program_address(PROGRAM_ADDRESS_SEEDS, &crate::id()).unwrap(); - assert_eq!(id(), native_mint_id); - } -} diff --git a/token/program-2022/src/offchain.rs b/token/program-2022/src/offchain.rs deleted file mode 100644 index eda4d658620..00000000000 --- a/token/program-2022/src/offchain.rs +++ /dev/null @@ -1,486 +0,0 @@ -//! Offchain helper for fetching required accounts to build instructions - -pub use spl_transfer_hook_interface::offchain::{AccountDataResult, AccountFetchError}; -use { - crate::{ - extension::{transfer_fee, transfer_hook, StateWithExtensions}, - state::Mint, - }, - solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey}, - spl_transfer_hook_interface::offchain::add_extra_account_metas_for_execute, - std::future::Future, -}; - -/// Offchain helper to create a `TransferChecked` instruction with all -/// additional required account metas for a transfer, including the ones -/// required by the transfer hook. -/// -/// To be client-agnostic and to avoid pulling in the full solana-sdk, this -/// simply takes a function that will return its data as `Future>` for -/// the given address. Can be called in the following way: -/// -/// ```rust,ignore -/// let instruction = create_transfer_checked_instruction_with_extra_metas( -/// &spl_token_2022::id(), -/// &source, -/// &mint, -/// &destination, -/// &authority, -/// &[], -/// amount, -/// decimals, -/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)), -/// ) -/// .await? -/// ``` -#[allow(clippy::too_many_arguments)] -pub async fn create_transfer_checked_instruction_with_extra_metas( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, - fetch_account_data_fn: F, -) -> Result -where - F: Fn(Pubkey) -> Fut, - Fut: Future, -{ - let mut transfer_instruction = crate::instruction::transfer_checked( - token_program_id, - source_pubkey, - mint_pubkey, - destination_pubkey, - authority_pubkey, - signer_pubkeys, - amount, - decimals, - )?; - - add_extra_account_metas( - &mut transfer_instruction, - source_pubkey, - mint_pubkey, - destination_pubkey, - authority_pubkey, - amount, - fetch_account_data_fn, - ) - .await?; - - Ok(transfer_instruction) -} - -/// Offchain helper to create a `TransferCheckedWithFee` instruction with all -/// additional required account metas for a transfer, including the ones -/// required by the transfer hook. -/// -/// To be client-agnostic and to avoid pulling in the full solana-sdk, this -/// simply takes a function that will return its data as `Future>` for -/// the given address. Can be called in the following way: -/// -/// ```rust,ignore -/// let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas( -/// &spl_token_2022::id(), -/// &source, -/// &mint, -/// &destination, -/// &authority, -/// &[], -/// amount, -/// decimals, -/// fee, -/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)), -/// ) -/// .await? -/// ``` -#[allow(clippy::too_many_arguments)] -pub async fn create_transfer_checked_with_fee_instruction_with_extra_metas( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, - fee: u64, - fetch_account_data_fn: F, -) -> Result -where - F: Fn(Pubkey) -> Fut, - Fut: Future, -{ - let mut transfer_instruction = transfer_fee::instruction::transfer_checked_with_fee( - token_program_id, - source_pubkey, - mint_pubkey, - destination_pubkey, - authority_pubkey, - signer_pubkeys, - amount, - decimals, - fee, - )?; - - add_extra_account_metas( - &mut transfer_instruction, - source_pubkey, - mint_pubkey, - destination_pubkey, - authority_pubkey, - amount, - fetch_account_data_fn, - ) - .await?; - - Ok(transfer_instruction) -} - -/// Offchain helper to add required account metas to an instruction, including -/// the ones required by the transfer hook. -/// -/// To be client-agnostic and to avoid pulling in the full solana-sdk, this -/// simply takes a function that will return its data as `Future>` for -/// the given address. Can be called in the following way: -/// -/// ```rust,ignore -/// let mut transfer_instruction = spl_token_2022::instruction::transfer_checked( -/// &spl_token_2022::id(), -/// source_pubkey, -/// mint_pubkey, -/// destination_pubkey, -/// authority_pubkey, -/// signer_pubkeys, -/// amount, -/// decimals, -/// )?; -/// add_extra_account_metas( -/// &mut transfer_instruction, -/// source_pubkey, -/// mint_pubkey, -/// destination_pubkey, -/// authority_pubkey, -/// amount, -/// fetch_account_data_fn, -/// ).await?; -/// ``` -pub async fn add_extra_account_metas( - instruction: &mut Instruction, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - amount: u64, - fetch_account_data_fn: F, -) -> Result<(), AccountFetchError> -where - F: Fn(Pubkey) -> Fut, - Fut: Future, -{ - let mint_data = fetch_account_data_fn(*mint_pubkey) - .await? - .ok_or(ProgramError::InvalidAccountData)?; - let mint = StateWithExtensions::::unpack(&mint_data)?; - - if let Some(program_id) = transfer_hook::get_program_id(&mint) { - add_extra_account_metas_for_execute( - instruction, - &program_id, - source_pubkey, - mint_pubkey, - destination_pubkey, - authority_pubkey, - amount, - fetch_account_data_fn, - ) - .await?; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::extension::{ - transfer_hook::TransferHook, BaseStateWithExtensionsMut, ExtensionType, - StateWithExtensionsMut, - }, - solana_program::{instruction::AccountMeta, program_option::COption}, - solana_program_test::tokio, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_tlv_account_resolution::{ - account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, - }, - spl_transfer_hook_interface::{ - get_extra_account_metas_address, instruction::ExecuteInstruction, - }, - }; - - const DECIMALS: u8 = 0; - const MINT_PUBKEY: Pubkey = Pubkey::new_from_array([1u8; 32]); - const TRANSFER_HOOK_PROGRAM_ID: Pubkey = Pubkey::new_from_array([2u8; 32]); - const EXTRA_META_1: Pubkey = Pubkey::new_from_array([3u8; 32]); - const EXTRA_META_2: Pubkey = Pubkey::new_from_array([4u8; 32]); - - // Mock to return the mint data or the validation state account data - async fn mock_fetch_account_data_fn(address: Pubkey) -> AccountDataResult { - if address == MINT_PUBKEY { - let mint_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferHook]) - .unwrap(); - let mut data = vec![0u8; mint_len]; - let mut mint = StateWithExtensionsMut::::unpack_uninitialized(&mut data).unwrap(); - - let extension = mint.init_extension::(true).unwrap(); - extension.program_id = - OptionalNonZeroPubkey::try_from(Some(TRANSFER_HOOK_PROGRAM_ID)).unwrap(); - - mint.base.mint_authority = COption::Some(Pubkey::new_unique()); - mint.base.decimals = DECIMALS; - mint.base.is_initialized = true; - mint.base.freeze_authority = COption::None; - mint.pack_base(); - mint.init_account_type().unwrap(); - - Ok(Some(data)) - } else if address - == get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID) - { - let extra_metas = vec![ - ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::AccountKey { index: 0 }, // source - Seed::AccountKey { index: 2 }, // destination - Seed::AccountKey { index: 4 }, // validation state - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 8, - length: 8, - }, // amount - Seed::AccountKey { index: 2 }, // destination - Seed::AccountKey { index: 5 }, // extra meta 1 - Seed::AccountKey { index: 7 }, // extra meta 3 (PDA) - ], - false, - true, - ) - .unwrap(), - ]; - let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap(); - let mut data = vec![0u8; account_size]; - ExtraAccountMetaList::init::(&mut data, &extra_metas)?; - Ok(Some(data)) - } else { - Ok(None) - } - } - - #[tokio::test] - async fn test_create_transfer_checked_instruction_with_extra_metas() { - let source = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let authority = Pubkey::new_unique(); - let amount = 100u64; - - let validate_state_pubkey = - get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID); - let extra_meta_3_pubkey = Pubkey::find_program_address( - &[ - source.as_ref(), - destination.as_ref(), - validate_state_pubkey.as_ref(), - ], - &TRANSFER_HOOK_PROGRAM_ID, - ) - .0; - let extra_meta_4_pubkey = Pubkey::find_program_address( - &[ - amount.to_le_bytes().as_ref(), - destination.as_ref(), - EXTRA_META_1.as_ref(), - extra_meta_3_pubkey.as_ref(), - ], - &TRANSFER_HOOK_PROGRAM_ID, - ) - .0; - - let instruction = create_transfer_checked_instruction_with_extra_metas( - &crate::id(), - &source, - &MINT_PUBKEY, - &destination, - &authority, - &[], - amount, - DECIMALS, - mock_fetch_account_data_fn, - ) - .await - .unwrap(); - - let check_metas = [ - AccountMeta::new(source, false), - AccountMeta::new_readonly(MINT_PUBKEY, false), - AccountMeta::new(destination, false), - AccountMeta::new_readonly(authority, true), - AccountMeta::new_readonly(EXTRA_META_1, true), - AccountMeta::new_readonly(EXTRA_META_2, true), - AccountMeta::new(extra_meta_3_pubkey, false), - AccountMeta::new(extra_meta_4_pubkey, false), - AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), - AccountMeta::new_readonly(validate_state_pubkey, false), - ]; - - assert_eq!(instruction.accounts, check_metas); - - // With additional signers - let signer_1 = Pubkey::new_unique(); - let signer_2 = Pubkey::new_unique(); - let signer_3 = Pubkey::new_unique(); - - let instruction = create_transfer_checked_instruction_with_extra_metas( - &crate::id(), - &source, - &MINT_PUBKEY, - &destination, - &authority, - &[&signer_1, &signer_2, &signer_3], - amount, - DECIMALS, - mock_fetch_account_data_fn, - ) - .await - .unwrap(); - - let check_metas = [ - AccountMeta::new(source, false), - AccountMeta::new_readonly(MINT_PUBKEY, false), - AccountMeta::new(destination, false), - AccountMeta::new_readonly(authority, false), // False because of additional signers - AccountMeta::new_readonly(signer_1, true), - AccountMeta::new_readonly(signer_2, true), - AccountMeta::new_readonly(signer_3, true), - AccountMeta::new_readonly(EXTRA_META_1, true), - AccountMeta::new_readonly(EXTRA_META_2, true), - AccountMeta::new(extra_meta_3_pubkey, false), - AccountMeta::new(extra_meta_4_pubkey, false), - AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), - AccountMeta::new_readonly(validate_state_pubkey, false), - ]; - - assert_eq!(instruction.accounts, check_metas); - } - - #[tokio::test] - async fn test_create_transfer_checked_with_fee_instruction_with_extra_metas() { - let source = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let authority = Pubkey::new_unique(); - let amount = 100u64; - let fee = 1u64; - - let validate_state_pubkey = - get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID); - let extra_meta_3_pubkey = Pubkey::find_program_address( - &[ - source.as_ref(), - destination.as_ref(), - validate_state_pubkey.as_ref(), - ], - &TRANSFER_HOOK_PROGRAM_ID, - ) - .0; - let extra_meta_4_pubkey = Pubkey::find_program_address( - &[ - amount.to_le_bytes().as_ref(), - destination.as_ref(), - EXTRA_META_1.as_ref(), - extra_meta_3_pubkey.as_ref(), - ], - &TRANSFER_HOOK_PROGRAM_ID, - ) - .0; - - let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas( - &crate::id(), - &source, - &MINT_PUBKEY, - &destination, - &authority, - &[], - amount, - DECIMALS, - fee, - mock_fetch_account_data_fn, - ) - .await - .unwrap(); - - let check_metas = [ - AccountMeta::new(source, false), - AccountMeta::new_readonly(MINT_PUBKEY, false), - AccountMeta::new(destination, false), - AccountMeta::new_readonly(authority, true), - AccountMeta::new_readonly(EXTRA_META_1, true), - AccountMeta::new_readonly(EXTRA_META_2, true), - AccountMeta::new(extra_meta_3_pubkey, false), - AccountMeta::new(extra_meta_4_pubkey, false), - AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), - AccountMeta::new_readonly(validate_state_pubkey, false), - ]; - - assert_eq!(instruction.accounts, check_metas); - - // With additional signers - let signer_1 = Pubkey::new_unique(); - let signer_2 = Pubkey::new_unique(); - let signer_3 = Pubkey::new_unique(); - - let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas( - &crate::id(), - &source, - &MINT_PUBKEY, - &destination, - &authority, - &[&signer_1, &signer_2, &signer_3], - amount, - DECIMALS, - fee, - mock_fetch_account_data_fn, - ) - .await - .unwrap(); - - let check_metas = [ - AccountMeta::new(source, false), - AccountMeta::new_readonly(MINT_PUBKEY, false), - AccountMeta::new(destination, false), - AccountMeta::new_readonly(authority, false), // False because of additional signers - AccountMeta::new_readonly(signer_1, true), - AccountMeta::new_readonly(signer_2, true), - AccountMeta::new_readonly(signer_3, true), - AccountMeta::new_readonly(EXTRA_META_1, true), - AccountMeta::new_readonly(EXTRA_META_2, true), - AccountMeta::new(extra_meta_3_pubkey, false), - AccountMeta::new(extra_meta_4_pubkey, false), - AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false), - AccountMeta::new_readonly(validate_state_pubkey, false), - ]; - - assert_eq!(instruction.accounts, check_metas); - } -} diff --git a/token/program-2022/src/onchain.rs b/token/program-2022/src/onchain.rs deleted file mode 100644 index 874822f00fc..00000000000 --- a/token/program-2022/src/onchain.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! On-chain program invoke helper to perform on-chain `transfer_checked` with -//! correct accounts - -use { - crate::{ - extension::{transfer_fee, transfer_hook, StateWithExtensions}, - instruction, - state::Mint, - }, - solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, instruction::AccountMeta, - program::invoke_signed, pubkey::Pubkey, - }, - spl_transfer_hook_interface::onchain::add_extra_accounts_for_execute_cpi, -}; - -/// Helper to CPI into token-2022 on-chain, looking through the additional -/// account infos to create the proper instruction with the proper account infos -#[allow(clippy::too_many_arguments)] -pub fn invoke_transfer_checked<'a>( - token_program_id: &Pubkey, - source_info: AccountInfo<'a>, - mint_info: AccountInfo<'a>, - destination_info: AccountInfo<'a>, - authority_info: AccountInfo<'a>, - additional_accounts: &[AccountInfo<'a>], - amount: u64, - decimals: u8, - seeds: &[&[&[u8]]], -) -> ProgramResult { - let mut cpi_instruction = instruction::transfer_checked( - token_program_id, - source_info.key, - mint_info.key, - destination_info.key, - authority_info.key, - &[], // add them later, to avoid unnecessary clones - amount, - decimals, - )?; - - let mut cpi_account_infos = vec![ - source_info.clone(), - mint_info.clone(), - destination_info.clone(), - authority_info.clone(), - ]; - - // if it's a signer, it might be a multisig signer, throw it in! - additional_accounts - .iter() - .filter(|ai| ai.is_signer) - .for_each(|ai| { - cpi_account_infos.push(ai.clone()); - cpi_instruction - .accounts - .push(AccountMeta::new_readonly(*ai.key, ai.is_signer)); - }); - - // scope the borrowing to avoid a double-borrow during CPI - { - let mint_data = mint_info.try_borrow_data()?; - let mint = StateWithExtensions::::unpack(&mint_data)?; - if let Some(program_id) = transfer_hook::get_program_id(&mint) { - add_extra_accounts_for_execute_cpi( - &mut cpi_instruction, - &mut cpi_account_infos, - &program_id, - source_info, - mint_info.clone(), - destination_info, - authority_info, - amount, - additional_accounts, - )?; - } - } - - invoke_signed(&cpi_instruction, &cpi_account_infos, seeds) -} - -/// Helper to CPI into token-2022 on-chain, looking through the additional -/// account infos to create the proper instruction with the fee -/// and proper account infos -#[allow(clippy::too_many_arguments)] -pub fn invoke_transfer_checked_with_fee<'a>( - token_program_id: &Pubkey, - source_info: AccountInfo<'a>, - mint_info: AccountInfo<'a>, - destination_info: AccountInfo<'a>, - authority_info: AccountInfo<'a>, - additional_accounts: &[AccountInfo<'a>], - amount: u64, - decimals: u8, - fee: u64, - seeds: &[&[&[u8]]], -) -> ProgramResult { - let mut cpi_instruction = transfer_fee::instruction::transfer_checked_with_fee( - token_program_id, - source_info.key, - mint_info.key, - destination_info.key, - authority_info.key, - &[], // add them later, to avoid unnecessary clones - amount, - decimals, - fee, - )?; - - let mut cpi_account_infos = vec![ - source_info.clone(), - mint_info.clone(), - destination_info.clone(), - authority_info.clone(), - ]; - - // if it's a signer, it might be a multisig signer, throw it in! - additional_accounts - .iter() - .filter(|ai| ai.is_signer) - .for_each(|ai| { - cpi_account_infos.push(ai.clone()); - cpi_instruction - .accounts - .push(AccountMeta::new_readonly(*ai.key, ai.is_signer)); - }); - - // scope the borrowing to avoid a double-borrow during CPI - { - let mint_data = mint_info.try_borrow_data()?; - let mint = StateWithExtensions::::unpack(&mint_data)?; - if let Some(program_id) = transfer_hook::get_program_id(&mint) { - add_extra_accounts_for_execute_cpi( - &mut cpi_instruction, - &mut cpi_account_infos, - &program_id, - source_info, - mint_info.clone(), - destination_info, - authority_info, - amount, - additional_accounts, - )?; - } - } - - invoke_signed(&cpi_instruction, &cpi_account_infos, seeds) -} diff --git a/token/program-2022/src/pod.rs b/token/program-2022/src/pod.rs deleted file mode 100644 index a6e68ded0f3..00000000000 --- a/token/program-2022/src/pod.rs +++ /dev/null @@ -1,315 +0,0 @@ -//! Rewrites of the base state types represented as Pods - -#[cfg(test)] -use crate::state::{Account, Mint, Multisig}; -use { - crate::{ - instruction::MAX_SIGNERS, - state::{AccountState, PackedSizeOf}, - }, - bytemuck::{Pod, Zeroable}, - solana_program::{ - program_error::ProgramError, program_option::COption, program_pack::IsInitialized, - pubkey::Pubkey, - }, - spl_pod::{ - bytemuck::pod_get_packed_len, - optional_keys::OptionalNonZeroPubkey, - primitives::{PodBool, PodU64}, - }, -}; - -/// [Mint] data stored as a Pod type -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct PodMint { - /// Optional authority used to mint new tokens. The mint authority may only - /// be provided during mint creation. If no mint authority is present - /// then the mint has a fixed supply and no further tokens may be - /// minted. - pub mint_authority: PodCOption, - /// Total supply of tokens. - pub supply: PodU64, - /// Number of base 10 digits to the right of the decimal place. - pub decimals: u8, - /// If `true`, this structure has been initialized - pub is_initialized: PodBool, - /// Optional authority to freeze token accounts. - pub freeze_authority: PodCOption, -} -impl IsInitialized for PodMint { - fn is_initialized(&self) -> bool { - self.is_initialized.into() - } -} -impl PackedSizeOf for PodMint { - const SIZE_OF: usize = pod_get_packed_len::(); -} -#[cfg(test)] -impl From for PodMint { - fn from(mint: Mint) -> Self { - Self { - mint_authority: mint.mint_authority.into(), - supply: mint.supply.into(), - decimals: mint.decimals, - is_initialized: mint.is_initialized.into(), - freeze_authority: mint.freeze_authority.into(), - } - } -} - -/// [Account] data stored as a Pod type -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct PodAccount { - /// The mint associated with this account - pub mint: Pubkey, - /// The owner of this account. - pub owner: Pubkey, - /// The amount of tokens this account holds. - pub amount: PodU64, - /// If `delegate` is `Some` then `delegated_amount` represents - /// the amount authorized by the delegate - pub delegate: PodCOption, - /// The account's [`AccountState`], stored as a `u8` - pub state: u8, - /// If `is_some`, this is a native token, and the value logs the rent-exempt - /// reserve. An Account is required to be rent-exempt, so the value is - /// used by the Processor to ensure that wrapped SOL accounts do not - /// drop below this threshold. - pub is_native: PodCOption, - /// The amount delegated - pub delegated_amount: PodU64, - /// Optional authority to close the account. - pub close_authority: PodCOption, -} -impl PodAccount { - /// Checks if account is frozen - pub fn is_frozen(&self) -> bool { - self.state == AccountState::Frozen as u8 - } - /// Checks if account is native - pub fn is_native(&self) -> bool { - self.is_native.is_some() - } - /// Checks if a token Account's owner is the `system_program` or the - /// incinerator - pub fn is_owned_by_system_program_or_incinerator(&self) -> bool { - solana_program::system_program::check_id(&self.owner) - || solana_program::incinerator::check_id(&self.owner) - } -} -impl IsInitialized for PodAccount { - fn is_initialized(&self) -> bool { - self.state == AccountState::Initialized as u8 || self.state == AccountState::Frozen as u8 - } -} -impl PackedSizeOf for PodAccount { - const SIZE_OF: usize = pod_get_packed_len::(); -} -#[cfg(test)] -impl From for PodAccount { - fn from(account: Account) -> Self { - Self { - mint: account.mint, - owner: account.owner, - amount: account.amount.into(), - delegate: account.delegate.into(), - state: account.state.into(), - is_native: account.is_native.map(PodU64::from_primitive).into(), - delegated_amount: account.delegated_amount.into(), - close_authority: account.close_authority.into(), - } - } -} - -/// [Multisig] data stored as a Pod type -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct PodMultisig { - /// Number of signers required - pub m: u8, - /// Number of valid signers - pub n: u8, - /// If `true`, this structure has been initialized - pub is_initialized: PodBool, - /// Signer public keys - pub signers: [Pubkey; MAX_SIGNERS], -} -impl IsInitialized for PodMultisig { - fn is_initialized(&self) -> bool { - self.is_initialized.into() - } -} -impl PackedSizeOf for PodMultisig { - const SIZE_OF: usize = pod_get_packed_len::(); -} -#[cfg(test)] -impl From for PodMultisig { - fn from(multisig: Multisig) -> Self { - Self { - m: multisig.m, - n: multisig.n, - is_initialized: multisig.is_initialized.into(), - signers: multisig.signers, - } - } -} - -/// `COption` stored as a Pod type -#[repr(C, packed)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub struct PodCOption -where - T: Pod + Default, -{ - pub(crate) option: [u8; 4], - pub(crate) value: T, -} -impl PodCOption -where - T: Pod + Default, -{ - /// Represents that no value is stored in the option, like `Option::None` - pub const NONE: [u8; 4] = [0; 4]; - /// Represents that some value is stored in the option, like - /// `Option::Some(v)` - pub const SOME: [u8; 4] = [1, 0, 0, 0]; - - /// Create a `PodCOption` equivalent of `Option::None` - /// - /// This could be made `const` by using `std::mem::zeroed`, but that would - /// require `unsafe` code, which is prohibited at the crate level. - pub fn none() -> Self { - Self { - option: Self::NONE, - value: T::default(), - } - } - - /// Create a `PodCOption` equivalent of `Option::Some(value)` - pub const fn some(value: T) -> Self { - Self { - option: Self::SOME, - value, - } - } - - /// Get the underlying value or another provided value if it isn't set, - /// equivalent of `Option::unwrap_or` - pub fn unwrap_or(self, default: T) -> T { - if self.option == Self::NONE { - default - } else { - self.value - } - } - - /// Checks to see if a value is set, equivalent of `Option::is_some` - pub fn is_some(&self) -> bool { - self.option == Self::SOME - } - - /// Checks to see if no value is set, equivalent of `Option::is_none` - pub fn is_none(&self) -> bool { - self.option == Self::NONE - } - - /// Converts the option into a Result, similar to `Option::ok_or` - pub fn ok_or(self, error: E) -> Result { - match self { - Self { - option: Self::SOME, - value, - } => Ok(value), - _ => Err(error), - } - } -} -impl From> for PodCOption { - fn from(opt: COption) -> Self { - match opt { - COption::None => Self { - option: Self::NONE, - value: T::default(), - }, - COption::Some(v) => Self { - option: Self::SOME, - value: v, - }, - } - } -} -impl TryFrom> for OptionalNonZeroPubkey { - type Error = ProgramError; - fn try_from(p: PodCOption) -> Result { - match p { - PodCOption { - option: PodCOption::::SOME, - value, - } if value == Pubkey::default() => Err(ProgramError::InvalidArgument), - PodCOption { - option: PodCOption::::SOME, - value, - } => Ok(Self(value)), - PodCOption { - option: PodCOption::::NONE, - value: _, - } => Ok(Self(Pubkey::default())), - _ => unreachable!(), - } - } -} - -#[cfg(test)] -pub mod test { - use { - super::*, - crate::state::{ - test::{ - TEST_ACCOUNT, TEST_ACCOUNT_SLICE, TEST_MINT, TEST_MINT_SLICE, TEST_MULTISIG, - TEST_MULTISIG_SLICE, - }, - AccountState, - }, - spl_pod::bytemuck::pod_from_bytes, - }; - - pub const TEST_POD_MINT: PodMint = PodMint { - mint_authority: PodCOption::some(Pubkey::new_from_array([1; 32])), - supply: PodU64::from_primitive(42), - decimals: 7, - is_initialized: PodBool::from_bool(true), - freeze_authority: PodCOption::some(Pubkey::new_from_array([2; 32])), - }; - pub const TEST_POD_ACCOUNT: PodAccount = PodAccount { - mint: Pubkey::new_from_array([1; 32]), - owner: Pubkey::new_from_array([2; 32]), - amount: PodU64::from_primitive(3), - delegate: PodCOption::some(Pubkey::new_from_array([4; 32])), - state: AccountState::Frozen as u8, - is_native: PodCOption::some(PodU64::from_primitive(5)), - delegated_amount: PodU64::from_primitive(6), - close_authority: PodCOption::some(Pubkey::new_from_array([7; 32])), - }; - - #[test] - fn pod_mint_to_mint_equality() { - let pod_mint = pod_from_bytes::(TEST_MINT_SLICE).unwrap(); - assert_eq!(*pod_mint, PodMint::from(TEST_MINT)); - assert_eq!(*pod_mint, TEST_POD_MINT); - } - - #[test] - fn pod_account_to_account_equality() { - let pod_account = pod_from_bytes::(TEST_ACCOUNT_SLICE).unwrap(); - assert_eq!(*pod_account, PodAccount::from(TEST_ACCOUNT)); - assert_eq!(*pod_account, TEST_POD_ACCOUNT); - } - - #[test] - fn pod_multisig_to_multisig_equality() { - let pod_multisig = pod_from_bytes::(TEST_MULTISIG_SLICE).unwrap(); - assert_eq!(*pod_multisig, PodMultisig::from(TEST_MULTISIG)); - } -} diff --git a/token/program-2022/src/pod_instruction.rs b/token/program-2022/src/pod_instruction.rs deleted file mode 100644 index 4222e1972ba..00000000000 --- a/token/program-2022/src/pod_instruction.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! Rewrites of the instruction data types represented as Pods - -use { - crate::pod::PodCOption, - bytemuck::{Pod, Zeroable}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - program_error::ProgramError, - pubkey::{Pubkey, PUBKEY_BYTES}, - }, - spl_pod::{ - bytemuck::{pod_from_bytes, pod_get_packed_len}, - primitives::PodU64, - }, -}; - -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub(crate) struct InitializeMintData { - /// Number of base 10 digits to the right of the decimal place. - pub(crate) decimals: u8, - /// The authority/multisignature to mint tokens. - pub(crate) mint_authority: Pubkey, - // The freeze authority option comes later, but cannot be included as - // plain old data in this struct -} -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub(crate) struct InitializeMultisigData { - /// The number of signers (M) required to validate this multisignature - /// account. - pub(crate) m: u8, -} -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub(crate) struct AmountData { - /// The amount of tokens to transfer. - pub(crate) amount: PodU64, -} -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub(crate) struct AmountCheckedData { - /// The amount of tokens to transfer. - pub(crate) amount: PodU64, - /// Decimals of the mint - pub(crate) decimals: u8, -} -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -pub(crate) struct SetAuthorityData { - /// The type of authority to update. - pub(crate) authority_type: u8, - // The new authority option comes later, but cannot be included as - // plain old data in this struct -} - -/// All of the base instructions in Token-2022, reduced down to their one-byte -/// discriminant. -/// -/// All instructions that expect data afterwards include a comment with the data -/// type expected. For example, `PodTokenInstruction::InitializeMint` expects -/// `InitializeMintData`. -#[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] -#[repr(u8)] -pub(crate) enum PodTokenInstruction { - // 0 - InitializeMint, // InitializeMintData - InitializeAccount, - InitializeMultisig, // InitializeMultisigData - Transfer, // AmountData - Approve, // AmountData - // 5 - Revoke, - SetAuthority, // SetAuthorityData - MintTo, // AmountData - Burn, // AmountData - CloseAccount, - // 10 - FreezeAccount, - ThawAccount, - TransferChecked, // AmountCheckedData - ApproveChecked, // AmountCheckedData - MintToChecked, // AmountCheckedData - // 15 - BurnChecked, // AmountCheckedData - InitializeAccount2, // Pubkey - SyncNative, - InitializeAccount3, // Pubkey - InitializeMultisig2, // InitializeMultisigData - // 20 - InitializeMint2, // InitializeMintData - GetAccountDataSize, // &[ExtensionType] - InitializeImmutableOwner, - AmountToUiAmount, // AmountData - UiAmountToAmount, // &str - // 25 - InitializeMintCloseAuthority, // COption - TransferFeeExtension, - ConfidentialTransferExtension, - DefaultAccountStateExtension, - Reallocate, // &[ExtensionType] - // 30 - MemoTransferExtension, - CreateNativeMint, - InitializeNonTransferableMint, - InterestBearingMintExtension, - CpiGuardExtension, - // 35 - InitializePermanentDelegate, // Pubkey - TransferHookExtension, - ConfidentialTransferFeeExtension, - WithdrawExcessLamports, - MetadataPointerExtension, - // 40 - GroupPointerExtension, - GroupMemberPointerExtension, - ConfidentialMintBurnExtension, - ScaledUiAmountExtension, - PausableExtension, -} - -fn unpack_pubkey_option(input: &[u8]) -> Result, ProgramError> { - match input.split_first() { - Option::Some((&0, _)) => Ok(PodCOption::none()), - Option::Some((&1, rest)) => { - let pk = rest - .get(..PUBKEY_BYTES) - .and_then(|x| Pubkey::try_from(x).ok()) - .ok_or(ProgramError::InvalidInstructionData)?; - Ok(PodCOption::some(pk)) - } - _ => Err(ProgramError::InvalidInstructionData), - } -} - -/// Specialty function for deserializing `Pod` data and a `COption` -/// -/// `COption` is not `Pod` compatible when serialized in an instruction, but -/// since it is always at the end of an instruction, so we can do this safely -pub(crate) fn decode_instruction_data_with_coption_pubkey( - input_with_type: &[u8], -) -> Result<(&T, PodCOption), ProgramError> { - let end_of_t = pod_get_packed_len::().saturating_add(1); - let value = input_with_type - .get(1..end_of_t) - .ok_or(ProgramError::InvalidInstructionData) - .and_then(pod_from_bytes)?; - let pubkey = unpack_pubkey_option(&input_with_type[end_of_t..])?; - Ok((value, pubkey)) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - extension::ExtensionType, - instruction::{decode_instruction_data, decode_instruction_type}, - }, - proptest::prelude::*, - }; - - // Test function that mimics the "unpacking" in `Processor::process` by - // trying to deserialize the relevant type data after the instruction type - fn check_pod_instruction(input: &[u8]) -> Result<(), ProgramError> { - if let Ok(instruction_type) = decode_instruction_type(input) { - match instruction_type { - PodTokenInstruction::InitializeMint | PodTokenInstruction::InitializeMint2 => { - let _ = - decode_instruction_data_with_coption_pubkey::(input)?; - } - PodTokenInstruction::InitializeAccount2 - | PodTokenInstruction::InitializeAccount3 - | PodTokenInstruction::InitializePermanentDelegate => { - let _ = decode_instruction_data::(input)?; - } - PodTokenInstruction::InitializeMultisig - | PodTokenInstruction::InitializeMultisig2 => { - let _ = decode_instruction_data::(input)?; - } - PodTokenInstruction::SetAuthority => { - let _ = decode_instruction_data_with_coption_pubkey::(input)?; - } - PodTokenInstruction::Transfer - | PodTokenInstruction::Approve - | PodTokenInstruction::MintTo - | PodTokenInstruction::Burn - | PodTokenInstruction::AmountToUiAmount => { - let _ = decode_instruction_data::(input)?; - } - PodTokenInstruction::TransferChecked - | PodTokenInstruction::ApproveChecked - | PodTokenInstruction::MintToChecked - | PodTokenInstruction::BurnChecked => { - let _ = decode_instruction_data::(input)?; - } - PodTokenInstruction::InitializeMintCloseAuthority => { - let _ = decode_instruction_data_with_coption_pubkey::<()>(input)?; - } - PodTokenInstruction::UiAmountToAmount => { - let _ = std::str::from_utf8(&input[1..]) - .map_err(|_| ProgramError::InvalidInstructionData)?; - } - PodTokenInstruction::GetAccountDataSize | PodTokenInstruction::Reallocate => { - let _ = input[1..] - .chunks(std::mem::size_of::()) - .map(ExtensionType::try_from) - .collect::, _>>()?; - } - _ => { - // no extra data to deserialize - } - } - } - Ok(()) - } - - proptest! { - #![proptest_config(ProptestConfig::with_cases(1024))] - #[test] - fn test_instruction_unpack_proptest( - data in prop::collection::vec(any::(), 0..255) - ) { - let _no_panic = check_pod_instruction(&data); - } - } -} diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs deleted file mode 100644 index 908d50a0547..00000000000 --- a/token/program-2022/src/processor.rs +++ /dev/null @@ -1,8295 +0,0 @@ -//! Program state processor - -use { - crate::{ - check_program_account, - error::TokenError, - extension::{ - confidential_mint_burn::{self, ConfidentialMintBurn}, - confidential_transfer::{self, ConfidentialTransferAccount, ConfidentialTransferMint}, - confidential_transfer_fee::{ - self, ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, - }, - cpi_guard::{self, in_cpi, CpiGuard}, - default_account_state::{self, DefaultAccountState}, - group_member_pointer::{self, GroupMemberPointer}, - group_pointer::{self, GroupPointer}, - immutable_owner::ImmutableOwner, - interest_bearing_mint::{self, InterestBearingConfig}, - memo_transfer::{self, check_previous_sibling_instruction_is_memo, memo_required}, - metadata_pointer::{self, MetadataPointer}, - mint_close_authority::MintCloseAuthority, - non_transferable::{NonTransferable, NonTransferableAccount}, - pausable::{self, PausableAccount, PausableConfig}, - permanent_delegate::{get_permanent_delegate, PermanentDelegate}, - reallocate, - scaled_ui_amount::{self, ScaledUiAmountConfig}, - token_group, token_metadata, - transfer_fee::{self, TransferFeeAmount, TransferFeeConfig}, - transfer_hook::{self, TransferHook, TransferHookAccount}, - AccountType, BaseStateWithExtensions, BaseStateWithExtensionsMut, ExtensionType, - PodStateWithExtensions, PodStateWithExtensionsMut, - }, - instruction::{ - decode_instruction_data, decode_instruction_type, is_valid_signer_index, AuthorityType, - MAX_SIGNERS, - }, - native_mint, - pod::{PodAccount, PodCOption, PodMint, PodMultisig}, - pod_instruction::{ - decode_instruction_data_with_coption_pubkey, AmountCheckedData, AmountData, - InitializeMintData, InitializeMultisigData, PodTokenInstruction, SetAuthorityData, - }, - state::{Account, AccountState, Mint, PackedSizeOf}, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - clock::Clock, - entrypoint::ProgramResult, - msg, - program::{invoke, invoke_signed, set_return_data}, - program_error::ProgramError, - program_pack::Pack, - pubkey::Pubkey, - system_instruction, system_program, - sysvar::{rent::Rent, Sysvar}, - }, - spl_pod::{ - bytemuck::{pod_from_bytes, pod_from_bytes_mut}, - primitives::{PodBool, PodU64}, - }, - spl_token_group_interface::instruction::TokenGroupInstruction, - spl_token_metadata_interface::instruction::TokenMetadataInstruction, - std::convert::{TryFrom, TryInto}, -}; - -/// Program state handler. -pub struct Processor {} -impl Processor { - fn _process_initialize_mint( - accounts: &[AccountInfo], - decimals: u8, - mint_authority: &Pubkey, - freeze_authority: PodCOption, - rent_sysvar_account: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - let mint_data_len = mint_info.data_len(); - let mut mint_data = mint_info.data.borrow_mut(); - let rent = if rent_sysvar_account { - Rent::from_account_info(next_account_info(account_info_iter)?)? - } else { - Rent::get()? - }; - - if !rent.is_exempt(mint_info.lamports(), mint_data_len) { - return Err(TokenError::NotRentExempt.into()); - } - - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - let extension_types = mint.get_extension_types()?; - if ExtensionType::try_calculate_account_len::(&extension_types)? != mint_data_len { - return Err(ProgramError::InvalidAccountData); - } - ExtensionType::check_for_invalid_mint_extension_combinations(&extension_types)?; - - if let Ok(default_account_state) = mint.get_extension_mut::() { - let default_account_state = AccountState::try_from(default_account_state.state) - .or(Err(ProgramError::InvalidAccountData))?; - if default_account_state == AccountState::Frozen && freeze_authority.is_none() { - return Err(TokenError::MintCannotFreeze.into()); - } - } - - mint.base.mint_authority = PodCOption::some(*mint_authority); - mint.base.decimals = decimals; - mint.base.is_initialized = PodBool::from_bool(true); - mint.base.freeze_authority = freeze_authority; - mint.init_account_type()?; - - Ok(()) - } - - /// Processes an [`InitializeMint`](enum.TokenInstruction.html) instruction. - pub fn process_initialize_mint( - accounts: &[AccountInfo], - decimals: u8, - mint_authority: &Pubkey, - freeze_authority: PodCOption, - ) -> ProgramResult { - Self::_process_initialize_mint(accounts, decimals, mint_authority, freeze_authority, true) - } - - /// Processes an [`InitializeMint2`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_mint2( - accounts: &[AccountInfo], - decimals: u8, - mint_authority: &Pubkey, - freeze_authority: PodCOption, - ) -> ProgramResult { - Self::_process_initialize_mint(accounts, decimals, mint_authority, freeze_authority, false) - } - - fn _process_initialize_account( - accounts: &[AccountInfo], - owner: Option<&Pubkey>, - rent_sysvar_account: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let new_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let owner = if let Some(owner) = owner { - owner - } else { - next_account_info(account_info_iter)?.key - }; - let new_account_info_data_len = new_account_info.data_len(); - let rent = if rent_sysvar_account { - Rent::from_account_info(next_account_info(account_info_iter)?)? - } else { - Rent::get()? - }; - - let mut account_data = new_account_info.data.borrow_mut(); - // unpack_uninitialized checks account.base.is_initialized() under the hood - let mut account = - PodStateWithExtensionsMut::::unpack_uninitialized(&mut account_data)?; - - if !rent.is_exempt(new_account_info.lamports(), new_account_info_data_len) { - return Err(TokenError::NotRentExempt.into()); - } - - // get_required_account_extensions checks mint validity - let mint_data = mint_info.data.borrow(); - let mint = PodStateWithExtensions::::unpack(&mint_data) - .map_err(|_| Into::::into(TokenError::InvalidMint))?; - if mint - .get_extension::() - .map(|e| Option::::from(e.delegate).is_some()) - .unwrap_or(false) - { - msg!("Warning: Mint has a permanent delegate, so tokens in this account may be seized at any time"); - } - let required_extensions = - Self::get_required_account_extensions_from_unpacked_mint(mint_info.owner, &mint)?; - if ExtensionType::try_calculate_account_len::(&required_extensions)? - > new_account_info_data_len - { - return Err(ProgramError::InvalidAccountData); - } - for extension in required_extensions { - account.init_account_extension_from_type(extension)?; - } - - let starting_state = - if let Ok(default_account_state) = mint.get_extension::() { - AccountState::try_from(default_account_state.state) - .or(Err(ProgramError::InvalidAccountData))? - } else { - AccountState::Initialized - }; - - account.base.mint = *mint_info.key; - account.base.owner = *owner; - account.base.close_authority = PodCOption::none(); - account.base.delegate = PodCOption::none(); - account.base.delegated_amount = 0.into(); - account.base.state = starting_state.into(); - if mint_info.key == &native_mint::id() { - let rent_exempt_reserve = rent.minimum_balance(new_account_info_data_len); - account.base.is_native = PodCOption::some(rent_exempt_reserve.into()); - account.base.amount = new_account_info - .lamports() - .checked_sub(rent_exempt_reserve) - .ok_or(TokenError::Overflow)? - .into(); - } else { - account.base.is_native = PodCOption::none(); - account.base.amount = 0.into(); - }; - - account.init_account_type()?; - - Ok(()) - } - - /// Processes an [`InitializeAccount`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_account(accounts: &[AccountInfo]) -> ProgramResult { - Self::_process_initialize_account(accounts, None, true) - } - - /// Processes an [`InitializeAccount2`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_account2(accounts: &[AccountInfo], owner: &Pubkey) -> ProgramResult { - Self::_process_initialize_account(accounts, Some(owner), true) - } - - /// Processes an [`InitializeAccount3`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_account3(accounts: &[AccountInfo], owner: &Pubkey) -> ProgramResult { - Self::_process_initialize_account(accounts, Some(owner), false) - } - - fn _process_initialize_multisig( - accounts: &[AccountInfo], - m: u8, - rent_sysvar_account: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let multisig_info = next_account_info(account_info_iter)?; - let multisig_info_data_len = multisig_info.data_len(); - let rent = if rent_sysvar_account { - Rent::from_account_info(next_account_info(account_info_iter)?)? - } else { - Rent::get()? - }; - - let mut multisig_data = multisig_info.data.borrow_mut(); - let multisig = pod_from_bytes_mut::(&mut multisig_data)?; - if bool::from(multisig.is_initialized) { - return Err(TokenError::AlreadyInUse.into()); - } - - if !rent.is_exempt(multisig_info.lamports(), multisig_info_data_len) { - return Err(TokenError::NotRentExempt.into()); - } - - let signer_infos = account_info_iter.as_slice(); - multisig.m = m; - multisig.n = signer_infos.len() as u8; - if !is_valid_signer_index(multisig.n as usize) { - return Err(TokenError::InvalidNumberOfProvidedSigners.into()); - } - if !is_valid_signer_index(multisig.m as usize) { - return Err(TokenError::InvalidNumberOfRequiredSigners.into()); - } - for (i, signer_info) in signer_infos.iter().enumerate() { - multisig.signers[i] = *signer_info.key; - } - multisig.is_initialized = true.into(); - - Ok(()) - } - - /// Processes a [`InitializeMultisig`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_multisig(accounts: &[AccountInfo], m: u8) -> ProgramResult { - Self::_process_initialize_multisig(accounts, m, true) - } - - /// Processes a [`InitializeMultisig2`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_multisig2(accounts: &[AccountInfo], m: u8) -> ProgramResult { - Self::_process_initialize_multisig(accounts, m, false) - } - - /// Processes a [`Transfer`](enum.TokenInstruction.html) instruction. - pub fn process_transfer( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: Option, - expected_fee: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_account_info = next_account_info(account_info_iter)?; - - let expected_mint_info = if let Some(expected_decimals) = expected_decimals { - Some((next_account_info(account_info_iter)?, expected_decimals)) - } else { - // Transfer must not be called with an expected fee but no mint, - // otherwise it's a programmer error. - assert!(expected_fee.is_none()); - None - }; - - let destination_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - let mut source_account_data = source_account_info.data.borrow_mut(); - let mut source_account = - PodStateWithExtensionsMut::::unpack(&mut source_account_data)?; - if source_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - let source_amount = u64::from(source_account.base.amount); - if source_amount < amount { - return Err(TokenError::InsufficientFunds.into()); - } - if source_account - .get_extension::() - .is_ok() - { - return Err(TokenError::NonTransferable.into()); - } - - let (fee, maybe_permanent_delegate, maybe_transfer_hook_program_id) = - if let Some((mint_info, expected_decimals)) = expected_mint_info { - if &source_account.base.mint != mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - let mint_data = mint_info.try_borrow_data()?; - let mint = PodStateWithExtensions::::unpack(&mint_data)?; - - if expected_decimals != mint.base.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - - let fee = if let Ok(transfer_fee_config) = mint.get_extension::() - { - transfer_fee_config - .calculate_epoch_fee(Clock::get()?.epoch, amount) - .ok_or(TokenError::Overflow)? - } else { - 0 - }; - - if let Ok(extension) = mint.get_extension::() { - if extension.paused.into() { - return Err(TokenError::MintPaused.into()); - } - } - - let maybe_permanent_delegate = get_permanent_delegate(&mint); - let maybe_transfer_hook_program_id = transfer_hook::get_program_id(&mint); - - ( - fee, - maybe_permanent_delegate, - maybe_transfer_hook_program_id, - ) - } else { - // Transfer hook extension exists on the account, but no mint - // was provided to figure out required accounts, abort - if source_account - .get_extension::() - .is_ok() - { - return Err(TokenError::MintRequiredForTransfer.into()); - } - - // Transfer fee amount extension exists on the account, but no mint - // was provided to calculate the fee, abort - if source_account - .get_extension_mut::() - .is_ok() - { - return Err(TokenError::MintRequiredForTransfer.into()); - } - - // Pausable extension exists on the account, but no mint - // was provided to see if it's paused, abort - if source_account.get_extension::().is_ok() { - return Err(TokenError::MintRequiredForTransfer.into()); - } - - (0, None, None) - }; - if let Some(expected_fee) = expected_fee { - if expected_fee != fee { - msg!("Calculated fee {}, received {}", fee, expected_fee); - return Err(TokenError::FeeMismatch.into()); - } - } - - let self_transfer = source_account_info.key == destination_account_info.key; - if let Ok(cpi_guard) = source_account.get_extension::() { - // Blocks all cases where the authority has signed if CPI Guard is - // enabled, including: - // * the account is delegated to the owner - // * the account owner is the permanent delegate - if *authority_info.key == source_account.base.owner - && cpi_guard.lock_cpi.into() - && in_cpi() - { - return Err(TokenError::CpiGuardTransferBlocked.into()); - } - } - match (source_account.base.delegate, maybe_permanent_delegate) { - (_, Some(ref delegate)) if authority_info.key == delegate => Self::validate_owner( - program_id, - delegate, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?, - ( - PodCOption { - option: PodCOption::::SOME, - value: delegate, - }, - _, - ) if authority_info.key == &delegate => { - Self::validate_owner( - program_id, - &delegate, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - let delegated_amount = u64::from(source_account.base.delegated_amount); - if delegated_amount < amount { - return Err(TokenError::InsufficientFunds.into()); - } - if !self_transfer { - source_account.base.delegated_amount = delegated_amount - .checked_sub(amount) - .ok_or(TokenError::Overflow)? - .into(); - if u64::from(source_account.base.delegated_amount) == 0 { - source_account.base.delegate = PodCOption::none(); - } - } - } - _ => { - Self::validate_owner( - program_id, - &source_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - } - } - - // Revisit this later to see if it's worth adding a check to reduce - // compute costs, ie: - // if self_transfer || amount == 0 - check_program_account(source_account_info.owner)?; - check_program_account(destination_account_info.owner)?; - - // This check MUST occur just before the amounts are manipulated - // to ensure self-transfers are fully validated - if self_transfer { - if memo_required(&source_account) { - check_previous_sibling_instruction_is_memo()?; - } - return Ok(()); - } - - // self-transfer was dealt with earlier, so this *should* be safe - let mut destination_account_data = destination_account_info.data.borrow_mut(); - let mut destination_account = - PodStateWithExtensionsMut::::unpack(&mut destination_account_data)?; - - if destination_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - if source_account.base.mint != destination_account.base.mint { - return Err(TokenError::MintMismatch.into()); - } - - if memo_required(&destination_account) { - check_previous_sibling_instruction_is_memo()?; - } - - if let Ok(confidential_transfer_state) = - destination_account.get_extension::() - { - confidential_transfer_state.non_confidential_transfer_allowed()? - } - - source_account.base.amount = source_amount - .checked_sub(amount) - .ok_or(TokenError::Overflow)? - .into(); - let credited_amount = amount.checked_sub(fee).ok_or(TokenError::Overflow)?; - destination_account.base.amount = u64::from(destination_account.base.amount) - .checked_add(credited_amount) - .ok_or(TokenError::Overflow)? - .into(); - if fee > 0 { - if let Ok(extension) = destination_account.get_extension_mut::() { - let new_withheld_amount = u64::from(extension.withheld_amount) - .checked_add(fee) - .ok_or(TokenError::Overflow)?; - extension.withheld_amount = new_withheld_amount.into(); - } else { - // Use the generic error since this should never happen. If there's - // a fee, then the mint has a fee configured, which means all accounts - // must have the withholding. - return Err(TokenError::InvalidState.into()); - } - } - - if source_account.base.is_native() { - let source_starting_lamports = source_account_info.lamports(); - **source_account_info.lamports.borrow_mut() = source_starting_lamports - .checked_sub(amount) - .ok_or(TokenError::Overflow)?; - - let destination_starting_lamports = destination_account_info.lamports(); - **destination_account_info.lamports.borrow_mut() = destination_starting_lamports - .checked_add(amount) - .ok_or(TokenError::Overflow)?; - } - - if let Some(program_id) = maybe_transfer_hook_program_id { - if let Some((mint_info, _)) = expected_mint_info { - // set transferring flags - transfer_hook::set_transferring(&mut source_account)?; - transfer_hook::set_transferring(&mut destination_account)?; - - // must drop these to avoid the double-borrow during CPI - drop(source_account_data); - drop(destination_account_data); - spl_transfer_hook_interface::onchain::invoke_execute( - &program_id, - source_account_info.clone(), - mint_info.clone(), - destination_account_info.clone(), - authority_info.clone(), - account_info_iter.as_slice(), - amount, - )?; - - // unset transferring flag - transfer_hook::unset_transferring(source_account_info)?; - transfer_hook::unset_transferring(destination_account_info)?; - } else { - return Err(TokenError::MintRequiredForTransfer.into()); - } - } - - Ok(()) - } - - /// Processes an [`Approve`](enum.TokenInstruction.html) instruction. - pub fn process_approve( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_account_info = next_account_info(account_info_iter)?; - - let expected_mint_info = if let Some(expected_decimals) = expected_decimals { - Some((next_account_info(account_info_iter)?, expected_decimals)) - } else { - None - }; - let delegate_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut source_account_data = source_account_info.data.borrow_mut(); - let source_account = - PodStateWithExtensionsMut::::unpack(&mut source_account_data)?; - - if source_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if let Some((mint_info, expected_decimals)) = expected_mint_info { - if &source_account.base.mint != mint_info.key { - return Err(TokenError::MintMismatch.into()); - } - - let mint_data = mint_info.data.borrow(); - let mint = PodStateWithExtensions::::unpack(&mint_data)?; - if expected_decimals != mint.base.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - } - - Self::validate_owner( - program_id, - &source_account.base.owner, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?; - - if let Ok(cpi_guard) = source_account.get_extension::() { - if cpi_guard.lock_cpi.into() && in_cpi() { - return Err(TokenError::CpiGuardApproveBlocked.into()); - } - } - - source_account.base.delegate = PodCOption::some(*delegate_info.key); - source_account.base.delegated_amount = amount.into(); - - Ok(()) - } - - /// Processes an [`Revoke`](enum.TokenInstruction.html) instruction. - pub fn process_revoke(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let source_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - let mut source_account_data = source_account_info.data.borrow_mut(); - let source_account = - PodStateWithExtensionsMut::::unpack(&mut source_account_data)?; - if source_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - Self::validate_owner( - program_id, - match &source_account.base.delegate { - PodCOption { - option: PodCOption::::SOME, - value: delegate, - } if authority_info.key == delegate => delegate, - _ => &source_account.base.owner, - }, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - source_account.base.delegate = PodCOption::none(); - source_account.base.delegated_amount = 0.into(); - - Ok(()) - } - - /// Processes a [`SetAuthority`](enum.TokenInstruction.html) instruction. - pub fn process_set_authority( - program_id: &Pubkey, - accounts: &[AccountInfo], - authority_type: AuthorityType, - new_authority: PodCOption, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - let mut account_data = account_info.data.borrow_mut(); - if let Ok(mut account) = PodStateWithExtensionsMut::::unpack(&mut account_data) - { - if account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - match authority_type { - AuthorityType::AccountOwner => { - Self::validate_owner( - program_id, - &account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - if account.get_extension_mut::().is_ok() { - return Err(TokenError::ImmutableOwner.into()); - } - - if let Ok(cpi_guard) = account.get_extension::() { - if cpi_guard.lock_cpi.into() && in_cpi() { - return Err(TokenError::CpiGuardSetAuthorityBlocked.into()); - } else if cpi_guard.lock_cpi.into() { - return Err(TokenError::CpiGuardOwnerChangeBlocked.into()); - } - } - - if let PodCOption { - option: PodCOption::::SOME, - value: authority, - } = new_authority - { - account.base.owner = authority; - } else { - return Err(TokenError::InvalidInstruction.into()); - } - - account.base.delegate = PodCOption::none(); - account.base.delegated_amount = 0.into(); - - if account.base.is_native() { - account.base.close_authority = PodCOption::none(); - } - } - AuthorityType::CloseAccount => { - let authority = account.base.close_authority.unwrap_or(account.base.owner); - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - if let Ok(cpi_guard) = account.get_extension::() { - if cpi_guard.lock_cpi.into() && in_cpi() && new_authority.is_some() { - return Err(TokenError::CpiGuardSetAuthorityBlocked.into()); - } - } - - account.base.close_authority = new_authority; - } - _ => { - return Err(TokenError::AuthorityTypeNotSupported.into()); - } - } - } else if let Ok(mut mint) = PodStateWithExtensionsMut::::unpack(&mut account_data) - { - match authority_type { - AuthorityType::MintTokens => { - // Once a mint's supply is fixed, it cannot be undone by setting a new - // mint_authority - let mint_authority = mint - .base - .mint_authority - .ok_or(Into::::into(TokenError::FixedSupply))?; - Self::validate_owner( - program_id, - &mint_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - mint.base.mint_authority = new_authority; - } - AuthorityType::FreezeAccount => { - // Once a mint's freeze authority is disabled, it cannot be re-enabled by - // setting a new freeze_authority - let freeze_authority = mint - .base - .freeze_authority - .ok_or(Into::::into(TokenError::MintCannotFreeze))?; - Self::validate_owner( - program_id, - &freeze_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - mint.base.freeze_authority = new_authority; - } - AuthorityType::CloseMint => { - let extension = mint.get_extension_mut::()?; - let maybe_close_authority: Option = extension.close_authority.into(); - let close_authority = - maybe_close_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &close_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.close_authority = new_authority.try_into()?; - } - AuthorityType::TransferFeeConfig => { - let extension = mint.get_extension_mut::()?; - let maybe_transfer_fee_config_authority: Option = - extension.transfer_fee_config_authority.into(); - let transfer_fee_config_authority = maybe_transfer_fee_config_authority - .ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &transfer_fee_config_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.transfer_fee_config_authority = new_authority.try_into()?; - } - AuthorityType::WithheldWithdraw => { - let extension = mint.get_extension_mut::()?; - let maybe_withdraw_withheld_authority: Option = - extension.withdraw_withheld_authority.into(); - let withdraw_withheld_authority = maybe_withdraw_withheld_authority - .ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &withdraw_withheld_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.withdraw_withheld_authority = new_authority.try_into()?; - } - AuthorityType::InterestRate => { - let extension = mint.get_extension_mut::()?; - let maybe_rate_authority: Option = extension.rate_authority.into(); - let rate_authority = - maybe_rate_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &rate_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.rate_authority = new_authority.try_into()?; - } - AuthorityType::PermanentDelegate => { - let extension = mint.get_extension_mut::()?; - let maybe_delegate: Option = extension.delegate.into(); - let delegate = maybe_delegate.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &delegate, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.delegate = new_authority.try_into()?; - } - AuthorityType::ConfidentialTransferMint => { - let extension = mint.get_extension_mut::()?; - let maybe_confidential_transfer_mint_authority: Option = - extension.authority.into(); - let confidential_transfer_mint_authority = - maybe_confidential_transfer_mint_authority - .ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &confidential_transfer_mint_authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.authority = new_authority.try_into()?; - } - AuthorityType::TransferHookProgramId => { - let extension = mint.get_extension_mut::()?; - let maybe_authority: Option = extension.authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.authority = new_authority.try_into()?; - } - AuthorityType::ConfidentialTransferFeeConfig => { - let extension = mint.get_extension_mut::()?; - let maybe_authority: Option = extension.authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.authority = new_authority.try_into()?; - } - AuthorityType::MetadataPointer => { - let extension = mint.get_extension_mut::()?; - let maybe_authority: Option = extension.authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.authority = new_authority.try_into()?; - } - AuthorityType::GroupPointer => { - let extension = mint.get_extension_mut::()?; - let maybe_authority: Option = extension.authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.authority = new_authority.try_into()?; - } - AuthorityType::GroupMemberPointer => { - let extension = mint.get_extension_mut::()?; - let maybe_authority: Option = extension.authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.authority = new_authority.try_into()?; - } - AuthorityType::ScaledUiAmount => { - let extension = mint.get_extension_mut::()?; - let maybe_authority: Option = extension.authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.authority = new_authority.try_into()?; - } - AuthorityType::Pause => { - let extension = mint.get_extension_mut::()?; - let maybe_authority: Option = extension.authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - extension.authority = new_authority.try_into()?; - } - _ => { - return Err(TokenError::AuthorityTypeNotSupported.into()); - } - } - } else { - return Err(ProgramError::InvalidAccountData); - } - - Ok(()) - } - - /// Processes a [`MintTo`](enum.TokenInstruction.html) instruction. - pub fn process_mint_to( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - let owner_info_data_len = owner_info.data_len(); - - let mut destination_account_data = destination_account_info.data.borrow_mut(); - let destination_account = - PodStateWithExtensionsMut::::unpack(&mut destination_account_data)?; - if destination_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if destination_account.base.is_native() { - return Err(TokenError::NativeNotSupported.into()); - } - if mint_info.key != &destination_account.base.mint { - return Err(TokenError::MintMismatch.into()); - } - - let mut mint_data = mint_info.data.borrow_mut(); - let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - - // If the mint if non-transferable, only allow minting to accounts - // with immutable ownership. - if mint.get_extension::().is_ok() - && destination_account - .get_extension::() - .is_err() - { - return Err(TokenError::NonTransferableNeedsImmutableOwnership.into()); - } - - if let Ok(extension) = mint.get_extension::() { - if extension.paused.into() { - return Err(TokenError::MintPaused.into()); - } - } - - if mint.get_extension::().is_ok() { - return Err(TokenError::IllegalMintBurnConversion.into()); - } - - if let Some(expected_decimals) = expected_decimals { - if expected_decimals != mint.base.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - } - - match &mint.base.mint_authority { - PodCOption { - option: PodCOption::::SOME, - value: mint_authority, - } => Self::validate_owner( - program_id, - mint_authority, - owner_info, - owner_info_data_len, - account_info_iter.as_slice(), - )?, - _ => return Err(TokenError::FixedSupply.into()), - } - - // Revisit this later to see if it's worth adding a check to reduce - // compute costs, ie: - // if amount == 0 - check_program_account(mint_info.owner)?; - check_program_account(destination_account_info.owner)?; - - destination_account.base.amount = u64::from(destination_account.base.amount) - .checked_add(amount) - .ok_or(TokenError::Overflow)? - .into(); - - mint.base.supply = u64::from(mint.base.supply) - .checked_add(amount) - .ok_or(TokenError::Overflow)? - .into(); - - Ok(()) - } - - /// Processes a [`Burn`](enum.TokenInstruction.html) instruction. - pub fn process_burn( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - let mut source_account_data = source_account_info.data.borrow_mut(); - let source_account = - PodStateWithExtensionsMut::::unpack(&mut source_account_data)?; - let mut mint_data = mint_info.data.borrow_mut(); - let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - - if source_account.base.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - if source_account.base.is_native() { - return Err(TokenError::NativeNotSupported.into()); - } - if u64::from(source_account.base.amount) < amount { - return Err(TokenError::InsufficientFunds.into()); - } - if mint_info.key != &source_account.base.mint { - return Err(TokenError::MintMismatch.into()); - } - - if let Some(expected_decimals) = expected_decimals { - if expected_decimals != mint.base.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - } - if let Ok(extension) = mint.get_extension::() { - if extension.paused.into() { - return Err(TokenError::MintPaused.into()); - } - } - let maybe_permanent_delegate = get_permanent_delegate(&mint); - - if let Ok(cpi_guard) = source_account.get_extension::() { - // Blocks all cases where the authority has signed if CPI Guard is - // enabled, including: - // * the account is delegated to the owner - // * the account owner is the permanent delegate - if *authority_info.key == source_account.base.owner - && cpi_guard.lock_cpi.into() - && in_cpi() - { - return Err(TokenError::CpiGuardBurnBlocked.into()); - } - } - - if !source_account - .base - .is_owned_by_system_program_or_incinerator() - { - match (&source_account.base.delegate, maybe_permanent_delegate) { - (_, Some(ref delegate)) if authority_info.key == delegate => Self::validate_owner( - program_id, - delegate, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?, - ( - PodCOption { - option: PodCOption::::SOME, - value: delegate, - }, - _, - ) if authority_info.key == delegate => { - Self::validate_owner( - program_id, - delegate, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - if u64::from(source_account.base.delegated_amount) < amount { - return Err(TokenError::InsufficientFunds.into()); - } - source_account.base.delegated_amount = - u64::from(source_account.base.delegated_amount) - .checked_sub(amount) - .ok_or(TokenError::Overflow)? - .into(); - if u64::from(source_account.base.delegated_amount) == 0 { - source_account.base.delegate = PodCOption::none(); - } - } - _ => { - Self::validate_owner( - program_id, - &source_account.base.owner, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - } - } - } - - // Revisit this later to see if it's worth adding a check to reduce - // compute costs, ie: - // if amount == 0 - check_program_account(source_account_info.owner)?; - check_program_account(mint_info.owner)?; - - source_account.base.amount = u64::from(source_account.base.amount) - .checked_sub(amount) - .ok_or(TokenError::Overflow)? - .into(); - mint.base.supply = u64::from(mint.base.supply) - .checked_sub(amount) - .ok_or(TokenError::Overflow)? - .into(); - - Ok(()) - } - - /// Processes a [`CloseAccount`](enum.TokenInstruction.html) instruction. - pub fn process_close_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let source_account_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - if source_account_info.key == destination_account_info.key { - return Err(ProgramError::InvalidAccountData); - } - - let source_account_data = source_account_info.data.borrow(); - if let Ok(source_account) = - PodStateWithExtensions::::unpack(&source_account_data) - { - if !source_account.base.is_native() && u64::from(source_account.base.amount) != 0 { - return Err(TokenError::NonNativeHasBalance.into()); - } - - let authority = source_account - .base - .close_authority - .unwrap_or(source_account.base.owner); - - if !source_account - .base - .is_owned_by_system_program_or_incinerator() - { - if let Ok(cpi_guard) = source_account.get_extension::() { - if cpi_guard.lock_cpi.into() - && in_cpi() - && destination_account_info.key != &source_account.base.owner - { - return Err(TokenError::CpiGuardCloseAccountBlocked.into()); - } - } - - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - } else if !solana_program::incinerator::check_id(destination_account_info.key) { - return Err(ProgramError::InvalidAccountData); - } - - if let Ok(confidential_transfer_state) = - source_account.get_extension::() - { - confidential_transfer_state.closable()? - } - - if let Ok(confidential_transfer_fee_state) = - source_account.get_extension::() - { - confidential_transfer_fee_state.closable()? - } - - if let Ok(transfer_fee_state) = source_account.get_extension::() { - transfer_fee_state.closable()? - } - } else if let Ok(mint) = PodStateWithExtensions::::unpack(&source_account_data) { - let extension = mint.get_extension::()?; - let maybe_authority: Option = extension.close_authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Self::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; - - if u64::from(mint.base.supply) != 0 { - return Err(TokenError::MintHasSupply.into()); - } - } else { - return Err(ProgramError::UninitializedAccount); - } - - let destination_starting_lamports = destination_account_info.lamports(); - **destination_account_info.lamports.borrow_mut() = destination_starting_lamports - .checked_add(source_account_info.lamports()) - .ok_or(TokenError::Overflow)?; - - **source_account_info.lamports.borrow_mut() = 0; - drop(source_account_data); - delete_account(source_account_info)?; - - Ok(()) - } - - /// Processes a [`FreezeAccount`](enum.TokenInstruction.html) or a - /// [`ThawAccount`](enum.TokenInstruction.html) instruction. - pub fn process_toggle_freeze_account( - program_id: &Pubkey, - accounts: &[AccountInfo], - freeze: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let source_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); - - let mut source_account_data = source_account_info.data.borrow_mut(); - let source_account = - PodStateWithExtensionsMut::::unpack(&mut source_account_data)?; - if freeze && source_account.base.is_frozen() || !freeze && !source_account.base.is_frozen() - { - return Err(TokenError::InvalidState.into()); - } - if source_account.base.is_native() { - return Err(TokenError::NativeNotSupported.into()); - } - if mint_info.key != &source_account.base.mint { - return Err(TokenError::MintMismatch.into()); - } - - let mint_data = mint_info.data.borrow(); - let mint = PodStateWithExtensions::::unpack(&mint_data)?; - match &mint.base.freeze_authority { - PodCOption { - option: PodCOption::::SOME, - value: authority, - } => Self::validate_owner( - program_id, - authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - ), - _ => Err(TokenError::MintCannotFreeze.into()), - }?; - - source_account.base.state = if freeze { - AccountState::Frozen.into() - } else { - AccountState::Initialized.into() - }; - - Ok(()) - } - - /// Processes a [`SyncNative`](enum.TokenInstruction.html) instruction - pub fn process_sync_native(accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let native_account_info = next_account_info(account_info_iter)?; - - check_program_account(native_account_info.owner)?; - let mut native_account_data = native_account_info.data.borrow_mut(); - let native_account = - PodStateWithExtensionsMut::::unpack(&mut native_account_data)?; - - match native_account.base.is_native { - PodCOption { - option: PodCOption::::SOME, - value: amount, - } => { - let new_amount = native_account_info - .lamports() - .checked_sub(u64::from(amount)) - .ok_or(TokenError::Overflow)?; - if new_amount < u64::from(native_account.base.amount) { - return Err(TokenError::InvalidState.into()); - } - native_account.base.amount = new_amount.into(); - } - _ => return Err(TokenError::NonNativeNotSupported.into()), - } - - Ok(()) - } - - /// Processes an - /// [`InitializeMintCloseAuthority`](enum.TokenInstruction.html) - /// instruction - pub fn process_initialize_mint_close_authority( - accounts: &[AccountInfo], - close_authority: PodCOption, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - let extension = mint.init_extension::(true)?; - extension.close_authority = close_authority.try_into()?; - - Ok(()) - } - - /// Processes a [`GetAccountDataSize`](enum.TokenInstruction.html) - /// instruction - pub fn process_get_account_data_size( - accounts: &[AccountInfo], - new_extension_types: &[ExtensionType], - ) -> ProgramResult { - if new_extension_types - .iter() - .any(|&t| t.get_account_type() != AccountType::Account) - { - return Err(TokenError::ExtensionTypeMismatch.into()); - } - - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - - let mut account_extensions = Self::get_required_account_extensions(mint_account_info)?; - // ExtensionType::try_calculate_account_len() dedupes types, so just a dumb - // concatenation is fine here - account_extensions.extend_from_slice(new_extension_types); - - let account_len = ExtensionType::try_calculate_account_len::(&account_extensions)?; - set_return_data(&account_len.to_le_bytes()); - - Ok(()) - } - - /// Processes an [`InitializeImmutableOwner`](enum.TokenInstruction.html) - /// instruction - pub fn process_initialize_immutable_owner(accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let token_account_data = &mut token_account_info.data.borrow_mut(); - let mut token_account = - PodStateWithExtensionsMut::::unpack_uninitialized(token_account_data)?; - token_account - .init_extension::(true) - .map(|_| ()) - } - - /// Processes an [`AmountToUiAmount`](enum.TokenInstruction.html) - /// instruction - pub fn process_amount_to_ui_amount(accounts: &[AccountInfo], amount: u64) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - check_program_account(mint_info.owner)?; - - let mint_data = mint_info.data.borrow(); - let mint = PodStateWithExtensions::::unpack(&mint_data) - .map_err(|_| Into::::into(TokenError::InvalidMint))?; - let ui_amount = if let Ok(extension) = mint.get_extension::() { - let unix_timestamp = Clock::get()?.unix_timestamp; - extension - .amount_to_ui_amount(amount, mint.base.decimals, unix_timestamp) - .ok_or(ProgramError::InvalidArgument)? - } else if let Ok(extension) = mint.get_extension::() { - let unix_timestamp = Clock::get()?.unix_timestamp; - extension - .amount_to_ui_amount(amount, mint.base.decimals, unix_timestamp) - .ok_or(ProgramError::InvalidArgument)? - } else { - crate::amount_to_ui_amount_string_trimmed(amount, mint.base.decimals) - }; - - set_return_data(&ui_amount.into_bytes()); - Ok(()) - } - - /// Processes an [`AmountToUiAmount`](enum.TokenInstruction.html) - /// instruction - pub fn process_ui_amount_to_amount(accounts: &[AccountInfo], ui_amount: &str) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - check_program_account(mint_info.owner)?; - - let mint_data = mint_info.data.borrow(); - let mint = PodStateWithExtensions::::unpack(&mint_data) - .map_err(|_| Into::::into(TokenError::InvalidMint))?; - let amount = if let Ok(extension) = mint.get_extension::() { - let unix_timestamp = Clock::get()?.unix_timestamp; - extension.try_ui_amount_into_amount(ui_amount, mint.base.decimals, unix_timestamp)? - } else if let Ok(extension) = mint.get_extension::() { - let unix_timestamp = Clock::get()?.unix_timestamp; - extension.try_ui_amount_into_amount(ui_amount, mint.base.decimals, unix_timestamp)? - } else { - crate::try_ui_amount_into_amount(ui_amount.to_string(), mint.base.decimals)? - }; - - set_return_data(&amount.to_le_bytes()); - Ok(()) - } - - /// Processes a [`CreateNativeMint`](enum.TokenInstruction.html) instruction - pub fn process_create_native_mint(accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let payer_info = next_account_info(account_info_iter)?; - let native_mint_info = next_account_info(account_info_iter)?; - let system_program_info = next_account_info(account_info_iter)?; - - if *native_mint_info.key != native_mint::id() { - return Err(TokenError::InvalidMint.into()); - } - - let rent = Rent::get()?; - let new_minimum_balance = rent.minimum_balance(Mint::get_packed_len()); - let lamports_diff = new_minimum_balance.saturating_sub(native_mint_info.lamports()); - invoke( - &system_instruction::transfer(payer_info.key, native_mint_info.key, lamports_diff), - &[ - payer_info.clone(), - native_mint_info.clone(), - system_program_info.clone(), - ], - )?; - - invoke_signed( - &system_instruction::allocate(native_mint_info.key, Mint::get_packed_len() as u64), - &[native_mint_info.clone(), system_program_info.clone()], - &[native_mint::PROGRAM_ADDRESS_SEEDS], - )?; - - invoke_signed( - &system_instruction::assign(native_mint_info.key, &crate::id()), - &[native_mint_info.clone(), system_program_info.clone()], - &[native_mint::PROGRAM_ADDRESS_SEEDS], - )?; - - Mint::pack( - Mint { - decimals: native_mint::DECIMALS, - is_initialized: true, - ..Mint::default() - }, - &mut native_mint_info.data.borrow_mut(), - ) - } - - /// Processes an - /// [`InitializeNonTransferableMint`](enum.TokenInstruction.html) - /// instruction - pub fn process_initialize_non_transferable_mint(accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - mint.init_extension::(true)?; - - Ok(()) - } - - /// Processes an [`InitializePermanentDelegate`](enum.TokenInstruction.html) - /// instruction - pub fn process_initialize_permanent_delegate( - accounts: &[AccountInfo], - delegate: &Pubkey, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - let extension = mint.init_extension::(true)?; - extension.delegate = Some(*delegate).try_into()?; - - Ok(()) - } - - /// Withdraw Excess Lamports is used to recover Lamports transferred to any - /// `TokenProgram` owned account by moving them to another account - /// of the source account. - pub fn process_withdraw_excess_lamports( - program_id: &Pubkey, - accounts: &[AccountInfo], - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_info = next_account_info(account_info_iter)?; - let destination_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - let source_data = source_info.data.borrow(); - - if let Ok(account) = PodStateWithExtensions::::unpack(&source_data) { - if account.base.is_native() { - return Err(TokenError::NativeNotSupported.into()); - } - Self::validate_owner( - program_id, - &account.base.owner, - authority_info, - authority_info.data_len(), - account_info_iter.as_slice(), - )?; - } else if let Ok(mint) = PodStateWithExtensions::::unpack(&source_data) { - match &mint.base.mint_authority { - PodCOption { - option: PodCOption::::SOME, - value: mint_authority, - } => { - Self::validate_owner( - program_id, - mint_authority, - authority_info, - authority_info.data_len(), - account_info_iter.as_slice(), - )?; - } - _ => return Err(TokenError::AuthorityTypeNotSupported.into()), - } - } else if source_data.len() == PodMultisig::SIZE_OF { - Self::validate_owner( - program_id, - source_info.key, - authority_info, - authority_info.data_len(), - account_info_iter.as_slice(), - )?; - } else { - return Err(TokenError::InvalidState.into()); - } - - let source_rent_exempt_reserve = Rent::get()?.minimum_balance(source_info.data_len()); - - let transfer_amount = source_info - .lamports() - .checked_sub(source_rent_exempt_reserve) - .ok_or(TokenError::NotRentExempt)?; - - let source_starting_lamports = source_info.lamports(); - **source_info.lamports.borrow_mut() = source_starting_lamports - .checked_sub(transfer_amount) - .ok_or(TokenError::Overflow)?; - - let destination_starting_lamports = destination_info.lamports(); - **destination_info.lamports.borrow_mut() = destination_starting_lamports - .checked_add(transfer_amount) - .ok_or(TokenError::Overflow)?; - - Ok(()) - } - - /// Processes an [`Instruction`](enum.Instruction.html). - pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { - if let Ok(instruction_type) = decode_instruction_type(input) { - match instruction_type { - PodTokenInstruction::InitializeMint => { - msg!("Instruction: InitializeMint"); - let (data, freeze_authority) = - decode_instruction_data_with_coption_pubkey::(input)?; - Self::process_initialize_mint( - accounts, - data.decimals, - &data.mint_authority, - freeze_authority, - ) - } - PodTokenInstruction::InitializeMint2 => { - msg!("Instruction: InitializeMint2"); - let (data, freeze_authority) = - decode_instruction_data_with_coption_pubkey::(input)?; - Self::process_initialize_mint2( - accounts, - data.decimals, - &data.mint_authority, - freeze_authority, - ) - } - PodTokenInstruction::InitializeAccount => { - msg!("Instruction: InitializeAccount"); - Self::process_initialize_account(accounts) - } - PodTokenInstruction::InitializeAccount2 => { - msg!("Instruction: InitializeAccount2"); - let owner = decode_instruction_data::(input)?; - Self::process_initialize_account2(accounts, owner) - } - PodTokenInstruction::InitializeAccount3 => { - msg!("Instruction: InitializeAccount3"); - let owner = decode_instruction_data::(input)?; - Self::process_initialize_account3(accounts, owner) - } - PodTokenInstruction::InitializeMultisig => { - msg!("Instruction: InitializeMultisig"); - let data = decode_instruction_data::(input)?; - Self::process_initialize_multisig(accounts, data.m) - } - PodTokenInstruction::InitializeMultisig2 => { - msg!("Instruction: InitializeMultisig2"); - let data = decode_instruction_data::(input)?; - Self::process_initialize_multisig2(accounts, data.m) - } - #[allow(deprecated)] - PodTokenInstruction::Transfer => { - msg!("Instruction: Transfer"); - let data = decode_instruction_data::(input)?; - Self::process_transfer(program_id, accounts, data.amount.into(), None, None) - } - PodTokenInstruction::Approve => { - msg!("Instruction: Approve"); - let data = decode_instruction_data::(input)?; - Self::process_approve(program_id, accounts, data.amount.into(), None) - } - PodTokenInstruction::Revoke => { - msg!("Instruction: Revoke"); - Self::process_revoke(program_id, accounts) - } - PodTokenInstruction::SetAuthority => { - msg!("Instruction: SetAuthority"); - let (data, new_authority) = - decode_instruction_data_with_coption_pubkey::(input)?; - Self::process_set_authority( - program_id, - accounts, - AuthorityType::from(data.authority_type)?, - new_authority, - ) - } - PodTokenInstruction::MintTo => { - msg!("Instruction: MintTo"); - let data = decode_instruction_data::(input)?; - Self::process_mint_to(program_id, accounts, data.amount.into(), None) - } - PodTokenInstruction::Burn => { - msg!("Instruction: Burn"); - let data = decode_instruction_data::(input)?; - Self::process_burn(program_id, accounts, data.amount.into(), None) - } - PodTokenInstruction::CloseAccount => { - msg!("Instruction: CloseAccount"); - Self::process_close_account(program_id, accounts) - } - PodTokenInstruction::FreezeAccount => { - msg!("Instruction: FreezeAccount"); - Self::process_toggle_freeze_account(program_id, accounts, true) - } - PodTokenInstruction::ThawAccount => { - msg!("Instruction: ThawAccount"); - Self::process_toggle_freeze_account(program_id, accounts, false) - } - PodTokenInstruction::TransferChecked => { - msg!("Instruction: TransferChecked"); - let data = decode_instruction_data::(input)?; - Self::process_transfer( - program_id, - accounts, - data.amount.into(), - Some(data.decimals), - None, - ) - } - PodTokenInstruction::ApproveChecked => { - msg!("Instruction: ApproveChecked"); - let data = decode_instruction_data::(input)?; - Self::process_approve( - program_id, - accounts, - data.amount.into(), - Some(data.decimals), - ) - } - PodTokenInstruction::MintToChecked => { - msg!("Instruction: MintToChecked"); - let data = decode_instruction_data::(input)?; - Self::process_mint_to( - program_id, - accounts, - data.amount.into(), - Some(data.decimals), - ) - } - PodTokenInstruction::BurnChecked => { - msg!("Instruction: BurnChecked"); - let data = decode_instruction_data::(input)?; - Self::process_burn( - program_id, - accounts, - data.amount.into(), - Some(data.decimals), - ) - } - PodTokenInstruction::SyncNative => { - msg!("Instruction: SyncNative"); - Self::process_sync_native(accounts) - } - PodTokenInstruction::GetAccountDataSize => { - msg!("Instruction: GetAccountDataSize"); - let extension_types = input[1..] - .chunks(std::mem::size_of::()) - .map(ExtensionType::try_from) - .collect::, _>>()?; - Self::process_get_account_data_size(accounts, &extension_types) - } - PodTokenInstruction::InitializeMintCloseAuthority => { - msg!("Instruction: InitializeMintCloseAuthority"); - let (_, close_authority) = - decode_instruction_data_with_coption_pubkey::<()>(input)?; - Self::process_initialize_mint_close_authority(accounts, close_authority) - } - PodTokenInstruction::TransferFeeExtension => { - transfer_fee::processor::process_instruction(program_id, accounts, &input[1..]) - } - PodTokenInstruction::ConfidentialTransferExtension => { - confidential_transfer::processor::process_instruction( - program_id, - accounts, - &input[1..], - ) - } - PodTokenInstruction::DefaultAccountStateExtension => { - default_account_state::processor::process_instruction( - program_id, - accounts, - &input[1..], - ) - } - PodTokenInstruction::InitializeImmutableOwner => { - msg!("Instruction: InitializeImmutableOwner"); - Self::process_initialize_immutable_owner(accounts) - } - PodTokenInstruction::AmountToUiAmount => { - msg!("Instruction: AmountToUiAmount"); - let data = decode_instruction_data::(input)?; - Self::process_amount_to_ui_amount(accounts, data.amount.into()) - } - PodTokenInstruction::UiAmountToAmount => { - msg!("Instruction: UiAmountToAmount"); - let ui_amount = std::str::from_utf8(&input[1..]) - .map_err(|_| TokenError::InvalidInstruction)?; - Self::process_ui_amount_to_amount(accounts, ui_amount) - } - PodTokenInstruction::Reallocate => { - msg!("Instruction: Reallocate"); - let extension_types = input[1..] - .chunks(std::mem::size_of::()) - .map(ExtensionType::try_from) - .collect::, _>>()?; - reallocate::process_reallocate(program_id, accounts, extension_types) - } - PodTokenInstruction::MemoTransferExtension => { - memo_transfer::processor::process_instruction(program_id, accounts, &input[1..]) - } - PodTokenInstruction::CreateNativeMint => { - msg!("Instruction: CreateNativeMint"); - Self::process_create_native_mint(accounts) - } - PodTokenInstruction::InitializeNonTransferableMint => { - msg!("Instruction: InitializeNonTransferableMint"); - Self::process_initialize_non_transferable_mint(accounts) - } - PodTokenInstruction::InterestBearingMintExtension => { - interest_bearing_mint::processor::process_instruction( - program_id, - accounts, - &input[1..], - ) - } - PodTokenInstruction::CpiGuardExtension => { - cpi_guard::processor::process_instruction(program_id, accounts, &input[1..]) - } - PodTokenInstruction::InitializePermanentDelegate => { - msg!("Instruction: InitializePermanentDelegate"); - let delegate = decode_instruction_data::(input)?; - Self::process_initialize_permanent_delegate(accounts, delegate) - } - PodTokenInstruction::TransferHookExtension => { - transfer_hook::processor::process_instruction(program_id, accounts, &input[1..]) - } - PodTokenInstruction::ConfidentialTransferFeeExtension => { - confidential_transfer_fee::processor::process_instruction( - program_id, - accounts, - &input[1..], - ) - } - PodTokenInstruction::WithdrawExcessLamports => { - msg!("Instruction: WithdrawExcessLamports"); - Self::process_withdraw_excess_lamports(program_id, accounts) - } - PodTokenInstruction::MetadataPointerExtension => { - metadata_pointer::processor::process_instruction( - program_id, - accounts, - &input[1..], - ) - } - PodTokenInstruction::GroupPointerExtension => { - group_pointer::processor::process_instruction(program_id, accounts, &input[1..]) - } - PodTokenInstruction::GroupMemberPointerExtension => { - group_member_pointer::processor::process_instruction( - program_id, - accounts, - &input[1..], - ) - } - PodTokenInstruction::ConfidentialMintBurnExtension => { - msg!("Instruction: ConfidentialMintBurnExtension"); - confidential_mint_burn::processor::process_instruction( - program_id, - accounts, - &input[1..], - ) - } - PodTokenInstruction::ScaledUiAmountExtension => { - msg!("Instruction: ScaledUiAmountExtension"); - scaled_ui_amount::processor::process_instruction( - program_id, - accounts, - &input[1..], - ) - } - PodTokenInstruction::PausableExtension => { - msg!("Instruction: PausableExtension"); - pausable::processor::process_instruction(program_id, accounts, &input[1..]) - } - } - } else if let Ok(instruction) = TokenMetadataInstruction::unpack(input) { - token_metadata::processor::process_instruction(program_id, accounts, instruction) - } else if let Ok(instruction) = TokenGroupInstruction::unpack(input) { - token_group::processor::process_instruction(program_id, accounts, instruction) - } else { - Err(TokenError::InvalidInstruction.into()) - } - } - - /// Validates owner(s) are present. Used for Mints and Accounts only. - pub fn validate_owner( - program_id: &Pubkey, - expected_owner: &Pubkey, - owner_account_info: &AccountInfo, - owner_account_data_len: usize, - signers: &[AccountInfo], - ) -> ProgramResult { - if expected_owner != owner_account_info.key { - return Err(TokenError::OwnerMismatch.into()); - } - - if program_id == owner_account_info.owner && owner_account_data_len == PodMultisig::SIZE_OF - { - let multisig_data = &owner_account_info.data.borrow(); - let multisig = pod_from_bytes::(multisig_data)?; - let mut num_signers = 0; - let mut matched = [false; MAX_SIGNERS]; - for signer in signers.iter() { - for (position, key) in multisig.signers[0..multisig.n as usize].iter().enumerate() { - if key == signer.key && !matched[position] { - if !signer.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - matched[position] = true; - num_signers += 1; - } - } - } - if num_signers < multisig.m { - return Err(ProgramError::MissingRequiredSignature); - } - return Ok(()); - } else if !owner_account_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - Ok(()) - } - - fn get_required_account_extensions( - mint_account_info: &AccountInfo, - ) -> Result, ProgramError> { - let mint_data = mint_account_info.data.borrow(); - let state = PodStateWithExtensions::::unpack(&mint_data) - .map_err(|_| Into::::into(TokenError::InvalidMint))?; - Self::get_required_account_extensions_from_unpacked_mint(mint_account_info.owner, &state) - } - - fn get_required_account_extensions_from_unpacked_mint( - token_program_id: &Pubkey, - state: &PodStateWithExtensions, - ) -> Result, ProgramError> { - check_program_account(token_program_id)?; - let mint_extensions = state.get_extension_types()?; - Ok(ExtensionType::get_required_init_account_extensions( - &mint_extensions, - )) - } -} - -/// Helper function to mostly delete an account in a test environment. We could -/// potentially muck around the bytes assuming that a vec is passed in, but that -/// would be more trouble than it's worth. -#[cfg(not(target_os = "solana"))] -fn delete_account(account_info: &AccountInfo) -> Result<(), ProgramError> { - account_info.assign(&system_program::id()); - let mut account_data = account_info.data.borrow_mut(); - let data_len = account_data.len(); - solana_program::program_memory::sol_memset(*account_data, 0, data_len); - Ok(()) -} - -/// Helper function to totally delete an account on-chain -#[cfg(target_os = "solana")] -fn delete_account(account_info: &AccountInfo) -> Result<(), ProgramError> { - account_info.assign(&system_program::id()); - account_info.realloc(0, false) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - extension::transfer_fee::instruction::initialize_transfer_fee_config, instruction::*, - state::Multisig, - }, - serial_test::serial, - solana_program::{ - account_info::IntoAccountInfo, - clock::Epoch, - instruction::Instruction, - program_error::{self, PrintProgramError}, - program_option::COption, - sysvar::{clock::Clock, rent}, - }, - solana_sdk::account::{ - create_account_for_test, create_is_signer_account_infos, Account as SolanaAccount, - }, - std::sync::{Arc, RwLock}, - }; - - lazy_static::lazy_static! { - static ref EXPECTED_DATA: Arc>> = Arc::new(RwLock::new(Vec::new())); - } - - fn set_expected_data(expected_data: Vec) { - *EXPECTED_DATA.write().unwrap() = expected_data; - } - - struct SyscallStubs {} - impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { - fn sol_log(&self, _message: &str) {} - - fn sol_invoke_signed( - &self, - _instruction: &Instruction, - _account_infos: &[AccountInfo], - _signers_seeds: &[&[&[u8]]], - ) -> ProgramResult { - Err(ProgramError::Custom(42)) // Not supported - } - - fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 { - unsafe { - *(var_addr as *mut _ as *mut Clock) = Clock::default(); - } - solana_program::entrypoint::SUCCESS - } - - fn sol_get_epoch_schedule_sysvar(&self, _var_addr: *mut u8) -> u64 { - program_error::UNSUPPORTED_SYSVAR - } - - #[allow(deprecated)] - fn sol_get_fees_sysvar(&self, _var_addr: *mut u8) -> u64 { - program_error::UNSUPPORTED_SYSVAR - } - - fn sol_get_rent_sysvar(&self, var_addr: *mut u8) -> u64 { - unsafe { - *(var_addr as *mut _ as *mut Rent) = Rent::default(); - } - solana_program::entrypoint::SUCCESS - } - - fn sol_set_return_data(&self, data: &[u8]) { - assert_eq!(&*EXPECTED_DATA.read().unwrap(), data) - } - } - - fn do_process_instruction( - instruction: Instruction, - accounts: Vec<&mut SolanaAccount>, - ) -> ProgramResult { - { - use std::sync::Once; - static ONCE: Once = Once::new(); - - ONCE.call_once(|| { - solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); - }); - } - - let mut meta = instruction - .accounts - .iter() - .zip(accounts) - .map(|(account_meta, account)| (&account_meta.pubkey, account_meta.is_signer, account)) - .collect::>(); - - let account_infos = create_is_signer_account_infos(&mut meta); - Processor::process(&instruction.program_id, &account_infos, &instruction.data) - } - - fn do_process_instruction_dups( - instruction: Instruction, - account_infos: Vec, - ) -> ProgramResult { - Processor::process(&instruction.program_id, &account_infos, &instruction.data) - } - - fn return_token_error_as_program_error() -> ProgramError { - TokenError::MintMismatch.into() - } - - fn rent_sysvar() -> SolanaAccount { - create_account_for_test(&Rent::default()) - } - - fn mint_minimum_balance() -> u64 { - Rent::default().minimum_balance(Mint::get_packed_len()) - } - - fn account_minimum_balance() -> u64 { - Rent::default().minimum_balance(Account::get_packed_len()) - } - - fn multisig_minimum_balance() -> u64 { - Rent::default().minimum_balance(Multisig::get_packed_len()) - } - - fn native_mint() -> SolanaAccount { - let mut rent_sysvar = rent_sysvar(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &crate::id()); - do_process_instruction( - initialize_mint( - &crate::id(), - &crate::native_mint::id(), - &Pubkey::default(), - None, - crate::native_mint::DECIMALS, - ) - .unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - mint_account - } - - #[test] - fn test_print_error() { - let error = return_token_error_as_program_error(); - error.print::(); - } - - #[test] - fn test_error_as_custom() { - assert_eq!( - return_token_error_as_program_error(), - ProgramError::Custom(3) - ); - } - - #[test] - fn test_unique_account_sizes() { - assert_ne!(Mint::get_packed_len(), 0); - assert_ne!(Mint::get_packed_len(), Account::get_packed_len()); - assert_ne!(Mint::get_packed_len(), Multisig::get_packed_len()); - assert_ne!(Account::get_packed_len(), 0); - assert_ne!(Account::get_packed_len(), Multisig::get_packed_len()); - assert_ne!(Multisig::get_packed_len(), 0); - } - - #[test] - fn test_initialize_mint() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = SolanaAccount::new(42, Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut mint2_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // mint is not rent exempt - assert_eq!( - Err(TokenError::NotRentExempt.into()), - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar] - ) - ); - - mint_account.lamports = mint_minimum_balance(); - - // create new mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create twice - assert_eq!( - Err(TokenError::AlreadyInUse.into()), - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2,).unwrap(), - vec![&mut mint_account, &mut rent_sysvar] - ) - ); - - // create another mint that can freeze - do_process_instruction( - initialize_mint(&program_id, &mint2_key, &owner_key, Some(&owner_key), 2).unwrap(), - vec![&mut mint2_account, &mut rent_sysvar], - ) - .unwrap(); - let mint = Mint::unpack_unchecked(&mint2_account.data).unwrap(); - assert_eq!(mint.freeze_authority, COption::Some(owner_key)); - } - - #[test] - fn test_initialize_mint2() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = SolanaAccount::new(42, Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut mint2_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - - // mint is not rent exempt - assert_eq!( - Err(TokenError::NotRentExempt.into()), - do_process_instruction( - initialize_mint2(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account] - ) - ); - - mint_account.lamports = mint_minimum_balance(); - - // create new mint - do_process_instruction( - initialize_mint2(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - // create twice - assert_eq!( - Err(TokenError::AlreadyInUse.into()), - do_process_instruction( - initialize_mint2(&program_id, &mint_key, &owner_key, None, 2,).unwrap(), - vec![&mut mint_account] - ) - ); - - // create another mint that can freeze - do_process_instruction( - initialize_mint2(&program_id, &mint2_key, &owner_key, Some(&owner_key), 2).unwrap(), - vec![&mut mint2_account], - ) - .unwrap(); - let mint = Mint::unpack_unchecked(&mint2_account.data).unwrap(); - assert_eq!(mint.freeze_authority, COption::Some(owner_key)); - } - - #[test] - fn test_initialize_mint_account() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new(42, Account::get_packed_len(), &program_id); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // account is not rent exempt - assert_eq!( - Err(TokenError::NotRentExempt.into()), - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar - ], - ) - ); - - account_account.lamports = account_minimum_balance(); - - // mint is not valid (not initialized) - assert_eq!( - Err(TokenError::InvalidMint.into()), - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar - ], - ) - ); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // mint not owned by program - let not_program_id = Pubkey::new_unique(); - mint_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar - ], - ) - ); - mint_account.owner = program_id; - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create twice - assert_eq!( - Err(TokenError::AlreadyInUse.into()), - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar - ], - ) - ); - } - - #[test] - fn test_transfer_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut account2_info: AccountInfo = (&account2_key, false, &mut account2_account).into(); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_info: AccountInfo = (&account3_key, false, &mut account3_account).into(); - let account4_key = Pubkey::new_unique(); - let mut account4_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account4_info: AccountInfo = (&account4_key, true, &mut account4_account).into(); - let multisig_key = Pubkey::new_unique(); - let mut multisig_account = SolanaAccount::new( - multisig_minimum_balance(), - Multisig::get_packed_len(), - &program_id, - ); - let multisig_info: AccountInfo = (&multisig_key, true, &mut multisig_account).into(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner_info: AccountInfo = (&owner_key, true, &mut owner_account).into(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, false, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // create another account - do_process_instruction_dups( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - account2_info.clone(), - mint_info.clone(), - owner_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // mint to account - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-owner transfer - do_process_instruction_dups( - #[allow(deprecated)] - transfer( - &program_id, - &account1_key, - &account2_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-owner TransferChecked - do_process_instruction_dups( - transfer_checked( - &program_id, - &account1_key, - &mint_key, - &account2_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-delegate transfer - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.amount = 1000; - account.delegated_amount = 1000; - account.delegate = COption::Some(account1_key); - account.owner = owner_key; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - - do_process_instruction_dups( - #[allow(deprecated)] - transfer( - &program_id, - &account1_key, - &account2_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-delegate TransferChecked - do_process_instruction_dups( - transfer_checked( - &program_id, - &account1_key, - &mint_key, - &account2_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // test destination-owner transfer - do_process_instruction_dups( - initialize_account(&program_id, &account3_key, &mint_key, &account2_key).unwrap(), - vec![ - account3_info.clone(), - mint_info.clone(), - account2_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account3_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account3_info.clone(), owner_info.clone()], - ) - .unwrap(); - - account1_info.is_signer = false; - account2_info.is_signer = true; - do_process_instruction_dups( - #[allow(deprecated)] - transfer( - &program_id, - &account3_key, - &account2_key, - &account2_key, - &[], - 500, - ) - .unwrap(), - vec![ - account3_info.clone(), - account2_info.clone(), - account2_info.clone(), - ], - ) - .unwrap(); - - // destination-owner TransferChecked - do_process_instruction_dups( - transfer_checked( - &program_id, - &account3_key, - &mint_key, - &account2_key, - &account2_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account3_info.clone(), - mint_info.clone(), - account2_info.clone(), - account2_info.clone(), - ], - ) - .unwrap(); - - // test source-multisig signer - do_process_instruction_dups( - initialize_multisig(&program_id, &multisig_key, &[&account4_key], 1).unwrap(), - vec![ - multisig_info.clone(), - rent_info.clone(), - account4_info.clone(), - ], - ) - .unwrap(); - - do_process_instruction_dups( - initialize_account(&program_id, &account4_key, &mint_key, &multisig_key).unwrap(), - vec![ - account4_info.clone(), - mint_info.clone(), - multisig_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account4_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account4_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-multisig-signer transfer - do_process_instruction_dups( - #[allow(deprecated)] - transfer( - &program_id, - &account4_key, - &account2_key, - &multisig_key, - &[&account4_key], - 500, - ) - .unwrap(), - vec![ - account4_info.clone(), - account2_info.clone(), - multisig_info.clone(), - account4_info.clone(), - ], - ) - .unwrap(); - - // source-multisig-signer TransferChecked - do_process_instruction_dups( - transfer_checked( - &program_id, - &account4_key, - &mint_key, - &account2_key, - &multisig_key, - &[&account4_key], - 500, - 2, - ) - .unwrap(), - vec![ - account4_info.clone(), - mint_info.clone(), - account2_info.clone(), - multisig_info.clone(), - account4_info.clone(), - ], - ) - .unwrap(); - } - - #[test] - fn test_transfer() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - let mismatch_key = Pubkey::new_unique(); - let mut mismatch_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut rent_sysvar = rent_sysvar(); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create mismatch account - do_process_instruction( - initialize_account(&program_id, &mismatch_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut mismatch_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&mismatch_account.data).unwrap(); - account.mint = mint2_key; - Account::pack(account, &mut mismatch_account.data).unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // missing signer - #[allow(deprecated)] - let mut instruction = transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 1000, - ) - .unwrap(); - instruction.accounts[2].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction( - instruction, - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - // mismatch mint - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &mismatch_key, - &owner_key, - &[], - 1000 - ) - .unwrap(), - vec![ - &mut account_account, - &mut mismatch_account, - &mut owner_account, - ], - ) - ); - - // missing owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &owner2_key, - &[], - 1000 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - - // account not owned by program - let not_program_id = Pubkey::new_unique(); - account_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - #[allow(deprecated)] - transfer(&program_id, &account_key, &account2_key, &owner_key, &[], 0,).unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - account_account.owner = program_id; - - // account 2 not owned by program - let not_program_id = Pubkey::new_unique(); - account2_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - #[allow(deprecated)] - transfer(&program_id, &account_key, &account2_key, &owner_key, &[], 0,).unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - account2_account.owner = program_id; - - // transfer - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 1000, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - .unwrap(); - - // insufficient funds - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - #[allow(deprecated)] - transfer(&program_id, &account_key, &account2_key, &owner_key, &[], 1).unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - // transfer half back - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account2_key, - &account_key, - &owner_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut account_account, - &mut owner_account, - ], - ) - .unwrap(); - - // incorrect decimals - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - do_process_instruction( - transfer_checked( - &program_id, - &account2_key, - &mint_key, - &account_key, - &owner_key, - &[], - 1, - 10 // <-- incorrect decimals - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut account_account, - &mut owner_account, - ], - ) - ); - - // incorrect mint - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - transfer_checked( - &program_id, - &account2_key, - &account3_key, // <-- incorrect mint - &account_key, - &owner_key, - &[], - 1, - 2 - ) - .unwrap(), - vec![ - &mut account2_account, - &mut account3_account, // <-- incorrect mint - &mut account_account, - &mut owner_account, - ], - ) - ); - // transfer rest with explicit decimals - do_process_instruction( - transfer_checked( - &program_id, - &account2_key, - &mint_key, - &account_key, - &owner_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut account_account, - &mut owner_account, - ], - ) - .unwrap(); - - // insufficient funds - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - #[allow(deprecated)] - transfer(&program_id, &account2_key, &account_key, &owner_key, &[], 1).unwrap(), - vec![ - &mut account2_account, - &mut account_account, - &mut owner_account, - ], - ) - ); - - // approve delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // not a delegate of source account - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &owner2_key, // <-- incorrect owner or delegate - &[], - 1, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - - // insufficient funds approved via delegate - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &delegate_key, - &[], - 101 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut delegate_account, - ], - ) - ); - - // transfer via delegate - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &delegate_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut delegate_account, - ], - ) - .unwrap(); - - // insufficient funds approved via delegate - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &delegate_key, - &[], - 1 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut delegate_account, - ], - ) - ); - - // transfer rest - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 900, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - .unwrap(); - - // approve delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // insufficient funds in source account via delegate - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &delegate_key, - &[], - 100 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut delegate_account, - ], - ) - ); - } - - #[test] - fn test_self_transfer() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - let account_info = (&account_key, false, &mut account_account).into_account_info(); - let account3_info = (&account3_key, false, &mut account3_account).into_account_info(); - let delegate_info = (&delegate_key, true, &mut delegate_account).into_account_info(); - let owner_info = (&owner_key, true, &mut owner_account).into_account_info(); - let owner2_info = (&owner2_key, true, &mut owner2_account).into_account_info(); - let mint_info = (&mint_key, false, &mut mint_account).into_account_info(); - - // transfer - #[allow(deprecated)] - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner_info.key, - &[], - 1000, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - - // transfer checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_info.key, - &[], - 1000, - 2, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - - // missing signer - let mut owner_no_sign_info = owner_info.clone(); - #[allow(deprecated)] - let mut instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner_no_sign_info.key, - &[], - 1000, - ) - .unwrap(); - instruction.accounts[2].is_signer = false; - owner_no_sign_info.is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner_no_sign_info.clone(), - ], - &instruction.data, - ) - ); - - // missing signer checked - let mut instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_no_sign_info.key, - &[], - 1000, - 2, - ) - .unwrap(); - instruction.accounts[3].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_no_sign_info, - ], - &instruction.data, - ) - ); - - // missing owner - #[allow(deprecated)] - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner2_info.key, - &[], - 1000, - ) - .unwrap(); - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner2_info.clone(), - ], - &instruction.data, - ) - ); - - // missing owner checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner2_info.key, - &[], - 1000, - 2, - ) - .unwrap(); - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner2_info.clone(), - ], - &instruction.data, - ) - ); - - // insufficient funds - #[allow(deprecated)] - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner_info.key, - &[], - 1001, - ) - .unwrap(); - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - - // insufficient funds checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_info.key, - &[], - 1001, - 2, - ) - .unwrap(); - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - - // incorrect decimals - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_info.key, - &[], - 1, - 10, // <-- incorrect decimals - ) - .unwrap(); - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - - // incorrect mint - let instruction = transfer_checked( - &program_id, - account_info.key, - account3_info.key, // <-- incorrect mint - account_info.key, - owner_info.key, - &[], - 1, - 2, - ) - .unwrap(); - assert_eq!( - Err(TokenError::MintMismatch.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account3_info.clone(), // <-- incorrect mint - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - - // approve delegate - let instruction = approve( - &program_id, - account_info.key, - delegate_info.key, - owner_info.key, - &[], - 100, - ) - .unwrap(); - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - delegate_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - .unwrap(); - - // delegate transfer - #[allow(deprecated)] - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - delegate_info.key, - &[], - 100, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - delegate_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - assert_eq!(account.delegated_amount, 100); - - // delegate transfer checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - delegate_info.key, - &[], - 100, - 2, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - delegate_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - assert_eq!(account.delegated_amount, 100); - - // delegate insufficient funds - #[allow(deprecated)] - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - delegate_info.key, - &[], - 101, - ) - .unwrap(); - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - delegate_info.clone(), - ], - &instruction.data, - ) - ); - - // delegate insufficient funds checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - delegate_info.key, - &[], - 101, - 2, - ) - .unwrap(); - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - delegate_info.clone(), - ], - &instruction.data, - ) - ); - - // owner transfer with delegate assigned - #[allow(deprecated)] - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner_info.key, - &[], - 1000, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - - // owner transfer with delegate assigned checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_info.key, - &[], - 1000, - 2, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - } - - #[test] - fn test_mintable_token_with_zero_supply() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint-able token with zero supply - let decimals = 2; - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, decimals).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!( - mint, - Mint { - mint_authority: COption::Some(owner_key), - supply: 0, - decimals, - is_initialized: true, - freeze_authority: COption::None, - } - ); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - let _ = Mint::unpack(&mint_account.data).unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // mint to 2, with incorrect decimals - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - do_process_instruction( - mint_to_checked( - &program_id, - &mint_key, - &account_key, - &owner_key, - &[], - 42, - decimals + 1 - ) - .unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - ); - - let _ = Mint::unpack(&mint_account.data).unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // mint to 2 - do_process_instruction( - mint_to_checked( - &program_id, - &mint_key, - &account_key, - &owner_key, - &[], - 42, - decimals, - ) - .unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - let _ = Mint::unpack(&mint_account.data).unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 84); - } - - #[test] - fn test_approve_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_info: AccountInfo = (&account2_key, false, &mut account2_account).into(); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_info: AccountInfo = (&account3_key, true, &mut account3_account).into(); - let multisig_key = Pubkey::new_unique(); - let mut multisig_account = SolanaAccount::new( - multisig_minimum_balance(), - Multisig::get_packed_len(), - &program_id, - ); - let multisig_info: AccountInfo = (&multisig_key, true, &mut multisig_account).into(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner_info: AccountInfo = (&owner_key, true, &mut owner_account).into(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, false, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // create another account - do_process_instruction_dups( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - account2_info.clone(), - mint_info.clone(), - owner_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // mint to account - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-owner approve - do_process_instruction_dups( - approve( - &program_id, - &account1_key, - &account2_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-owner approve_checked - do_process_instruction_dups( - approve_checked( - &program_id, - &account1_key, - &mint_key, - &account2_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-owner revoke - do_process_instruction_dups( - revoke(&program_id, &account1_key, &account1_key, &[]).unwrap(), - vec![account1_info.clone(), account1_info.clone()], - ) - .unwrap(); - - // test source-multisig signer - do_process_instruction_dups( - initialize_multisig(&program_id, &multisig_key, &[&account3_key], 1).unwrap(), - vec![ - multisig_info.clone(), - rent_info.clone(), - account3_info.clone(), - ], - ) - .unwrap(); - - do_process_instruction_dups( - initialize_account(&program_id, &account3_key, &mint_key, &multisig_key).unwrap(), - vec![ - account3_info.clone(), - mint_info.clone(), - multisig_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account3_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account3_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-multisig-signer approve - do_process_instruction_dups( - approve( - &program_id, - &account3_key, - &account2_key, - &multisig_key, - &[&account3_key], - 500, - ) - .unwrap(), - vec![ - account3_info.clone(), - account2_info.clone(), - multisig_info.clone(), - account3_info.clone(), - ], - ) - .unwrap(); - - // source-multisig-signer approve_checked - do_process_instruction_dups( - approve_checked( - &program_id, - &account3_key, - &mint_key, - &account2_key, - &multisig_key, - &[&account3_key], - 500, - 2, - ) - .unwrap(), - vec![ - account3_info.clone(), - mint_info.clone(), - account2_info.clone(), - multisig_info.clone(), - account3_info.clone(), - ], - ) - .unwrap(); - - // source-owner multisig-signer - do_process_instruction_dups( - revoke(&program_id, &account3_key, &multisig_key, &[&account3_key]).unwrap(), - vec![ - account3_info.clone(), - multisig_info.clone(), - account3_info.clone(), - ], - ) - .unwrap(); - - // approve to source - do_process_instruction_dups( - approve_checked( - &program_id, - &account2_key, - &mint_key, - &account2_key, - &owner_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account2_info.clone(), - mint_info.clone(), - account2_info.clone(), - owner_info.clone(), - ], - ) - .unwrap(); - - // source-delegate revoke, force account2 to be a signer - let account2_info: AccountInfo = (&account2_key, true, &mut account2_account).into(); - do_process_instruction_dups( - revoke(&program_id, &account2_key, &account2_key, &[]).unwrap(), - vec![account2_info.clone(), account2_info.clone()], - ) - .unwrap(); - } - - #[test] - fn test_approve() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // missing signer - let mut instruction = approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100, - ) - .unwrap(); - instruction.accounts[2].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction( - instruction, - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - ); - - // no owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner2_key, - &[], - 100 - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner2_account, - ], - ) - ); - - // approve delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // approve delegate 2, with incorrect decimals - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - do_process_instruction( - approve_checked( - &program_id, - &account_key, - &mint_key, - &delegate_key, - &owner_key, - &[], - 100, - 0 // <-- incorrect decimals - ) - .unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account, - &mut owner_account, - ], - ) - ); - - // approve delegate 2, with incorrect mint - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - approve_checked( - &program_id, - &account_key, - &account2_key, // <-- bad mint - &delegate_key, - &owner_key, - &[], - 100, - 0 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, // <-- bad mint - &mut delegate_account, - &mut owner_account, - ], - ) - ); - - // approve delegate 2 - do_process_instruction( - approve_checked( - &program_id, - &account_key, - &mint_key, - &delegate_key, - &owner_key, - &[], - 100, - 2, - ) - .unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // revoke delegate - do_process_instruction( - revoke(&program_id, &account_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - - // approve delegate 3 - do_process_instruction( - approve_checked( - &program_id, - &account_key, - &mint_key, - &delegate_key, - &owner_key, - &[], - 100, - 2, - ) - .unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // revoke by delegate - do_process_instruction( - revoke(&program_id, &account_key, &delegate_key, &[]).unwrap(), - vec![&mut account_account, &mut delegate_account], - ) - .unwrap(); - - // fails the second time - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - revoke(&program_id, &account_key, &delegate_key, &[]).unwrap(), - vec![&mut account_account, &mut delegate_account], - ) - ); - } - - #[test] - fn test_set_authority_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, true, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &mint_key, Some(&mint_key), 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // set mint_authority when currently self - do_process_instruction_dups( - set_authority( - &program_id, - &mint_key, - Some(&owner_key), - AuthorityType::MintTokens, - &mint_key, - &[], - ) - .unwrap(), - vec![mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // set freeze_authority when currently self - do_process_instruction_dups( - set_authority( - &program_id, - &mint_key, - Some(&owner_key), - AuthorityType::FreezeAccount, - &mint_key, - &[], - ) - .unwrap(), - vec![mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // set account owner when currently self - do_process_instruction_dups( - set_authority( - &program_id, - &account1_key, - Some(&owner_key), - AuthorityType::AccountOwner, - &account1_key, - &[], - ) - .unwrap(), - vec![account1_info.clone(), account1_info.clone()], - ) - .unwrap(); - - // set close_authority when currently self - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.close_authority = COption::Some(account1_key); - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - - do_process_instruction_dups( - set_authority( - &program_id, - &account1_key, - Some(&owner_key), - AuthorityType::CloseAccount, - &account1_key, - &[], - ) - .unwrap(), - vec![account1_info.clone(), account1_info.clone()], - ) - .unwrap(); - } - - #[test] - fn test_set_authority() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let owner3_key = Pubkey::new_unique(); - let mut owner3_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut mint2_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create new mint with owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create mint with owner and freeze_authority - do_process_instruction( - initialize_mint(&program_id, &mint2_key, &owner_key, Some(&owner_key), 2).unwrap(), - vec![&mut mint2_account, &mut rent_sysvar], - ) - .unwrap(); - - // invalid account - assert_eq!( - Err(ProgramError::InvalidAccountData), - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::AccountOwner, - &owner_key, - &[] - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - ); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint2_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint2_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // missing owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner_key), - AuthorityType::AccountOwner, - &owner2_key, - &[] - ) - .unwrap(), - vec![&mut account_account, &mut owner2_account], - ) - ); - - // owner did not sign - let mut instruction = set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::AccountOwner, - &owner_key, - &[], - ) - .unwrap(); - instruction.accounts[1].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction(instruction, vec![&mut account_account, &mut owner_account,],) - ); - - // wrong authority type - assert_eq!( - Err(TokenError::AuthorityTypeNotSupported.into()), - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::FreezeAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - ); - - // account owner may not be set to None - assert_eq!( - Err(TokenError::InvalidInstruction.into()), - do_process_instruction( - set_authority( - &program_id, - &account_key, - None, - AuthorityType::AccountOwner, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - ); - - // set delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &owner2_key, - &owner_key, - &[], - u64::MAX, - ) - .unwrap(), - vec![ - &mut account_account, - &mut owner2_account, - &mut owner_account, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.delegate, COption::Some(owner2_key)); - assert_eq!(account.delegated_amount, u64::MAX); - - // set owner - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner3_key), - AuthorityType::AccountOwner, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - - // check delegate cleared - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.delegate, COption::None); - assert_eq!(account.delegated_amount, 0); - - // set owner without existing delegate - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::AccountOwner, - &owner3_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner3_account], - ) - .unwrap(); - - // set close_authority - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::CloseAccount, - &owner2_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner2_account], - ) - .unwrap(); - - // close_authority may be set to None - do_process_instruction( - set_authority( - &program_id, - &account_key, - None, - AuthorityType::CloseAccount, - &owner2_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner2_account], - ) - .unwrap(); - - // wrong owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - set_authority( - &program_id, - &mint_key, - Some(&owner3_key), - AuthorityType::MintTokens, - &owner2_key, - &[] - ) - .unwrap(), - vec![&mut mint_account, &mut owner2_account], - ) - ); - - // owner did not sign - let mut instruction = set_authority( - &program_id, - &mint_key, - Some(&owner2_key), - AuthorityType::MintTokens, - &owner_key, - &[], - ) - .unwrap(); - instruction.accounts[1].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction(instruction, vec![&mut mint_account, &mut owner_account],) - ); - - // cannot freeze - assert_eq!( - Err(TokenError::MintCannotFreeze.into()), - do_process_instruction( - set_authority( - &program_id, - &mint_key, - Some(&owner2_key), - AuthorityType::FreezeAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint_account, &mut owner_account], - ) - ); - - // set owner - do_process_instruction( - set_authority( - &program_id, - &mint_key, - Some(&owner2_key), - AuthorityType::MintTokens, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint_account, &mut owner_account], - ) - .unwrap(); - - // set owner to None - do_process_instruction( - set_authority( - &program_id, - &mint_key, - None, - AuthorityType::MintTokens, - &owner2_key, - &[], - ) - .unwrap(), - vec![&mut mint_account, &mut owner2_account], - ) - .unwrap(); - - // test unsetting mint_authority is one-way operation - assert_eq!( - Err(TokenError::FixedSupply.into()), - do_process_instruction( - set_authority( - &program_id, - &mint2_key, - Some(&owner2_key), - AuthorityType::MintTokens, - &owner_key, - &[] - ) - .unwrap(), - vec![&mut mint_account, &mut owner_account], - ) - ); - - // set freeze_authority - do_process_instruction( - set_authority( - &program_id, - &mint2_key, - Some(&owner2_key), - AuthorityType::FreezeAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint2_account, &mut owner_account], - ) - .unwrap(); - - // test unsetting freeze_authority is one-way operation - do_process_instruction( - set_authority( - &program_id, - &mint2_key, - None, - AuthorityType::FreezeAccount, - &owner2_key, - &[], - ) - .unwrap(), - vec![&mut mint2_account, &mut owner2_account], - ) - .unwrap(); - - assert_eq!( - Err(TokenError::MintCannotFreeze.into()), - do_process_instruction( - set_authority( - &program_id, - &mint2_key, - Some(&owner2_key), - AuthorityType::FreezeAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint2_account, &mut owner2_account], - ) - ); - } - - #[test] - fn test_set_authority_with_immutable_owner_extension() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - - let account_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::ImmutableOwner]) - .unwrap(); - let mut account_account = SolanaAccount::new( - Rent::default().minimum_balance(account_len), - account_len, - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint - assert_eq!( - Ok(()), - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - ); - - // create account - assert_eq!( - Ok(()), - do_process_instruction( - initialize_immutable_owner(&program_id, &account_key).unwrap(), - vec![&mut account_account], - ) - ); - assert_eq!( - Ok(()), - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - ); - - // Immutable Owner extension blocks account owner authority changes - assert_eq!( - Err(TokenError::ImmutableOwner.into()), - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::AccountOwner, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - ); - } - - #[test] - fn test_mint_to_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner_info: AccountInfo = (&owner_key, true, &mut owner_account).into(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, true, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &mint_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &owner_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - owner_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // mint_to when mint_authority is self - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &mint_key, &[], 42).unwrap(), - vec![mint_info.clone(), account1_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // mint_to_checked when mint_authority is self - do_process_instruction_dups( - mint_to_checked(&program_id, &mint_key, &account1_key, &mint_key, &[], 42, 2).unwrap(), - vec![mint_info.clone(), account1_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // mint_to when mint_authority is account owner - let mut mint = Mint::unpack_unchecked(&mint_info.data.borrow()).unwrap(); - mint.mint_authority = COption::Some(account1_key); - Mint::pack(mint, &mut mint_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - mint_to( - &program_id, - &mint_key, - &account1_key, - &account1_key, - &[], - 42, - ) - .unwrap(), - vec![ - mint_info.clone(), - account1_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // mint_to_checked when mint_authority is account owner - do_process_instruction_dups( - mint_to( - &program_id, - &mint_key, - &account1_key, - &account1_key, - &[], - 42, - ) - .unwrap(), - vec![ - mint_info.clone(), - account1_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - } - - #[test] - fn test_mint_to() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mismatch_key = Pubkey::new_unique(); - let mut mismatch_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let uninitialized_key = Pubkey::new_unique(); - let mut uninitialized_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut rent_sysvar = rent_sysvar(); - - // create new mint with owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create mismatch account - do_process_instruction( - initialize_account(&program_id, &mismatch_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut mismatch_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&mismatch_account.data).unwrap(); - account.mint = mint2_key; - Account::pack(account, &mut mismatch_account.data).unwrap(); - - // mint to - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!(mint.supply, 42); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // mint to another account to test supply accumulation - do_process_instruction( - mint_to(&program_id, &mint_key, &account2_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut account2_account, &mut owner_account], - ) - .unwrap(); - - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!(mint.supply, 84); - let account = Account::unpack_unchecked(&account2_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // missing signer - let mut instruction = - mint_to(&program_id, &mint_key, &account2_key, &owner_key, &[], 42).unwrap(); - instruction.accounts[2].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction( - instruction, - vec![&mut mint_account, &mut account2_account, &mut owner_account], - ) - ); - - // mismatch account - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - mint_to(&program_id, &mint_key, &mismatch_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut mismatch_account, &mut owner_account], - ) - ); - - // missing owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - mint_to(&program_id, &mint_key, &account2_key, &owner2_key, &[], 42).unwrap(), - vec![ - &mut mint_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - - // mint not owned by program - let not_program_id = Pubkey::new_unique(); - mint_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 0).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - ); - mint_account.owner = program_id; - - // account not owned by program - let not_program_id = Pubkey::new_unique(); - account_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 0).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - ); - account_account.owner = program_id; - - // uninitialized destination account - assert_eq!( - Err(ProgramError::UninitializedAccount), - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &uninitialized_key, - &owner_key, - &[], - 42 - ) - .unwrap(), - vec![ - &mut mint_account, - &mut uninitialized_account, - &mut owner_account, - ], - ) - ); - - // unset mint_authority and test minting fails - do_process_instruction( - set_authority( - &program_id, - &mint_key, - None, - AuthorityType::MintTokens, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint_account, &mut owner_account], - ) - .unwrap(); - assert_eq!( - Err(TokenError::FixedSupply.into()), - do_process_instruction( - mint_to(&program_id, &mint_key, &account2_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut account2_account, &mut owner_account], - ) - ); - } - - #[test] - fn test_burn_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner_info: AccountInfo = (&owner_key, true, &mut owner_account).into(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, true, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // mint to account - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-owner burn - do_process_instruction_dups( - burn( - &program_id, - &mint_key, - &account1_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-owner burn_checked - do_process_instruction_dups( - burn_checked( - &program_id, - &account1_key, - &mint_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // mint-owner burn - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.owner = mint_key; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - burn(&program_id, &account1_key, &mint_key, &mint_key, &[], 500).unwrap(), - vec![account1_info.clone(), mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // mint-owner burn_checked - do_process_instruction_dups( - burn_checked( - &program_id, - &account1_key, - &mint_key, - &mint_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![account1_info.clone(), mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // source-delegate burn - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.delegated_amount = 1000; - account.delegate = COption::Some(account1_key); - account.owner = owner_key; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - burn( - &program_id, - &account1_key, - &mint_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-delegate burn_checked - do_process_instruction_dups( - burn_checked( - &program_id, - &account1_key, - &mint_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // mint-delegate burn - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.delegated_amount = 1000; - account.delegate = COption::Some(mint_key); - account.owner = owner_key; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - burn(&program_id, &account1_key, &mint_key, &mint_key, &[], 500).unwrap(), - vec![account1_info.clone(), mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // mint-delegate burn_checked - do_process_instruction_dups( - burn_checked( - &program_id, - &account1_key, - &mint_key, - &mint_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![account1_info.clone(), mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - } - - #[test] - fn test_burn() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - let mismatch_key = Pubkey::new_unique(); - let mut mismatch_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut rent_sysvar = rent_sysvar(); - - // create new mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create mismatch account - do_process_instruction( - initialize_account(&program_id, &mismatch_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut mismatch_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // mint to mismatch account and change mint key - do_process_instruction( - mint_to(&program_id, &mint_key, &mismatch_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut mismatch_account, &mut owner_account], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&mismatch_account.data).unwrap(); - account.mint = mint2_key; - Account::pack(account, &mut mismatch_account.data).unwrap(); - - // missing signer - let mut instruction = - burn(&program_id, &account_key, &mint_key, &delegate_key, &[], 42).unwrap(); - instruction.accounts[1].is_signer = false; - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - instruction, - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account - ], - ) - ); - - // missing owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner2_key, &[], 42).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // account not owned by program - let not_program_id = Pubkey::new_unique(); - account_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 0).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - account_account.owner = program_id; - - // mint not owned by program - let not_program_id = Pubkey::new_unique(); - mint_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 0).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - mint_account.owner = program_id; - - // mint mismatch - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - burn(&program_id, &mismatch_key, &mint_key, &owner_key, &[], 42).unwrap(), - vec![&mut mismatch_account, &mut mint_account, &mut owner_account], - ) - ); - - // burn - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 21).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - - // burn_checked, with incorrect decimals - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - do_process_instruction( - burn_checked(&program_id, &account_key, &mint_key, &owner_key, &[], 21, 3).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - - // burn_checked - do_process_instruction( - burn_checked(&program_id, &account_key, &mint_key, &owner_key, &[], 21, 2).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!(mint.supply, 2000 - 42); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 1000 - 42); - - // insufficient funds - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - burn( - &program_id, - &account_key, - &mint_key, - &owner_key, - &[], - 100_000_000 - ) - .unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - - // approve delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 84, - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // not a delegate of source account - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - burn( - &program_id, - &account_key, - &mint_key, - &owner2_key, // <-- incorrect owner or delegate - &[], - 1, - ) - .unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // insufficient funds approved via delegate - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &delegate_key, &[], 85).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account - ], - ) - ); - - // burn via delegate - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &delegate_key, &[], 84).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account, - ], - ) - .unwrap(); - - // match - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!(mint.supply, 2000 - 42 - 84); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 1000 - 42 - 84); - - // insufficient funds approved via delegate - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &delegate_key, &[], 1).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account - ], - ) - ); - } - - #[test] - fn test_multisig() { - let program_id = crate::id(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let account_key = Pubkey::new_unique(); - let mut account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let multisig_key = Pubkey::new_unique(); - let mut multisig_account = SolanaAccount::new(42, Multisig::get_packed_len(), &program_id); - let multisig_delegate_key = Pubkey::new_unique(); - let mut multisig_delegate_account = SolanaAccount::new( - multisig_minimum_balance(), - Multisig::get_packed_len(), - &program_id, - ); - let signer_keys = vec![Pubkey::new_unique(); MAX_SIGNERS]; - let signer_key_refs: Vec<&Pubkey> = signer_keys.iter().collect(); - let mut signer_accounts = vec![SolanaAccount::new(0, 0, &program_id); MAX_SIGNERS]; - let mut rent_sysvar = rent_sysvar(); - - // multisig is not rent exempt - let account_info_iter = &mut signer_accounts.iter_mut(); - assert_eq!( - Err(TokenError::NotRentExempt.into()), - do_process_instruction( - initialize_multisig(&program_id, &multisig_key, &[&signer_keys[0]], 1).unwrap(), - vec![ - &mut multisig_account, - &mut rent_sysvar, - account_info_iter.next().unwrap(), - ], - ) - ); - - multisig_account.lamports = multisig_minimum_balance(); - let mut multisig_account2 = multisig_account.clone(); - - // single signer - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - initialize_multisig(&program_id, &multisig_key, &[&signer_keys[0]], 1).unwrap(), - vec![ - &mut multisig_account, - &mut rent_sysvar, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // single signer using `initialize_multisig2` - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - initialize_multisig2(&program_id, &multisig_key, &[&signer_keys[0]], 1).unwrap(), - vec![&mut multisig_account2, account_info_iter.next().unwrap()], - ) - .unwrap(); - - // multiple signer - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - initialize_multisig( - &program_id, - &multisig_delegate_key, - &signer_key_refs, - MAX_SIGNERS as u8, - ) - .unwrap(), - vec![ - &mut multisig_delegate_account, - &mut rent_sysvar, - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // create new mint with multisig owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &multisig_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account with multisig owner - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &multisig_key).unwrap(), - vec![ - &mut account, - &mut mint_account, - &mut multisig_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account with multisig owner - do_process_instruction( - initialize_account( - &program_id, - &account2_key, - &mint_key, - &multisig_delegate_key, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut multisig_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account_key, - &multisig_key, - &[&signer_keys[0]], - 1000, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // approve - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - approve( - &program_id, - &account_key, - &multisig_delegate_key, - &multisig_key, - &[&signer_keys[0]], - 100, - ) - .unwrap(), - vec![ - &mut account, - &mut multisig_delegate_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // transfer - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &multisig_key, - &[&signer_keys[0]], - 42, - ) - .unwrap(), - vec![ - &mut account, - &mut account2_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // transfer via delegate - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &multisig_delegate_key, - &signer_key_refs, - 42, - ) - .unwrap(), - vec![ - &mut account, - &mut account2_account, - &mut multisig_delegate_account, - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // mint to - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account2_key, - &multisig_key, - &[&signer_keys[0]], - 42, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account2_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // burn - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - burn( - &program_id, - &account_key, - &mint_key, - &multisig_key, - &[&signer_keys[0]], - 42, - ) - .unwrap(), - vec![ - &mut account, - &mut mint_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // burn via delegate - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - burn( - &program_id, - &account_key, - &mint_key, - &multisig_delegate_key, - &signer_key_refs, - 42, - ) - .unwrap(), - vec![ - &mut account, - &mut mint_account, - &mut multisig_delegate_account, - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // freeze account - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mint2_key = Pubkey::new_unique(); - let mut mint2_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - do_process_instruction( - initialize_mint( - &program_id, - &mint2_key, - &multisig_key, - Some(&multisig_key), - 2, - ) - .unwrap(), - vec![&mut mint2_account, &mut rent_sysvar], - ) - .unwrap(); - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint2_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint2_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - mint_to( - &program_id, - &mint2_key, - &account3_key, - &multisig_key, - &[&signer_keys[0]], - 1000, - ) - .unwrap(), - vec![ - &mut mint2_account, - &mut account3_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - freeze_account( - &program_id, - &account3_key, - &mint2_key, - &multisig_key, - &[&signer_keys[0]], - ) - .unwrap(), - vec![ - &mut account3_account, - &mut mint2_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // do SetAuthority on mint - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - set_authority( - &program_id, - &mint_key, - Some(&owner_key), - AuthorityType::MintTokens, - &multisig_key, - &[&signer_keys[0]], - ) - .unwrap(), - vec![ - &mut mint_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // do SetAuthority on account - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner_key), - AuthorityType::AccountOwner, - &multisig_key, - &[&signer_keys[0]], - ) - .unwrap(), - vec![ - &mut account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - } - - #[test] - fn test_validate_owner() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let account_to_validate = Pubkey::new_unique(); - let mut signer_keys = [Pubkey::default(); MAX_SIGNERS]; - for signer_key in signer_keys.iter_mut().take(MAX_SIGNERS) { - *signer_key = Pubkey::new_unique(); - } - let mut signer_lamports = 0; - let mut signer_data = vec![]; - let mut signers = vec![ - AccountInfo::new( - &owner_key, - true, - false, - &mut signer_lamports, - &mut signer_data, - &program_id, - false, - Epoch::default(), - ); - MAX_SIGNERS + 1 - ]; - for (signer, key) in signers.iter_mut().zip(&signer_keys) { - signer.key = key; - } - let mut lamports = 0; - let mut data = vec![0; Multisig::get_packed_len()]; - let mut multisig = Multisig::unpack_unchecked(&data).unwrap(); - multisig.m = MAX_SIGNERS as u8; - multisig.n = MAX_SIGNERS as u8; - multisig.signers = signer_keys; - multisig.is_initialized = true; - Multisig::pack(multisig, &mut data).unwrap(); - let owner_account_info = AccountInfo::new( - &owner_key, - false, - false, - &mut lamports, - &mut data, - &program_id, - false, - Epoch::default(), - ); - - // no multisig, but the account is its own authority, and data is mutably - // borrowed - { - let mut lamports = 0; - let mut data = vec![0; Account::get_packed_len()]; - let mut account = Account::unpack_unchecked(&data).unwrap(); - account.owner = account_to_validate; - Account::pack(account, &mut data).unwrap(); - let account_info = AccountInfo::new( - &account_to_validate, - true, - false, - &mut lamports, - &mut data, - &program_id, - false, - Epoch::default(), - ); - let account_info_data_len = account_info.data_len(); - let mut borrowed_data = account_info.try_borrow_mut_data().unwrap(); - Processor::validate_owner( - &program_id, - &account_to_validate, - &account_info, - account_info_data_len, - &[], - ) - .unwrap(); - // modify the data to be sure that it wasn't silently dropped by the compiler - borrowed_data[0] = 1; - } - - // full 11 of 11 - Processor::validate_owner( - &program_id, - &owner_key, - &owner_account_info, - owner_account_info.data_len(), - &signers, - ) - .unwrap(); - - // 1 of 11 - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 1; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - Processor::validate_owner( - &program_id, - &owner_key, - &owner_account_info, - owner_account_info.data_len(), - &signers, - ) - .unwrap(); - - // 2:1 - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 2; - multisig.n = 1; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner( - &program_id, - &owner_key, - &owner_account_info, - owner_account_info.data_len(), - &signers - ) - ); - - // 0:11 - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 0; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - Processor::validate_owner( - &program_id, - &owner_key, - &owner_account_info, - owner_account_info.data_len(), - &signers, - ) - .unwrap(); - - // 2:11 but 0 provided - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 2; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner( - &program_id, - &owner_key, - &owner_account_info, - owner_account_info.data_len(), - &[] - ) - ); - // 2:11 but 1 provided - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 2; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner( - &program_id, - &owner_key, - &owner_account_info, - owner_account_info.data_len(), - &signers[0..1] - ) - ); - - // 2:11, 2 from middle provided - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 2; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - Processor::validate_owner( - &program_id, - &owner_key, - &owner_account_info, - owner_account_info.data_len(), - &signers[5..7], - ) - .unwrap(); - - // 11:11, one is not a signer - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 11; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - signers[5].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner( - &program_id, - &owner_key, - &owner_account_info, - owner_account_info.data_len(), - &signers - ) - ); - signers[5].is_signer = true; - - // 11:11, single signer signs multiple times - { - let mut signer_lamports = 0; - let mut signer_data = vec![]; - let signers = vec![ - AccountInfo::new( - &signer_keys[5], - true, - false, - &mut signer_lamports, - &mut signer_data, - &program_id, - false, - Epoch::default(), - ); - MAX_SIGNERS + 1 - ]; - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 11; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner( - &program_id, - &owner_key, - &owner_account_info, - owner_account_info.data_len(), - &signers - ) - ); - } - } - - #[test] - fn test_owner_close_account_dups() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, false, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - let to_close_key = Pubkey::new_unique(); - let mut to_close_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let to_close_account_info: AccountInfo = - (&to_close_key, true, &mut to_close_account).into(); - let destination_account_key = Pubkey::new_unique(); - let mut destination_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let destination_account_info: AccountInfo = - (&destination_account_key, true, &mut destination_account).into(); - // create account - do_process_instruction_dups( - initialize_account(&program_id, &to_close_key, &mint_key, &to_close_key).unwrap(), - vec![ - to_close_account_info.clone(), - mint_info.clone(), - to_close_account_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // source-owner close - do_process_instruction_dups( - close_account( - &program_id, - &to_close_key, - &destination_account_key, - &to_close_key, - &[], - ) - .unwrap(), - vec![ - to_close_account_info.clone(), - destination_account_info.clone(), - to_close_account_info.clone(), - ], - ) - .unwrap(); - assert_eq!(*to_close_account_info.data.borrow(), &[0u8; Account::LEN]); - } - - #[test] - fn test_close_authority_close_account_dups() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, false, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - let to_close_key = Pubkey::new_unique(); - let mut to_close_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let to_close_account_info: AccountInfo = - (&to_close_key, true, &mut to_close_account).into(); - let destination_account_key = Pubkey::new_unique(); - let mut destination_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let destination_account_info: AccountInfo = - (&destination_account_key, true, &mut destination_account).into(); - // create account - do_process_instruction_dups( - initialize_account(&program_id, &to_close_key, &mint_key, &to_close_key).unwrap(), - vec![ - to_close_account_info.clone(), - mint_info.clone(), - to_close_account_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&to_close_account_info.data.borrow()).unwrap(); - account.close_authority = COption::Some(to_close_key); - account.owner = owner_key; - Account::pack(account, &mut to_close_account_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - close_account( - &program_id, - &to_close_key, - &destination_account_key, - &to_close_key, - &[], - ) - .unwrap(), - vec![ - to_close_account_info.clone(), - destination_account_info.clone(), - to_close_account_info.clone(), - ], - ) - .unwrap(); - assert_eq!(*to_close_account_info.data.borrow(), &[0u8; Account::LEN]); - } - - #[test] - fn test_close_account() { - let program_id = crate::id(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance() + 42, - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mut rent_sysvar = rent_sysvar(); - - // uninitialized - assert_eq!( - Err(ProgramError::UninitializedAccount), - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner2_account, - ], - ) - ); - - // initialize and mint to non-native account - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 42).unwrap(), - vec![ - &mut mint_account, - &mut account_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // initialize native account - do_process_instruction( - initialize_account( - &program_id, - &account2_key, - &crate::native_mint::id(), - &owner_key, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account2_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 42); - - // close non-native account with balance - assert_eq!( - Err(TokenError::NonNativeHasBalance.into()), - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner_account, - ], - ) - ); - assert_eq!(account_account.lamports, account_minimum_balance()); - - // empty account - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 42).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - - // wrong owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner2_account, - ], - ) - ); - - // close account - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner_account, - ], - ) - .unwrap(); - assert_eq!(account_account.lamports, 0); - assert_eq!(account3_account.lamports, 2 * account_minimum_balance()); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 0); - - // fund and initialize new non-native account to test close authority - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - account_account.lamports = 2; - - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::CloseAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - - // account owner cannot authorize close if close_authority is set - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner_account, - ], - ) - ); - - // close non-native account with close_authority - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner2_account, - ], - ) - .unwrap(); - assert_eq!(account_account.lamports, 0); - assert_eq!(account3_account.lamports, 2 * account_minimum_balance() + 2); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 0); - - // close native account - do_process_instruction( - close_account(&program_id, &account2_key, &account3_key, &owner_key, &[]).unwrap(), - vec![ - &mut account2_account, - &mut account3_account, - &mut owner_account, - ], - ) - .unwrap(); - assert_eq!(account2_account.data, [0u8; Account::LEN]); - assert_eq!( - account3_account.lamports, - 3 * account_minimum_balance() + 2 + 42 - ); - } - - #[test] - fn test_native_token() { - let program_id = crate::id(); - let mut mint_account = native_mint(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance() + 40, - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new(account_minimum_balance(), 0, &program_id); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let owner3_key = Pubkey::new_unique(); - let mut rent_sysvar = rent_sysvar(); - - // initialize native account - do_process_instruction( - initialize_account( - &program_id, - &account_key, - &crate::native_mint::id(), - &owner_key, - ) - .unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 40); - - // initialize native account - do_process_instruction( - initialize_account( - &program_id, - &account2_key, - &crate::native_mint::id(), - &owner_key, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account2_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 0); - - // mint_to unsupported - assert_eq!( - Err(TokenError::NativeNotSupported.into()), - do_process_instruction( - mint_to( - &program_id, - &crate::native_mint::id(), - &account_key, - &owner_key, - &[], - 42 - ) - .unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - ); - - // burn unsupported - let bogus_mint_key = Pubkey::new_unique(); - let mut bogus_mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - do_process_instruction( - initialize_mint(&program_id, &bogus_mint_key, &owner_key, None, 2).unwrap(), - vec![&mut bogus_mint_account, &mut rent_sysvar], - ) - .unwrap(); - - assert_eq!( - Err(TokenError::NativeNotSupported.into()), - do_process_instruction( - burn( - &program_id, - &account_key, - &bogus_mint_key, - &owner_key, - &[], - 42 - ) - .unwrap(), - vec![ - &mut account_account, - &mut bogus_mint_account, - &mut owner_account - ], - ) - ); - - // ensure can't transfer below rent-exempt reserve - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 50, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - // transfer between native accounts - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 40, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - .unwrap(); - assert_eq!(account_account.lamports, account_minimum_balance()); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 0); - assert_eq!(account2_account.lamports, account_minimum_balance() + 40); - let account = Account::unpack_unchecked(&account2_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 40); - - // set close authority - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner3_key), - AuthorityType::CloseAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.close_authority, COption::Some(owner3_key)); - - // set new account owner - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::AccountOwner, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - - // close authority cleared - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.close_authority, COption::None); - - // close native account - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner2_account, - ], - ) - .unwrap(); - assert_eq!(account_account.lamports, 0); - assert_eq!(account3_account.lamports, 2 * account_minimum_balance()); - assert_eq!(account_account.data, [0u8; Account::LEN]); - } - - #[test] - fn test_overflow() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_owner_key = Pubkey::new_unique(); - let mut mint_owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create new mint with owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &mint_owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create an account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner2_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner2_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint the max to an account - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account_key, - &mint_owner_key, - &[], - u64::MAX, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account_account, - &mut mint_owner_account, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, u64::MAX); - - // attempt to mint one more to account - assert_eq!( - Err(TokenError::Overflow.into()), - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account_key, - &mint_owner_key, - &[], - 1, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account_account, - &mut mint_owner_account, - ], - ) - ); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, u64::MAX); - - // attempt to mint one more to the other account - assert_eq!( - Err(TokenError::Overflow.into()), - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account2_key, - &mint_owner_key, - &[], - 1, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account2_account, - &mut mint_owner_account, - ], - ) - ); - - // burn some of the supply - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 100).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, u64::MAX - 100); - - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account_key, - &mint_owner_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account_account, - &mut mint_owner_account, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, u64::MAX); - - // manipulate account balance to attempt overflow transfer - let mut account = Account::unpack_unchecked(&account2_account.data).unwrap(); - account.amount = 1; - Account::pack(account, &mut account2_account.data).unwrap(); - - assert_eq!( - Err(TokenError::Overflow.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account2_key, - &account_key, - &owner2_key, - &[], - 1, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut account_account, - &mut owner2_account, - ], - ) - ); - } - - #[test] - fn test_frozen() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create new mint and fund first account - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // fund first account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // no transfer if either account is frozen - let mut account = Account::unpack_unchecked(&account2_account.data).unwrap(); - account.state = AccountState::Frozen; - Account::pack(account, &mut account2_account.data).unwrap(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - let mut account = Account::unpack_unchecked(&account_account.data).unwrap(); - account.state = AccountState::Initialized; - Account::pack(account, &mut account_account.data).unwrap(); - let mut account = Account::unpack_unchecked(&account2_account.data).unwrap(); - account.state = AccountState::Frozen; - Account::pack(account, &mut account2_account.data).unwrap(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - #[allow(deprecated)] - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - // no approve if account is frozen - let mut account = Account::unpack_unchecked(&account_account.data).unwrap(); - account.state = AccountState::Frozen; - Account::pack(account, &mut account_account.data).unwrap(); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100 - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - ); - - // no revoke if account is frozen - let mut account = Account::unpack_unchecked(&account_account.data).unwrap(); - account.delegate = COption::Some(delegate_key); - account.delegated_amount = 100; - Account::pack(account, &mut account_account.data).unwrap(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - revoke(&program_id, &account_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut owner_account], - ) - ); - - // no set authority if account is frozen - let new_owner_key = Pubkey::new_unique(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&new_owner_key), - AuthorityType::AccountOwner, - &owner_key, - &[] - ) - .unwrap(), - vec![&mut account_account, &mut owner_account,], - ) - ); - - // no mint_to if destination account is frozen - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 100).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account,], - ) - ); - - // no burn if account is frozen - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 100).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - } - - #[test] - fn test_freeze_thaw_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, true, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, Some(&account1_key), 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // freeze where mint freeze_authority is account - do_process_instruction_dups( - freeze_account(&program_id, &account1_key, &mint_key, &account1_key, &[]).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // thaw where mint freeze_authority is account - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.state = AccountState::Frozen; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - thaw_account(&program_id, &account1_key, &mint_key, &account1_key, &[]).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - } - - #[test] - fn test_freeze_account() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account_owner_key = Pubkey::new_unique(); - let mut account_owner_account = SolanaAccount::default(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create new mint with owner different from account owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &account_owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut account_owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // mint cannot freeze - assert_eq!( - Err(TokenError::MintCannotFreeze.into()), - do_process_instruction( - freeze_account(&program_id, &account_key, &mint_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - - // missing freeze_authority - let mut mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - mint.freeze_authority = COption::Some(owner_key); - Mint::pack(mint, &mut mint_account.data).unwrap(); - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - freeze_account(&program_id, &account_key, &mint_key, &owner2_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // check explicit thaw - assert_eq!( - Err(TokenError::InvalidState.into()), - do_process_instruction( - thaw_account(&program_id, &account_key, &mint_key, &owner2_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // freeze - do_process_instruction( - freeze_account(&program_id, &account_key, &mint_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.state, AccountState::Frozen); - - // check explicit freeze - assert_eq!( - Err(TokenError::InvalidState.into()), - do_process_instruction( - freeze_account(&program_id, &account_key, &mint_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - - // check thaw authority - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - thaw_account(&program_id, &account_key, &mint_key, &owner2_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // thaw - do_process_instruction( - thaw_account(&program_id, &account_key, &mint_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.state, AccountState::Initialized); - } - - #[test] - fn test_initialize_account2_and_3() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - do_process_instruction( - initialize_account2(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![&mut account2_account, &mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - assert_eq!(account_account, account2_account); - - do_process_instruction( - initialize_account3(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![&mut account3_account, &mut mint_account], - ) - .unwrap(); - - assert_eq!(account_account, account3_account); - } - - #[test] - fn initialize_account_on_non_transferable_mint() { - let program_id = crate::id(); - let account = Pubkey::new_unique(); - let account_len = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::NonTransferableAccount, - ]) - .unwrap(); - let mut account_without_enough_length = SolanaAccount::new( - Rent::default().minimum_balance(account_len), - account_len, - &program_id, - ); - - let account2 = Pubkey::new_unique(); - let account2_len = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::NonTransferableAccount, - ExtensionType::ImmutableOwner, - ]) - .unwrap(); - let mut account_with_enough_length = SolanaAccount::new( - Rent::default().minimum_balance(account2_len), - account2_len, - &program_id, - ); - - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mint_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::NonTransferable]) - .unwrap(); - let mut mint_account = SolanaAccount::new( - Rent::default().minimum_balance(mint_len), - mint_len, - &program_id, - ); - let mut rent_sysvar = rent_sysvar(); - - // create a non-transferable mint - assert_eq!( - Ok(()), - do_process_instruction( - initialize_non_transferable_mint(&program_id, &mint_key).unwrap(), - vec![&mut mint_account], - ) - ); - assert_eq!( - Ok(()), - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar] - ) - ); - - //fail when account space is not enough for adding the immutable ownership - // extension - assert_eq!( - Err(ProgramError::InvalidAccountData), - do_process_instruction( - initialize_account(&program_id, &account, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_without_enough_length, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ] - ) - ); - - //success to initialize an account with enough data space - assert_eq!( - Ok(()), - do_process_instruction( - initialize_account(&program_id, &account2, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_with_enough_length, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ] - ) - ); - } - - #[test] - fn test_sync_native() { - let program_id = crate::id(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let native_account_key = Pubkey::new_unique(); - let lamports = 40; - let mut native_account = SolanaAccount::new( - account_minimum_balance() + lamports, - Account::get_packed_len(), - &program_id, - ); - let non_native_account_key = Pubkey::new_unique(); - let mut non_native_account = SolanaAccount::new( - account_minimum_balance() + 50, - Account::get_packed_len(), - &program_id, - ); - - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mut rent_sysvar = rent_sysvar(); - - // initialize non-native mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // initialize non-native account - do_process_instruction( - initialize_account(&program_id, &non_native_account_key, &mint_key, &owner_key) - .unwrap(), - vec![ - &mut non_native_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - let account = Account::unpack_unchecked(&non_native_account.data).unwrap(); - assert!(!account.is_native()); - assert_eq!(account.amount, 0); - - // fail sync non-native - assert_eq!( - Err(TokenError::NonNativeNotSupported.into()), - do_process_instruction( - sync_native(&program_id, &non_native_account_key,).unwrap(), - vec![&mut non_native_account], - ) - ); - - // fail sync uninitialized - assert_eq!( - Err(ProgramError::UninitializedAccount), - do_process_instruction( - sync_native(&program_id, &native_account_key,).unwrap(), - vec![&mut native_account], - ) - ); - - // wrap native account - do_process_instruction( - initialize_account( - &program_id, - &native_account_key, - &crate::native_mint::id(), - &owner_key, - ) - .unwrap(), - vec![ - &mut native_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // fail sync, not owned by program - let not_program_id = Pubkey::new_unique(); - native_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - sync_native(&program_id, &native_account_key,).unwrap(), - vec![&mut native_account], - ) - ); - native_account.owner = program_id; - - let account = Account::unpack_unchecked(&native_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, lamports); - - // sync, no change - do_process_instruction( - sync_native(&program_id, &native_account_key).unwrap(), - vec![&mut native_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&native_account.data).unwrap(); - assert_eq!(account.amount, lamports); - - // transfer sol - let new_lamports = lamports + 50; - native_account.lamports = account_minimum_balance() + new_lamports; - - // success sync - do_process_instruction( - sync_native(&program_id, &native_account_key).unwrap(), - vec![&mut native_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&native_account.data).unwrap(); - assert_eq!(account.amount, new_lamports); - - // reduce sol - native_account.lamports -= 1; - - // fail sync - assert_eq!( - Err(TokenError::InvalidState.into()), - do_process_instruction( - sync_native(&program_id, &native_account_key,).unwrap(), - vec![&mut native_account], - ) - ); - } - - #[test] - #[serial] - fn test_get_account_data_size() { - // see integration tests for return-data validity - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mut rent_sysvar = rent_sysvar(); - - // Base mint - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_key = Pubkey::new_unique(); - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - set_expected_data( - ExtensionType::try_calculate_account_len::(&[]) - .unwrap() - .to_le_bytes() - .to_vec(), - ); - do_process_instruction( - get_account_data_size(&program_id, &mint_key, &[]).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data( - ExtensionType::try_calculate_account_len::(&[ - ExtensionType::TransferFeeAmount, - ]) - .unwrap() - .to_le_bytes() - .to_vec(), - ); - do_process_instruction( - get_account_data_size( - &program_id, - &mint_key, - &[ - ExtensionType::TransferFeeAmount, - ExtensionType::TransferFeeAmount, // Duplicate user input ignored... - ], - ) - .unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - // Native mint - let mut mint_account = native_mint(); - set_expected_data( - ExtensionType::try_calculate_account_len::(&[]) - .unwrap() - .to_le_bytes() - .to_vec(), - ); - do_process_instruction( - get_account_data_size(&program_id, &mint_key, &[]).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - // Extended mint - let mint_len = - ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferFeeConfig]) - .unwrap(); - let mut extended_mint_account = SolanaAccount::new( - Rent::default().minimum_balance(mint_len), - mint_len, - &program_id, - ); - let extended_mint_key = Pubkey::new_unique(); - do_process_instruction( - initialize_transfer_fee_config(&program_id, &extended_mint_key, None, None, 10, 4242) - .unwrap(), - vec![&mut extended_mint_account], - ) - .unwrap(); - do_process_instruction( - initialize_mint(&program_id, &extended_mint_key, &owner_key, None, 2).unwrap(), - vec![&mut extended_mint_account, &mut rent_sysvar], - ) - .unwrap(); - - set_expected_data( - ExtensionType::try_calculate_account_len::(&[ - ExtensionType::TransferFeeAmount, - ]) - .unwrap() - .to_le_bytes() - .to_vec(), - ); - do_process_instruction( - get_account_data_size(&program_id, &mint_key, &[]).unwrap(), - vec![&mut extended_mint_account], - ) - .unwrap(); - - do_process_instruction( - get_account_data_size( - &program_id, - &mint_key, - // User extension that's also added by the mint ignored... - &[ExtensionType::TransferFeeAmount], - ) - .unwrap(), - vec![&mut extended_mint_account], - ) - .unwrap(); - - // Invalid mint - let mut invalid_mint_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let invalid_mint_key = Pubkey::new_unique(); - do_process_instruction( - initialize_account(&program_id, &invalid_mint_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut invalid_mint_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - assert_eq!( - do_process_instruction( - get_account_data_size(&program_id, &invalid_mint_key, &[]).unwrap(), - vec![&mut invalid_mint_account], - ), - Err(TokenError::InvalidMint.into()) - ); - - // Invalid mint owner - let invalid_program_id = Pubkey::new_unique(); - let mut invalid_mint_account = SolanaAccount::new( - mint_minimum_balance(), - Mint::get_packed_len(), - &invalid_program_id, - ); - let invalid_mint_key = Pubkey::new_unique(); - let mut instruction = - initialize_mint(&program_id, &invalid_mint_key, &owner_key, None, 2).unwrap(); - instruction.program_id = invalid_program_id; - do_process_instruction( - instruction, - vec![&mut invalid_mint_account, &mut rent_sysvar], - ) - .unwrap(); - - assert_eq!( - do_process_instruction( - get_account_data_size(&program_id, &invalid_mint_key, &[]).unwrap(), - vec![&mut invalid_mint_account], - ), - Err(ProgramError::IncorrectProgramId) - ); - - // Invalid Extension Type for mint and uninitialized account - assert_eq!( - do_process_instruction( - get_account_data_size(&program_id, &mint_key, &[ExtensionType::Uninitialized]) - .unwrap(), - vec![&mut mint_account], - ), - Err(TokenError::ExtensionTypeMismatch.into()) - ); - assert_eq!( - do_process_instruction( - get_account_data_size( - &program_id, - &mint_key, - &[ - ExtensionType::MemoTransfer, - ExtensionType::MintCloseAuthority - ] - ) - .unwrap(), - vec![&mut mint_account], - ), - Err(TokenError::ExtensionTypeMismatch.into()) - ); - } - - #[test] - #[serial] - fn test_amount_to_ui_amount() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // fail if an invalid mint is passed in - assert_eq!( - Err(TokenError::InvalidMint.into()), - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 110).unwrap(), - vec![&mut mint_account], - ) - ); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - set_expected_data("0.23".as_bytes().to_vec()); - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 23).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data("1.1".as_bytes().to_vec()); - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 110).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data("42".as_bytes().to_vec()); - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 4200).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data("0".as_bytes().to_vec()); - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 0).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - } - - #[test] - #[serial] - fn test_ui_amount_to_amount() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // fail if an invalid mint is passed in - assert_eq!( - Err(TokenError::InvalidMint.into()), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "1.1").unwrap(), - vec![&mut mint_account], - ) - ); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - set_expected_data(23u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.23").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(20u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.20").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(20u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.2000").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(20u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, ".20").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(110u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "1.1").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(110u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "1.10").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(4200u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "42").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(4200u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "42.").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(0u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - // fail if invalid ui_amount passed in - assert_eq!( - Err(ProgramError::InvalidArgument), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "").unwrap(), - vec![&mut mint_account], - ) - ); - assert_eq!( - Err(ProgramError::InvalidArgument), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, ".").unwrap(), - vec![&mut mint_account], - ) - ); - assert_eq!( - Err(ProgramError::InvalidArgument), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.111").unwrap(), - vec![&mut mint_account], - ) - ); - assert_eq!( - Err(ProgramError::InvalidArgument), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.t").unwrap(), - vec![&mut mint_account], - ) - ); - } - - #[test] - #[serial] - fn test_withdraw_excess_lamports_from_multisig() { - { - use std::sync::Once; - static ONCE: Once = Once::new(); - - ONCE.call_once(|| { - solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); - }); - } - let program_id = crate::id(); - - let mut lamports = 0; - let mut destination_data = vec![]; - let system_program_id = system_program::id(); - let destination_key = Pubkey::new_unique(); - let destination_info = AccountInfo::new( - &destination_key, - true, - false, - &mut lamports, - &mut destination_data, - &system_program_id, - false, - Epoch::default(), - ); - - let multisig_key = Pubkey::new_unique(); - let mut multisig_account = SolanaAccount::new(0, Multisig::get_packed_len(), &program_id); - let excess_lamports = 4_000_000_000_000; - multisig_account.lamports = excess_lamports + multisig_minimum_balance(); - let mut signer_keys = [Pubkey::default(); MAX_SIGNERS]; - - for signer_key in signer_keys.iter_mut().take(MAX_SIGNERS) { - *signer_key = Pubkey::new_unique(); - } - let signer_refs: Vec<&Pubkey> = signer_keys.iter().collect(); - let mut signer_lamports = 0; - let mut signer_data = vec![]; - let mut signers: Vec> = vec![ - AccountInfo::new( - &destination_key, - true, - false, - &mut signer_lamports, - &mut signer_data, - &program_id, - false, - Epoch::default(), - ); - MAX_SIGNERS + 1 - ]; - for (signer, key) in signers.iter_mut().zip(&signer_keys) { - signer.key = key; - } - - let mut multisig = - Multisig::unpack_unchecked(&vec![0; Multisig::get_packed_len()]).unwrap(); - multisig.m = MAX_SIGNERS as u8; - multisig.n = MAX_SIGNERS as u8; - multisig.signers = signer_keys; - multisig.is_initialized = true; - Multisig::pack(multisig, &mut multisig_account.data).unwrap(); - - let multisig_info: AccountInfo = (&multisig_key, true, &mut multisig_account).into(); - - let mut signers_infos = vec![ - multisig_info.clone(), - destination_info.clone(), - multisig_info.clone(), - ]; - signers_infos.extend(signers); - do_process_instruction_dups( - withdraw_excess_lamports( - &program_id, - &multisig_key, - &destination_key, - &multisig_key, - &signer_refs, - ) - .unwrap(), - signers_infos, - ) - .unwrap(); - - assert_eq!(destination_info.lamports(), excess_lamports); - } - - #[test] - #[serial] - fn test_withdraw_excess_lamports_from_account() { - let excess_lamports = 4_000_000_000_000; - - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - excess_lamports + account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - - let system_program_id = system_program::id(); - let owner_key = Pubkey::new_unique(); - - let mut destination_lamports = 0; - let mut destination_data = vec![]; - let destination_key = Pubkey::new_unique(); - let destination_info = AccountInfo::new( - &destination_key, - true, - false, - &mut destination_lamports, - &mut destination_data, - &system_program_id, - false, - Epoch::default(), - ); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - - let mut rent_sysvar = rent_sysvar(); - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - let mint_info = AccountInfo::new( - &mint_key, - true, - false, - &mut mint_account.lamports, - &mut mint_account.data, - &program_id, - false, - Epoch::default(), - ); - - let account_info: AccountInfo = (&account_key, true, &mut account_account).into(); - - do_process_instruction_dups( - initialize_account3(&program_id, &account_key, &mint_key, &account_key).unwrap(), - vec![account_info.clone(), mint_info.clone()], - ) - .unwrap(); - - do_process_instruction_dups( - withdraw_excess_lamports( - &program_id, - &account_key, - &destination_key, - &account_key, - &[], - ) - .unwrap(), - vec![ - account_info.clone(), - destination_info.clone(), - account_info.clone(), - ], - ) - .unwrap(); - - assert_eq!(destination_info.lamports(), excess_lamports); - } - - #[test] - #[serial] - fn test_withdraw_excess_lamports_from_mint() { - let excess_lamports = 4_000_000_000_000; - - let program_id = crate::id(); - let system_program_id = system_program::id(); - - let mut destination_lamports = 0; - let mut destination_data = vec![]; - let destination_key = Pubkey::new_unique(); - let destination_info = AccountInfo::new( - &destination_key, - true, - false, - &mut destination_lamports, - &mut destination_data, - &system_program_id, - false, - Epoch::default(), - ); - let mint_key = Pubkey::new_unique(); - let mut mint_account = SolanaAccount::new( - excess_lamports + mint_minimum_balance(), - Mint::get_packed_len(), - &program_id, - ); - let mut rent_sysvar = rent_sysvar(); - - do_process_instruction( - initialize_mint(&program_id, &mint_key, &mint_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - let mint_info: AccountInfo = (&mint_key, true, &mut mint_account).into(); - - do_process_instruction_dups( - withdraw_excess_lamports(&program_id, &mint_key, &destination_key, &mint_key, &[]) - .unwrap(), - vec![ - mint_info.clone(), - destination_info.clone(), - mint_info.clone(), - ], - ) - .unwrap(); - - assert_eq!(destination_info.lamports(), excess_lamports); - } -} diff --git a/token/program-2022/src/serialization.rs b/token/program-2022/src/serialization.rs deleted file mode 100644 index 583ed0cc904..00000000000 --- a/token/program-2022/src/serialization.rs +++ /dev/null @@ -1,212 +0,0 @@ -//! Serialization module - contains helpers for serde types from other crates, -//! deserialization visitors - -/// Helper function to serialize / deserialize `COption` wrapped values -pub mod coption_fromstr { - use { - serde::{ - de::{Error, Unexpected, Visitor}, - Deserializer, Serializer, - }, - solana_program::program_option::COption, - std::{ - fmt::{self, Display}, - marker::PhantomData, - str::FromStr, - }, - }; - - /// Serialize values supporting `Display` trait wrapped in `COption` - pub fn serialize(x: &COption, s: S) -> Result - where - S: Serializer, - T: Display, - { - match *x { - COption::Some(ref value) => s.serialize_some(&value.to_string()), - COption::None => s.serialize_none(), - } - } - - struct COptionVisitor { - s: PhantomData, - } - - impl<'de, T> Visitor<'de> for COptionVisitor - where - T: FromStr, - { - type Value = COption; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a FromStr type") - } - - fn visit_some(self, d: D) -> Result - where - D: Deserializer<'de>, - { - d.deserialize_str(self) - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - T::from_str(v) - .map(|r| COption::Some(r)) - .map_err(|_| E::invalid_value(Unexpected::Str(v), &"value string")) - } - - fn visit_none(self) -> Result - where - E: Error, - { - Ok(COption::None) - } - } - - /// Deserialize values supporting `Display` trait wrapped in `COption` - pub fn deserialize<'de, D, T>(d: D) -> Result, D::Error> - where - D: Deserializer<'de>, - T: FromStr, - { - d.deserialize_option(COptionVisitor { s: PhantomData }) - } -} - -/// Helper to serialize / deserialize `PodAeCiphertext` values -pub mod aeciphertext_fromstr { - use { - serde::{ - de::{Error, Visitor}, - Deserializer, Serializer, - }, - solana_zk_sdk::encryption::pod::auth_encryption::PodAeCiphertext, - std::{fmt, str::FromStr}, - }; - - /// Serialize `AeCiphertext` values supporting `Display` trait - pub fn serialize(x: &PodAeCiphertext, s: S) -> Result - where - S: Serializer, - { - s.serialize_str(&x.to_string()) - } - - struct AeCiphertextVisitor; - - impl<'de> Visitor<'de> for AeCiphertextVisitor { - type Value = PodAeCiphertext; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a FromStr type") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - FromStr::from_str(v).map_err(Error::custom) - } - } - - /// Deserialize `AeCiphertext` values from `str` - pub fn deserialize<'de, D>(d: D) -> Result - where - D: Deserializer<'de>, - { - d.deserialize_str(AeCiphertextVisitor) - } -} - -/// Helper to serialize / deserialize `PodElGamalPubkey` values -pub mod elgamalpubkey_fromstr { - use { - serde::{ - de::{Error, Visitor}, - Deserializer, Serializer, - }, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, - std::{fmt, str::FromStr}, - }; - - /// Serialize `ElGamalPubkey` values supporting `Display` trait - pub fn serialize(x: &PodElGamalPubkey, s: S) -> Result - where - S: Serializer, - { - s.serialize_str(&x.to_string()) - } - - struct ElGamalPubkeyVisitor; - - impl<'de> Visitor<'de> for ElGamalPubkeyVisitor { - type Value = PodElGamalPubkey; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a FromStr type") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - FromStr::from_str(v).map_err(Error::custom) - } - } - - /// Deserialize `ElGamalPubkey` values from `str` - pub fn deserialize<'de, D>(d: D) -> Result - where - D: Deserializer<'de>, - { - d.deserialize_str(ElGamalPubkeyVisitor) - } -} - -/// Helper to serialize / deserialize `PodElGamalCiphertext` values -pub mod elgamalciphertext_fromstr { - use { - serde::{ - de::{Error, Visitor}, - Deserializer, Serializer, - }, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalCiphertext, - std::{fmt, str::FromStr}, - }; - - /// Serialize `ElGamalCiphertext` values supporting `Display` trait - pub fn serialize(x: &PodElGamalCiphertext, s: S) -> Result - where - S: Serializer, - { - s.serialize_str(&x.to_string()) - } - - struct ElGamalCiphertextVisitor; - - impl<'de> Visitor<'de> for ElGamalCiphertextVisitor { - type Value = PodElGamalCiphertext; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a FromStr type") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - FromStr::from_str(v).map_err(Error::custom) - } - } - - /// Deserialize `ElGamalCiphertext` values from `str` - pub fn deserialize<'de, D>(d: D) -> Result - where - D: Deserializer<'de>, - { - d.deserialize_str(ElGamalCiphertextVisitor) - } -} diff --git a/token/program-2022/src/state.rs b/token/program-2022/src/state.rs deleted file mode 100644 index ba999e603e3..00000000000 --- a/token/program-2022/src/state.rs +++ /dev/null @@ -1,553 +0,0 @@ -//! State transition types - -use { - crate::{ - extension::AccountType, - generic_token_account::{is_initialized_account, GenericTokenAccount}, - instruction::MAX_SIGNERS, - }, - arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_program::{ - program_error::ProgramError, - program_option::COption, - program_pack::{IsInitialized, Pack, Sealed}, - pubkey::Pubkey, - }, -}; - -/// Simplified version of the `Pack` trait which only gives the size of the -/// packed struct. Useful when a function doesn't need a type to implement all -/// of `Pack`, but a size is still needed. -pub trait PackedSizeOf { - /// The packed size of the struct - const SIZE_OF: usize; -} - -/// Mint data. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Mint { - /// Optional authority used to mint new tokens. The mint authority may only - /// be provided during mint creation. If no mint authority is present - /// then the mint has a fixed supply and no further tokens may be - /// minted. - pub mint_authority: COption, - /// Total supply of tokens. - pub supply: u64, - /// Number of base 10 digits to the right of the decimal place. - pub decimals: u8, - /// Is `true` if this structure has been initialized - pub is_initialized: bool, - /// Optional authority to freeze token accounts. - pub freeze_authority: COption, -} -impl Sealed for Mint {} -impl IsInitialized for Mint { - fn is_initialized(&self) -> bool { - self.is_initialized - } -} -impl Pack for Mint { - const LEN: usize = 82; - fn unpack_from_slice(src: &[u8]) -> Result { - let src = array_ref![src, 0, 82]; - let (mint_authority, supply, decimals, is_initialized, freeze_authority) = - array_refs![src, 36, 8, 1, 1, 36]; - let mint_authority = unpack_coption_key(mint_authority)?; - let supply = u64::from_le_bytes(*supply); - let decimals = decimals[0]; - let is_initialized = match is_initialized { - [0] => false, - [1] => true, - _ => return Err(ProgramError::InvalidAccountData), - }; - let freeze_authority = unpack_coption_key(freeze_authority)?; - Ok(Mint { - mint_authority, - supply, - decimals, - is_initialized, - freeze_authority, - }) - } - fn pack_into_slice(&self, dst: &mut [u8]) { - let dst = array_mut_ref![dst, 0, 82]; - let ( - mint_authority_dst, - supply_dst, - decimals_dst, - is_initialized_dst, - freeze_authority_dst, - ) = mut_array_refs![dst, 36, 8, 1, 1, 36]; - let &Mint { - ref mint_authority, - supply, - decimals, - is_initialized, - ref freeze_authority, - } = self; - pack_coption_key(mint_authority, mint_authority_dst); - *supply_dst = supply.to_le_bytes(); - decimals_dst[0] = decimals; - is_initialized_dst[0] = is_initialized as u8; - pack_coption_key(freeze_authority, freeze_authority_dst); - } -} -impl PackedSizeOf for Mint { - const SIZE_OF: usize = Self::LEN; -} - -/// Account data. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Account { - /// The mint associated with this account - pub mint: Pubkey, - /// The owner of this account. - pub owner: Pubkey, - /// The amount of tokens this account holds. - pub amount: u64, - /// If `delegate` is `Some` then `delegated_amount` represents - /// the amount authorized by the delegate - pub delegate: COption, - /// The account's state - pub state: AccountState, - /// If `is_some`, this is a native token, and the value logs the rent-exempt - /// reserve. An Account is required to be rent-exempt, so the value is - /// used by the Processor to ensure that wrapped SOL accounts do not - /// drop below this threshold. - pub is_native: COption, - /// The amount delegated - pub delegated_amount: u64, - /// Optional authority to close the account. - pub close_authority: COption, -} -impl Account { - /// Checks if account is frozen - pub fn is_frozen(&self) -> bool { - self.state == AccountState::Frozen - } - /// Checks if account is native - pub fn is_native(&self) -> bool { - self.is_native.is_some() - } - /// Checks if a token Account's owner is the `system_program` or the - /// incinerator - pub fn is_owned_by_system_program_or_incinerator(&self) -> bool { - solana_program::system_program::check_id(&self.owner) - || solana_program::incinerator::check_id(&self.owner) - } -} -impl Sealed for Account {} -impl IsInitialized for Account { - fn is_initialized(&self) -> bool { - self.state != AccountState::Uninitialized - } -} -impl Pack for Account { - const LEN: usize = 165; - fn unpack_from_slice(src: &[u8]) -> Result { - let src = array_ref![src, 0, 165]; - let (mint, owner, amount, delegate, state, is_native, delegated_amount, close_authority) = - array_refs![src, 32, 32, 8, 36, 1, 12, 8, 36]; - Ok(Account { - mint: Pubkey::new_from_array(*mint), - owner: Pubkey::new_from_array(*owner), - amount: u64::from_le_bytes(*amount), - delegate: unpack_coption_key(delegate)?, - state: AccountState::try_from_primitive(state[0]) - .or(Err(ProgramError::InvalidAccountData))?, - is_native: unpack_coption_u64(is_native)?, - delegated_amount: u64::from_le_bytes(*delegated_amount), - close_authority: unpack_coption_key(close_authority)?, - }) - } - fn pack_into_slice(&self, dst: &mut [u8]) { - let dst = array_mut_ref![dst, 0, 165]; - let ( - mint_dst, - owner_dst, - amount_dst, - delegate_dst, - state_dst, - is_native_dst, - delegated_amount_dst, - close_authority_dst, - ) = mut_array_refs![dst, 32, 32, 8, 36, 1, 12, 8, 36]; - let &Account { - ref mint, - ref owner, - amount, - ref delegate, - state, - ref is_native, - delegated_amount, - ref close_authority, - } = self; - mint_dst.copy_from_slice(mint.as_ref()); - owner_dst.copy_from_slice(owner.as_ref()); - *amount_dst = amount.to_le_bytes(); - pack_coption_key(delegate, delegate_dst); - state_dst[0] = state as u8; - pack_coption_u64(is_native, is_native_dst); - *delegated_amount_dst = delegated_amount.to_le_bytes(); - pack_coption_key(close_authority, close_authority_dst); - } -} -impl PackedSizeOf for Account { - const SIZE_OF: usize = Self::LEN; -} - -/// Account state. -#[repr(u8)] -#[derive(Clone, Copy, Debug, Default, PartialEq, IntoPrimitive, TryFromPrimitive)] -pub enum AccountState { - /// Account is not yet initialized - #[default] - Uninitialized, - /// Account is initialized; the account owner and/or delegate may perform - /// permitted operations on this account - Initialized, - /// Account has been frozen by the mint freeze authority. Neither the - /// account owner nor the delegate are able to perform operations on - /// this account. - Frozen, -} - -/// Multisignature data. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Multisig { - /// Number of signers required - pub m: u8, - /// Number of valid signers - pub n: u8, - /// Is `true` if this structure has been initialized - pub is_initialized: bool, - /// Signer public keys - pub signers: [Pubkey; MAX_SIGNERS], -} -impl Sealed for Multisig {} -impl IsInitialized for Multisig { - fn is_initialized(&self) -> bool { - self.is_initialized - } -} -impl Pack for Multisig { - const LEN: usize = 355; - fn unpack_from_slice(src: &[u8]) -> Result { - let src = array_ref![src, 0, 355]; - #[allow(clippy::ptr_offset_with_cast)] - let (m, n, is_initialized, signers_flat) = array_refs![src, 1, 1, 1, 32 * MAX_SIGNERS]; - let mut result = Multisig { - m: m[0], - n: n[0], - is_initialized: match is_initialized { - [0] => false, - [1] => true, - _ => return Err(ProgramError::InvalidAccountData), - }, - signers: [Pubkey::new_from_array([0u8; 32]); MAX_SIGNERS], - }; - for (src, dst) in signers_flat.chunks(32).zip(result.signers.iter_mut()) { - *dst = Pubkey::try_from(src).map_err(|_| ProgramError::InvalidAccountData)?; - } - Ok(result) - } - fn pack_into_slice(&self, dst: &mut [u8]) { - let dst = array_mut_ref![dst, 0, 355]; - #[allow(clippy::ptr_offset_with_cast)] - let (m, n, is_initialized, signers_flat) = mut_array_refs![dst, 1, 1, 1, 32 * MAX_SIGNERS]; - *m = [self.m]; - *n = [self.n]; - *is_initialized = [self.is_initialized as u8]; - for (i, src) in self.signers.iter().enumerate() { - let dst_array = array_mut_ref![signers_flat, 32 * i, 32]; - dst_array.copy_from_slice(src.as_ref()); - } - } -} -impl PackedSizeOf for Multisig { - const SIZE_OF: usize = Self::LEN; -} - -// Helpers -pub(crate) fn pack_coption_key(src: &COption, dst: &mut [u8; 36]) { - let (tag, body) = mut_array_refs![dst, 4, 32]; - match src { - COption::Some(key) => { - *tag = [1, 0, 0, 0]; - body.copy_from_slice(key.as_ref()); - } - COption::None => { - *tag = [0; 4]; - } - } -} -pub(crate) fn unpack_coption_key(src: &[u8; 36]) -> Result, ProgramError> { - let (tag, body) = array_refs![src, 4, 32]; - match *tag { - [0, 0, 0, 0] => Ok(COption::None), - [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), - _ => Err(ProgramError::InvalidAccountData), - } -} -fn pack_coption_u64(src: &COption, dst: &mut [u8; 12]) { - let (tag, body) = mut_array_refs![dst, 4, 8]; - match src { - COption::Some(amount) => { - *tag = [1, 0, 0, 0]; - *body = amount.to_le_bytes(); - } - COption::None => { - *tag = [0; 4]; - } - } -} -fn unpack_coption_u64(src: &[u8; 12]) -> Result, ProgramError> { - let (tag, body) = array_refs![src, 4, 8]; - match *tag { - [0, 0, 0, 0] => Ok(COption::None), - [1, 0, 0, 0] => Ok(COption::Some(u64::from_le_bytes(*body))), - _ => Err(ProgramError::InvalidAccountData), - } -} - -// `spl_token_program_2022::extension::AccountType::Account` ordinal value -const ACCOUNTTYPE_ACCOUNT: u8 = AccountType::Account as u8; -impl GenericTokenAccount for Account { - fn valid_account_data(account_data: &[u8]) -> bool { - // Use spl_token::state::Account::valid_account_data once possible - account_data.len() == Account::LEN && is_initialized_account(account_data) - || (account_data.len() > Account::LEN - && account_data.len() != Multisig::LEN - && ACCOUNTTYPE_ACCOUNT == account_data[Account::LEN] - && is_initialized_account(account_data)) - } -} - -#[cfg(test)] -pub(crate) mod test { - use {super::*, crate::generic_token_account::ACCOUNT_INITIALIZED_INDEX}; - - pub const TEST_MINT: Mint = Mint { - mint_authority: COption::Some(Pubkey::new_from_array([1; 32])), - supply: 42, - decimals: 7, - is_initialized: true, - freeze_authority: COption::Some(Pubkey::new_from_array([2; 32])), - }; - pub const TEST_MINT_SLICE: &[u8] = &[ - 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 42, 0, 0, 0, 0, 0, 0, 0, 7, 1, 1, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - ]; - - pub const TEST_ACCOUNT: Account = Account { - mint: Pubkey::new_from_array([1; 32]), - owner: Pubkey::new_from_array([2; 32]), - amount: 3, - delegate: COption::Some(Pubkey::new_from_array([4; 32])), - state: AccountState::Frozen, - is_native: COption::Some(5), - delegated_amount: 6, - close_authority: COption::Some(Pubkey::new_from_array([7; 32])), - }; - pub const TEST_ACCOUNT_SLICE: &[u8] = &[ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, - 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, - 0, 6, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - ]; - pub const TEST_MULTISIG: Multisig = Multisig { - m: 1, - n: 11, - is_initialized: true, - signers: [ - Pubkey::new_from_array([1; 32]), - Pubkey::new_from_array([2; 32]), - Pubkey::new_from_array([3; 32]), - Pubkey::new_from_array([4; 32]), - Pubkey::new_from_array([5; 32]), - Pubkey::new_from_array([6; 32]), - Pubkey::new_from_array([7; 32]), - Pubkey::new_from_array([8; 32]), - Pubkey::new_from_array([9; 32]), - Pubkey::new_from_array([10; 32]), - Pubkey::new_from_array([11; 32]), - ], - }; - pub const TEST_MULTISIG_SLICE: &[u8] = &[ - 1, 11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, - 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, - 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, - 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, - 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, - 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, - ]; - - #[test] - fn test_pack_unpack() { - // Mint - let check = TEST_MINT; - let mut packed = vec![0; Mint::get_packed_len() + 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Mint::pack(check, &mut packed) - ); - let mut packed = vec![0; Mint::get_packed_len() - 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Mint::pack(check, &mut packed) - ); - let mut packed = vec![0; Mint::get_packed_len()]; - Mint::pack(check, &mut packed).unwrap(); - assert_eq!(packed, TEST_MINT_SLICE); - let unpacked = Mint::unpack(&packed).unwrap(); - assert_eq!(unpacked, check); - - // Account - let check = TEST_ACCOUNT; - let mut packed = vec![0; Account::get_packed_len() + 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Account::pack(check, &mut packed) - ); - let mut packed = vec![0; Account::get_packed_len() - 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Account::pack(check, &mut packed) - ); - let mut packed = vec![0; Account::get_packed_len()]; - Account::pack(check, &mut packed).unwrap(); - let expect = TEST_ACCOUNT_SLICE; - assert_eq!(packed, expect); - let unpacked = Account::unpack(&packed).unwrap(); - assert_eq!(unpacked, check); - - // Multisig - let check = TEST_MULTISIG; - let mut packed = vec![0; Multisig::get_packed_len() + 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Multisig::pack(check, &mut packed) - ); - let mut packed = vec![0; Multisig::get_packed_len() - 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Multisig::pack(check, &mut packed) - ); - let mut packed = vec![0; Multisig::get_packed_len()]; - Multisig::pack(check, &mut packed).unwrap(); - let expect = TEST_MULTISIG_SLICE; - assert_eq!(packed, expect); - let unpacked = Multisig::unpack(&packed).unwrap(); - assert_eq!(unpacked, check); - } - - #[test] - fn test_unpack_token_owner() { - // Account data length < Account::LEN, unpack will not return a key - let src: [u8; 12] = [0; 12]; - let result = Account::unpack_account_owner(&src); - assert_eq!(result, Option::None); - - // The right account data size and initialized, unpack will return some key - let mut src: [u8; Account::LEN] = [0; Account::LEN]; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - let result = Account::unpack_account_owner(&src); - assert!(result.is_some()); - - // The right account data size and frozen, unpack will return some key - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8; - let result = Account::unpack_account_owner(&src); - assert!(result.is_some()); - - // Account data length > account data size, but not a valid extension, - // unpack will not return a key - let mut src: [u8; Account::LEN + 5] = [0; Account::LEN + 5]; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - let result = Account::unpack_account_owner(&src); - assert_eq!(result, Option::None); - - // Account data length > account data size with a valid extension and - // initialized, expect some key returned - let mut src: [u8; Account::LEN + 5] = [0; Account::LEN + 5]; - src[Account::LEN] = AccountType::Account as u8; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - let result = Account::unpack_account_owner(&src); - assert!(result.is_some()); - - // Account data length > account data size with a valid extension but - // uninitialized, expect None - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8; - let result = Account::unpack_account_owner(&src); - assert!(result.is_none()); - - // Account data length is multi-sig data size with a valid extension and - // initialized, expect none - let mut src: [u8; Multisig::LEN] = [0; Multisig::LEN]; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - src[Account::LEN] = AccountType::Account as u8; - let result = Account::unpack_account_owner(&src); - assert!(result.is_none()); - } - - #[test] - fn test_unpack_token_mint() { - // Account data length < Account::LEN, unpack will not return a key - let src: [u8; 12] = [0; 12]; - let result = Account::unpack_account_mint(&src); - assert_eq!(result, Option::None); - - // The right account data size and initialized, unpack will return some key - let mut src: [u8; Account::LEN] = [0; Account::LEN]; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - let result = Account::unpack_account_mint(&src); - assert!(result.is_some()); - - // The right account data size and frozen, unpack will return some key - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8; - let result = Account::unpack_account_mint(&src); - assert!(result.is_some()); - - // Account data length > account data size, but not a valid extension, - // unpack will not return a key - let mut src: [u8; Account::LEN + 5] = [0; Account::LEN + 5]; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - let result = Account::unpack_account_mint(&src); - assert_eq!(result, Option::None); - - // Account data length > account data size with a valid extension and - // initialized, expect some key returned - let mut src: [u8; Account::LEN + 5] = [0; Account::LEN + 5]; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - src[Account::LEN] = AccountType::Account as u8; - let result = Account::unpack_account_mint(&src); - assert!(result.is_some()); - - // Account data length > account data size with a valid extension but - // uninitialized, expect none - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8; - let result = Account::unpack_account_mint(&src); - assert!(result.is_none()); - - // Account data length is multi-sig data size with a valid extension and - // initialized, expect none - let mut src: [u8; Multisig::LEN] = [0; Multisig::LEN]; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - src[Account::LEN] = AccountType::Account as u8; - let result = Account::unpack_account_mint(&src); - assert!(result.is_none()); - } -} diff --git a/token/program-2022/tests/action.rs b/token/program-2022/tests/action.rs deleted file mode 100644 index 9eec4563f4f..00000000000 --- a/token/program-2022/tests/action.rs +++ /dev/null @@ -1,173 +0,0 @@ -use { - solana_program_test::BanksClient, - solana_sdk::{ - hash::Hash, - program_pack::Pack, - pubkey::Pubkey, - signature::{Keypair, Signer}, - system_instruction, - transaction::Transaction, - transport::TransportError, - }, - spl_token_2022::{ - id, instruction, - state::{Account, Mint}, - }, -}; - -pub async fn create_mint( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - pool_mint: &Keypair, - manager: &Pubkey, - decimals: u8, -) -> Result<(), TransportError> { - let rent = banks_client.get_rent().await.unwrap(); - let mint_rent = rent.minimum_balance(Mint::LEN); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &pool_mint.pubkey(), - mint_rent, - Mint::LEN as u64, - &id(), - ), - instruction::initialize_mint(&id(), &pool_mint.pubkey(), manager, None, decimals) - .unwrap(), - ], - Some(&payer.pubkey()), - &[payer, pool_mint], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} - -pub async fn create_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - account: &Keypair, - pool_mint: &Pubkey, - owner: &Pubkey, -) -> Result<(), TransportError> { - let rent = banks_client.get_rent().await.unwrap(); - let account_rent = rent.minimum_balance(Account::LEN); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &account.pubkey(), - account_rent, - Account::LEN as u64, - &id(), - ), - instruction::initialize_account(&id(), &account.pubkey(), pool_mint, owner).unwrap(), - ], - Some(&payer.pubkey()), - &[payer, account], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} - -pub async fn mint_to( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - mint: &Pubkey, - account: &Pubkey, - mint_authority: &Keypair, - amount: u64, -) -> Result<(), TransportError> { - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::mint_to(&id(), mint, account, &mint_authority.pubkey(), &[], amount) - .unwrap(), - ], - Some(&payer.pubkey()), - &[payer, mint_authority], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} - -#[allow(deprecated)] -pub async fn transfer( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - source: &Pubkey, - destination: &Pubkey, - authority: &Keypair, - amount: u64, -) -> Result<(), TransportError> { - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::transfer(&id(), source, destination, &authority.pubkey(), &[], amount) - .unwrap(), - ], - Some(&payer.pubkey()), - &[payer, authority], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -pub async fn transfer_checked( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - source: &Pubkey, - mint: &Pubkey, - destination: &Pubkey, - authority: &Keypair, - amount: u64, - decimals: u8, -) -> Result<(), TransportError> { - let transaction = Transaction::new_signed_with_payer( - &[instruction::transfer_checked( - &id(), - source, - mint, - destination, - &authority.pubkey(), - &[], - amount, - decimals, - ) - .unwrap()], - Some(&payer.pubkey()), - &[payer, authority], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} - -pub async fn burn( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - mint: &Pubkey, - account: &Pubkey, - authority: &Keypair, - amount: u64, -) -> Result<(), TransportError> { - let transaction = Transaction::new_signed_with_payer( - &[instruction::burn(&id(), account, mint, &authority.pubkey(), &[], amount).unwrap()], - Some(&payer.pubkey()), - &[payer, authority], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} diff --git a/token/program-2022/tests/assert_instruction_count.rs b/token/program-2022/tests/assert_instruction_count.rs deleted file mode 100644 index c8fcf753af2..00000000000 --- a/token/program-2022/tests/assert_instruction_count.rs +++ /dev/null @@ -1,402 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod action; -use { - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - program_pack::Pack, - pubkey::Pubkey, - signature::{Keypair, Signer}, - system_instruction, - transaction::Transaction, - }, - spl_token_2022::{ - id, instruction, - processor::Processor, - state::{Account, Mint}, - }, -}; - -const TRANSFER_AMOUNT: u64 = 1_000_000_000_000_000; - -#[tokio::test] -async fn initialize_mint() { - let mut pt = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); - pt.set_compute_max_units(5_000); // last known 1401 - let (banks_client, payer, recent_blockhash) = pt.start().await; - - let owner_key = Pubkey::new_unique(); - let mint = Keypair::new(); - let decimals = 9; - - let rent = banks_client.get_rent().await.unwrap(); - let mint_rent = rent.minimum_balance(Mint::LEN); - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::create_account( - &payer.pubkey(), - &mint.pubkey(), - mint_rent, - Mint::LEN as u64, - &id(), - )], - Some(&payer.pubkey()), - &[&payer, &mint], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::initialize_mint(&id(), &mint.pubkey(), &owner_key, None, decimals) - .unwrap(), - ], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -#[tokio::test] -async fn initialize_account() { - let mut pt = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); - pt.set_compute_max_units(8_000); // last known 1620 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let account = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - let rent = banks_client.get_rent().await.unwrap(); - let account_rent = rent.minimum_balance(Account::LEN); - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::create_account( - &payer.pubkey(), - &account.pubkey(), - account_rent, - Account::LEN as u64, - &id(), - )], - Some(&payer.pubkey()), - &[&payer, &account], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::initialize_account( - &id(), - &account.pubkey(), - &mint.pubkey(), - &owner.pubkey(), - ) - .unwrap()], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -#[tokio::test] -async fn mint_to() { - let mut pt = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); - pt.set_compute_max_units(8_000); // last known 967 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let account = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &account, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::mint_to( - &id(), - &mint.pubkey(), - &account.pubkey(), - &owner.pubkey(), - &[], - TRANSFER_AMOUNT, - ) - .unwrap()], - Some(&payer.pubkey()), - &[&payer, &owner], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -#[tokio::test] -async fn transfer() { - let mut pt = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); - pt.set_compute_max_units(8_000); // last known 1222 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let source = Keypair::new(); - let destination = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &source, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &destination, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - - action::mint_to( - &mut banks_client, - &payer, - recent_blockhash, - &mint.pubkey(), - &source.pubkey(), - &owner, - TRANSFER_AMOUNT, - ) - .await - .unwrap(); - - action::transfer( - &mut banks_client, - &payer, - recent_blockhash, - &source.pubkey(), - &destination.pubkey(), - &owner, - TRANSFER_AMOUNT, - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn transfer_checked() { - let mut pt = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); - pt.set_compute_max_units(8_000); // last known 1516 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let source = Keypair::new(); - let destination = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &source, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &destination, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - - action::mint_to( - &mut banks_client, - &payer, - recent_blockhash, - &mint.pubkey(), - &source.pubkey(), - &owner, - TRANSFER_AMOUNT, - ) - .await - .unwrap(); - - action::transfer_checked( - &mut banks_client, - &payer, - recent_blockhash, - &source.pubkey(), - &mint.pubkey(), - &destination.pubkey(), - &owner, - TRANSFER_AMOUNT, - decimals, - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn burn() { - let mut pt = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); - pt.set_compute_max_units(8_000); // last known 1070 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let account = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &account, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - - action::mint_to( - &mut banks_client, - &payer, - recent_blockhash, - &mint.pubkey(), - &account.pubkey(), - &owner, - TRANSFER_AMOUNT, - ) - .await - .unwrap(); - - action::burn( - &mut banks_client, - &payer, - recent_blockhash, - &mint.pubkey(), - &account.pubkey(), - &owner, - TRANSFER_AMOUNT, - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn close_account() { - let mut pt = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process)); - pt.set_compute_max_units(8_000); // last known 1111 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let account = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &account, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::close_account( - &id(), - &account.pubkey(), - &owner.pubkey(), - &owner.pubkey(), - &[], - ) - .unwrap()], - Some(&payer.pubkey()), - &[&payer, &owner], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} diff --git a/token/program-2022/tests/serialization.rs b/token/program-2022/tests/serialization.rs deleted file mode 100644 index 66ad530810d..00000000000 --- a/token/program-2022/tests/serialization.rs +++ /dev/null @@ -1,168 +0,0 @@ -#![cfg(feature = "serde-traits")] - -use { - base64::{engine::general_purpose::STANDARD, Engine}, - solana_program::program_option::COption, - solana_sdk::pubkey::Pubkey, - spl_pod::optional_keys::{OptionalNonZeroElGamalPubkey, OptionalNonZeroPubkey}, - spl_token_2022::{extension::confidential_transfer, instruction}, - std::str::FromStr, -}; - -#[test] -fn serde_instruction_coption_pubkey() { - let inst = instruction::TokenInstruction::InitializeMint2 { - decimals: 0, - mint_authority: Pubkey::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap(), - freeze_authority: COption::Some( - Pubkey::from_str("8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh").unwrap(), - ), - }; - - let serialized = serde_json::to_string(&inst).unwrap(); - assert_eq!(&serialized, "{\"initializeMint2\":{\"decimals\":0,\"mintAuthority\":\"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM\",\"freezeAuthority\":\"8opHzTAnfzRpPEx21XtnrVTX28YQuCpAjcn1PczScKh\"}}"); - - serde_json::from_str::(&serialized).unwrap(); -} - -#[test] -fn serde_instruction_coption_pubkey_with_none() { - let inst = instruction::TokenInstruction::InitializeMintCloseAuthority { - close_authority: COption::None, - }; - - let serialized = serde_json::to_string(&inst).unwrap(); - assert_eq!( - &serialized, - "{\"initializeMintCloseAuthority\":{\"closeAuthority\":null}}" - ); - - serde_json::from_str::(&serialized).unwrap(); -} - -#[test] -fn serde_instruction_optional_nonzero_pubkeys_podbool() { - // tests serde of ix containing OptionalNonZeroPubkey, PodBool and - // OptionalNonZeroElGamalPubkey - let authority_option: Option = - Some(Pubkey::from_str("4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM").unwrap()); - let authority: OptionalNonZeroPubkey = authority_option.try_into().unwrap(); - - let pubkey_string = STANDARD.encode([ - 162, 23, 108, 36, 130, 143, 18, 219, 196, 134, 242, 145, 179, 49, 229, 193, 74, 64, 3, 158, - 68, 235, 124, 88, 247, 144, 164, 254, 228, 12, 173, 85, - ]); - let elgamal_pubkey_pod_option = Some(FromStr::from_str(&pubkey_string).unwrap()); - - let auditor_elgamal_pubkey: OptionalNonZeroElGamalPubkey = - elgamal_pubkey_pod_option.try_into().unwrap(); - - let inst = confidential_transfer::instruction::InitializeMintData { - authority, - auto_approve_new_accounts: false.into(), - auditor_elgamal_pubkey, - }; - - let serialized = serde_json::to_string(&inst).unwrap(); - let serialized_expected = &"{\"authority\":\"4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM\",\"autoApproveNewAccounts\":false,\"auditorElgamalPubkey\":\"ohdsJIKPEtvEhvKRszHlwUpAA55E63xY95Ck/uQMrVU=\"}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = - serde_json::from_str::(&serialized) - .unwrap(); - assert_eq!(inst, deserialized); -} - -#[test] -fn serde_instruction_optional_nonzero_pubkeys_podbool_with_none() { - // tests serde of ix containing OptionalNonZeroPubkey, PodBool and - // OptionalNonZeroElGamalPubkey with null values - let authority: OptionalNonZeroPubkey = None.try_into().unwrap(); - - let auditor_elgamal_pubkey: OptionalNonZeroElGamalPubkey = - OptionalNonZeroElGamalPubkey::default(); - - let inst = confidential_transfer::instruction::InitializeMintData { - authority, - auto_approve_new_accounts: false.into(), - auditor_elgamal_pubkey, - }; - - let serialized = serde_json::to_string(&inst).unwrap(); - let serialized_expected = - &"{\"authority\":null,\"autoApproveNewAccounts\":false,\"auditorElgamalPubkey\":null}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = - serde_json::from_str::( - serialized_expected, - ) - .unwrap(); - assert_eq!(inst, deserialized); -} - -#[test] -fn serde_instruction_decryptable_balance_podu64() { - let ciphertext_string = STANDARD.encode([ - 56, 22, 102, 48, 112, 106, 58, 25, 25, 244, 194, 217, 73, 137, 73, 38, 24, 26, 36, 25, 235, - 234, 68, 181, 11, 82, 170, 163, 89, 205, 113, 160, 55, 16, 35, 151, - ]); - let decryptable_zero_balance = FromStr::from_str(&ciphertext_string).unwrap(); - - let inst = confidential_transfer::instruction::ConfigureAccountInstructionData { - decryptable_zero_balance, - maximum_pending_balance_credit_counter: 1099.into(), - proof_instruction_offset: 100, - }; - - let serialized = serde_json::to_string(&inst).unwrap(); - let serialized_expected = &"{\"decryptableZeroBalance\":\"OBZmMHBqOhkZ9MLZSYlJJhgaJBnr6kS1C1Kqo1nNcaA3ECOX\",\"maximumPendingBalanceCreditCounter\":1099,\"proofInstructionOffset\":100}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = serde_json::from_str::< - confidential_transfer::instruction::ConfigureAccountInstructionData, - >(serialized_expected) - .unwrap(); - assert_eq!(inst, deserialized); -} - -#[test] -fn serde_instruction_elgamal_pubkey() { - use spl_token_2022::extension::confidential_transfer_fee::instruction::InitializeConfidentialTransferFeeConfigData; - - let pubkey_string = STANDARD.encode([ - 162, 23, 108, 36, 130, 143, 18, 219, 196, 134, 242, 145, 179, 49, 229, 193, 74, 64, 3, 158, - 68, 235, 124, 88, 247, 144, 164, 254, 228, 12, 173, 85, - ]); - let withdraw_withheld_authority_elgamal_pubkey = FromStr::from_str(&pubkey_string).unwrap(); - - let inst = InitializeConfidentialTransferFeeConfigData { - authority: OptionalNonZeroPubkey::default(), - withdraw_withheld_authority_elgamal_pubkey, - }; - - let serialized = serde_json::to_string(&inst).unwrap(); - let serialized_expected = "{\"authority\":null,\"withdrawWithheldAuthorityElgamalPubkey\":\"ohdsJIKPEtvEhvKRszHlwUpAA55E63xY95Ck/uQMrVU=\"}"; - assert_eq!(&serialized, serialized_expected); - - let deserialized = - serde_json::from_str::(serialized_expected) - .unwrap(); - assert_eq!(inst, deserialized); -} - -#[test] -fn serde_instruction_basis_points() { - use spl_token_2022::extension::interest_bearing_mint::instruction::InitializeInstructionData; - - let inst = InitializeInstructionData { - rate_authority: OptionalNonZeroPubkey::default(), - rate: 127.into(), - }; - - let serialized = serde_json::to_string(&inst).unwrap(); - let serialized_expected = "{\"rateAuthority\":null,\"rate\":127}"; - assert_eq!(&serialized, serialized_expected); - - serde_json::from_str::(serialized_expected).unwrap(); -} diff --git a/token/program/Cargo.toml b/token/program/Cargo.toml deleted file mode 100644 index 72153f7eb79..00000000000 --- a/token/program/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "spl-token" -version = "7.0.0" -description = "Solana Program Library Token" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" -exclude = ["js/**"] - -[features] -no-entrypoint = [] -test-sbf = [] - -[dependencies] -arrayref = "0.3.9" -bytemuck = "1.21.0" -num-derive = "0.4" -num-traits = "0.2" -num_enum = "0.7.3" -solana-program = "2.1.0" -thiserror = "2.0" - -[dev-dependencies] -lazy_static = "1.5.0" -proptest = "1.6" -serial_test = "3.2.0" -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[lints] -workspace = true diff --git a/token/program/README.md b/token/program/README.md deleted file mode 100644 index 81167bfdded..00000000000 --- a/token/program/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Token program - -A token program on the Solana blockchain, usable for fungible and non-fungible tokens. - -This program provides an interface and implementation that third parties can -utilize to create and use their tokens. - -Full documentation is available at the [SPL Token docs](https://spl.solana.com/token). - -## Audit - -The repository [README](https://github.com/solana-labs/solana-program-library#audits) -contains information about program audits. diff --git a/token/program/Xargo.toml b/token/program/Xargo.toml deleted file mode 100644 index 1744f098ae1..00000000000 --- a/token/program/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] \ No newline at end of file diff --git a/token/program/inc/token.h b/token/program/inc/token.h deleted file mode 100644 index 145c0c5e07b..00000000000 --- a/token/program/inc/token.h +++ /dev/null @@ -1,687 +0,0 @@ -/* Autogenerated SPL Token program C Bindings */ - -#pragma once - -#include -#include -#include -#include - -/** - * Minimum number of multisignature signers (min N) - */ -#define Token_MIN_SIGNERS 1 - -/** - * Maximum number of multisignature signers (max N) - */ -#define Token_MAX_SIGNERS 11 - -/** - * Account state. - */ -enum Token_AccountState -#ifdef __cplusplus - : uint8_t -#endif // __cplusplus - { - /** - * Account is not yet initialized - */ - Token_AccountState_Uninitialized, - /** - * Account is initialized; the account owner and/or delegate may perform permitted operations - * on this account - */ - Token_AccountState_Initialized, - /** - * Account has been frozen by the mint freeze authority. Neither the account owner nor - * the delegate are able to perform operations on this account. - */ - Token_AccountState_Frozen, -}; -#ifndef __cplusplus -typedef uint8_t Token_AccountState; -#endif // __cplusplus - -/** - * Specifies the authority type for SetAuthority instructions - */ -enum Token_AuthorityType -#ifdef __cplusplus - : uint8_t -#endif // __cplusplus - { - /** - * Authority to mint new tokens - */ - Token_AuthorityType_MintTokens, - /** - * Authority to freeze any account associated with the Mint - */ - Token_AuthorityType_FreezeAccount, - /** - * Owner of a given token account - */ - Token_AuthorityType_AccountOwner, - /** - * Authority to close a token account - */ - Token_AuthorityType_CloseAccount, -}; -#ifndef __cplusplus -typedef uint8_t Token_AuthorityType; -#endif // __cplusplus - -typedef uint8_t Token_Pubkey[32]; - -/** - * A C representation of Rust's `std::option::Option` - */ -typedef enum Token_COption_Pubkey_Tag { - /** - * No value - */ - Token_COption_Pubkey_None_Pubkey, - /** - * Some value `T` - */ - Token_COption_Pubkey_Some_Pubkey, -} Token_COption_Pubkey_Tag; - -typedef struct Token_COption_Pubkey { - Token_COption_Pubkey_Tag tag; - union { - struct { - Token_Pubkey some; - }; - }; -} Token_COption_Pubkey; - -/** - * Instructions supported by the token program. - */ -typedef enum Token_TokenInstruction_Tag { - /** - * Initializes a new mint and optionally deposits all the newly minted - * tokens in an account. - * - * The `InitializeMint` instruction requires no signers and MUST be - * included within the same Transaction as the system program's - * `CreateAccount` instruction that creates the account being initialized. - * Otherwise another party can acquire ownership of the uninitialized - * account. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The mint to initialize. - * 1. `[]` Rent sysvar - * - */ - Token_TokenInstruction_InitializeMint, - /** - * Initializes a new account to hold tokens. If this account is associated - * with the native mint then the token balance of the initialized account - * will be equal to the amount of SOL in the account. If this account is - * associated with another mint, that mint must be initialized before this - * command can succeed. - * - * The `InitializeAccount` instruction requires no signers and MUST be - * included within the same Transaction as the system program's - * `CreateAccount` instruction that creates the account being initialized. - * Otherwise another party can acquire ownership of the uninitialized - * account. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The account to initialize. - * 1. `[]` The mint this account will be associated with. - * 2. `[]` The new account's owner/multisignature. - * 3. `[]` Rent sysvar - */ - Token_TokenInstruction_InitializeAccount, - /** - * Initializes a multisignature account with N provided signers. - * - * Multisignature accounts can used in place of any single owner/delegate - * accounts in any token instruction that require an owner/delegate to be - * present. The variant field represents the number of signers (M) - * required to validate this multisignature account. - * - * The `InitializeMultisig` instruction requires no signers and MUST be - * included within the same Transaction as the system program's - * `CreateAccount` instruction that creates the account being initialized. - * Otherwise another party can acquire ownership of the uninitialized - * account. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The multisignature account to initialize. - * 1. `[]` Rent sysvar - * 2. ..2+N. `[]` The signer accounts, must equal to N where 1 <= N <= - * 11. - */ - Token_TokenInstruction_InitializeMultisig, - /** - * Transfers tokens from one account to another either directly or via a - * delegate. If this account is associated with the native mint then equal - * amounts of SOL and Tokens will be transferred to the destination - * account. - * - * Accounts expected by this instruction: - * - * * Single owner/delegate - * 0. `[writable]` The source account. - * 1. `[writable]` The destination account. - * 2. `[signer]` The source account's owner/delegate. - * - * * Multisignature owner/delegate - * 0. `[writable]` The source account. - * 1. `[writable]` The destination account. - * 2. `[]` The source account's multisignature owner/delegate. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_Transfer, - /** - * Approves a delegate. A delegate is given the authority over tokens on - * behalf of the source account's owner. - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The source account. - * 1. `[]` The delegate. - * 2. `[signer]` The source account owner. - * - * * Multisignature owner - * 0. `[writable]` The source account. - * 1. `[]` The delegate. - * 2. `[]` The source account's multisignature owner. - * 3. ..3+M `[signer]` M signer accounts - */ - Token_TokenInstruction_Approve, - /** - * Revokes the delegate's authority. - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The source account. - * 1. `[signer]` The source account owner. - * - * * Multisignature owner - * 0. `[writable]` The source account. - * 1. `[]` The source account's multisignature owner. - * 2. ..2+M `[signer]` M signer accounts - */ - Token_TokenInstruction_Revoke, - /** - * Sets a new authority of a mint or account. - * - * Accounts expected by this instruction: - * - * * Single authority - * 0. `[writable]` The mint or account to change the authority of. - * 1. `[signer]` The current authority of the mint or account. - * - * * Multisignature authority - * 0. `[writable]` The mint or account to change the authority of. - * 1. `[]` The mint's or account's current multisignature authority. - * 2. ..2+M `[signer]` M signer accounts - */ - Token_TokenInstruction_SetAuthority, - /** - * Mints new tokens to an account. The native mint does not support - * minting. - * - * Accounts expected by this instruction: - * - * * Single authority - * 0. `[writable]` The mint. - * 1. `[writable]` The account to mint tokens to. - * 2. `[signer]` The mint's minting authority. - * - * * Multisignature authority - * 0. `[writable]` The mint. - * 1. `[writable]` The account to mint tokens to. - * 2. `[]` The mint's multisignature mint-tokens authority. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_MintTo, - /** - * Burns tokens by removing them from an account. `Burn` does not support - * accounts associated with the native mint, use `CloseAccount` instead. - * - * Accounts expected by this instruction: - * - * * Single owner/delegate - * 0. `[writable]` The account to burn from. - * 1. `[writable]` The token mint. - * 2. `[signer]` The account's owner/delegate. - * - * * Multisignature owner/delegate - * 0. `[writable]` The account to burn from. - * 1. `[writable]` The token mint. - * 2. `[]` The account's multisignature owner/delegate. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_Burn, - /** - * Close an account by transferring all its SOL to the destination account. - * Non-native accounts may only be closed if its token amount is zero. - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The account to close. - * 1. `[writable]` The destination account. - * 2. `[signer]` The account's owner. - * - * * Multisignature owner - * 0. `[writable]` The account to close. - * 1. `[writable]` The destination account. - * 2. `[]` The account's multisignature owner. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_CloseAccount, - /** - * Freeze an Initialized account using the Mint's freeze_authority (if - * set). - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The account to freeze. - * 1. `[]` The token mint. - * 2. `[signer]` The mint freeze authority. - * - * * Multisignature owner - * 0. `[writable]` The account to freeze. - * 1. `[]` The token mint. - * 2. `[]` The mint's multisignature freeze authority. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_FreezeAccount, - /** - * Thaw a Frozen account using the Mint's freeze_authority (if set). - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The account to freeze. - * 1. `[]` The token mint. - * 2. `[signer]` The mint freeze authority. - * - * * Multisignature owner - * 0. `[writable]` The account to freeze. - * 1. `[]` The token mint. - * 2. `[]` The mint's multisignature freeze authority. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_ThawAccount, - /** - * Transfers tokens from one account to another either directly or via a - * delegate. If this account is associated with the native mint then equal - * amounts of SOL and Tokens will be transferred to the destination - * account. - * - * This instruction differs from Transfer in that the token mint and - * decimals value is checked by the caller. This may be useful when - * creating transactions offline or within a hardware wallet. - * - * Accounts expected by this instruction: - * - * * Single owner/delegate - * 0. `[writable]` The source account. - * 1. `[]` The token mint. - * 2. `[writable]` The destination account. - * 3. `[signer]` The source account's owner/delegate. - * - * * Multisignature owner/delegate - * 0. `[writable]` The source account. - * 1. `[]` The token mint. - * 2. `[writable]` The destination account. - * 3. `[]` The source account's multisignature owner/delegate. - * 4. ..4+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_TransferChecked, - /** - * Approves a delegate. A delegate is given the authority over tokens on - * behalf of the source account's owner. - * - * This instruction differs from Approve in that the token mint and - * decimals value is checked by the caller. This may be useful when - * creating transactions offline or within a hardware wallet. - * - * Accounts expected by this instruction: - * - * * Single owner - * 0. `[writable]` The source account. - * 1. `[]` The token mint. - * 2. `[]` The delegate. - * 3. `[signer]` The source account owner. - * - * * Multisignature owner - * 0. `[writable]` The source account. - * 1. `[]` The token mint. - * 2. `[]` The delegate. - * 3. `[]` The source account's multisignature owner. - * 4. ..4+M `[signer]` M signer accounts - */ - Token_TokenInstruction_ApproveChecked, - /** - * Mints new tokens to an account. The native mint does not support - * minting. - * - * This instruction differs from MintTo in that the decimals value is - * checked by the caller. This may be useful when creating transactions - * offline or within a hardware wallet. - * - * Accounts expected by this instruction: - * - * * Single authority - * 0. `[writable]` The mint. - * 1. `[writable]` The account to mint tokens to. - * 2. `[signer]` The mint's minting authority. - * - * * Multisignature authority - * 0. `[writable]` The mint. - * 1. `[writable]` The account to mint tokens to. - * 2. `[]` The mint's multisignature mint-tokens authority. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_MintToChecked, - /** - * Burns tokens by removing them from an account. `BurnChecked` does not - * support accounts associated with the native mint, use `CloseAccount` - * instead. - * - * This instruction differs from Burn in that the decimals value is checked - * by the caller. This may be useful when creating transactions offline or - * within a hardware wallet. - * - * Accounts expected by this instruction: - * - * * Single owner/delegate - * 0. `[writable]` The account to burn from. - * 1. `[writable]` The token mint. - * 2. `[signer]` The account's owner/delegate. - * - * * Multisignature owner/delegate - * 0. `[writable]` The account to burn from. - * 1. `[writable]` The token mint. - * 2. `[]` The account's multisignature owner/delegate. - * 3. ..3+M `[signer]` M signer accounts. - */ - Token_TokenInstruction_BurnChecked, - /** - * Like InitializeAccount, but the owner pubkey is passed via instruction data - * rather than the accounts list. This variant may be preferable when using - * Cross Program Invocation from an instruction that does not need the owner's - * `AccountInfo` otherwise. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The account to initialize. - * 1. `[]` The mint this account will be associated with. - * 3. `[]` Rent sysvar - */ - Token_TokenInstruction_InitializeAccount2, - /** - * Given a wrapped / native token account (a token account containing SOL) - * updates its amount field based on the account's underlying `lamports`. - * This is useful if a non-wrapped SOL account uses `system_instruction::transfer` - * to move lamports to a wrapped token account, and needs to have its token - * `amount` field updated. - * - * Accounts expected by this instruction: - * - * 0. `[writable]` The native token account to sync with its underlying lamports. - */ - Token_TokenInstruction_SyncNative, -} Token_TokenInstruction_Tag; - -typedef struct Token_TokenInstruction_Token_InitializeMint_Body { - /** - * Number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; - /** - * The authority/multisignature to mint tokens. - */ - Token_Pubkey mint_authority; - /** - * The freeze authority/multisignature of the mint. - */ - struct Token_COption_Pubkey freeze_authority; -} Token_TokenInstruction_Token_InitializeMint_Body; - -typedef struct Token_TokenInstruction_Token_InitializeMultisig_Body { - /** - * The number of signers (M) required to validate this multisignature - * account. - */ - uint8_t m; -} Token_TokenInstruction_Token_InitializeMultisig_Body; - -typedef struct Token_TokenInstruction_Token_Transfer_Body { - /** - * The amount of tokens to transfer. - */ - uint64_t amount; -} Token_TokenInstruction_Token_Transfer_Body; - -typedef struct Token_TokenInstruction_Token_Approve_Body { - /** - * The amount of tokens the delegate is approved for. - */ - uint64_t amount; -} Token_TokenInstruction_Token_Approve_Body; - -typedef struct Token_TokenInstruction_Token_SetAuthority_Body { - /** - * The type of authority to update. - */ - Token_AuthorityType authority_type; - /** - * The new authority - */ - struct Token_COption_Pubkey new_authority; -} Token_TokenInstruction_Token_SetAuthority_Body; - -typedef struct Token_TokenInstruction_Token_MintTo_Body { - /** - * The amount of new tokens to mint. - */ - uint64_t amount; -} Token_TokenInstruction_Token_MintTo_Body; - -typedef struct Token_TokenInstruction_Token_Burn_Body { - /** - * The amount of tokens to burn. - */ - uint64_t amount; -} Token_TokenInstruction_Token_Burn_Body; - -typedef struct Token_TokenInstruction_Token_TransferChecked_Body { - /** - * The amount of tokens to transfer. - */ - uint64_t amount; - /** - * Expected number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; -} Token_TokenInstruction_Token_TransferChecked_Body; - -typedef struct Token_TokenInstruction_Token_ApproveChecked_Body { - /** - * The amount of tokens the delegate is approved for. - */ - uint64_t amount; - /** - * Expected number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; -} Token_TokenInstruction_Token_ApproveChecked_Body; - -typedef struct Token_TokenInstruction_Token_MintToChecked_Body { - /** - * The amount of new tokens to mint. - */ - uint64_t amount; - /** - * Expected number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; -} Token_TokenInstruction_Token_MintToChecked_Body; - -typedef struct Token_TokenInstruction_Token_BurnChecked_Body { - /** - * The amount of tokens to burn. - */ - uint64_t amount; - /** - * Expected number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; -} Token_TokenInstruction_Token_BurnChecked_Body; - -typedef struct Token_TokenInstruction_Token_InitializeAccount2_Body { - /** - * The new account's owner/multisignature. - */ - Token_Pubkey owner; -} Token_TokenInstruction_Token_InitializeAccount2_Body; - -typedef struct Token_TokenInstruction { - Token_TokenInstruction_Tag tag; - union { - Token_TokenInstruction_Token_InitializeMint_Body initialize_mint; - Token_TokenInstruction_Token_InitializeMultisig_Body initialize_multisig; - Token_TokenInstruction_Token_Transfer_Body transfer; - Token_TokenInstruction_Token_Approve_Body approve; - Token_TokenInstruction_Token_SetAuthority_Body set_authority; - Token_TokenInstruction_Token_MintTo_Body mint_to; - Token_TokenInstruction_Token_Burn_Body burn; - Token_TokenInstruction_Token_TransferChecked_Body transfer_checked; - Token_TokenInstruction_Token_ApproveChecked_Body approve_checked; - Token_TokenInstruction_Token_MintToChecked_Body mint_to_checked; - Token_TokenInstruction_Token_BurnChecked_Body burn_checked; - Token_TokenInstruction_Token_InitializeAccount2_Body initialize_account2; - }; -} Token_TokenInstruction; - -/** - * Mint data. - */ -typedef struct Token_Mint { - /** - * Optional authority used to mint new tokens. The mint authority may only be provided during - * mint creation. If no mint authority is present then the mint has a fixed supply and no - * further tokens may be minted. - */ - struct Token_COption_Pubkey mint_authority; - /** - * Total supply of tokens. - */ - uint64_t supply; - /** - * Number of base 10 digits to the right of the decimal place. - */ - uint8_t decimals; - /** - * Is `true` if this structure has been initialized - */ - bool is_initialized; - /** - * Optional authority to freeze token accounts. - */ - struct Token_COption_Pubkey freeze_authority; -} Token_Mint; - -/** - * A C representation of Rust's `std::option::Option` - */ -typedef enum Token_COption_u64_Tag { - /** - * No value - */ - Token_COption_u64_None_u64, - /** - * Some value `T` - */ - Token_COption_u64_Some_u64, -} Token_COption_u64_Tag; - -typedef struct Token_COption_u64 { - Token_COption_u64_Tag tag; - union { - struct { - uint64_t some; - }; - }; -} Token_COption_u64; - -/** - * Account data. - */ -typedef struct Token_Account { - /** - * The mint associated with this account - */ - Token_Pubkey mint; - /** - * The owner of this account. - */ - Token_Pubkey owner; - /** - * The amount of tokens this account holds. - */ - uint64_t amount; - /** - * If `delegate` is `Some` then `delegated_amount` represents - * the amount authorized by the delegate - */ - struct Token_COption_Pubkey delegate; - /** - * The account's state - */ - Token_AccountState state; - /** - * If is_some, this is a native token, and the value logs the rent-exempt reserve. An Account - * is required to be rent-exempt, so the value is used by the Processor to ensure that wrapped - * SOL accounts do not drop below this threshold. - */ - struct Token_COption_u64 is_native; - /** - * The amount delegated - */ - uint64_t delegated_amount; - /** - * Optional authority to close the account. - */ - struct Token_COption_Pubkey close_authority; -} Token_Account; - -/** - * Multisignature data. - */ -typedef struct Token_Multisig { - /** - * Number of signers required - */ - uint8_t m; - /** - * Number of valid signers - */ - uint8_t n; - /** - * Is `true` if this structure has been initialized - */ - bool is_initialized; - /** - * Signer public keys - */ - Token_Pubkey signers[Token_MAX_SIGNERS]; -} Token_Multisig; diff --git a/token/program/program-id.md b/token/program/program-id.md deleted file mode 100644 index f397edf07a1..00000000000 --- a/token/program/program-id.md +++ /dev/null @@ -1 +0,0 @@ -TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA diff --git a/token/program/src/entrypoint.rs b/token/program/src/entrypoint.rs deleted file mode 100644 index 366ee78d012..00000000000 --- a/token/program/src/entrypoint.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Program entrypoint - -use { - crate::{error::TokenError, processor::Processor}, - solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError, - pubkey::Pubkey, - }, -}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - if let Err(error) = Processor::process(program_id, accounts, instruction_data) { - // catch the error so we can print it - error.print::(); - return Err(error); - } - Ok(()) -} diff --git a/token/program/src/error.rs b/token/program/src/error.rs deleted file mode 100644 index a758d2c9ba7..00000000000 --- a/token/program/src/error.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! Error types - -use { - num_derive::FromPrimitive, - solana_program::{ - decode_error::DecodeError, - msg, - program_error::{PrintProgramError, ProgramError}, - }, - thiserror::Error, -}; - -/// Errors that may be returned by the Token program. -#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] -pub enum TokenError { - // 0 - /// Lamport balance below rent-exempt threshold. - #[error("Lamport balance below rent-exempt threshold")] - NotRentExempt, - /// Insufficient funds for the operation requested. - #[error("Insufficient funds")] - InsufficientFunds, - /// Invalid Mint. - #[error("Invalid Mint")] - InvalidMint, - /// Account not associated with this Mint. - #[error("Account not associated with this Mint")] - MintMismatch, - /// Owner does not match. - #[error("Owner does not match")] - OwnerMismatch, - - // 5 - /// This token's supply is fixed and new tokens cannot be minted. - #[error("Fixed supply")] - FixedSupply, - /// The account cannot be initialized because it is already being used. - #[error("Already in use")] - AlreadyInUse, - /// Invalid number of provided signers. - #[error("Invalid number of provided signers")] - InvalidNumberOfProvidedSigners, - /// Invalid number of required signers. - #[error("Invalid number of required signers")] - InvalidNumberOfRequiredSigners, - /// State is uninitialized. - #[error("State is uninitialized")] - UninitializedState, - - // 10 - /// Instruction does not support native tokens - #[error("Instruction does not support native tokens")] - NativeNotSupported, - /// Non-native account can only be closed if its balance is zero - #[error("Non-native account can only be closed if its balance is zero")] - NonNativeHasBalance, - /// Invalid instruction - #[error("Invalid instruction")] - InvalidInstruction, - /// State is invalid for requested operation. - #[error("State is invalid for requested operation")] - InvalidState, - /// Operation overflowed - #[error("Operation overflowed")] - Overflow, - - // 15 - /// Account does not support specified authority type. - #[error("Account does not support specified authority type")] - AuthorityTypeNotSupported, - /// This token mint cannot freeze accounts. - #[error("This token mint cannot freeze accounts")] - MintCannotFreeze, - /// Account is frozen; all account operations will fail - #[error("Account is frozen")] - AccountFrozen, - /// Mint decimals mismatch between the client and mint - #[error("The provided decimals value different from the Mint decimals")] - MintDecimalsMismatch, - /// Instruction does not support non-native tokens - #[error("Instruction does not support non-native tokens")] - NonNativeNotSupported, -} -impl From for ProgramError { - fn from(e: TokenError) -> Self { - ProgramError::Custom(e as u32) - } -} -impl DecodeError for TokenError { - fn type_of() -> &'static str { - "TokenError" - } -} - -impl PrintProgramError for TokenError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - TokenError::NotRentExempt => msg!("Error: Lamport balance below rent-exempt threshold"), - TokenError::InsufficientFunds => msg!("Error: insufficient funds"), - TokenError::InvalidMint => msg!("Error: Invalid Mint"), - TokenError::MintMismatch => msg!("Error: Account not associated with this Mint"), - TokenError::OwnerMismatch => msg!("Error: owner does not match"), - TokenError::FixedSupply => msg!("Error: the total supply of this token is fixed"), - TokenError::AlreadyInUse => msg!("Error: account or token already in use"), - TokenError::InvalidNumberOfProvidedSigners => { - msg!("Error: Invalid number of provided signers") - } - TokenError::InvalidNumberOfRequiredSigners => { - msg!("Error: Invalid number of required signers") - } - TokenError::UninitializedState => msg!("Error: State is uninitialized"), - TokenError::NativeNotSupported => { - msg!("Error: Instruction does not support native tokens") - } - TokenError::NonNativeHasBalance => { - msg!("Error: Non-native account can only be closed if its balance is zero") - } - TokenError::InvalidInstruction => msg!("Error: Invalid instruction"), - TokenError::InvalidState => msg!("Error: Invalid account state for operation"), - TokenError::Overflow => msg!("Error: Operation overflowed"), - TokenError::AuthorityTypeNotSupported => { - msg!("Error: Account does not support specified authority type") - } - TokenError::MintCannotFreeze => msg!("Error: This token mint cannot freeze accounts"), - TokenError::AccountFrozen => msg!("Error: Account is frozen"), - TokenError::MintDecimalsMismatch => { - msg!("Error: decimals different from the Mint decimals") - } - TokenError::NonNativeNotSupported => { - msg!("Error: Instruction does not support non-native tokens") - } - } - } -} diff --git a/token/program/src/instruction.rs b/token/program/src/instruction.rs deleted file mode 100644 index b797a7d8c45..00000000000 --- a/token/program/src/instruction.rs +++ /dev/null @@ -1,1705 +0,0 @@ -//! Instruction types - -use { - crate::{check_program_account, error::TokenError}, - solana_program::{ - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - program_option::COption, - pubkey::Pubkey, - sysvar, - }, - std::{convert::TryInto, mem::size_of}, -}; - -/// Minimum number of multisignature signers (min N) -pub const MIN_SIGNERS: usize = 1; -/// Maximum number of multisignature signers (max N) -pub const MAX_SIGNERS: usize = 11; -/// Serialized length of a `u64`, for unpacking -const U64_BYTES: usize = 8; - -/// Instructions supported by the token program. -#[repr(C)] -#[derive(Clone, Debug, PartialEq)] -pub enum TokenInstruction<'a> { - /// Initializes a new mint and optionally deposits all the newly minted - /// tokens in an account. - /// - /// The `InitializeMint` instruction requires no signers and MUST be - /// included within the same Transaction as the system program's - /// `CreateAccount` instruction that creates the account being initialized. - /// Otherwise another party can acquire ownership of the uninitialized - /// account. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - /// 1. `[]` Rent sysvar - InitializeMint { - /// Number of base 10 digits to the right of the decimal place. - decimals: u8, - /// The authority/multisignature to mint tokens. - mint_authority: Pubkey, - /// The freeze authority/multisignature of the mint. - freeze_authority: COption, - }, - /// Initializes a new account to hold tokens. If this account is associated - /// with the native mint then the token balance of the initialized account - /// will be equal to the amount of SOL in the account. If this account is - /// associated with another mint, that mint must be initialized before this - /// command can succeed. - /// - /// The `InitializeAccount` instruction requires no signers and MUST be - /// included within the same Transaction as the system program's - /// `CreateAccount` instruction that creates the account being initialized. - /// Otherwise another party can acquire ownership of the uninitialized - /// account. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to initialize. - /// 1. `[]` The mint this account will be associated with. - /// 2. `[]` The new account's owner/multisignature. - /// 3. `[]` Rent sysvar - InitializeAccount, - /// Initializes a multisignature account with N provided signers. - /// - /// Multisignature accounts can used in place of any single owner/delegate - /// accounts in any token instruction that require an owner/delegate to be - /// present. The variant field represents the number of signers (M) - /// required to validate this multisignature account. - /// - /// The `InitializeMultisig` instruction requires no signers and MUST be - /// included within the same Transaction as the system program's - /// `CreateAccount` instruction that creates the account being initialized. - /// Otherwise another party can acquire ownership of the uninitialized - /// account. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The multisignature account to initialize. - /// 1. `[]` Rent sysvar - /// 2. ..`2+N`. `[]` The signer accounts, must equal to N where `1 <= N <= - /// 11`. - InitializeMultisig { - /// The number of signers (M) required to validate this multisignature - /// account. - m: u8, - }, - /// Transfers tokens from one account to another either directly or via a - /// delegate. If this account is associated with the native mint then equal - /// amounts of SOL and Tokens will be transferred to the destination - /// account. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The source account. - /// 1. `[writable]` The destination account. - /// 2. `[signer]` The source account's owner/delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The source account. - /// 1. `[writable]` The destination account. - /// 2. `[]` The source account's multisignature owner/delegate. - /// 3. ..`3+M` `[signer]` M signer accounts. - Transfer { - /// The amount of tokens to transfer. - amount: u64, - }, - /// Approves a delegate. A delegate is given the authority over tokens on - /// behalf of the source account's owner. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The source account. - /// 1. `[]` The delegate. - /// 2. `[signer]` The source account owner. - /// - /// * Multisignature owner - /// 0. `[writable]` The source account. - /// 1. `[]` The delegate. - /// 2. `[]` The source account's multisignature owner. - /// 3. ..`3+M` `[signer]` M signer accounts - Approve { - /// The amount of tokens the delegate is approved for. - amount: u64, - }, - /// Revokes the delegate's authority. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The source account. - /// 1. `[signer]` The source account owner. - /// - /// * Multisignature owner - /// 0. `[writable]` The source account. - /// 1. `[]` The source account's multisignature owner. - /// 2. ..`2+M` `[signer]` M signer accounts - Revoke, - /// Sets a new authority of a mint or account. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint or account to change the authority of. - /// 1. `[signer]` The current authority of the mint or account. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint or account to change the authority of. - /// 1. `[]` The mint's or account's current multisignature authority. - /// 2. ..`2+M` `[signer]` M signer accounts - SetAuthority { - /// The type of authority to update. - authority_type: AuthorityType, - /// The new authority - new_authority: COption, - }, - /// Mints new tokens to an account. The native mint does not support - /// minting. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[writable]` The account to mint tokens to. - /// 2. `[signer]` The mint's minting authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[writable]` The account to mint tokens to. - /// 2. `[]` The mint's multisignature mint-tokens authority. - /// 3. ..`3+M` `[signer]` M signer accounts. - MintTo { - /// The amount of new tokens to mint. - amount: u64, - }, - /// Burns tokens by removing them from an account. `Burn` does not support - /// accounts associated with the native mint, use `CloseAccount` instead. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The account to burn from. - /// 1. `[writable]` The token mint. - /// 2. `[signer]` The account's owner/delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The account to burn from. - /// 1. `[writable]` The token mint. - /// 2. `[]` The account's multisignature owner/delegate. - /// 3. ..`3+M` `[signer]` M signer accounts. - Burn { - /// The amount of tokens to burn. - amount: u64, - }, - /// Close an account by transferring all its SOL to the destination account. - /// Non-native accounts may only be closed if its token amount is zero. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The account to close. - /// 1. `[writable]` The destination account. - /// 2. `[signer]` The account's owner. - /// - /// * Multisignature owner - /// 0. `[writable]` The account to close. - /// 1. `[writable]` The destination account. - /// 2. `[]` The account's multisignature owner. - /// 3. ..`3+M` `[signer]` M signer accounts. - CloseAccount, - /// Freeze an Initialized account using the Mint's `freeze_authority` (if - /// set). - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The account to freeze. - /// 1. `[]` The token mint. - /// 2. `[signer]` The mint freeze authority. - /// - /// * Multisignature owner - /// 0. `[writable]` The account to freeze. - /// 1. `[]` The token mint. - /// 2. `[]` The mint's multisignature freeze authority. - /// 3. ..`3+M` `[signer]` M signer accounts. - FreezeAccount, - /// Thaw a Frozen account using the Mint's `freeze_authority` (if set). - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The account to freeze. - /// 1. `[]` The token mint. - /// 2. `[signer]` The mint freeze authority. - /// - /// * Multisignature owner - /// 0. `[writable]` The account to freeze. - /// 1. `[]` The token mint. - /// 2. `[]` The mint's multisignature freeze authority. - /// 3. ..`3+M` `[signer]` M signer accounts. - ThawAccount, - - /// Transfers tokens from one account to another either directly or via a - /// delegate. If this account is associated with the native mint then equal - /// amounts of SOL and Tokens will be transferred to the destination - /// account. - /// - /// This instruction differs from Transfer in that the token mint and - /// decimals value is checked by the caller. This may be useful when - /// creating transactions offline or within a hardware wallet. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The source account. - /// 1. `[]` The token mint. - /// 2. `[writable]` The destination account. - /// 3. `[signer]` The source account's owner/delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The source account. - /// 1. `[]` The token mint. - /// 2. `[writable]` The destination account. - /// 3. `[]` The source account's multisignature owner/delegate. - /// 4. ..`4+M` `[signer]` M signer accounts. - TransferChecked { - /// The amount of tokens to transfer. - amount: u64, - /// Expected number of base 10 digits to the right of the decimal place. - decimals: u8, - }, - /// Approves a delegate. A delegate is given the authority over tokens on - /// behalf of the source account's owner. - /// - /// This instruction differs from Approve in that the token mint and - /// decimals value is checked by the caller. This may be useful when - /// creating transactions offline or within a hardware wallet. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner - /// 0. `[writable]` The source account. - /// 1. `[]` The token mint. - /// 2. `[]` The delegate. - /// 3. `[signer]` The source account owner. - /// - /// * Multisignature owner - /// 0. `[writable]` The source account. - /// 1. `[]` The token mint. - /// 2. `[]` The delegate. - /// 3. `[]` The source account's multisignature owner. - /// 4. ..`4+M` `[signer]` M signer accounts - ApproveChecked { - /// The amount of tokens the delegate is approved for. - amount: u64, - /// Expected number of base 10 digits to the right of the decimal place. - decimals: u8, - }, - /// Mints new tokens to an account. The native mint does not support - /// minting. - /// - /// This instruction differs from `MintTo` in that the decimals value is - /// checked by the caller. This may be useful when creating transactions - /// offline or within a hardware wallet. - /// - /// Accounts expected by this instruction: - /// - /// * Single authority - /// 0. `[writable]` The mint. - /// 1. `[writable]` The account to mint tokens to. - /// 2. `[signer]` The mint's minting authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The mint. - /// 1. `[writable]` The account to mint tokens to. - /// 2. `[]` The mint's multisignature mint-tokens authority. - /// 3. ..`3+M` `[signer]` M signer accounts. - MintToChecked { - /// The amount of new tokens to mint. - amount: u64, - /// Expected number of base 10 digits to the right of the decimal place. - decimals: u8, - }, - /// Burns tokens by removing them from an account. `BurnChecked` does not - /// support accounts associated with the native mint, use `CloseAccount` - /// instead. - /// - /// This instruction differs from Burn in that the decimals value is checked - /// by the caller. This may be useful when creating transactions offline or - /// within a hardware wallet. - /// - /// Accounts expected by this instruction: - /// - /// * Single owner/delegate - /// 0. `[writable]` The account to burn from. - /// 1. `[writable]` The token mint. - /// 2. `[signer]` The account's owner/delegate. - /// - /// * Multisignature owner/delegate - /// 0. `[writable]` The account to burn from. - /// 1. `[writable]` The token mint. - /// 2. `[]` The account's multisignature owner/delegate. - /// 3. ..`3+M` `[signer]` M signer accounts. - BurnChecked { - /// The amount of tokens to burn. - amount: u64, - /// Expected number of base 10 digits to the right of the decimal place. - decimals: u8, - }, - /// Like [`InitializeAccount`], but the owner pubkey is passed via - /// instruction data rather than the accounts list. This variant may be - /// preferable when using Cross Program Invocation from an instruction - /// that does not need the owner's `AccountInfo` otherwise. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to initialize. - /// 1. `[]` The mint this account will be associated with. - /// 3. `[]` Rent sysvar - InitializeAccount2 { - /// The new account's owner/multisignature. - owner: Pubkey, - }, - /// Given a wrapped / native token account (a token account containing SOL) - /// updates its amount field based on the account's underlying `lamports`. - /// This is useful if a non-wrapped SOL account uses - /// `system_instruction::transfer` to move lamports to a wrapped token - /// account, and needs to have its token `amount` field updated. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The native token account to sync with its underlying - /// lamports. - SyncNative, - /// Like [`InitializeAccount2`], but does not require the Rent sysvar to be - /// provided - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to initialize. - /// 1. `[]` The mint this account will be associated with. - InitializeAccount3 { - /// The new account's owner/multisignature. - owner: Pubkey, - }, - /// Like [`InitializeMultisig`], but does not require the Rent sysvar to be - /// provided - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The multisignature account to initialize. - /// 1. ..`1+N` `[]` The signer accounts, must equal to N where `1 <= N <= - /// 11`. - InitializeMultisig2 { - /// The number of signers (M) required to validate this multisignature - /// account. - m: u8, - }, - /// Like [`InitializeMint`], but does not require the Rent sysvar to be - /// provided - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint to initialize. - InitializeMint2 { - /// Number of base 10 digits to the right of the decimal place. - decimals: u8, - /// The authority/multisignature to mint tokens. - mint_authority: Pubkey, - /// The freeze authority/multisignature of the mint. - freeze_authority: COption, - }, - /// Gets the required size of an account for the given mint as a - /// little-endian `u64`. - /// - /// Return data can be fetched using `sol_get_return_data` and deserializing - /// the return data as a little-endian `u64`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[]` The mint to calculate for - GetAccountDataSize, // typically, there's also data, but this program ignores it - /// Initialize the Immutable Owner extension for the given token account - /// - /// Fails if the account has already been initialized, so must be called - /// before `InitializeAccount`. - /// - /// No-ops in this version of the program, but is included for compatibility - /// with the Associated Token Account program. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The account to initialize. - /// - /// Data expected by this instruction: - /// None - InitializeImmutableOwner, - /// Convert an Amount of tokens to a `UiAmount` string, using the given - /// mint. In this version of the program, the mint can only specify the - /// number of decimals. - /// - /// Fails on an invalid mint. - /// - /// Return data can be fetched using `sol_get_return_data` and deserialized - /// with `String::from_utf8`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[]` The mint to calculate for - AmountToUiAmount { - /// The amount of tokens to reformat. - amount: u64, - }, - /// Convert a `UiAmount` of tokens to a little-endian `u64` raw Amount, - /// using the given mint. In this version of the program, the mint can - /// only specify the number of decimals. - /// - /// Return data can be fetched using `sol_get_return_data` and deserializing - /// the return data as a little-endian `u64`. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[]` The mint to calculate for - UiAmountToAmount { - /// The `ui_amount` of tokens to reformat. - ui_amount: &'a str, - }, - // Any new variants also need to be added to program-2022 `TokenInstruction`, so that the - // latter remains a superset of this instruction set. New variants also need to be added to - // token/js/src/instructions/types.ts to maintain @solana/spl-token compatibility -} -impl<'a> TokenInstruction<'a> { - /// Unpacks a byte buffer into a - /// [`TokenInstruction`](enum.TokenInstruction.html). - pub fn unpack(input: &'a [u8]) -> Result { - use TokenError::InvalidInstruction; - - let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?; - Ok(match tag { - 0 => { - let (&decimals, rest) = rest.split_first().ok_or(InvalidInstruction)?; - let (mint_authority, rest) = Self::unpack_pubkey(rest)?; - let (freeze_authority, _rest) = Self::unpack_pubkey_option(rest)?; - Self::InitializeMint { - mint_authority, - freeze_authority, - decimals, - } - } - 1 => Self::InitializeAccount, - 2 => { - let &m = rest.first().ok_or(InvalidInstruction)?; - Self::InitializeMultisig { m } - } - 3 | 4 | 7 | 8 => { - let amount = rest - .get(..8) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(InvalidInstruction)?; - match tag { - 3 => Self::Transfer { amount }, - 4 => Self::Approve { amount }, - 7 => Self::MintTo { amount }, - 8 => Self::Burn { amount }, - _ => unreachable!(), - } - } - 5 => Self::Revoke, - 6 => { - let (authority_type, rest) = rest - .split_first() - .ok_or_else(|| ProgramError::from(InvalidInstruction)) - .and_then(|(&t, rest)| Ok((AuthorityType::from(t)?, rest)))?; - let (new_authority, _rest) = Self::unpack_pubkey_option(rest)?; - - Self::SetAuthority { - authority_type, - new_authority, - } - } - 9 => Self::CloseAccount, - 10 => Self::FreezeAccount, - 11 => Self::ThawAccount, - 12 => { - let (amount, decimals, _rest) = Self::unpack_amount_decimals(rest)?; - Self::TransferChecked { amount, decimals } - } - 13 => { - let (amount, decimals, _rest) = Self::unpack_amount_decimals(rest)?; - Self::ApproveChecked { amount, decimals } - } - 14 => { - let (amount, decimals, _rest) = Self::unpack_amount_decimals(rest)?; - Self::MintToChecked { amount, decimals } - } - 15 => { - let (amount, decimals, _rest) = Self::unpack_amount_decimals(rest)?; - Self::BurnChecked { amount, decimals } - } - 16 => { - let (owner, _rest) = Self::unpack_pubkey(rest)?; - Self::InitializeAccount2 { owner } - } - 17 => Self::SyncNative, - 18 => { - let (owner, _rest) = Self::unpack_pubkey(rest)?; - Self::InitializeAccount3 { owner } - } - 19 => { - let &m = rest.first().ok_or(InvalidInstruction)?; - Self::InitializeMultisig2 { m } - } - 20 => { - let (&decimals, rest) = rest.split_first().ok_or(InvalidInstruction)?; - let (mint_authority, rest) = Self::unpack_pubkey(rest)?; - let (freeze_authority, _rest) = Self::unpack_pubkey_option(rest)?; - Self::InitializeMint2 { - mint_authority, - freeze_authority, - decimals, - } - } - 21 => Self::GetAccountDataSize, - 22 => Self::InitializeImmutableOwner, - 23 => { - let (amount, _rest) = Self::unpack_u64(rest)?; - Self::AmountToUiAmount { amount } - } - 24 => { - let ui_amount = std::str::from_utf8(rest).map_err(|_| InvalidInstruction)?; - Self::UiAmountToAmount { ui_amount } - } - _ => return Err(TokenError::InvalidInstruction.into()), - }) - } - - /// Packs a [`TokenInstruction`](enum.TokenInstruction.html) into a byte - /// buffer. - pub fn pack(&self) -> Vec { - let mut buf = Vec::with_capacity(size_of::()); - match self { - &Self::InitializeMint { - ref mint_authority, - ref freeze_authority, - decimals, - } => { - buf.push(0); - buf.push(decimals); - buf.extend_from_slice(mint_authority.as_ref()); - Self::pack_pubkey_option(freeze_authority, &mut buf); - } - Self::InitializeAccount => buf.push(1), - &Self::InitializeMultisig { m } => { - buf.push(2); - buf.push(m); - } - &Self::Transfer { amount } => { - buf.push(3); - buf.extend_from_slice(&amount.to_le_bytes()); - } - &Self::Approve { amount } => { - buf.push(4); - buf.extend_from_slice(&amount.to_le_bytes()); - } - &Self::MintTo { amount } => { - buf.push(7); - buf.extend_from_slice(&amount.to_le_bytes()); - } - &Self::Burn { amount } => { - buf.push(8); - buf.extend_from_slice(&amount.to_le_bytes()); - } - Self::Revoke => buf.push(5), - Self::SetAuthority { - authority_type, - ref new_authority, - } => { - buf.push(6); - buf.push(authority_type.into()); - Self::pack_pubkey_option(new_authority, &mut buf); - } - Self::CloseAccount => buf.push(9), - Self::FreezeAccount => buf.push(10), - Self::ThawAccount => buf.push(11), - &Self::TransferChecked { amount, decimals } => { - buf.push(12); - buf.extend_from_slice(&amount.to_le_bytes()); - buf.push(decimals); - } - &Self::ApproveChecked { amount, decimals } => { - buf.push(13); - buf.extend_from_slice(&amount.to_le_bytes()); - buf.push(decimals); - } - &Self::MintToChecked { amount, decimals } => { - buf.push(14); - buf.extend_from_slice(&amount.to_le_bytes()); - buf.push(decimals); - } - &Self::BurnChecked { amount, decimals } => { - buf.push(15); - buf.extend_from_slice(&amount.to_le_bytes()); - buf.push(decimals); - } - &Self::InitializeAccount2 { owner } => { - buf.push(16); - buf.extend_from_slice(owner.as_ref()); - } - &Self::SyncNative => { - buf.push(17); - } - &Self::InitializeAccount3 { owner } => { - buf.push(18); - buf.extend_from_slice(owner.as_ref()); - } - &Self::InitializeMultisig2 { m } => { - buf.push(19); - buf.push(m); - } - &Self::InitializeMint2 { - ref mint_authority, - ref freeze_authority, - decimals, - } => { - buf.push(20); - buf.push(decimals); - buf.extend_from_slice(mint_authority.as_ref()); - Self::pack_pubkey_option(freeze_authority, &mut buf); - } - &Self::GetAccountDataSize => { - buf.push(21); - } - &Self::InitializeImmutableOwner => { - buf.push(22); - } - &Self::AmountToUiAmount { amount } => { - buf.push(23); - buf.extend_from_slice(&amount.to_le_bytes()); - } - Self::UiAmountToAmount { ui_amount } => { - buf.push(24); - buf.extend_from_slice(ui_amount.as_bytes()); - } - }; - buf - } - - fn unpack_pubkey(input: &[u8]) -> Result<(Pubkey, &[u8]), ProgramError> { - if input.len() >= 32 { - let (key, rest) = input.split_at(32); - let pk = Pubkey::try_from(key).map_err(|_| TokenError::InvalidInstruction)?; - Ok((pk, rest)) - } else { - Err(TokenError::InvalidInstruction.into()) - } - } - - fn unpack_pubkey_option(input: &[u8]) -> Result<(COption, &[u8]), ProgramError> { - match input.split_first() { - Option::Some((&0, rest)) => Ok((COption::None, rest)), - Option::Some((&1, rest)) if rest.len() >= 32 => { - let (key, rest) = rest.split_at(32); - let pk = Pubkey::try_from(key).map_err(|_| TokenError::InvalidInstruction)?; - Ok((COption::Some(pk), rest)) - } - _ => Err(TokenError::InvalidInstruction.into()), - } - } - - fn pack_pubkey_option(value: &COption, buf: &mut Vec) { - match *value { - COption::Some(ref key) => { - buf.push(1); - buf.extend_from_slice(&key.to_bytes()); - } - COption::None => buf.push(0), - } - } - - fn unpack_u64(input: &[u8]) -> Result<(u64, &[u8]), ProgramError> { - let value = input - .get(..U64_BYTES) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(TokenError::InvalidInstruction)?; - Ok((value, &input[U64_BYTES..])) - } - - fn unpack_amount_decimals(input: &[u8]) -> Result<(u64, u8, &[u8]), ProgramError> { - let (amount, rest) = Self::unpack_u64(input)?; - let (&decimals, rest) = rest.split_first().ok_or(TokenError::InvalidInstruction)?; - Ok((amount, decimals, rest)) - } -} - -/// Specifies the authority type for `SetAuthority` instructions -#[repr(u8)] -#[derive(Clone, Debug, PartialEq)] -pub enum AuthorityType { - /// Authority to mint new tokens - MintTokens, - /// Authority to freeze any account associated with the Mint - FreezeAccount, - /// Owner of a given token account - AccountOwner, - /// Authority to close a token account - CloseAccount, -} - -impl AuthorityType { - fn into(&self) -> u8 { - match self { - AuthorityType::MintTokens => 0, - AuthorityType::FreezeAccount => 1, - AuthorityType::AccountOwner => 2, - AuthorityType::CloseAccount => 3, - } - } - - fn from(index: u8) -> Result { - match index { - 0 => Ok(AuthorityType::MintTokens), - 1 => Ok(AuthorityType::FreezeAccount), - 2 => Ok(AuthorityType::AccountOwner), - 3 => Ok(AuthorityType::CloseAccount), - _ => Err(TokenError::InvalidInstruction.into()), - } - } -} - -/// Creates a `InitializeMint` instruction. -pub fn initialize_mint( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - mint_authority_pubkey: &Pubkey, - freeze_authority_pubkey: Option<&Pubkey>, - decimals: u8, -) -> Result { - check_program_account(token_program_id)?; - let freeze_authority = freeze_authority_pubkey.cloned().into(); - let data = TokenInstruction::InitializeMint { - mint_authority: *mint_authority_pubkey, - freeze_authority, - decimals, - } - .pack(); - - let accounts = vec![ - AccountMeta::new(*mint_pubkey, false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - ]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeMint2` instruction. -pub fn initialize_mint2( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - mint_authority_pubkey: &Pubkey, - freeze_authority_pubkey: Option<&Pubkey>, - decimals: u8, -) -> Result { - check_program_account(token_program_id)?; - let freeze_authority = freeze_authority_pubkey.cloned().into(); - let data = TokenInstruction::InitializeMint2 { - mint_authority: *mint_authority_pubkey, - freeze_authority, - decimals, - } - .pack(); - - let accounts = vec![AccountMeta::new(*mint_pubkey, false)]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeAccount` instruction. -pub fn initialize_account( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::InitializeAccount.pack(); - - let accounts = vec![ - AccountMeta::new(*account_pubkey, false), - AccountMeta::new_readonly(*mint_pubkey, false), - AccountMeta::new_readonly(*owner_pubkey, false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - ]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeAccount2` instruction. -pub fn initialize_account2( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::InitializeAccount2 { - owner: *owner_pubkey, - } - .pack(); - - let accounts = vec![ - AccountMeta::new(*account_pubkey, false), - AccountMeta::new_readonly(*mint_pubkey, false), - AccountMeta::new_readonly(sysvar::rent::id(), false), - ]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeAccount3` instruction. -pub fn initialize_account3( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::InitializeAccount3 { - owner: *owner_pubkey, - } - .pack(); - - let accounts = vec![ - AccountMeta::new(*account_pubkey, false), - AccountMeta::new_readonly(*mint_pubkey, false), - ]; - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeMultisig` instruction. -pub fn initialize_multisig( - token_program_id: &Pubkey, - multisig_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - m: u8, -) -> Result { - check_program_account(token_program_id)?; - if !is_valid_signer_index(m as usize) - || !is_valid_signer_index(signer_pubkeys.len()) - || m as usize > signer_pubkeys.len() - { - return Err(ProgramError::MissingRequiredSignature); - } - let data = TokenInstruction::InitializeMultisig { m }.pack(); - - let mut accounts = Vec::with_capacity(1 + 1 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*multisig_pubkey, false)); - accounts.push(AccountMeta::new_readonly(sysvar::rent::id(), false)); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, false)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `InitializeMultisig2` instruction. -pub fn initialize_multisig2( - token_program_id: &Pubkey, - multisig_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - m: u8, -) -> Result { - check_program_account(token_program_id)?; - if !is_valid_signer_index(m as usize) - || !is_valid_signer_index(signer_pubkeys.len()) - || m as usize > signer_pubkeys.len() - { - return Err(ProgramError::MissingRequiredSignature); - } - let data = TokenInstruction::InitializeMultisig2 { m }.pack(); - - let mut accounts = Vec::with_capacity(1 + 1 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*multisig_pubkey, false)); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, false)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `Transfer` instruction. -pub fn transfer( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::Transfer { amount }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new(*destination_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *authority_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates an `Approve` instruction. -pub fn approve( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - delegate_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::Approve { amount }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*delegate_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `Revoke` instruction. -pub fn revoke( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::Revoke.pack(); - - let mut accounts = Vec::with_capacity(2 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `SetAuthority` instruction. -pub fn set_authority( - token_program_id: &Pubkey, - owned_pubkey: &Pubkey, - new_authority_pubkey: Option<&Pubkey>, - authority_type: AuthorityType, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let new_authority = new_authority_pubkey.cloned().into(); - let data = TokenInstruction::SetAuthority { - authority_type, - new_authority, - } - .pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*owned_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `MintTo` instruction. -pub fn mint_to( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - account_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::MintTo { amount }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*mint_pubkey, false)); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `Burn` instruction. -pub fn burn( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::Burn { amount }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *authority_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `CloseAccount` instruction. -pub fn close_account( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::CloseAccount.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new(*destination_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `FreezeAccount` instruction. -pub fn freeze_account( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::FreezeAccount.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `ThawAccount` instruction. -pub fn thaw_account( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::ThawAccount.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `TransferChecked` instruction. -#[allow(clippy::too_many_arguments)] -pub fn transfer_checked( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::TransferChecked { amount, decimals }.pack(); - - let mut accounts = Vec::with_capacity(4 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*mint_pubkey, false)); - accounts.push(AccountMeta::new(*destination_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *authority_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates an `ApproveChecked` instruction. -#[allow(clippy::too_many_arguments)] -pub fn approve_checked( - token_program_id: &Pubkey, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - delegate_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::ApproveChecked { amount, decimals }.pack(); - - let mut accounts = Vec::with_capacity(4 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*source_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly(*delegate_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `MintToChecked` instruction. -pub fn mint_to_checked( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - account_pubkey: &Pubkey, - owner_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::MintToChecked { amount, decimals }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*mint_pubkey, false)); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *owner_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `BurnChecked` instruction. -pub fn burn_checked( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - signer_pubkeys: &[&Pubkey], - amount: u64, - decimals: u8, -) -> Result { - check_program_account(token_program_id)?; - let data = TokenInstruction::BurnChecked { amount, decimals }.pack(); - - let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); - accounts.push(AccountMeta::new(*account_pubkey, false)); - accounts.push(AccountMeta::new(*mint_pubkey, false)); - accounts.push(AccountMeta::new_readonly( - *authority_pubkey, - signer_pubkeys.is_empty(), - )); - for signer_pubkey in signer_pubkeys.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - - Ok(Instruction { - program_id: *token_program_id, - accounts, - data, - }) -} - -/// Creates a `SyncNative` instruction -pub fn sync_native( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new(*account_pubkey, false)], - data: TokenInstruction::SyncNative.pack(), - }) -} - -/// Creates a `GetAccountDataSize` instruction -pub fn get_account_data_size( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new_readonly(*mint_pubkey, false)], - data: TokenInstruction::GetAccountDataSize.pack(), - }) -} - -/// Creates a `InitializeImmutableOwner` instruction -pub fn initialize_immutable_owner( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, -) -> Result { - check_program_account(token_program_id)?; - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new(*account_pubkey, false)], - data: TokenInstruction::InitializeImmutableOwner.pack(), - }) -} - -/// Creates an `AmountToUiAmount` instruction -pub fn amount_to_ui_amount( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - amount: u64, -) -> Result { - check_program_account(token_program_id)?; - - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new_readonly(*mint_pubkey, false)], - data: TokenInstruction::AmountToUiAmount { amount }.pack(), - }) -} - -/// Creates a `UiAmountToAmount` instruction -pub fn ui_amount_to_amount( - token_program_id: &Pubkey, - mint_pubkey: &Pubkey, - ui_amount: &str, -) -> Result { - check_program_account(token_program_id)?; - - Ok(Instruction { - program_id: *token_program_id, - accounts: vec![AccountMeta::new_readonly(*mint_pubkey, false)], - data: TokenInstruction::UiAmountToAmount { ui_amount }.pack(), - }) -} - -/// Utility function that checks index is between `MIN_SIGNERS` and -/// `MAX_SIGNERS` -pub fn is_valid_signer_index(index: usize) -> bool { - (MIN_SIGNERS..=MAX_SIGNERS).contains(&index) -} - -#[cfg(test)] -mod test { - use {super::*, proptest::prelude::*}; - - #[test] - fn test_instruction_packing() { - let check = TokenInstruction::InitializeMint { - decimals: 2, - mint_authority: Pubkey::new_from_array([1u8; 32]), - freeze_authority: COption::None, - }; - let packed = check.pack(); - let mut expect = Vec::from([0u8, 2]); - expect.extend_from_slice(&[1u8; 32]); - expect.extend_from_slice(&[0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::InitializeMint { - decimals: 2, - mint_authority: Pubkey::new_from_array([2u8; 32]), - freeze_authority: COption::Some(Pubkey::new_from_array([3u8; 32])), - }; - let packed = check.pack(); - let mut expect = vec![0u8, 2]; - expect.extend_from_slice(&[2u8; 32]); - expect.extend_from_slice(&[1]); - expect.extend_from_slice(&[3u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::InitializeAccount; - let packed = check.pack(); - let expect = Vec::from([1u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::InitializeMultisig { m: 1 }; - let packed = check.pack(); - let expect = Vec::from([2u8, 1]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::Transfer { amount: 1 }; - let packed = check.pack(); - let expect = Vec::from([3u8, 1, 0, 0, 0, 0, 0, 0, 0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::Approve { amount: 1 }; - let packed = check.pack(); - let expect = Vec::from([4u8, 1, 0, 0, 0, 0, 0, 0, 0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::Revoke; - let packed = check.pack(); - let expect = Vec::from([5u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::SetAuthority { - authority_type: AuthorityType::FreezeAccount, - new_authority: COption::Some(Pubkey::new_from_array([4u8; 32])), - }; - let packed = check.pack(); - let mut expect = Vec::from([6u8, 1]); - expect.extend_from_slice(&[1]); - expect.extend_from_slice(&[4u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::MintTo { amount: 1 }; - let packed = check.pack(); - let expect = Vec::from([7u8, 1, 0, 0, 0, 0, 0, 0, 0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::Burn { amount: 1 }; - let packed = check.pack(); - let expect = Vec::from([8u8, 1, 0, 0, 0, 0, 0, 0, 0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::CloseAccount; - let packed = check.pack(); - let expect = Vec::from([9u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::FreezeAccount; - let packed = check.pack(); - let expect = Vec::from([10u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::ThawAccount; - let packed = check.pack(); - let expect = Vec::from([11u8]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::TransferChecked { - amount: 1, - decimals: 2, - }; - let packed = check.pack(); - let expect = Vec::from([12u8, 1, 0, 0, 0, 0, 0, 0, 0, 2]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::ApproveChecked { - amount: 1, - decimals: 2, - }; - let packed = check.pack(); - let expect = Vec::from([13u8, 1, 0, 0, 0, 0, 0, 0, 0, 2]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::MintToChecked { - amount: 1, - decimals: 2, - }; - let packed = check.pack(); - let expect = Vec::from([14u8, 1, 0, 0, 0, 0, 0, 0, 0, 2]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::BurnChecked { - amount: 1, - decimals: 2, - }; - let packed = check.pack(); - let expect = Vec::from([15u8, 1, 0, 0, 0, 0, 0, 0, 0, 2]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::InitializeAccount2 { - owner: Pubkey::new_from_array([2u8; 32]), - }; - let packed = check.pack(); - let mut expect = vec![16u8]; - expect.extend_from_slice(&[2u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::SyncNative; - let packed = check.pack(); - let expect = vec![17u8]; - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::InitializeAccount3 { - owner: Pubkey::new_from_array([2u8; 32]), - }; - let packed = check.pack(); - let mut expect = vec![18u8]; - expect.extend_from_slice(&[2u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::InitializeMultisig2 { m: 1 }; - let packed = check.pack(); - let expect = Vec::from([19u8, 1]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::InitializeMint2 { - decimals: 2, - mint_authority: Pubkey::new_from_array([1u8; 32]), - freeze_authority: COption::None, - }; - let packed = check.pack(); - let mut expect = Vec::from([20u8, 2]); - expect.extend_from_slice(&[1u8; 32]); - expect.extend_from_slice(&[0]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::InitializeMint2 { - decimals: 2, - mint_authority: Pubkey::new_from_array([2u8; 32]), - freeze_authority: COption::Some(Pubkey::new_from_array([3u8; 32])), - }; - let packed = check.pack(); - let mut expect = vec![20u8, 2]; - expect.extend_from_slice(&[2u8; 32]); - expect.extend_from_slice(&[1]); - expect.extend_from_slice(&[3u8; 32]); - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::GetAccountDataSize; - let packed = check.pack(); - let expect = vec![21u8]; - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::InitializeImmutableOwner; - let packed = check.pack(); - let expect = vec![22u8]; - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::AmountToUiAmount { amount: 42 }; - let packed = check.pack(); - let expect = vec![23u8, 42, 0, 0, 0, 0, 0, 0, 0]; - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - - let check = TokenInstruction::UiAmountToAmount { ui_amount: "0.42" }; - let packed = check.pack(); - let expect = vec![24u8, 48, 46, 52, 50]; - assert_eq!(packed, expect); - let unpacked = TokenInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - } - - #[test] - fn test_instruction_unpack_panic() { - for i in 0..255u8 { - for j in 1..10 { - let mut data = vec![0; j]; - data[0] = i; - let _no_panic = TokenInstruction::unpack(&data); - } - } - } - - proptest! { - #![proptest_config(ProptestConfig::with_cases(1024))] - #[test] - fn test_instruction_unpack_proptest( - data in prop::collection::vec(any::(), 0..255) - ) { - let _no_panic = TokenInstruction::unpack(&data); - } - } -} diff --git a/token/program/src/lib.rs b/token/program/src/lib.rs deleted file mode 100644 index 25175df702f..00000000000 --- a/token/program/src/lib.rs +++ /dev/null @@ -1,93 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -//! An ERC20-like Token program for the Solana blockchain - -pub mod error; -pub mod instruction; -pub mod native_mint; -pub mod processor; -pub mod state; - -#[cfg(not(feature = "no-entrypoint"))] -mod entrypoint; - -// Export current sdk types for downstream users building with a different sdk -// version -pub use solana_program; -use solana_program::{entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey}; - -/// Convert the UI representation of a token amount (using the decimals field -/// defined in its mint) to the raw amount -pub fn ui_amount_to_amount(ui_amount: f64, decimals: u8) -> u64 { - (ui_amount * 10_usize.pow(decimals as u32) as f64) as u64 -} - -/// Convert a raw amount to its UI representation (using the decimals field -/// defined in its mint) -pub fn amount_to_ui_amount(amount: u64, decimals: u8) -> f64 { - amount as f64 / 10_usize.pow(decimals as u32) as f64 -} - -/// Convert a raw amount to its UI representation (using the decimals field -/// defined in its mint) -pub fn amount_to_ui_amount_string(amount: u64, decimals: u8) -> String { - let decimals = decimals as usize; - if decimals > 0 { - // Left-pad zeros to decimals + 1, so we at least have an integer zero - let mut s = format!("{:01$}", amount, decimals + 1); - // Add the decimal point (Sorry, "," locales!) - s.insert(s.len() - decimals, '.'); - s - } else { - amount.to_string() - } -} - -/// Convert a raw amount to its UI representation using the given decimals field -/// Excess zeroes or unneeded decimal point are trimmed. -pub fn amount_to_ui_amount_string_trimmed(amount: u64, decimals: u8) -> String { - let mut s = amount_to_ui_amount_string(amount, decimals); - if decimals > 0 { - let zeros_trimmed = s.trim_end_matches('0'); - s = zeros_trimmed.trim_end_matches('.').to_string(); - } - s -} - -/// Try to convert a UI representation of a token amount to its raw amount using -/// the given decimals field -pub fn try_ui_amount_into_amount(ui_amount: String, decimals: u8) -> Result { - let decimals = decimals as usize; - let mut parts = ui_amount.split('.'); - // splitting a string, even an empty one, will always yield an iterator of - // at least length == 1 - let mut amount_str = parts.next().unwrap().to_string(); - let after_decimal = parts.next().unwrap_or(""); - let after_decimal = after_decimal.trim_end_matches('0'); - if (amount_str.is_empty() && after_decimal.is_empty()) - || parts.next().is_some() - || after_decimal.len() > decimals - { - return Err(ProgramError::InvalidArgument); - } - - amount_str.push_str(after_decimal); - for _ in 0..decimals.saturating_sub(after_decimal.len()) { - amount_str.push('0'); - } - amount_str - .parse::() - .map_err(|_| ProgramError::InvalidArgument) -} - -solana_program::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); - -/// Checks that the supplied program ID is the correct one for SPL-token -pub fn check_program_account(spl_token_program_id: &Pubkey) -> ProgramResult { - if spl_token_program_id != &id() { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} diff --git a/token/program/src/native_mint.rs b/token/program/src/native_mint.rs deleted file mode 100644 index bd489eee14f..00000000000 --- a/token/program/src/native_mint.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! The Mint that represents the native token - -/// There are `10^9` lamports in one SOL -pub const DECIMALS: u8 = 9; - -// The Mint for native SOL Token accounts -solana_program::declare_id!("So11111111111111111111111111111111111111112"); - -#[cfg(test)] -mod tests { - use {super::*, solana_program::native_token::*}; - - #[test] - fn test_decimals() { - assert!( - (lamports_to_sol(42) - crate::amount_to_ui_amount(42, DECIMALS)).abs() < f64::EPSILON - ); - assert_eq!( - sol_to_lamports(42.), - crate::ui_amount_to_amount(42., DECIMALS) - ); - } -} diff --git a/token/program/src/processor.rs b/token/program/src/processor.rs deleted file mode 100644 index 4702693abae..00000000000 --- a/token/program/src/processor.rs +++ /dev/null @@ -1,7026 +0,0 @@ -//! Program state processor - -use { - crate::{ - amount_to_ui_amount_string_trimmed, - error::TokenError, - instruction::{is_valid_signer_index, AuthorityType, TokenInstruction, MAX_SIGNERS}, - state::{Account, AccountState, Mint, Multisig}, - try_ui_amount_into_amount, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program::set_return_data, - program_error::ProgramError, - program_memory::sol_memcmp, - program_option::COption, - program_pack::{IsInitialized, Pack}, - pubkey::{Pubkey, PUBKEY_BYTES}, - system_program, - sysvar::{rent::Rent, Sysvar}, - }, -}; - -/// Program state handler. -pub struct Processor {} -impl Processor { - fn _process_initialize_mint( - accounts: &[AccountInfo], - decimals: u8, - mint_authority: Pubkey, - freeze_authority: COption, - rent_sysvar_account: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - let mint_data_len = mint_info.data_len(); - let rent = if rent_sysvar_account { - Rent::from_account_info(next_account_info(account_info_iter)?)? - } else { - Rent::get()? - }; - - let mut mint = Mint::unpack_unchecked(&mint_info.data.borrow())?; - if mint.is_initialized { - return Err(TokenError::AlreadyInUse.into()); - } - - if !rent.is_exempt(mint_info.lamports(), mint_data_len) { - return Err(TokenError::NotRentExempt.into()); - } - - mint.mint_authority = COption::Some(mint_authority); - mint.decimals = decimals; - mint.is_initialized = true; - mint.freeze_authority = freeze_authority; - - Mint::pack(mint, &mut mint_info.data.borrow_mut())?; - - Ok(()) - } - - /// Processes an [`InitializeMint`](enum.TokenInstruction.html) instruction. - pub fn process_initialize_mint( - accounts: &[AccountInfo], - decimals: u8, - mint_authority: Pubkey, - freeze_authority: COption, - ) -> ProgramResult { - Self::_process_initialize_mint(accounts, decimals, mint_authority, freeze_authority, true) - } - - /// Processes an [`InitializeMint2`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_mint2( - accounts: &[AccountInfo], - decimals: u8, - mint_authority: Pubkey, - freeze_authority: COption, - ) -> ProgramResult { - Self::_process_initialize_mint(accounts, decimals, mint_authority, freeze_authority, false) - } - - fn _process_initialize_account( - program_id: &Pubkey, - accounts: &[AccountInfo], - owner: Option<&Pubkey>, - rent_sysvar_account: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let new_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let owner = if let Some(owner) = owner { - owner - } else { - next_account_info(account_info_iter)?.key - }; - let new_account_info_data_len = new_account_info.data_len(); - let rent = if rent_sysvar_account { - Rent::from_account_info(next_account_info(account_info_iter)?)? - } else { - Rent::get()? - }; - - let mut account = Account::unpack_unchecked(&new_account_info.data.borrow())?; - if account.is_initialized() { - return Err(TokenError::AlreadyInUse.into()); - } - - if !rent.is_exempt(new_account_info.lamports(), new_account_info_data_len) { - return Err(TokenError::NotRentExempt.into()); - } - - let is_native_mint = Self::cmp_pubkeys(mint_info.key, &crate::native_mint::id()); - if !is_native_mint { - Self::check_account_owner(program_id, mint_info)?; - let _ = Mint::unpack(&mint_info.data.borrow_mut()) - .map_err(|_| Into::::into(TokenError::InvalidMint))?; - } - - account.mint = *mint_info.key; - account.owner = *owner; - account.close_authority = COption::None; - account.delegate = COption::None; - account.delegated_amount = 0; - account.state = AccountState::Initialized; - if is_native_mint { - let rent_exempt_reserve = rent.minimum_balance(new_account_info_data_len); - account.is_native = COption::Some(rent_exempt_reserve); - account.amount = new_account_info - .lamports() - .checked_sub(rent_exempt_reserve) - .ok_or(TokenError::Overflow)?; - } else { - account.is_native = COption::None; - account.amount = 0; - }; - - Account::pack(account, &mut new_account_info.data.borrow_mut())?; - - Ok(()) - } - - /// Processes an [`InitializeAccount`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_account( - program_id: &Pubkey, - accounts: &[AccountInfo], - ) -> ProgramResult { - Self::_process_initialize_account(program_id, accounts, None, true) - } - - /// Processes an [`InitializeAccount2`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_account2( - program_id: &Pubkey, - accounts: &[AccountInfo], - owner: Pubkey, - ) -> ProgramResult { - Self::_process_initialize_account(program_id, accounts, Some(&owner), true) - } - - /// Processes an [`InitializeAccount3`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_account3( - program_id: &Pubkey, - accounts: &[AccountInfo], - owner: Pubkey, - ) -> ProgramResult { - Self::_process_initialize_account(program_id, accounts, Some(&owner), false) - } - - fn _process_initialize_multisig( - accounts: &[AccountInfo], - m: u8, - rent_sysvar_account: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let multisig_info = next_account_info(account_info_iter)?; - let multisig_info_data_len = multisig_info.data_len(); - let rent = if rent_sysvar_account { - Rent::from_account_info(next_account_info(account_info_iter)?)? - } else { - Rent::get()? - }; - - let mut multisig = Multisig::unpack_unchecked(&multisig_info.data.borrow())?; - if multisig.is_initialized { - return Err(TokenError::AlreadyInUse.into()); - } - - if !rent.is_exempt(multisig_info.lamports(), multisig_info_data_len) { - return Err(TokenError::NotRentExempt.into()); - } - - let signer_infos = account_info_iter.as_slice(); - multisig.m = m; - multisig.n = signer_infos.len() as u8; - if !is_valid_signer_index(multisig.n as usize) { - return Err(TokenError::InvalidNumberOfProvidedSigners.into()); - } - if !is_valid_signer_index(multisig.m as usize) { - return Err(TokenError::InvalidNumberOfRequiredSigners.into()); - } - for (i, signer_info) in signer_infos.iter().enumerate() { - multisig.signers[i] = *signer_info.key; - } - multisig.is_initialized = true; - - Multisig::pack(multisig, &mut multisig_info.data.borrow_mut())?; - - Ok(()) - } - - /// Processes a [`InitializeMultisig`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_multisig(accounts: &[AccountInfo], m: u8) -> ProgramResult { - Self::_process_initialize_multisig(accounts, m, true) - } - - /// Processes a [`InitializeMultisig2`](enum.TokenInstruction.html) - /// instruction. - pub fn process_initialize_multisig2(accounts: &[AccountInfo], m: u8) -> ProgramResult { - Self::_process_initialize_multisig(accounts, m, false) - } - - /// Processes a [`Transfer`](enum.TokenInstruction.html) instruction. - pub fn process_transfer( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_account_info = next_account_info(account_info_iter)?; - - let expected_mint_info = if let Some(expected_decimals) = expected_decimals { - Some((next_account_info(account_info_iter)?, expected_decimals)) - } else { - None - }; - - let destination_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - let mut source_account = Account::unpack(&source_account_info.data.borrow())?; - let mut destination_account = Account::unpack(&destination_account_info.data.borrow())?; - - if source_account.is_frozen() || destination_account.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - if source_account.amount < amount { - return Err(TokenError::InsufficientFunds.into()); - } - if !Self::cmp_pubkeys(&source_account.mint, &destination_account.mint) { - return Err(TokenError::MintMismatch.into()); - } - - if let Some((mint_info, expected_decimals)) = expected_mint_info { - if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) { - return Err(TokenError::MintMismatch.into()); - } - - let mint = Mint::unpack(&mint_info.data.borrow_mut())?; - if expected_decimals != mint.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - } - - let self_transfer = - Self::cmp_pubkeys(source_account_info.key, destination_account_info.key); - - match source_account.delegate { - COption::Some(ref delegate) if Self::cmp_pubkeys(authority_info.key, delegate) => { - Self::validate_owner( - program_id, - delegate, - authority_info, - account_info_iter.as_slice(), - )?; - if source_account.delegated_amount < amount { - return Err(TokenError::InsufficientFunds.into()); - } - if !self_transfer { - source_account.delegated_amount = source_account - .delegated_amount - .checked_sub(amount) - .ok_or(TokenError::Overflow)?; - if source_account.delegated_amount == 0 { - source_account.delegate = COption::None; - } - } - } - _ => Self::validate_owner( - program_id, - &source_account.owner, - authority_info, - account_info_iter.as_slice(), - )?, - }; - - if self_transfer || amount == 0 { - Self::check_account_owner(program_id, source_account_info)?; - Self::check_account_owner(program_id, destination_account_info)?; - } - - // This check MUST occur just before the amounts are manipulated - // to ensure self-transfers are fully validated - if self_transfer { - return Ok(()); - } - - source_account.amount = source_account - .amount - .checked_sub(amount) - .ok_or(TokenError::Overflow)?; - destination_account.amount = destination_account - .amount - .checked_add(amount) - .ok_or(TokenError::Overflow)?; - - if source_account.is_native() { - let source_starting_lamports = source_account_info.lamports(); - **source_account_info.lamports.borrow_mut() = source_starting_lamports - .checked_sub(amount) - .ok_or(TokenError::Overflow)?; - - let destination_starting_lamports = destination_account_info.lamports(); - **destination_account_info.lamports.borrow_mut() = destination_starting_lamports - .checked_add(amount) - .ok_or(TokenError::Overflow)?; - } - - Account::pack(source_account, &mut source_account_info.data.borrow_mut())?; - Account::pack( - destination_account, - &mut destination_account_info.data.borrow_mut(), - )?; - - Ok(()) - } - - /// Processes an [`Approve`](enum.TokenInstruction.html) instruction. - pub fn process_approve( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_account_info = next_account_info(account_info_iter)?; - - let expected_mint_info = if let Some(expected_decimals) = expected_decimals { - Some((next_account_info(account_info_iter)?, expected_decimals)) - } else { - None - }; - let delegate_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - - let mut source_account = Account::unpack(&source_account_info.data.borrow())?; - - if source_account.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if let Some((mint_info, expected_decimals)) = expected_mint_info { - if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) { - return Err(TokenError::MintMismatch.into()); - } - - let mint = Mint::unpack(&mint_info.data.borrow_mut())?; - if expected_decimals != mint.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - } - - Self::validate_owner( - program_id, - &source_account.owner, - owner_info, - account_info_iter.as_slice(), - )?; - - source_account.delegate = COption::Some(*delegate_info.key); - source_account.delegated_amount = amount; - - Account::pack(source_account, &mut source_account_info.data.borrow_mut())?; - - Ok(()) - } - - /// Processes an [`Revoke`](enum.TokenInstruction.html) instruction. - pub fn process_revoke(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let source_account_info = next_account_info(account_info_iter)?; - - let mut source_account = Account::unpack(&source_account_info.data.borrow())?; - - let owner_info = next_account_info(account_info_iter)?; - - if source_account.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - Self::validate_owner( - program_id, - &source_account.owner, - owner_info, - account_info_iter.as_slice(), - )?; - - source_account.delegate = COption::None; - source_account.delegated_amount = 0; - - Account::pack(source_account, &mut source_account_info.data.borrow_mut())?; - - Ok(()) - } - - /// Processes a [`SetAuthority`](enum.TokenInstruction.html) instruction. - pub fn process_set_authority( - program_id: &Pubkey, - accounts: &[AccountInfo], - authority_type: AuthorityType, - new_authority: COption, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - if account_info.data_len() == Account::get_packed_len() { - let mut account = Account::unpack(&account_info.data.borrow())?; - - if account.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - match authority_type { - AuthorityType::AccountOwner => { - Self::validate_owner( - program_id, - &account.owner, - authority_info, - account_info_iter.as_slice(), - )?; - - if let COption::Some(authority) = new_authority { - account.owner = authority; - } else { - return Err(TokenError::InvalidInstruction.into()); - } - - account.delegate = COption::None; - account.delegated_amount = 0; - - if account.is_native() { - account.close_authority = COption::None; - } - } - AuthorityType::CloseAccount => { - let authority = account.close_authority.unwrap_or(account.owner); - Self::validate_owner( - program_id, - &authority, - authority_info, - account_info_iter.as_slice(), - )?; - account.close_authority = new_authority; - } - _ => { - return Err(TokenError::AuthorityTypeNotSupported.into()); - } - } - Account::pack(account, &mut account_info.data.borrow_mut())?; - } else if account_info.data_len() == Mint::get_packed_len() { - let mut mint = Mint::unpack(&account_info.data.borrow())?; - match authority_type { - AuthorityType::MintTokens => { - // Once a mint's supply is fixed, it cannot be undone by setting a new - // mint_authority - let mint_authority = mint - .mint_authority - .ok_or(Into::::into(TokenError::FixedSupply))?; - Self::validate_owner( - program_id, - &mint_authority, - authority_info, - account_info_iter.as_slice(), - )?; - mint.mint_authority = new_authority; - } - AuthorityType::FreezeAccount => { - // Once a mint's freeze authority is disabled, it cannot be re-enabled by - // setting a new freeze_authority - let freeze_authority = mint - .freeze_authority - .ok_or(Into::::into(TokenError::MintCannotFreeze))?; - Self::validate_owner( - program_id, - &freeze_authority, - authority_info, - account_info_iter.as_slice(), - )?; - mint.freeze_authority = new_authority; - } - _ => { - return Err(TokenError::AuthorityTypeNotSupported.into()); - } - } - Mint::pack(mint, &mut account_info.data.borrow_mut())?; - } else { - return Err(ProgramError::InvalidArgument); - } - - Ok(()) - } - - /// Processes a [`MintTo`](enum.TokenInstruction.html) instruction. - pub fn process_mint_to( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - let owner_info = next_account_info(account_info_iter)?; - - let mut destination_account = Account::unpack(&destination_account_info.data.borrow())?; - if destination_account.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - - if destination_account.is_native() { - return Err(TokenError::NativeNotSupported.into()); - } - if !Self::cmp_pubkeys(mint_info.key, &destination_account.mint) { - return Err(TokenError::MintMismatch.into()); - } - - let mut mint = Mint::unpack(&mint_info.data.borrow())?; - if let Some(expected_decimals) = expected_decimals { - if expected_decimals != mint.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - } - - match mint.mint_authority { - COption::Some(mint_authority) => Self::validate_owner( - program_id, - &mint_authority, - owner_info, - account_info_iter.as_slice(), - )?, - COption::None => return Err(TokenError::FixedSupply.into()), - } - - if amount == 0 { - Self::check_account_owner(program_id, mint_info)?; - Self::check_account_owner(program_id, destination_account_info)?; - } - - destination_account.amount = destination_account - .amount - .checked_add(amount) - .ok_or(TokenError::Overflow)?; - - mint.supply = mint - .supply - .checked_add(amount) - .ok_or(TokenError::Overflow)?; - - Account::pack( - destination_account, - &mut destination_account_info.data.borrow_mut(), - )?; - Mint::pack(mint, &mut mint_info.data.borrow_mut())?; - - Ok(()) - } - - /// Processes a [`Burn`](enum.TokenInstruction.html) instruction. - pub fn process_burn( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - expected_decimals: Option, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - let mut source_account = Account::unpack(&source_account_info.data.borrow())?; - let mut mint = Mint::unpack(&mint_info.data.borrow())?; - - if source_account.is_frozen() { - return Err(TokenError::AccountFrozen.into()); - } - if source_account.is_native() { - return Err(TokenError::NativeNotSupported.into()); - } - if source_account.amount < amount { - return Err(TokenError::InsufficientFunds.into()); - } - if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) { - return Err(TokenError::MintMismatch.into()); - } - - if let Some(expected_decimals) = expected_decimals { - if expected_decimals != mint.decimals { - return Err(TokenError::MintDecimalsMismatch.into()); - } - } - - if !source_account.is_owned_by_system_program_or_incinerator() { - match source_account.delegate { - COption::Some(ref delegate) if Self::cmp_pubkeys(authority_info.key, delegate) => { - Self::validate_owner( - program_id, - delegate, - authority_info, - account_info_iter.as_slice(), - )?; - - if source_account.delegated_amount < amount { - return Err(TokenError::InsufficientFunds.into()); - } - source_account.delegated_amount = source_account - .delegated_amount - .checked_sub(amount) - .ok_or(TokenError::Overflow)?; - if source_account.delegated_amount == 0 { - source_account.delegate = COption::None; - } - } - _ => Self::validate_owner( - program_id, - &source_account.owner, - authority_info, - account_info_iter.as_slice(), - )?, - } - } - - if amount == 0 { - Self::check_account_owner(program_id, source_account_info)?; - Self::check_account_owner(program_id, mint_info)?; - } - - source_account.amount = source_account - .amount - .checked_sub(amount) - .ok_or(TokenError::Overflow)?; - mint.supply = mint - .supply - .checked_sub(amount) - .ok_or(TokenError::Overflow)?; - - Account::pack(source_account, &mut source_account_info.data.borrow_mut())?; - Mint::pack(mint, &mut mint_info.data.borrow_mut())?; - - Ok(()) - } - - /// Processes a [`CloseAccount`](enum.TokenInstruction.html) instruction. - pub fn process_close_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let source_account_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - if Self::cmp_pubkeys(source_account_info.key, destination_account_info.key) { - return Err(ProgramError::InvalidAccountData); - } - - let source_account = Account::unpack(&source_account_info.data.borrow())?; - if !source_account.is_native() && source_account.amount != 0 { - return Err(TokenError::NonNativeHasBalance.into()); - } - - let authority = source_account - .close_authority - .unwrap_or(source_account.owner); - if !source_account.is_owned_by_system_program_or_incinerator() { - Self::validate_owner( - program_id, - &authority, - authority_info, - account_info_iter.as_slice(), - )?; - } else if !solana_program::incinerator::check_id(destination_account_info.key) { - return Err(ProgramError::InvalidAccountData); - } - - let destination_starting_lamports = destination_account_info.lamports(); - **destination_account_info.lamports.borrow_mut() = destination_starting_lamports - .checked_add(source_account_info.lamports()) - .ok_or(TokenError::Overflow)?; - - **source_account_info.lamports.borrow_mut() = 0; - delete_account(source_account_info)?; - - Ok(()) - } - - /// Processes a [`FreezeAccount`](enum.TokenInstruction.html) or a - /// [`ThawAccount`](enum.TokenInstruction.html) instruction. - pub fn process_toggle_freeze_account( - program_id: &Pubkey, - accounts: &[AccountInfo], - freeze: bool, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let source_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - let mut source_account = Account::unpack(&source_account_info.data.borrow())?; - if freeze && source_account.is_frozen() || !freeze && !source_account.is_frozen() { - return Err(TokenError::InvalidState.into()); - } - if source_account.is_native() { - return Err(TokenError::NativeNotSupported.into()); - } - if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) { - return Err(TokenError::MintMismatch.into()); - } - - let mint = Mint::unpack(&mint_info.data.borrow_mut())?; - match mint.freeze_authority { - COption::Some(authority) => Self::validate_owner( - program_id, - &authority, - authority_info, - account_info_iter.as_slice(), - ), - COption::None => Err(TokenError::MintCannotFreeze.into()), - }?; - - source_account.state = if freeze { - AccountState::Frozen - } else { - AccountState::Initialized - }; - - Account::pack(source_account, &mut source_account_info.data.borrow_mut())?; - - Ok(()) - } - - /// Processes a [`SyncNative`](enum.TokenInstruction.html) instruction - pub fn process_sync_native(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let native_account_info = next_account_info(account_info_iter)?; - Self::check_account_owner(program_id, native_account_info)?; - - let mut native_account = Account::unpack(&native_account_info.data.borrow())?; - - if let COption::Some(rent_exempt_reserve) = native_account.is_native { - let new_amount = native_account_info - .lamports() - .checked_sub(rent_exempt_reserve) - .ok_or(TokenError::Overflow)?; - if new_amount < native_account.amount { - return Err(TokenError::InvalidState.into()); - } - native_account.amount = new_amount; - } else { - return Err(TokenError::NonNativeNotSupported.into()); - } - - Account::pack(native_account, &mut native_account_info.data.borrow_mut())?; - Ok(()) - } - - /// Processes a [`GetAccountDataSize`](enum.TokenInstruction.html) - /// instruction - pub fn process_get_account_data_size( - program_id: &Pubkey, - accounts: &[AccountInfo], - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - // make sure the mint is valid - let mint_info = next_account_info(account_info_iter)?; - Self::check_account_owner(program_id, mint_info)?; - let _ = Mint::unpack(&mint_info.data.borrow()) - .map_err(|_| Into::::into(TokenError::InvalidMint))?; - set_return_data(&Account::LEN.to_le_bytes()); - Ok(()) - } - - /// Processes an [`InitializeImmutableOwner`](enum.TokenInstruction.html) - /// instruction - pub fn process_initialize_immutable_owner(accounts: &[AccountInfo]) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let token_account_info = next_account_info(account_info_iter)?; - let account = Account::unpack_unchecked(&token_account_info.data.borrow())?; - if account.is_initialized() { - return Err(TokenError::AlreadyInUse.into()); - } - msg!("Please upgrade to SPL Token 2022 for immutable owner support"); - Ok(()) - } - - /// Processes an [`AmountToUiAmount`](enum.TokenInstruction.html) - /// instruction - pub fn process_amount_to_ui_amount( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - Self::check_account_owner(program_id, mint_info)?; - - let mint = Mint::unpack(&mint_info.data.borrow_mut()) - .map_err(|_| Into::::into(TokenError::InvalidMint))?; - let ui_amount = amount_to_ui_amount_string_trimmed(amount, mint.decimals); - - set_return_data(&ui_amount.into_bytes()); - Ok(()) - } - - /// Processes an [`AmountToUiAmount`](enum.TokenInstruction.html) - /// instruction - pub fn process_ui_amount_to_amount( - program_id: &Pubkey, - accounts: &[AccountInfo], - ui_amount: &str, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_info = next_account_info(account_info_iter)?; - Self::check_account_owner(program_id, mint_info)?; - - let mint = Mint::unpack(&mint_info.data.borrow_mut()) - .map_err(|_| Into::::into(TokenError::InvalidMint))?; - let amount = try_ui_amount_into_amount(ui_amount.to_string(), mint.decimals)?; - - set_return_data(&amount.to_le_bytes()); - Ok(()) - } - - /// Processes an [`Instruction`](enum.Instruction.html). - pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { - let instruction = TokenInstruction::unpack(input)?; - - match instruction { - TokenInstruction::InitializeMint { - decimals, - mint_authority, - freeze_authority, - } => { - msg!("Instruction: InitializeMint"); - Self::process_initialize_mint(accounts, decimals, mint_authority, freeze_authority) - } - TokenInstruction::InitializeMint2 { - decimals, - mint_authority, - freeze_authority, - } => { - msg!("Instruction: InitializeMint2"); - Self::process_initialize_mint2(accounts, decimals, mint_authority, freeze_authority) - } - TokenInstruction::InitializeAccount => { - msg!("Instruction: InitializeAccount"); - Self::process_initialize_account(program_id, accounts) - } - TokenInstruction::InitializeAccount2 { owner } => { - msg!("Instruction: InitializeAccount2"); - Self::process_initialize_account2(program_id, accounts, owner) - } - TokenInstruction::InitializeAccount3 { owner } => { - msg!("Instruction: InitializeAccount3"); - Self::process_initialize_account3(program_id, accounts, owner) - } - TokenInstruction::InitializeMultisig { m } => { - msg!("Instruction: InitializeMultisig"); - Self::process_initialize_multisig(accounts, m) - } - TokenInstruction::InitializeMultisig2 { m } => { - msg!("Instruction: InitializeMultisig2"); - Self::process_initialize_multisig2(accounts, m) - } - TokenInstruction::Transfer { amount } => { - msg!("Instruction: Transfer"); - Self::process_transfer(program_id, accounts, amount, None) - } - TokenInstruction::Approve { amount } => { - msg!("Instruction: Approve"); - Self::process_approve(program_id, accounts, amount, None) - } - TokenInstruction::Revoke => { - msg!("Instruction: Revoke"); - Self::process_revoke(program_id, accounts) - } - TokenInstruction::SetAuthority { - authority_type, - new_authority, - } => { - msg!("Instruction: SetAuthority"); - Self::process_set_authority(program_id, accounts, authority_type, new_authority) - } - TokenInstruction::MintTo { amount } => { - msg!("Instruction: MintTo"); - Self::process_mint_to(program_id, accounts, amount, None) - } - TokenInstruction::Burn { amount } => { - msg!("Instruction: Burn"); - Self::process_burn(program_id, accounts, amount, None) - } - TokenInstruction::CloseAccount => { - msg!("Instruction: CloseAccount"); - Self::process_close_account(program_id, accounts) - } - TokenInstruction::FreezeAccount => { - msg!("Instruction: FreezeAccount"); - Self::process_toggle_freeze_account(program_id, accounts, true) - } - TokenInstruction::ThawAccount => { - msg!("Instruction: ThawAccount"); - Self::process_toggle_freeze_account(program_id, accounts, false) - } - TokenInstruction::TransferChecked { amount, decimals } => { - msg!("Instruction: TransferChecked"); - Self::process_transfer(program_id, accounts, amount, Some(decimals)) - } - TokenInstruction::ApproveChecked { amount, decimals } => { - msg!("Instruction: ApproveChecked"); - Self::process_approve(program_id, accounts, amount, Some(decimals)) - } - TokenInstruction::MintToChecked { amount, decimals } => { - msg!("Instruction: MintToChecked"); - Self::process_mint_to(program_id, accounts, amount, Some(decimals)) - } - TokenInstruction::BurnChecked { amount, decimals } => { - msg!("Instruction: BurnChecked"); - Self::process_burn(program_id, accounts, amount, Some(decimals)) - } - TokenInstruction::SyncNative => { - msg!("Instruction: SyncNative"); - Self::process_sync_native(program_id, accounts) - } - TokenInstruction::GetAccountDataSize => { - msg!("Instruction: GetAccountDataSize"); - Self::process_get_account_data_size(program_id, accounts) - } - TokenInstruction::InitializeImmutableOwner => { - msg!("Instruction: InitializeImmutableOwner"); - Self::process_initialize_immutable_owner(accounts) - } - TokenInstruction::AmountToUiAmount { amount } => { - msg!("Instruction: AmountToUiAmount"); - Self::process_amount_to_ui_amount(program_id, accounts, amount) - } - TokenInstruction::UiAmountToAmount { ui_amount } => { - msg!("Instruction: UiAmountToAmount"); - Self::process_ui_amount_to_amount(program_id, accounts, ui_amount) - } - } - } - - /// Checks that the account is owned by the expected program - pub fn check_account_owner(program_id: &Pubkey, account_info: &AccountInfo) -> ProgramResult { - if !Self::cmp_pubkeys(program_id, account_info.owner) { - Err(ProgramError::IncorrectProgramId) - } else { - Ok(()) - } - } - - /// Checks two pubkeys for equality in a computationally cheap way using - /// `sol_memcmp` - pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool { - sol_memcmp(a.as_ref(), b.as_ref(), PUBKEY_BYTES) == 0 - } - - /// Validates owner(s) are present - pub fn validate_owner( - program_id: &Pubkey, - expected_owner: &Pubkey, - owner_account_info: &AccountInfo, - signers: &[AccountInfo], - ) -> ProgramResult { - if !Self::cmp_pubkeys(expected_owner, owner_account_info.key) { - return Err(TokenError::OwnerMismatch.into()); - } - if Self::cmp_pubkeys(program_id, owner_account_info.owner) - && owner_account_info.data_len() == Multisig::get_packed_len() - { - let multisig = Multisig::unpack(&owner_account_info.data.borrow())?; - let mut num_signers = 0; - let mut matched = [false; MAX_SIGNERS]; - for signer in signers.iter() { - for (position, key) in multisig.signers[0..multisig.n as usize].iter().enumerate() { - if Self::cmp_pubkeys(key, signer.key) && !matched[position] { - if !signer.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - matched[position] = true; - num_signers += 1; - } - } - } - if num_signers < multisig.m { - return Err(ProgramError::MissingRequiredSignature); - } - return Ok(()); - } else if !owner_account_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - Ok(()) - } -} - -/// Helper function to mostly delete an account in a test environment. We could -/// potentially muck around the bytes assuming that a vec is passed in, but that -/// would be more trouble than it's worth. -#[cfg(not(target_os = "solana"))] -fn delete_account(account_info: &AccountInfo) -> Result<(), ProgramError> { - account_info.assign(&system_program::id()); - let mut account_data = account_info.data.borrow_mut(); - let data_len = account_data.len(); - solana_program::program_memory::sol_memset(*account_data, 0, data_len); - Ok(()) -} - -/// Helper function to totally delete an account on-chain -#[cfg(target_os = "solana")] -fn delete_account(account_info: &AccountInfo) -> Result<(), ProgramError> { - account_info.assign(&system_program::id()); - account_info.realloc(0, false) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::instruction::*, - serial_test::serial, - solana_program::{ - account_info::IntoAccountInfo, - clock::Epoch, - instruction::Instruction, - program_error::{self, PrintProgramError}, - sysvar::rent, - }, - solana_sdk::account::{ - create_account_for_test, create_is_signer_account_infos, Account as SolanaAccount, - }, - std::sync::{Arc, RwLock}, - }; - - lazy_static::lazy_static! { - static ref EXPECTED_DATA: Arc>> = Arc::new(RwLock::new(Vec::new())); - } - - fn set_expected_data(expected_data: Vec) { - *EXPECTED_DATA.write().unwrap() = expected_data; - } - - struct SyscallStubs {} - impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { - fn sol_log(&self, _message: &str) {} - - fn sol_invoke_signed( - &self, - _instruction: &Instruction, - _account_infos: &[AccountInfo], - _signers_seeds: &[&[&[u8]]], - ) -> ProgramResult { - Err(ProgramError::Custom(42)) // Not supported - } - - fn sol_get_clock_sysvar(&self, _var_addr: *mut u8) -> u64 { - program_error::UNSUPPORTED_SYSVAR - } - - fn sol_get_epoch_schedule_sysvar(&self, _var_addr: *mut u8) -> u64 { - program_error::UNSUPPORTED_SYSVAR - } - - #[allow(deprecated)] - fn sol_get_fees_sysvar(&self, _var_addr: *mut u8) -> u64 { - program_error::UNSUPPORTED_SYSVAR - } - - fn sol_get_rent_sysvar(&self, var_addr: *mut u8) -> u64 { - unsafe { - *(var_addr as *mut _ as *mut Rent) = Rent::default(); - } - solana_program::entrypoint::SUCCESS - } - - fn sol_set_return_data(&self, data: &[u8]) { - assert_eq!(&*EXPECTED_DATA.write().unwrap(), data) - } - } - - fn do_process_instruction( - instruction: Instruction, - accounts: Vec<&mut SolanaAccount>, - ) -> ProgramResult { - { - use std::sync::Once; - static ONCE: Once = Once::new(); - - ONCE.call_once(|| { - solana_sdk::program_stubs::set_syscall_stubs(Box::new(SyscallStubs {})); - }); - } - - let mut meta = instruction - .accounts - .iter() - .zip(accounts) - .map(|(account_meta, account)| (&account_meta.pubkey, account_meta.is_signer, account)) - .collect::>(); - - let account_infos = create_is_signer_account_infos(&mut meta); - Processor::process(&instruction.program_id, &account_infos, &instruction.data) - } - - fn do_process_instruction_dups( - instruction: Instruction, - account_infos: Vec, - ) -> ProgramResult { - Processor::process(&instruction.program_id, &account_infos, &instruction.data) - } - - fn return_token_error_as_program_error() -> ProgramError { - TokenError::MintMismatch.into() - } - - fn rent_sysvar() -> SolanaAccount { - create_account_for_test(&Rent::default()) - } - - fn mint_minimum_balance() -> u64 { - Rent::default().minimum_balance(Mint::get_packed_len()) - } - - fn account_minimum_balance() -> u64 { - Rent::default().minimum_balance(Account::get_packed_len()) - } - - fn multisig_minimum_balance() -> u64 { - Rent::default().minimum_balance(Multisig::get_packed_len()) - } - - #[test] - fn test_print_error() { - let error = return_token_error_as_program_error(); - error.print::(); - } - - #[test] - fn test_error_as_custom() { - assert_eq!( - return_token_error_as_program_error(), - ProgramError::Custom(3) - ); - } - - #[test] - fn test_unique_account_sizes() { - assert_ne!(Mint::get_packed_len(), 0); - assert_ne!(Mint::get_packed_len(), Account::get_packed_len()); - assert_ne!(Mint::get_packed_len(), Multisig::get_packed_len()); - assert_ne!(Account::get_packed_len(), 0); - assert_ne!(Account::get_packed_len(), Multisig::get_packed_len()); - assert_ne!(Multisig::get_packed_len(), 0); - } - - #[test] - fn test_pack_unpack() { - // Mint - let check = Mint { - mint_authority: COption::Some(Pubkey::new_from_array([1; 32])), - supply: 42, - decimals: 7, - is_initialized: true, - freeze_authority: COption::Some(Pubkey::new_from_array([2; 32])), - }; - let mut packed = vec![0; Mint::get_packed_len() + 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Mint::pack(check, &mut packed) - ); - let mut packed = vec![0; Mint::get_packed_len() - 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Mint::pack(check, &mut packed) - ); - let mut packed = vec![0; Mint::get_packed_len()]; - Mint::pack(check, &mut packed).unwrap(); - let expect = vec![ - 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 42, 0, 0, 0, 0, 0, 0, 0, 7, 1, 1, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - ]; - assert_eq!(packed, expect); - let unpacked = Mint::unpack(&packed).unwrap(); - assert_eq!(unpacked, check); - - // Account - let check = Account { - mint: Pubkey::new_from_array([1; 32]), - owner: Pubkey::new_from_array([2; 32]), - amount: 3, - delegate: COption::Some(Pubkey::new_from_array([4; 32])), - state: AccountState::Frozen, - is_native: COption::Some(5), - delegated_amount: 6, - close_authority: COption::Some(Pubkey::new_from_array([7; 32])), - }; - let mut packed = vec![0; Account::get_packed_len() + 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Account::pack(check, &mut packed) - ); - let mut packed = vec![0; Account::get_packed_len() - 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Account::pack(check, &mut packed) - ); - let mut packed = vec![0; Account::get_packed_len()]; - Account::pack(check, &mut packed).unwrap(); - let expect = vec![ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, - 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 1, 0, 0, 0, 5, 0, 0, - 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - ]; - assert_eq!(packed, expect); - let unpacked = Account::unpack(&packed).unwrap(); - assert_eq!(unpacked, check); - - // Multisig - let check = Multisig { - m: 1, - n: 2, - is_initialized: true, - signers: [Pubkey::new_from_array([3; 32]); MAX_SIGNERS], - }; - let mut packed = vec![0; Multisig::get_packed_len() + 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Multisig::pack(check, &mut packed) - ); - let mut packed = vec![0; Multisig::get_packed_len() - 1]; - assert_eq!( - Err(ProgramError::InvalidAccountData), - Multisig::pack(check, &mut packed) - ); - let mut packed = vec![0; Multisig::get_packed_len()]; - Multisig::pack(check, &mut packed).unwrap(); - let expect = vec![ - 1, 2, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, - ]; - assert_eq!(packed, expect); - let unpacked = Multisig::unpack(&packed).unwrap(); - assert_eq!(unpacked, check); - } - - #[test] - fn test_initialize_mint() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = SolanaAccount::new(42, Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut mint2_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // mint is not rent exempt - assert_eq!( - Err(TokenError::NotRentExempt.into()), - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar] - ) - ); - - mint_account.lamports = mint_minimum_balance(); - - // create new mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create twice - assert_eq!( - Err(TokenError::AlreadyInUse.into()), - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2,).unwrap(), - vec![&mut mint_account, &mut rent_sysvar] - ) - ); - - // create another mint that can freeze - do_process_instruction( - initialize_mint(&program_id, &mint2_key, &owner_key, Some(&owner_key), 2).unwrap(), - vec![&mut mint2_account, &mut rent_sysvar], - ) - .unwrap(); - let mint = Mint::unpack_unchecked(&mint2_account.data).unwrap(); - assert_eq!(mint.freeze_authority, COption::Some(owner_key)); - } - - #[test] - fn test_initialize_mint2() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = SolanaAccount::new(42, Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut mint2_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - - // mint is not rent exempt - assert_eq!( - Err(TokenError::NotRentExempt.into()), - do_process_instruction( - initialize_mint2(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account] - ) - ); - - mint_account.lamports = mint_minimum_balance(); - - // create new mint - do_process_instruction( - initialize_mint2(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - // create twice - assert_eq!( - Err(TokenError::AlreadyInUse.into()), - do_process_instruction( - initialize_mint2(&program_id, &mint_key, &owner_key, None, 2,).unwrap(), - vec![&mut mint_account] - ) - ); - - // create another mint that can freeze - do_process_instruction( - initialize_mint2(&program_id, &mint2_key, &owner_key, Some(&owner_key), 2).unwrap(), - vec![&mut mint2_account], - ) - .unwrap(); - let mint = Mint::unpack_unchecked(&mint2_account.data).unwrap(); - assert_eq!(mint.freeze_authority, COption::Some(owner_key)); - } - - #[test] - fn test_initialize_mint_account() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new(42, Account::get_packed_len(), &program_id); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // account is not rent exempt - assert_eq!( - Err(TokenError::NotRentExempt.into()), - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar - ], - ) - ); - - account_account.lamports = account_minimum_balance(); - - // mint is not valid (not initialized) - assert_eq!( - Err(TokenError::InvalidMint.into()), - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar - ], - ) - ); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // mint not owned by program - let not_program_id = Pubkey::new_unique(); - mint_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar - ], - ) - ); - mint_account.owner = program_id; - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create twice - assert_eq!( - Err(TokenError::AlreadyInUse.into()), - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar - ], - ) - ); - } - - #[test] - fn test_transfer_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut account2_info: AccountInfo = (&account2_key, false, &mut account2_account).into(); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_info: AccountInfo = (&account3_key, false, &mut account3_account).into(); - let account4_key = Pubkey::new_unique(); - let mut account4_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account4_info: AccountInfo = (&account4_key, true, &mut account4_account).into(); - let multisig_key = Pubkey::new_unique(); - let mut multisig_account = SolanaAccount::new( - multisig_minimum_balance(), - Multisig::get_packed_len(), - &program_id, - ); - let multisig_info: AccountInfo = (&multisig_key, true, &mut multisig_account).into(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner_info: AccountInfo = (&owner_key, true, &mut owner_account).into(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, false, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // create another account - do_process_instruction_dups( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - account2_info.clone(), - mint_info.clone(), - owner_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // mint to account - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-owner transfer - do_process_instruction_dups( - transfer( - &program_id, - &account1_key, - &account2_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-owner TransferChecked - do_process_instruction_dups( - transfer_checked( - &program_id, - &account1_key, - &mint_key, - &account2_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-delegate transfer - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.amount = 1000; - account.delegated_amount = 1000; - account.delegate = COption::Some(account1_key); - account.owner = owner_key; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - - do_process_instruction_dups( - transfer( - &program_id, - &account1_key, - &account2_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-delegate TransferChecked - do_process_instruction_dups( - transfer_checked( - &program_id, - &account1_key, - &mint_key, - &account2_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // test destination-owner transfer - do_process_instruction_dups( - initialize_account(&program_id, &account3_key, &mint_key, &account2_key).unwrap(), - vec![ - account3_info.clone(), - mint_info.clone(), - account2_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account3_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account3_info.clone(), owner_info.clone()], - ) - .unwrap(); - - account1_info.is_signer = false; - account2_info.is_signer = true; - do_process_instruction_dups( - transfer( - &program_id, - &account3_key, - &account2_key, - &account2_key, - &[], - 500, - ) - .unwrap(), - vec![ - account3_info.clone(), - account2_info.clone(), - account2_info.clone(), - ], - ) - .unwrap(); - - // destination-owner TransferChecked - do_process_instruction_dups( - transfer_checked( - &program_id, - &account3_key, - &mint_key, - &account2_key, - &account2_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account3_info.clone(), - mint_info.clone(), - account2_info.clone(), - account2_info.clone(), - ], - ) - .unwrap(); - - // test source-multisig signer - do_process_instruction_dups( - initialize_multisig(&program_id, &multisig_key, &[&account4_key], 1).unwrap(), - vec![ - multisig_info.clone(), - rent_info.clone(), - account4_info.clone(), - ], - ) - .unwrap(); - - do_process_instruction_dups( - initialize_account(&program_id, &account4_key, &mint_key, &multisig_key).unwrap(), - vec![ - account4_info.clone(), - mint_info.clone(), - multisig_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account4_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account4_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-multisig-signer transfer - do_process_instruction_dups( - transfer( - &program_id, - &account4_key, - &account2_key, - &multisig_key, - &[&account4_key], - 500, - ) - .unwrap(), - vec![ - account4_info.clone(), - account2_info.clone(), - multisig_info.clone(), - account4_info.clone(), - ], - ) - .unwrap(); - - // source-multisig-signer TransferChecked - do_process_instruction_dups( - transfer_checked( - &program_id, - &account4_key, - &mint_key, - &account2_key, - &multisig_key, - &[&account4_key], - 500, - 2, - ) - .unwrap(), - vec![ - account4_info.clone(), - mint_info.clone(), - account2_info.clone(), - multisig_info.clone(), - account4_info.clone(), - ], - ) - .unwrap(); - } - - #[test] - fn test_transfer() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - let mismatch_key = Pubkey::new_unique(); - let mut mismatch_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut rent_sysvar = rent_sysvar(); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create mismatch account - do_process_instruction( - initialize_account(&program_id, &mismatch_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut mismatch_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&mismatch_account.data).unwrap(); - account.mint = mint2_key; - Account::pack(account, &mut mismatch_account.data).unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // missing signer - let mut instruction = transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 1000, - ) - .unwrap(); - instruction.accounts[2].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction( - instruction, - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - // mismatch mint - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - transfer( - &program_id, - &account_key, - &mismatch_key, - &owner_key, - &[], - 1000 - ) - .unwrap(), - vec![ - &mut account_account, - &mut mismatch_account, - &mut owner_account, - ], - ) - ); - - // missing owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &owner2_key, - &[], - 1000 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - - // account not owned by program - let not_program_id = Pubkey::new_unique(); - account_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - transfer(&program_id, &account_key, &account2_key, &owner_key, &[], 0,).unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - account_account.owner = program_id; - - // account 2 not owned by program - let not_program_id = Pubkey::new_unique(); - account2_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - transfer(&program_id, &account_key, &account2_key, &owner_key, &[], 0,).unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - account2_account.owner = program_id; - - // transfer - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 1000, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - .unwrap(); - - // insufficient funds - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - transfer(&program_id, &account_key, &account2_key, &owner_key, &[], 1).unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - // transfer half back - do_process_instruction( - transfer( - &program_id, - &account2_key, - &account_key, - &owner_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut account_account, - &mut owner_account, - ], - ) - .unwrap(); - - // incorrect decimals - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - do_process_instruction( - transfer_checked( - &program_id, - &account2_key, - &mint_key, - &account_key, - &owner_key, - &[], - 1, - 10 // <-- incorrect decimals - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut account_account, - &mut owner_account, - ], - ) - ); - - // incorrect mint - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - transfer_checked( - &program_id, - &account2_key, - &account3_key, // <-- incorrect mint - &account_key, - &owner_key, - &[], - 1, - 2 - ) - .unwrap(), - vec![ - &mut account2_account, - &mut account3_account, // <-- incorrect mint - &mut account_account, - &mut owner_account, - ], - ) - ); - // transfer rest with explicit decimals - do_process_instruction( - transfer_checked( - &program_id, - &account2_key, - &mint_key, - &account_key, - &owner_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut account_account, - &mut owner_account, - ], - ) - .unwrap(); - - // insufficient funds - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - transfer(&program_id, &account2_key, &account_key, &owner_key, &[], 1).unwrap(), - vec![ - &mut account2_account, - &mut account_account, - &mut owner_account, - ], - ) - ); - - // approve delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // not a delegate of source account - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &owner2_key, // <-- incorrect owner or delegate - &[], - 1, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - - // insufficient funds approved via delegate - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &delegate_key, - &[], - 101 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut delegate_account, - ], - ) - ); - - // transfer via delegate - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &delegate_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut delegate_account, - ], - ) - .unwrap(); - - // insufficient funds approved via delegate - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &delegate_key, - &[], - 1 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut delegate_account, - ], - ) - ); - - // transfer rest - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 900, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - .unwrap(); - - // approve delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // insufficient funds in source account via delegate - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &delegate_key, - &[], - 100 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut delegate_account, - ], - ) - ); - } - - #[test] - fn test_self_transfer() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - let account_info = (&account_key, false, &mut account_account).into_account_info(); - let account3_info = (&account3_key, false, &mut account3_account).into_account_info(); - let delegate_info = (&delegate_key, true, &mut delegate_account).into_account_info(); - let owner_info = (&owner_key, true, &mut owner_account).into_account_info(); - let owner2_info = (&owner2_key, true, &mut owner2_account).into_account_info(); - let mint_info = (&mint_key, false, &mut mint_account).into_account_info(); - - // transfer - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner_info.key, - &[], - 1000, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - - // transfer checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_info.key, - &[], - 1000, - 2, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - - // missing signer - let mut owner_no_sign_info = owner_info.clone(); - let mut instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner_no_sign_info.key, - &[], - 1000, - ) - .unwrap(); - instruction.accounts[2].is_signer = false; - owner_no_sign_info.is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner_no_sign_info.clone(), - ], - &instruction.data, - ) - ); - - // missing signer checked - let mut instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_no_sign_info.key, - &[], - 1000, - 2, - ) - .unwrap(); - instruction.accounts[3].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_no_sign_info, - ], - &instruction.data, - ) - ); - - // missing owner - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner2_info.key, - &[], - 1000, - ) - .unwrap(); - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner2_info.clone(), - ], - &instruction.data, - ) - ); - - // missing owner checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner2_info.key, - &[], - 1000, - 2, - ) - .unwrap(); - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner2_info.clone(), - ], - &instruction.data, - ) - ); - - // insufficient funds - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner_info.key, - &[], - 1001, - ) - .unwrap(); - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - - // insufficient funds checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_info.key, - &[], - 1001, - 2, - ) - .unwrap(); - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - - // incorrect decimals - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_info.key, - &[], - 1, - 10, // <-- incorrect decimals - ) - .unwrap(); - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - - // incorrect mint - let instruction = transfer_checked( - &program_id, - account_info.key, - account3_info.key, // <-- incorrect mint - account_info.key, - owner_info.key, - &[], - 1, - 2, - ) - .unwrap(); - assert_eq!( - Err(TokenError::MintMismatch.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account3_info.clone(), // <-- incorrect mint - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - - // approve delegate - let instruction = approve( - &program_id, - account_info.key, - delegate_info.key, - owner_info.key, - &[], - 100, - ) - .unwrap(); - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - delegate_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - .unwrap(); - - // delegate transfer - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - delegate_info.key, - &[], - 100, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - delegate_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - assert_eq!(account.delegated_amount, 100); - - // delegate transfer checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - delegate_info.key, - &[], - 100, - 2, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - delegate_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - assert_eq!(account.delegated_amount, 100); - - // delegate insufficient funds - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - delegate_info.key, - &[], - 101, - ) - .unwrap(); - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - delegate_info.clone(), - ], - &instruction.data, - ) - ); - - // delegate insufficient funds checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - delegate_info.key, - &[], - 101, - 2, - ) - .unwrap(); - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - delegate_info.clone(), - ], - &instruction.data, - ) - ); - - // owner transfer with delegate assigned - let instruction = transfer( - &program_id, - account_info.key, - account_info.key, - owner_info.key, - &[], - 1000, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - - // owner transfer with delegate assigned checked - let instruction = transfer_checked( - &program_id, - account_info.key, - mint_info.key, - account_info.key, - owner_info.key, - &[], - 1000, - 2, - ) - .unwrap(); - assert_eq!( - Ok(()), - Processor::process( - &instruction.program_id, - &[ - account_info.clone(), - mint_info.clone(), - account_info.clone(), - owner_info.clone(), - ], - &instruction.data, - ) - ); - // no balance change... - let account = Account::unpack_unchecked(&account_info.try_borrow_data().unwrap()).unwrap(); - assert_eq!(account.amount, 1000); - } - - #[test] - fn test_mintable_token_with_zero_supply() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint-able token with zero supply - let decimals = 2; - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, decimals).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!( - mint, - Mint { - mint_authority: COption::Some(owner_key), - supply: 0, - decimals, - is_initialized: true, - freeze_authority: COption::None, - } - ); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - let _ = Mint::unpack(&mint_account.data).unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // mint to 2, with incorrect decimals - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - do_process_instruction( - mint_to_checked( - &program_id, - &mint_key, - &account_key, - &owner_key, - &[], - 42, - decimals + 1 - ) - .unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - ); - - let _ = Mint::unpack(&mint_account.data).unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // mint to 2 - do_process_instruction( - mint_to_checked( - &program_id, - &mint_key, - &account_key, - &owner_key, - &[], - 42, - decimals, - ) - .unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - let _ = Mint::unpack(&mint_account.data).unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 84); - } - - #[test] - fn test_approve_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_info: AccountInfo = (&account2_key, false, &mut account2_account).into(); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_info: AccountInfo = (&account3_key, true, &mut account3_account).into(); - let multisig_key = Pubkey::new_unique(); - let mut multisig_account = SolanaAccount::new( - multisig_minimum_balance(), - Multisig::get_packed_len(), - &program_id, - ); - let multisig_info: AccountInfo = (&multisig_key, true, &mut multisig_account).into(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner_info: AccountInfo = (&owner_key, true, &mut owner_account).into(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, false, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // create another account - do_process_instruction_dups( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - account2_info.clone(), - mint_info.clone(), - owner_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // mint to account - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-owner approve - do_process_instruction_dups( - approve( - &program_id, - &account1_key, - &account2_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-owner approve_checked - do_process_instruction_dups( - approve_checked( - &program_id, - &account1_key, - &mint_key, - &account2_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account2_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-owner revoke - do_process_instruction_dups( - revoke(&program_id, &account1_key, &account1_key, &[]).unwrap(), - vec![account1_info.clone(), account1_info.clone()], - ) - .unwrap(); - - // test source-multisig signer - do_process_instruction_dups( - initialize_multisig(&program_id, &multisig_key, &[&account3_key], 1).unwrap(), - vec![ - multisig_info.clone(), - rent_info.clone(), - account3_info.clone(), - ], - ) - .unwrap(); - - do_process_instruction_dups( - initialize_account(&program_id, &account3_key, &mint_key, &multisig_key).unwrap(), - vec![ - account3_info.clone(), - mint_info.clone(), - multisig_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account3_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account3_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-multisig-signer approve - do_process_instruction_dups( - approve( - &program_id, - &account3_key, - &account2_key, - &multisig_key, - &[&account3_key], - 500, - ) - .unwrap(), - vec![ - account3_info.clone(), - account2_info.clone(), - multisig_info.clone(), - account3_info.clone(), - ], - ) - .unwrap(); - - // source-multisig-signer approve_checked - do_process_instruction_dups( - approve_checked( - &program_id, - &account3_key, - &mint_key, - &account2_key, - &multisig_key, - &[&account3_key], - 500, - 2, - ) - .unwrap(), - vec![ - account3_info.clone(), - mint_info.clone(), - account2_info.clone(), - multisig_info.clone(), - account3_info.clone(), - ], - ) - .unwrap(); - - // source-owner multisig-signer - do_process_instruction_dups( - revoke(&program_id, &account3_key, &multisig_key, &[&account3_key]).unwrap(), - vec![ - account3_info.clone(), - multisig_info.clone(), - account3_info.clone(), - ], - ) - .unwrap(); - } - - #[test] - fn test_approve() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // missing signer - let mut instruction = approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100, - ) - .unwrap(); - instruction.accounts[2].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction( - instruction, - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - ); - - // no owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner2_key, - &[], - 100 - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner2_account, - ], - ) - ); - - // approve delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // approve delegate 2, with incorrect decimals - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - do_process_instruction( - approve_checked( - &program_id, - &account_key, - &mint_key, - &delegate_key, - &owner_key, - &[], - 100, - 0 // <-- incorrect decimals - ) - .unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account, - &mut owner_account, - ], - ) - ); - - // approve delegate 2, with incorrect mint - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - approve_checked( - &program_id, - &account_key, - &account2_key, // <-- bad mint - &delegate_key, - &owner_key, - &[], - 100, - 0 - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, // <-- bad mint - &mut delegate_account, - &mut owner_account, - ], - ) - ); - - // approve delegate 2 - do_process_instruction( - approve_checked( - &program_id, - &account_key, - &mint_key, - &delegate_key, - &owner_key, - &[], - 100, - 2, - ) - .unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // revoke delegate - do_process_instruction( - revoke(&program_id, &account_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - } - - #[test] - fn test_set_authority_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, true, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &mint_key, Some(&mint_key), 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // set mint_authority when currently self - do_process_instruction_dups( - set_authority( - &program_id, - &mint_key, - Some(&owner_key), - AuthorityType::MintTokens, - &mint_key, - &[], - ) - .unwrap(), - vec![mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // set freeze_authority when currently self - do_process_instruction_dups( - set_authority( - &program_id, - &mint_key, - Some(&owner_key), - AuthorityType::FreezeAccount, - &mint_key, - &[], - ) - .unwrap(), - vec![mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // set account owner when currently self - do_process_instruction_dups( - set_authority( - &program_id, - &account1_key, - Some(&owner_key), - AuthorityType::AccountOwner, - &account1_key, - &[], - ) - .unwrap(), - vec![account1_info.clone(), account1_info.clone()], - ) - .unwrap(); - - // set close_authority when currently self - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.close_authority = COption::Some(account1_key); - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - - do_process_instruction_dups( - set_authority( - &program_id, - &account1_key, - Some(&owner_key), - AuthorityType::CloseAccount, - &account1_key, - &[], - ) - .unwrap(), - vec![account1_info.clone(), account1_info.clone()], - ) - .unwrap(); - } - - #[test] - fn test_set_authority() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let owner3_key = Pubkey::new_unique(); - let mut owner3_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut mint2_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create new mint with owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create mint with owner and freeze_authority - do_process_instruction( - initialize_mint(&program_id, &mint2_key, &owner_key, Some(&owner_key), 2).unwrap(), - vec![&mut mint2_account, &mut rent_sysvar], - ) - .unwrap(); - - // invalid account - assert_eq!( - Err(ProgramError::UninitializedAccount), - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::AccountOwner, - &owner_key, - &[] - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - ); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint2_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint2_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // missing owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner_key), - AuthorityType::AccountOwner, - &owner2_key, - &[] - ) - .unwrap(), - vec![&mut account_account, &mut owner2_account], - ) - ); - - // owner did not sign - let mut instruction = set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::AccountOwner, - &owner_key, - &[], - ) - .unwrap(); - instruction.accounts[1].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction(instruction, vec![&mut account_account, &mut owner_account,],) - ); - - // wrong authority type - assert_eq!( - Err(TokenError::AuthorityTypeNotSupported.into()), - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::FreezeAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - ); - - // account owner may not be set to None - assert_eq!( - Err(TokenError::InvalidInstruction.into()), - do_process_instruction( - set_authority( - &program_id, - &account_key, - None, - AuthorityType::AccountOwner, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - ); - - // set delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &owner2_key, - &owner_key, - &[], - u64::MAX, - ) - .unwrap(), - vec![ - &mut account_account, - &mut owner2_account, - &mut owner_account, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.delegate, COption::Some(owner2_key)); - assert_eq!(account.delegated_amount, u64::MAX); - - // set owner - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner3_key), - AuthorityType::AccountOwner, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - - // check delegate cleared - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.delegate, COption::None); - assert_eq!(account.delegated_amount, 0); - - // set owner without existing delegate - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::AccountOwner, - &owner3_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner3_account], - ) - .unwrap(); - - // set close_authority - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::CloseAccount, - &owner2_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner2_account], - ) - .unwrap(); - - // close_authority may be set to None - do_process_instruction( - set_authority( - &program_id, - &account_key, - None, - AuthorityType::CloseAccount, - &owner2_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner2_account], - ) - .unwrap(); - - // wrong owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - set_authority( - &program_id, - &mint_key, - Some(&owner3_key), - AuthorityType::MintTokens, - &owner2_key, - &[] - ) - .unwrap(), - vec![&mut mint_account, &mut owner2_account], - ) - ); - - // owner did not sign - let mut instruction = set_authority( - &program_id, - &mint_key, - Some(&owner2_key), - AuthorityType::MintTokens, - &owner_key, - &[], - ) - .unwrap(); - instruction.accounts[1].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction(instruction, vec![&mut mint_account, &mut owner_account],) - ); - - // cannot freeze - assert_eq!( - Err(TokenError::MintCannotFreeze.into()), - do_process_instruction( - set_authority( - &program_id, - &mint_key, - Some(&owner2_key), - AuthorityType::FreezeAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint_account, &mut owner_account], - ) - ); - - // set owner - do_process_instruction( - set_authority( - &program_id, - &mint_key, - Some(&owner2_key), - AuthorityType::MintTokens, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint_account, &mut owner_account], - ) - .unwrap(); - - // set owner to None - do_process_instruction( - set_authority( - &program_id, - &mint_key, - None, - AuthorityType::MintTokens, - &owner2_key, - &[], - ) - .unwrap(), - vec![&mut mint_account, &mut owner2_account], - ) - .unwrap(); - - // test unsetting mint_authority is one-way operation - assert_eq!( - Err(TokenError::FixedSupply.into()), - do_process_instruction( - set_authority( - &program_id, - &mint2_key, - Some(&owner2_key), - AuthorityType::MintTokens, - &owner_key, - &[] - ) - .unwrap(), - vec![&mut mint_account, &mut owner_account], - ) - ); - - // set freeze_authority - do_process_instruction( - set_authority( - &program_id, - &mint2_key, - Some(&owner2_key), - AuthorityType::FreezeAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint2_account, &mut owner_account], - ) - .unwrap(); - - // test unsetting freeze_authority is one-way operation - do_process_instruction( - set_authority( - &program_id, - &mint2_key, - None, - AuthorityType::FreezeAccount, - &owner2_key, - &[], - ) - .unwrap(), - vec![&mut mint2_account, &mut owner2_account], - ) - .unwrap(); - - assert_eq!( - Err(TokenError::MintCannotFreeze.into()), - do_process_instruction( - set_authority( - &program_id, - &mint2_key, - Some(&owner2_key), - AuthorityType::FreezeAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint2_account, &mut owner2_account], - ) - ); - } - - #[test] - fn test_mint_to_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner_info: AccountInfo = (&owner_key, true, &mut owner_account).into(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, true, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &mint_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &owner_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - owner_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // mint_to when mint_authority is self - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &mint_key, &[], 42).unwrap(), - vec![mint_info.clone(), account1_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // mint_to_checked when mint_authority is self - do_process_instruction_dups( - mint_to_checked(&program_id, &mint_key, &account1_key, &mint_key, &[], 42, 2).unwrap(), - vec![mint_info.clone(), account1_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // mint_to when mint_authority is account owner - let mut mint = Mint::unpack_unchecked(&mint_info.data.borrow()).unwrap(); - mint.mint_authority = COption::Some(account1_key); - Mint::pack(mint, &mut mint_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - mint_to( - &program_id, - &mint_key, - &account1_key, - &account1_key, - &[], - 42, - ) - .unwrap(), - vec![ - mint_info.clone(), - account1_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // mint_to_checked when mint_authority is account owner - do_process_instruction_dups( - mint_to( - &program_id, - &mint_key, - &account1_key, - &account1_key, - &[], - 42, - ) - .unwrap(), - vec![ - mint_info.clone(), - account1_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - } - - #[test] - fn test_mint_to() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mismatch_key = Pubkey::new_unique(); - let mut mismatch_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let uninitialized_key = Pubkey::new_unique(); - let mut uninitialized_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut rent_sysvar = rent_sysvar(); - - // create new mint with owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create mismatch account - do_process_instruction( - initialize_account(&program_id, &mismatch_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut mismatch_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&mismatch_account.data).unwrap(); - account.mint = mint2_key; - Account::pack(account, &mut mismatch_account.data).unwrap(); - - // mint to - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!(mint.supply, 42); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // mint to another account to test supply accumulation - do_process_instruction( - mint_to(&program_id, &mint_key, &account2_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut account2_account, &mut owner_account], - ) - .unwrap(); - - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!(mint.supply, 84); - let account = Account::unpack_unchecked(&account2_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // missing signer - let mut instruction = - mint_to(&program_id, &mint_key, &account2_key, &owner_key, &[], 42).unwrap(); - instruction.accounts[2].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - do_process_instruction( - instruction, - vec![&mut mint_account, &mut account2_account, &mut owner_account], - ) - ); - - // mismatch account - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - mint_to(&program_id, &mint_key, &mismatch_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut mismatch_account, &mut owner_account], - ) - ); - - // missing owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - mint_to(&program_id, &mint_key, &account2_key, &owner2_key, &[], 42).unwrap(), - vec![ - &mut mint_account, - &mut account2_account, - &mut owner2_account, - ], - ) - ); - - // mint not owned by program - let not_program_id = Pubkey::new_unique(); - mint_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 0).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - ); - mint_account.owner = program_id; - - // account not owned by program - let not_program_id = Pubkey::new_unique(); - account_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 0).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - ); - account_account.owner = program_id; - - // uninitialized destination account - assert_eq!( - Err(ProgramError::UninitializedAccount), - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &uninitialized_key, - &owner_key, - &[], - 42 - ) - .unwrap(), - vec![ - &mut mint_account, - &mut uninitialized_account, - &mut owner_account, - ], - ) - ); - - // unset mint_authority and test minting fails - do_process_instruction( - set_authority( - &program_id, - &mint_key, - None, - AuthorityType::MintTokens, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut mint_account, &mut owner_account], - ) - .unwrap(); - assert_eq!( - Err(TokenError::FixedSupply.into()), - do_process_instruction( - mint_to(&program_id, &mint_key, &account2_key, &owner_key, &[], 42).unwrap(), - vec![&mut mint_account, &mut account2_account, &mut owner_account], - ) - ); - } - - #[test] - fn test_burn_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner_info: AccountInfo = (&owner_key, true, &mut owner_account).into(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, true, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // mint to account - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - - // source-owner burn - do_process_instruction_dups( - burn( - &program_id, - &mint_key, - &account1_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-owner burn_checked - do_process_instruction_dups( - burn_checked( - &program_id, - &account1_key, - &mint_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // mint-owner burn - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.owner = mint_key; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - burn(&program_id, &account1_key, &mint_key, &mint_key, &[], 500).unwrap(), - vec![account1_info.clone(), mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // mint-owner burn_checked - do_process_instruction_dups( - burn_checked( - &program_id, - &account1_key, - &mint_key, - &mint_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![account1_info.clone(), mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // source-delegate burn - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.delegated_amount = 1000; - account.delegate = COption::Some(account1_key); - account.owner = owner_key; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - burn( - &program_id, - &account1_key, - &mint_key, - &account1_key, - &[], - 500, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // source-delegate burn_checked - do_process_instruction_dups( - burn_checked( - &program_id, - &account1_key, - &mint_key, - &account1_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // mint-delegate burn - do_process_instruction_dups( - mint_to(&program_id, &mint_key, &account1_key, &owner_key, &[], 1000).unwrap(), - vec![mint_info.clone(), account1_info.clone(), owner_info.clone()], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.delegated_amount = 1000; - account.delegate = COption::Some(mint_key); - account.owner = owner_key; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - burn(&program_id, &account1_key, &mint_key, &mint_key, &[], 500).unwrap(), - vec![account1_info.clone(), mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - - // mint-delegate burn_checked - do_process_instruction_dups( - burn_checked( - &program_id, - &account1_key, - &mint_key, - &mint_key, - &[], - 500, - 2, - ) - .unwrap(), - vec![account1_info.clone(), mint_info.clone(), mint_info.clone()], - ) - .unwrap(); - } - - #[test] - fn test_burn() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - let mismatch_key = Pubkey::new_unique(); - let mut mismatch_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint2_key = Pubkey::new_unique(); - let mut rent_sysvar = rent_sysvar(); - - // create new mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create mismatch account - do_process_instruction( - initialize_account(&program_id, &mismatch_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut mismatch_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // mint to mismatch account and change mint key - do_process_instruction( - mint_to(&program_id, &mint_key, &mismatch_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut mismatch_account, &mut owner_account], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&mismatch_account.data).unwrap(); - account.mint = mint2_key; - Account::pack(account, &mut mismatch_account.data).unwrap(); - - // missing signer - let mut instruction = - burn(&program_id, &account_key, &mint_key, &delegate_key, &[], 42).unwrap(); - instruction.accounts[1].is_signer = false; - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - instruction, - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account - ], - ) - ); - - // missing owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner2_key, &[], 42).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // account not owned by program - let not_program_id = Pubkey::new_unique(); - account_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 0).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - account_account.owner = program_id; - - // mint not owned by program - let not_program_id = Pubkey::new_unique(); - mint_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 0).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - mint_account.owner = program_id; - - // mint mismatch - assert_eq!( - Err(TokenError::MintMismatch.into()), - do_process_instruction( - burn(&program_id, &mismatch_key, &mint_key, &owner_key, &[], 42).unwrap(), - vec![&mut mismatch_account, &mut mint_account, &mut owner_account], - ) - ); - - // burn - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 21).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - - // burn_checked, with incorrect decimals - assert_eq!( - Err(TokenError::MintDecimalsMismatch.into()), - do_process_instruction( - burn_checked(&program_id, &account_key, &mint_key, &owner_key, &[], 21, 3).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - - // burn_checked - do_process_instruction( - burn_checked(&program_id, &account_key, &mint_key, &owner_key, &[], 21, 2).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!(mint.supply, 2000 - 42); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 1000 - 42); - - // insufficient funds - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - burn( - &program_id, - &account_key, - &mint_key, - &owner_key, - &[], - 100_000_000 - ) - .unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - - // approve delegate - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 84, - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - .unwrap(); - - // not a delegate of source account - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - burn( - &program_id, - &account_key, - &mint_key, - &owner2_key, // <-- incorrect owner or delegate - &[], - 1, - ) - .unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // insufficient funds approved via delegate - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &delegate_key, &[], 85).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account - ], - ) - ); - - // burn via delegate - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &delegate_key, &[], 84).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account, - ], - ) - .unwrap(); - - // match - let mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - assert_eq!(mint.supply, 2000 - 42 - 84); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 1000 - 42 - 84); - - // insufficient funds approved via delegate - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &delegate_key, &[], 1).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut delegate_account - ], - ) - ); - } - - #[test] - fn test_burn_and_close_system_and_incinerator_tokens() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let incinerator_account_key = Pubkey::new_unique(); - let mut incinerator_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let system_account_key = Pubkey::new_unique(); - let mut system_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let recipient_key = Pubkey::new_unique(); - let mut recipient_account = SolanaAccount::default(); - let mut mock_incinerator_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - - // create new mint - do_process_instruction( - initialize_mint2(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account3(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![&mut account_account, &mut mint_account], - ) - .unwrap(); - - // create incinerator- and system-owned accounts - do_process_instruction( - initialize_account3( - &program_id, - &incinerator_account_key, - &mint_key, - &solana_program::incinerator::id(), - ) - .unwrap(), - vec![&mut incinerator_account, &mut mint_account], - ) - .unwrap(); - do_process_instruction( - initialize_account3( - &program_id, - &system_account_key, - &mint_key, - &solana_program::system_program::id(), - ) - .unwrap(), - vec![&mut system_account, &mut mint_account], - ) - .unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // transfer half to incinerator, half to system program - do_process_instruction( - transfer( - &program_id, - &account_key, - &incinerator_account_key, - &owner_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut account_account, - &mut incinerator_account, - &mut owner_account, - ], - ) - .unwrap(); - do_process_instruction( - transfer( - &program_id, - &account_key, - &system_account_key, - &owner_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut account_account, - &mut system_account, - &mut owner_account, - ], - ) - .unwrap(); - - // close with balance fails - assert_eq!( - Err(TokenError::NonNativeHasBalance.into()), - do_process_instruction( - close_account( - &program_id, - &incinerator_account_key, - &solana_program::incinerator::id(), - &owner_key, - &[] - ) - .unwrap(), - vec![ - &mut incinerator_account, - &mut mock_incinerator_account, - &mut owner_account, - ], - ) - ); - assert_eq!( - Err(TokenError::NonNativeHasBalance.into()), - do_process_instruction( - close_account( - &program_id, - &system_account_key, - &solana_program::incinerator::id(), - &owner_key, - &[] - ) - .unwrap(), - vec![ - &mut system_account, - &mut mock_incinerator_account, - &mut owner_account, - ], - ) - ); - - // anyone can burn - do_process_instruction( - burn( - &program_id, - &incinerator_account_key, - &mint_key, - &recipient_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut incinerator_account, - &mut mint_account, - &mut recipient_account, - ], - ) - .unwrap(); - do_process_instruction( - burn( - &program_id, - &system_account_key, - &mint_key, - &recipient_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut system_account, - &mut mint_account, - &mut recipient_account, - ], - ) - .unwrap(); - - // closing fails if destination is not the incinerator - assert_eq!( - Err(ProgramError::InvalidAccountData), - do_process_instruction( - close_account( - &program_id, - &incinerator_account_key, - &recipient_key, - &owner_key, - &[] - ) - .unwrap(), - vec![ - &mut incinerator_account, - &mut recipient_account, - &mut owner_account, - ], - ) - ); - assert_eq!( - Err(ProgramError::InvalidAccountData), - do_process_instruction( - close_account( - &program_id, - &system_account_key, - &recipient_key, - &owner_key, - &[] - ) - .unwrap(), - vec![ - &mut system_account, - &mut recipient_account, - &mut owner_account, - ], - ) - ); - - // closing succeeds with incinerator recipient - do_process_instruction( - close_account( - &program_id, - &incinerator_account_key, - &solana_program::incinerator::id(), - &owner_key, - &[], - ) - .unwrap(), - vec![ - &mut incinerator_account, - &mut mock_incinerator_account, - &mut owner_account, - ], - ) - .unwrap(); - - do_process_instruction( - close_account( - &program_id, - &system_account_key, - &solana_program::incinerator::id(), - &owner_key, - &[], - ) - .unwrap(), - vec![ - &mut system_account, - &mut mock_incinerator_account, - &mut owner_account, - ], - ) - .unwrap(); - } - - #[test] - fn test_multisig() { - let program_id = crate::id(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let account_key = Pubkey::new_unique(); - let mut account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let multisig_key = Pubkey::new_unique(); - let mut multisig_account = SolanaAccount::new(42, Multisig::get_packed_len(), &program_id); - let multisig_delegate_key = Pubkey::new_unique(); - let mut multisig_delegate_account = SolanaAccount::new( - multisig_minimum_balance(), - Multisig::get_packed_len(), - &program_id, - ); - let signer_keys = vec![Pubkey::new_unique(); MAX_SIGNERS]; - let signer_key_refs: Vec<&Pubkey> = signer_keys.iter().collect(); - let mut signer_accounts = vec![SolanaAccount::new(0, 0, &program_id); MAX_SIGNERS]; - let mut rent_sysvar = rent_sysvar(); - - // multisig is not rent exempt - let account_info_iter = &mut signer_accounts.iter_mut(); - assert_eq!( - Err(TokenError::NotRentExempt.into()), - do_process_instruction( - initialize_multisig(&program_id, &multisig_key, &[&signer_keys[0]], 1).unwrap(), - vec![ - &mut multisig_account, - &mut rent_sysvar, - account_info_iter.next().unwrap(), - ], - ) - ); - - multisig_account.lamports = multisig_minimum_balance(); - let mut multisig_account2 = multisig_account.clone(); - - // single signer - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - initialize_multisig(&program_id, &multisig_key, &[&signer_keys[0]], 1).unwrap(), - vec![ - &mut multisig_account, - &mut rent_sysvar, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // single signer using `initialize_multisig2` - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - initialize_multisig2(&program_id, &multisig_key, &[&signer_keys[0]], 1).unwrap(), - vec![&mut multisig_account2, account_info_iter.next().unwrap()], - ) - .unwrap(); - - // multiple signer - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - initialize_multisig( - &program_id, - &multisig_delegate_key, - &signer_key_refs, - MAX_SIGNERS as u8, - ) - .unwrap(), - vec![ - &mut multisig_delegate_account, - &mut rent_sysvar, - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // create new mint with multisig owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &multisig_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account with multisig owner - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &multisig_key).unwrap(), - vec![ - &mut account, - &mut mint_account, - &mut multisig_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account with multisig owner - do_process_instruction( - initialize_account( - &program_id, - &account2_key, - &mint_key, - &multisig_delegate_key, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut multisig_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account_key, - &multisig_key, - &[&signer_keys[0]], - 1000, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // approve - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - approve( - &program_id, - &account_key, - &multisig_delegate_key, - &multisig_key, - &[&signer_keys[0]], - 100, - ) - .unwrap(), - vec![ - &mut account, - &mut multisig_delegate_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // transfer - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &multisig_key, - &[&signer_keys[0]], - 42, - ) - .unwrap(), - vec![ - &mut account, - &mut account2_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // transfer via delegate - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &multisig_delegate_key, - &signer_key_refs, - 42, - ) - .unwrap(), - vec![ - &mut account, - &mut account2_account, - &mut multisig_delegate_account, - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // mint to - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account2_key, - &multisig_key, - &[&signer_keys[0]], - 42, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account2_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // burn - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - burn( - &program_id, - &account_key, - &mint_key, - &multisig_key, - &[&signer_keys[0]], - 42, - ) - .unwrap(), - vec![ - &mut account, - &mut mint_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // burn via delegate - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - burn( - &program_id, - &account_key, - &mint_key, - &multisig_delegate_key, - &signer_key_refs, - 42, - ) - .unwrap(), - vec![ - &mut account, - &mut mint_account, - &mut multisig_delegate_account, - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // freeze account - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mint2_key = Pubkey::new_unique(); - let mut mint2_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - do_process_instruction( - initialize_mint( - &program_id, - &mint2_key, - &multisig_key, - Some(&multisig_key), - 2, - ) - .unwrap(), - vec![&mut mint2_account, &mut rent_sysvar], - ) - .unwrap(); - do_process_instruction( - initialize_account(&program_id, &account3_key, &mint2_key, &owner_key).unwrap(), - vec![ - &mut account3_account, - &mut mint2_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - mint_to( - &program_id, - &mint2_key, - &account3_key, - &multisig_key, - &[&signer_keys[0]], - 1000, - ) - .unwrap(), - vec![ - &mut mint2_account, - &mut account3_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - freeze_account( - &program_id, - &account3_key, - &mint2_key, - &multisig_key, - &[&signer_keys[0]], - ) - .unwrap(), - vec![ - &mut account3_account, - &mut mint2_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // do SetAuthority on mint - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - set_authority( - &program_id, - &mint_key, - Some(&owner_key), - AuthorityType::MintTokens, - &multisig_key, - &[&signer_keys[0]], - ) - .unwrap(), - vec![ - &mut mint_account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - - // do SetAuthority on account - let account_info_iter = &mut signer_accounts.iter_mut(); - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner_key), - AuthorityType::AccountOwner, - &multisig_key, - &[&signer_keys[0]], - ) - .unwrap(), - vec![ - &mut account, - &mut multisig_account, - account_info_iter.next().unwrap(), - ], - ) - .unwrap(); - } - - #[test] - fn test_validate_owner() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mut signer_keys = [Pubkey::default(); MAX_SIGNERS]; - for signer_key in signer_keys.iter_mut().take(MAX_SIGNERS) { - *signer_key = Pubkey::new_unique(); - } - let mut signer_lamports = 0; - let mut signer_data = vec![]; - let mut signers = vec![ - AccountInfo::new( - &owner_key, - true, - false, - &mut signer_lamports, - &mut signer_data, - &program_id, - false, - Epoch::default(), - ); - MAX_SIGNERS + 1 - ]; - for (signer, key) in signers.iter_mut().zip(&signer_keys) { - signer.key = key; - } - let mut lamports = 0; - let mut data = vec![0; Multisig::get_packed_len()]; - let mut multisig = Multisig::unpack_unchecked(&data).unwrap(); - multisig.m = MAX_SIGNERS as u8; - multisig.n = MAX_SIGNERS as u8; - multisig.signers = signer_keys; - multisig.is_initialized = true; - Multisig::pack(multisig, &mut data).unwrap(); - let owner_account_info = AccountInfo::new( - &owner_key, - false, - false, - &mut lamports, - &mut data, - &program_id, - false, - Epoch::default(), - ); - - // full 11 of 11 - Processor::validate_owner(&program_id, &owner_key, &owner_account_info, &signers).unwrap(); - - // 1 of 11 - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 1; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - Processor::validate_owner(&program_id, &owner_key, &owner_account_info, &signers).unwrap(); - - // 2:1 - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 2; - multisig.n = 1; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner(&program_id, &owner_key, &owner_account_info, &signers) - ); - - // 0:11 - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 0; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - Processor::validate_owner(&program_id, &owner_key, &owner_account_info, &signers).unwrap(); - - // 2:11 but 0 provided - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 2; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner(&program_id, &owner_key, &owner_account_info, &[]) - ); - // 2:11 but 1 provided - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 2; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner(&program_id, &owner_key, &owner_account_info, &signers[0..1]) - ); - - // 2:11, 2 from middle provided - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 2; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - Processor::validate_owner(&program_id, &owner_key, &owner_account_info, &signers[5..7]) - .unwrap(); - - // 11:11, one is not a signer - { - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 11; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - } - signers[5].is_signer = false; - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner(&program_id, &owner_key, &owner_account_info, &signers) - ); - signers[5].is_signer = true; - - // 11:11, single signer signs multiple times - { - let mut signer_lamports = 0; - let mut signer_data = vec![]; - let signers = vec![ - AccountInfo::new( - &signer_keys[5], - true, - false, - &mut signer_lamports, - &mut signer_data, - &program_id, - false, - Epoch::default(), - ); - MAX_SIGNERS + 1 - ]; - let mut multisig = - Multisig::unpack_unchecked(&owner_account_info.data.borrow()).unwrap(); - multisig.m = 11; - multisig.n = 11; - Multisig::pack(multisig, &mut owner_account_info.data.borrow_mut()).unwrap(); - assert_eq!( - Err(ProgramError::MissingRequiredSignature), - Processor::validate_owner(&program_id, &owner_key, &owner_account_info, &signers) - ); - } - } - - #[test] - fn test_owner_close_account_dups() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, false, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - let to_close_key = Pubkey::new_unique(); - let mut to_close_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let to_close_account_info: AccountInfo = - (&to_close_key, true, &mut to_close_account).into(); - let destination_account_key = Pubkey::new_unique(); - let mut destination_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let destination_account_info: AccountInfo = - (&destination_account_key, true, &mut destination_account).into(); - // create account - do_process_instruction_dups( - initialize_account(&program_id, &to_close_key, &mint_key, &to_close_key).unwrap(), - vec![ - to_close_account_info.clone(), - mint_info.clone(), - to_close_account_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // source-owner close - do_process_instruction_dups( - close_account( - &program_id, - &to_close_key, - &destination_account_key, - &to_close_key, - &[], - ) - .unwrap(), - vec![ - to_close_account_info.clone(), - destination_account_info.clone(), - to_close_account_info.clone(), - ], - ) - .unwrap(); - assert_eq!(*to_close_account_info.data.borrow(), &[0u8; Account::LEN]); - } - - #[test] - fn test_close_authority_close_account_dups() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, false, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - let to_close_key = Pubkey::new_unique(); - let mut to_close_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let to_close_account_info: AccountInfo = - (&to_close_key, true, &mut to_close_account).into(); - let destination_account_key = Pubkey::new_unique(); - let mut destination_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let destination_account_info: AccountInfo = - (&destination_account_key, true, &mut destination_account).into(); - // create account - do_process_instruction_dups( - initialize_account(&program_id, &to_close_key, &mint_key, &to_close_key).unwrap(), - vec![ - to_close_account_info.clone(), - mint_info.clone(), - to_close_account_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - let mut account = Account::unpack_unchecked(&to_close_account_info.data.borrow()).unwrap(); - account.close_authority = COption::Some(to_close_key); - account.owner = owner_key; - Account::pack(account, &mut to_close_account_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - close_account( - &program_id, - &to_close_key, - &destination_account_key, - &to_close_key, - &[], - ) - .unwrap(), - vec![ - to_close_account_info.clone(), - destination_account_info.clone(), - to_close_account_info.clone(), - ], - ) - .unwrap(); - assert_eq!(*to_close_account_info.data.borrow(), &[0u8; Account::LEN]); - } - - #[test] - fn test_close_account() { - let program_id = crate::id(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance() + 42, - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mut rent_sysvar = rent_sysvar(); - - // uninitialized - assert_eq!( - Err(ProgramError::UninitializedAccount), - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner2_account, - ], - ) - ); - - // initialize and mint to non-native account - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 42).unwrap(), - vec![ - &mut mint_account, - &mut account_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 42); - - // initialize native account - do_process_instruction( - initialize_account( - &program_id, - &account2_key, - &crate::native_mint::id(), - &owner_key, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account2_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 42); - - // close non-native account with balance - assert_eq!( - Err(TokenError::NonNativeHasBalance.into()), - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner_account, - ], - ) - ); - assert_eq!(account_account.lamports, account_minimum_balance()); - - // empty account - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 42).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - - // wrong owner - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner2_account, - ], - ) - ); - - // close account - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner_account, - ], - ) - .unwrap(); - assert_eq!(account_account.lamports, 0); - assert_eq!(account3_account.lamports, 2 * account_minimum_balance()); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 0); - - // fund and initialize new non-native account to test close authority - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - account_account.lamports = 2; - - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::CloseAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - - // account owner cannot authorize close if close_authority is set - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner_account, - ], - ) - ); - - // close non-native account with close_authority - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner2_account, - ], - ) - .unwrap(); - assert_eq!(account_account.lamports, 0); - assert_eq!(account3_account.lamports, 2 * account_minimum_balance() + 2); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, 0); - - // close native account - do_process_instruction( - close_account(&program_id, &account2_key, &account3_key, &owner_key, &[]).unwrap(), - vec![ - &mut account2_account, - &mut account3_account, - &mut owner_account, - ], - ) - .unwrap(); - assert_eq!(account2_account.data, [0u8; Account::LEN]); - assert_eq!( - account3_account.lamports, - 3 * account_minimum_balance() + 2 + 42 - ); - } - - #[test] - fn test_native_token() { - let program_id = crate::id(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance() + 40, - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account3_key = Pubkey::new_unique(); - let mut account3_account = SolanaAccount::new(account_minimum_balance(), 0, &program_id); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let owner3_key = Pubkey::new_unique(); - let mut rent_sysvar = rent_sysvar(); - - // initialize native account - do_process_instruction( - initialize_account( - &program_id, - &account_key, - &crate::native_mint::id(), - &owner_key, - ) - .unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 40); - - // initialize native account - do_process_instruction( - initialize_account( - &program_id, - &account2_key, - &crate::native_mint::id(), - &owner_key, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account2_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 0); - - // mint_to unsupported - assert_eq!( - Err(TokenError::NativeNotSupported.into()), - do_process_instruction( - mint_to( - &program_id, - &crate::native_mint::id(), - &account_key, - &owner_key, - &[], - 42 - ) - .unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - ); - - // burn unsupported - let bogus_mint_key = Pubkey::new_unique(); - let mut bogus_mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - do_process_instruction( - initialize_mint(&program_id, &bogus_mint_key, &owner_key, None, 2).unwrap(), - vec![&mut bogus_mint_account, &mut rent_sysvar], - ) - .unwrap(); - - assert_eq!( - Err(TokenError::NativeNotSupported.into()), - do_process_instruction( - burn( - &program_id, - &account_key, - &bogus_mint_key, - &owner_key, - &[], - 42 - ) - .unwrap(), - vec![ - &mut account_account, - &mut bogus_mint_account, - &mut owner_account - ], - ) - ); - - // ensure can't transfer below rent-exempt reserve - assert_eq!( - Err(TokenError::InsufficientFunds.into()), - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 50, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - // transfer between native accounts - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 40, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - .unwrap(); - assert_eq!(account_account.lamports, account_minimum_balance()); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 0); - assert_eq!(account2_account.lamports, account_minimum_balance() + 40); - let account = Account::unpack_unchecked(&account2_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, 40); - - // set close authority - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner3_key), - AuthorityType::CloseAccount, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.close_authority, COption::Some(owner3_key)); - - // set new account owner - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&owner2_key), - AuthorityType::AccountOwner, - &owner_key, - &[], - ) - .unwrap(), - vec![&mut account_account, &mut owner_account], - ) - .unwrap(); - - // close authority cleared - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.close_authority, COption::None); - - // close native account - do_process_instruction( - close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), - vec![ - &mut account_account, - &mut account3_account, - &mut owner2_account, - ], - ) - .unwrap(); - assert_eq!(account_account.lamports, 0); - assert_eq!(account3_account.lamports, 2 * account_minimum_balance()); - assert_eq!(account_account.data, [0u8; Account::LEN]); - } - - #[test] - fn test_overflow() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_owner_key = Pubkey::new_unique(); - let mut mint_owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create new mint with owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &mint_owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create an account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner2_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner2_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint the max to an account - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account_key, - &mint_owner_key, - &[], - u64::MAX, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account_account, - &mut mint_owner_account, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, u64::MAX); - - // attempt to mint one more to account - assert_eq!( - Err(TokenError::Overflow.into()), - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account_key, - &mint_owner_key, - &[], - 1, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account_account, - &mut mint_owner_account, - ], - ) - ); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, u64::MAX); - - // attempt to mint one more to the other account - assert_eq!( - Err(TokenError::Overflow.into()), - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account2_key, - &mint_owner_key, - &[], - 1, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account2_account, - &mut mint_owner_account, - ], - ) - ); - - // burn some of the supply - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 100).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, u64::MAX - 100); - - do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account_key, - &mint_owner_key, - &[], - 100, - ) - .unwrap(), - vec![ - &mut mint_account, - &mut account_account, - &mut mint_owner_account, - ], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.amount, u64::MAX); - - // manipulate account balance to attempt overflow transfer - let mut account = Account::unpack_unchecked(&account2_account.data).unwrap(); - account.amount = 1; - Account::pack(account, &mut account2_account.data).unwrap(); - - assert_eq!( - Err(TokenError::Overflow.into()), - do_process_instruction( - transfer( - &program_id, - &account2_key, - &account_key, - &owner2_key, - &[], - 1, - ) - .unwrap(), - vec![ - &mut account2_account, - &mut account_account, - &mut owner2_account, - ], - ) - ); - } - - #[test] - fn test_frozen() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account2_key = Pubkey::new_unique(); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create new mint and fund first account - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // create another account - do_process_instruction( - initialize_account(&program_id, &account2_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account2_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // fund first account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // no transfer if either account is frozen - let mut account = Account::unpack_unchecked(&account2_account.data).unwrap(); - account.state = AccountState::Frozen; - Account::pack(account, &mut account2_account.data).unwrap(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - let mut account = Account::unpack_unchecked(&account_account.data).unwrap(); - account.state = AccountState::Initialized; - Account::pack(account, &mut account_account.data).unwrap(); - let mut account = Account::unpack_unchecked(&account2_account.data).unwrap(); - account.state = AccountState::Frozen; - Account::pack(account, &mut account2_account.data).unwrap(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - transfer( - &program_id, - &account_key, - &account2_key, - &owner_key, - &[], - 500, - ) - .unwrap(), - vec![ - &mut account_account, - &mut account2_account, - &mut owner_account, - ], - ) - ); - - // no approve if account is frozen - let mut account = Account::unpack_unchecked(&account_account.data).unwrap(); - account.state = AccountState::Frozen; - Account::pack(account, &mut account_account.data).unwrap(); - let delegate_key = Pubkey::new_unique(); - let mut delegate_account = SolanaAccount::default(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - approve( - &program_id, - &account_key, - &delegate_key, - &owner_key, - &[], - 100 - ) - .unwrap(), - vec![ - &mut account_account, - &mut delegate_account, - &mut owner_account, - ], - ) - ); - - // no revoke if account is frozen - let mut account = Account::unpack_unchecked(&account_account.data).unwrap(); - account.delegate = COption::Some(delegate_key); - account.delegated_amount = 100; - Account::pack(account, &mut account_account.data).unwrap(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - revoke(&program_id, &account_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut owner_account], - ) - ); - - // no set authority if account is frozen - let new_owner_key = Pubkey::new_unique(); - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - set_authority( - &program_id, - &account_key, - Some(&new_owner_key), - AuthorityType::AccountOwner, - &owner_key, - &[] - ) - .unwrap(), - vec![&mut account_account, &mut owner_account,], - ) - ); - - // no mint_to if destination account is frozen - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 100).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account,], - ) - ); - - // no burn if account is frozen - assert_eq!( - Err(TokenError::AccountFrozen.into()), - do_process_instruction( - burn(&program_id, &account_key, &mint_key, &owner_key, &[], 100).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - } - - #[test] - fn test_freeze_thaw_dups() { - let program_id = crate::id(); - let account1_key = Pubkey::new_unique(); - let mut account1_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account1_info: AccountInfo = (&account1_key, true, &mut account1_account).into(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_info: AccountInfo = (&mint_key, true, &mut mint_account).into(); - let rent_key = rent::id(); - let mut rent_sysvar = rent_sysvar(); - let rent_info: AccountInfo = (&rent_key, false, &mut rent_sysvar).into(); - - // create mint - do_process_instruction_dups( - initialize_mint(&program_id, &mint_key, &owner_key, Some(&account1_key), 2).unwrap(), - vec![mint_info.clone(), rent_info.clone()], - ) - .unwrap(); - - // create account - do_process_instruction_dups( - initialize_account(&program_id, &account1_key, &mint_key, &account1_key).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - rent_info.clone(), - ], - ) - .unwrap(); - - // freeze where mint freeze_authority is account - do_process_instruction_dups( - freeze_account(&program_id, &account1_key, &mint_key, &account1_key, &[]).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - - // thaw where mint freeze_authority is account - let mut account = Account::unpack_unchecked(&account1_info.data.borrow()).unwrap(); - account.state = AccountState::Frozen; - Account::pack(account, &mut account1_info.data.borrow_mut()).unwrap(); - do_process_instruction_dups( - thaw_account(&program_id, &account1_key, &mint_key, &account1_key, &[]).unwrap(), - vec![ - account1_info.clone(), - mint_info.clone(), - account1_info.clone(), - ], - ) - .unwrap(); - } - - #[test] - fn test_freeze_account() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let account_owner_key = Pubkey::new_unique(); - let mut account_owner_account = SolanaAccount::default(); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let owner2_key = Pubkey::new_unique(); - let mut owner2_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create new mint with owner different from account owner - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &account_owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut account_owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // mint to account - do_process_instruction( - mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 1000).unwrap(), - vec![&mut mint_account, &mut account_account, &mut owner_account], - ) - .unwrap(); - - // mint cannot freeze - assert_eq!( - Err(TokenError::MintCannotFreeze.into()), - do_process_instruction( - freeze_account(&program_id, &account_key, &mint_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - - // missing freeze_authority - let mut mint = Mint::unpack_unchecked(&mint_account.data).unwrap(); - mint.freeze_authority = COption::Some(owner_key); - Mint::pack(mint, &mut mint_account.data).unwrap(); - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - freeze_account(&program_id, &account_key, &mint_key, &owner2_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // check explicit thaw - assert_eq!( - Err(TokenError::InvalidState.into()), - do_process_instruction( - thaw_account(&program_id, &account_key, &mint_key, &owner2_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // freeze - do_process_instruction( - freeze_account(&program_id, &account_key, &mint_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.state, AccountState::Frozen); - - // check explicit freeze - assert_eq!( - Err(TokenError::InvalidState.into()), - do_process_instruction( - freeze_account(&program_id, &account_key, &mint_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - ); - - // check thaw authority - assert_eq!( - Err(TokenError::OwnerMismatch.into()), - do_process_instruction( - thaw_account(&program_id, &account_key, &mint_key, &owner2_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner2_account], - ) - ); - - // thaw - do_process_instruction( - thaw_account(&program_id, &account_key, &mint_key, &owner_key, &[]).unwrap(), - vec![&mut account_account, &mut mint_account, &mut owner_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&account_account.data).unwrap(); - assert_eq!(account.state, AccountState::Initialized); - } - - #[test] - fn test_initialize_account2_and_3() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut account2_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let mut account3_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - do_process_instruction( - initialize_account2(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![&mut account2_account, &mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - assert_eq!(account_account, account2_account); - - do_process_instruction( - initialize_account3(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![&mut account3_account, &mut mint_account], - ) - .unwrap(); - - assert_eq!(account_account, account3_account); - } - - #[test] - fn test_sync_native() { - let program_id = crate::id(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let native_account_key = Pubkey::new_unique(); - let lamports = 40; - let mut native_account = SolanaAccount::new( - account_minimum_balance() + lamports, - Account::get_packed_len(), - &program_id, - ); - let non_native_account_key = Pubkey::new_unique(); - let mut non_native_account = SolanaAccount::new( - account_minimum_balance() + 50, - Account::get_packed_len(), - &program_id, - ); - - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mut rent_sysvar = rent_sysvar(); - - // initialize non-native mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // initialize non-native account - do_process_instruction( - initialize_account(&program_id, &non_native_account_key, &mint_key, &owner_key) - .unwrap(), - vec![ - &mut non_native_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - let account = Account::unpack_unchecked(&non_native_account.data).unwrap(); - assert!(!account.is_native()); - assert_eq!(account.amount, 0); - - // fail sync non-native - assert_eq!( - Err(TokenError::NonNativeNotSupported.into()), - do_process_instruction( - sync_native(&program_id, &non_native_account_key,).unwrap(), - vec![&mut non_native_account], - ) - ); - - // fail sync uninitialized - assert_eq!( - Err(ProgramError::UninitializedAccount), - do_process_instruction( - sync_native(&program_id, &native_account_key,).unwrap(), - vec![&mut native_account], - ) - ); - - // wrap native account - do_process_instruction( - initialize_account( - &program_id, - &native_account_key, - &crate::native_mint::id(), - &owner_key, - ) - .unwrap(), - vec![ - &mut native_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // fail sync, not owned by program - let not_program_id = Pubkey::new_unique(); - native_account.owner = not_program_id; - assert_eq!( - Err(ProgramError::IncorrectProgramId), - do_process_instruction( - sync_native(&program_id, &native_account_key,).unwrap(), - vec![&mut native_account], - ) - ); - native_account.owner = program_id; - - let account = Account::unpack_unchecked(&native_account.data).unwrap(); - assert!(account.is_native()); - assert_eq!(account.amount, lamports); - - // sync, no change - do_process_instruction( - sync_native(&program_id, &native_account_key).unwrap(), - vec![&mut native_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&native_account.data).unwrap(); - assert_eq!(account.amount, lamports); - - // transfer sol - let new_lamports = lamports + 50; - native_account.lamports = account_minimum_balance() + new_lamports; - - // success sync - do_process_instruction( - sync_native(&program_id, &native_account_key).unwrap(), - vec![&mut native_account], - ) - .unwrap(); - let account = Account::unpack_unchecked(&native_account.data).unwrap(); - assert_eq!(account.amount, new_lamports); - - // reduce sol - native_account.lamports -= 1; - - // fail sync - assert_eq!( - Err(TokenError::InvalidState.into()), - do_process_instruction( - sync_native(&program_id, &native_account_key,).unwrap(), - vec![&mut native_account], - ) - ); - } - - #[test] - #[serial] - fn test_get_account_data_size() { - // see integration tests for return-data validity - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mut rent_sysvar = rent_sysvar(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mint_key = Pubkey::new_unique(); - // fail if an invalid mint is passed in - assert_eq!( - Err(TokenError::InvalidMint.into()), - do_process_instruction( - get_account_data_size(&program_id, &mint_key).unwrap(), - vec![&mut mint_account], - ) - ); - - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - set_expected_data(Account::LEN.to_le_bytes().to_vec()); - do_process_instruction( - get_account_data_size(&program_id, &mint_key).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - } - - #[test] - fn test_initialize_immutable_owner() { - let program_id = crate::id(); - let account_key = Pubkey::new_unique(); - let mut account_account = SolanaAccount::new( - account_minimum_balance(), - Account::get_packed_len(), - &program_id, - ); - let owner_key = Pubkey::new_unique(); - let mut owner_account = SolanaAccount::default(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - // success initialize immutable - do_process_instruction( - initialize_immutable_owner(&program_id, &account_key).unwrap(), - vec![&mut account_account], - ) - .unwrap(); - - // create account - do_process_instruction( - initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), - vec![ - &mut account_account, - &mut mint_account, - &mut owner_account, - &mut rent_sysvar, - ], - ) - .unwrap(); - - // fail post-init - assert_eq!( - Err(TokenError::AlreadyInUse.into()), - do_process_instruction( - initialize_immutable_owner(&program_id, &account_key).unwrap(), - vec![&mut account_account], - ) - ); - } - - #[test] - #[serial] - fn test_amount_to_ui_amount() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // fail if an invalid mint is passed in - assert_eq!( - Err(TokenError::InvalidMint.into()), - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 110).unwrap(), - vec![&mut mint_account], - ) - ); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - set_expected_data("0.23".as_bytes().to_vec()); - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 23).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data("1.1".as_bytes().to_vec()); - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 110).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data("42".as_bytes().to_vec()); - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 4200).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data("0".as_bytes().to_vec()); - do_process_instruction( - amount_to_ui_amount(&program_id, &mint_key, 0).unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - } - - #[test] - #[serial] - fn test_ui_amount_to_amount() { - let program_id = crate::id(); - let owner_key = Pubkey::new_unique(); - let mint_key = Pubkey::new_unique(); - let mut mint_account = - SolanaAccount::new(mint_minimum_balance(), Mint::get_packed_len(), &program_id); - let mut rent_sysvar = rent_sysvar(); - - // fail if an invalid mint is passed in - assert_eq!( - Err(TokenError::InvalidMint.into()), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "1.1").unwrap(), - vec![&mut mint_account], - ) - ); - - // create mint - do_process_instruction( - initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), - vec![&mut mint_account, &mut rent_sysvar], - ) - .unwrap(); - - set_expected_data(23u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.23").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(20u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.20").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(20u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.2000").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(20u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, ".20").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(110u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "1.1").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(110u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "1.10").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(4200u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "42").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(4200u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "42.").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - set_expected_data(0u64.to_le_bytes().to_vec()); - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0").unwrap(), - vec![&mut mint_account], - ) - .unwrap(); - - // fail if invalid ui_amount passed in - assert_eq!( - Err(ProgramError::InvalidArgument), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "").unwrap(), - vec![&mut mint_account], - ) - ); - assert_eq!( - Err(ProgramError::InvalidArgument), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, ".").unwrap(), - vec![&mut mint_account], - ) - ); - assert_eq!( - Err(ProgramError::InvalidArgument), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.111").unwrap(), - vec![&mut mint_account], - ) - ); - assert_eq!( - Err(ProgramError::InvalidArgument), - do_process_instruction( - ui_amount_to_amount(&program_id, &mint_key, "0.t").unwrap(), - vec![&mut mint_account], - ) - ); - } -} diff --git a/token/program/src/state.rs b/token/program/src/state.rs deleted file mode 100644 index f7595e793ec..00000000000 --- a/token/program/src/state.rs +++ /dev/null @@ -1,492 +0,0 @@ -//! State transition types - -use { - crate::instruction::MAX_SIGNERS, - arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}, - num_enum::TryFromPrimitive, - solana_program::{ - program_error::ProgramError, - program_option::COption, - program_pack::{IsInitialized, Pack, Sealed}, - pubkey::{Pubkey, PUBKEY_BYTES}, - }, -}; - -/// Mint data. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Mint { - /// Optional authority used to mint new tokens. The mint authority may only - /// be provided during mint creation. If no mint authority is present - /// then the mint has a fixed supply and no further tokens may be - /// minted. - pub mint_authority: COption, - /// Total supply of tokens. - pub supply: u64, - /// Number of base 10 digits to the right of the decimal place. - pub decimals: u8, - /// Is `true` if this structure has been initialized - pub is_initialized: bool, - /// Optional authority to freeze token accounts. - pub freeze_authority: COption, -} -impl Sealed for Mint {} -impl IsInitialized for Mint { - fn is_initialized(&self) -> bool { - self.is_initialized - } -} -impl Pack for Mint { - const LEN: usize = 82; - fn unpack_from_slice(src: &[u8]) -> Result { - let src = array_ref![src, 0, 82]; - let (mint_authority, supply, decimals, is_initialized, freeze_authority) = - array_refs![src, 36, 8, 1, 1, 36]; - let mint_authority = unpack_coption_key(mint_authority)?; - let supply = u64::from_le_bytes(*supply); - let decimals = decimals[0]; - let is_initialized = match is_initialized { - [0] => false, - [1] => true, - _ => return Err(ProgramError::InvalidAccountData), - }; - let freeze_authority = unpack_coption_key(freeze_authority)?; - Ok(Mint { - mint_authority, - supply, - decimals, - is_initialized, - freeze_authority, - }) - } - fn pack_into_slice(&self, dst: &mut [u8]) { - let dst = array_mut_ref![dst, 0, 82]; - let ( - mint_authority_dst, - supply_dst, - decimals_dst, - is_initialized_dst, - freeze_authority_dst, - ) = mut_array_refs![dst, 36, 8, 1, 1, 36]; - let &Mint { - ref mint_authority, - supply, - decimals, - is_initialized, - ref freeze_authority, - } = self; - pack_coption_key(mint_authority, mint_authority_dst); - *supply_dst = supply.to_le_bytes(); - decimals_dst[0] = decimals; - is_initialized_dst[0] = is_initialized as u8; - pack_coption_key(freeze_authority, freeze_authority_dst); - } -} - -/// Account data. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Account { - /// The mint associated with this account - pub mint: Pubkey, - /// The owner of this account. - pub owner: Pubkey, - /// The amount of tokens this account holds. - pub amount: u64, - /// If `delegate` is `Some` then `delegated_amount` represents - /// the amount authorized by the delegate - pub delegate: COption, - /// The account's state - pub state: AccountState, - /// If `is_native.is_some`, this is a native token, and the value logs the - /// rent-exempt reserve. An Account is required to be rent-exempt, so - /// the value is used by the Processor to ensure that wrapped SOL - /// accounts do not drop below this threshold. - pub is_native: COption, - /// The amount delegated - pub delegated_amount: u64, - /// Optional authority to close the account. - pub close_authority: COption, -} -impl Account { - /// Checks if account is frozen - pub fn is_frozen(&self) -> bool { - self.state == AccountState::Frozen - } - /// Checks if account is native - pub fn is_native(&self) -> bool { - self.is_native.is_some() - } - /// Checks if a token Account's owner is the `system_program` or the - /// incinerator - pub fn is_owned_by_system_program_or_incinerator(&self) -> bool { - solana_program::system_program::check_id(&self.owner) - || solana_program::incinerator::check_id(&self.owner) - } -} -impl Sealed for Account {} -impl IsInitialized for Account { - fn is_initialized(&self) -> bool { - self.state != AccountState::Uninitialized - } -} -impl Pack for Account { - const LEN: usize = 165; - fn unpack_from_slice(src: &[u8]) -> Result { - let src = array_ref![src, 0, 165]; - let (mint, owner, amount, delegate, state, is_native, delegated_amount, close_authority) = - array_refs![src, 32, 32, 8, 36, 1, 12, 8, 36]; - Ok(Account { - mint: Pubkey::new_from_array(*mint), - owner: Pubkey::new_from_array(*owner), - amount: u64::from_le_bytes(*amount), - delegate: unpack_coption_key(delegate)?, - state: AccountState::try_from_primitive(state[0]) - .or(Err(ProgramError::InvalidAccountData))?, - is_native: unpack_coption_u64(is_native)?, - delegated_amount: u64::from_le_bytes(*delegated_amount), - close_authority: unpack_coption_key(close_authority)?, - }) - } - fn pack_into_slice(&self, dst: &mut [u8]) { - let dst = array_mut_ref![dst, 0, 165]; - let ( - mint_dst, - owner_dst, - amount_dst, - delegate_dst, - state_dst, - is_native_dst, - delegated_amount_dst, - close_authority_dst, - ) = mut_array_refs![dst, 32, 32, 8, 36, 1, 12, 8, 36]; - let &Account { - ref mint, - ref owner, - amount, - ref delegate, - state, - ref is_native, - delegated_amount, - ref close_authority, - } = self; - mint_dst.copy_from_slice(mint.as_ref()); - owner_dst.copy_from_slice(owner.as_ref()); - *amount_dst = amount.to_le_bytes(); - pack_coption_key(delegate, delegate_dst); - state_dst[0] = state as u8; - pack_coption_u64(is_native, is_native_dst); - *delegated_amount_dst = delegated_amount.to_le_bytes(); - pack_coption_key(close_authority, close_authority_dst); - } -} - -/// Account state. -#[repr(u8)] -#[derive(Clone, Copy, Debug, Default, PartialEq, TryFromPrimitive)] -pub enum AccountState { - /// Account is not yet initialized - #[default] - Uninitialized, - /// Account is initialized; the account owner and/or delegate may perform - /// permitted operations on this account - Initialized, - /// Account has been frozen by the mint freeze authority. Neither the - /// account owner nor the delegate are able to perform operations on - /// this account. - Frozen, -} - -/// Multisignature data. -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Multisig { - /// Number of signers required - pub m: u8, - /// Number of valid signers - pub n: u8, - /// Is `true` if this structure has been initialized - pub is_initialized: bool, - /// Signer public keys - pub signers: [Pubkey; MAX_SIGNERS], -} -impl Sealed for Multisig {} -impl IsInitialized for Multisig { - fn is_initialized(&self) -> bool { - self.is_initialized - } -} -impl Pack for Multisig { - const LEN: usize = 355; - fn unpack_from_slice(src: &[u8]) -> Result { - let src = array_ref![src, 0, 355]; - #[allow(clippy::ptr_offset_with_cast)] - let (m, n, is_initialized, signers_flat) = array_refs![src, 1, 1, 1, 32 * MAX_SIGNERS]; - let mut result = Multisig { - m: m[0], - n: n[0], - is_initialized: match is_initialized { - [0] => false, - [1] => true, - _ => return Err(ProgramError::InvalidAccountData), - }, - signers: [Pubkey::new_from_array([0u8; 32]); MAX_SIGNERS], - }; - for (src, dst) in signers_flat.chunks(32).zip(result.signers.iter_mut()) { - *dst = Pubkey::try_from(src).map_err(|_| ProgramError::InvalidAccountData)?; - } - Ok(result) - } - fn pack_into_slice(&self, dst: &mut [u8]) { - let dst = array_mut_ref![dst, 0, 355]; - #[allow(clippy::ptr_offset_with_cast)] - let (m, n, is_initialized, signers_flat) = mut_array_refs![dst, 1, 1, 1, 32 * MAX_SIGNERS]; - *m = [self.m]; - *n = [self.n]; - *is_initialized = [self.is_initialized as u8]; - for (i, src) in self.signers.iter().enumerate() { - let dst_array = array_mut_ref![signers_flat, 32 * i, 32]; - dst_array.copy_from_slice(src.as_ref()); - } - } -} - -// Helpers -fn pack_coption_key(src: &COption, dst: &mut [u8; 36]) { - let (tag, body) = mut_array_refs![dst, 4, 32]; - match src { - COption::Some(key) => { - *tag = [1, 0, 0, 0]; - body.copy_from_slice(key.as_ref()); - } - COption::None => { - *tag = [0; 4]; - } - } -} -fn unpack_coption_key(src: &[u8; 36]) -> Result, ProgramError> { - let (tag, body) = array_refs![src, 4, 32]; - match *tag { - [0, 0, 0, 0] => Ok(COption::None), - [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))), - _ => Err(ProgramError::InvalidAccountData), - } -} -fn pack_coption_u64(src: &COption, dst: &mut [u8; 12]) { - let (tag, body) = mut_array_refs![dst, 4, 8]; - match src { - COption::Some(amount) => { - *tag = [1, 0, 0, 0]; - *body = amount.to_le_bytes(); - } - COption::None => { - *tag = [0; 4]; - } - } -} -fn unpack_coption_u64(src: &[u8; 12]) -> Result, ProgramError> { - let (tag, body) = array_refs![src, 4, 8]; - match *tag { - [0, 0, 0, 0] => Ok(COption::None), - [1, 0, 0, 0] => Ok(COption::Some(u64::from_le_bytes(*body))), - _ => Err(ProgramError::InvalidAccountData), - } -} - -const SPL_TOKEN_ACCOUNT_MINT_OFFSET: usize = 0; -const SPL_TOKEN_ACCOUNT_OWNER_OFFSET: usize = 32; - -/// A trait for token Account structs to enable efficiently unpacking various -/// fields without unpacking the complete state. -pub trait GenericTokenAccount { - /// Check if the account data is a valid token account - fn valid_account_data(account_data: &[u8]) -> bool; - - /// Call after account length has already been verified to unpack the - /// account owner - fn unpack_account_owner_unchecked(account_data: &[u8]) -> &Pubkey { - Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_OWNER_OFFSET) - } - - /// Call after account length has already been verified to unpack the - /// account mint - fn unpack_account_mint_unchecked(account_data: &[u8]) -> &Pubkey { - Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_MINT_OFFSET) - } - - /// Call after account length has already been verified to unpack a Pubkey - /// at the specified offset. Panics if `account_data.len()` is less than - /// `PUBKEY_BYTES` - fn unpack_pubkey_unchecked(account_data: &[u8], offset: usize) -> &Pubkey { - bytemuck::from_bytes(&account_data[offset..offset + PUBKEY_BYTES]) - } - - /// Unpacks an account's owner from opaque account data. - fn unpack_account_owner(account_data: &[u8]) -> Option<&Pubkey> { - if Self::valid_account_data(account_data) { - Some(Self::unpack_account_owner_unchecked(account_data)) - } else { - None - } - } - - /// Unpacks an account's mint from opaque account data. - fn unpack_account_mint(account_data: &[u8]) -> Option<&Pubkey> { - if Self::valid_account_data(account_data) { - Some(Self::unpack_account_mint_unchecked(account_data)) - } else { - None - } - } -} - -/// The offset of state field in Account's C representation -pub const ACCOUNT_INITIALIZED_INDEX: usize = 108; - -/// Check if the account data buffer represents an initialized account. -/// This is checking the `state` (`AccountState`) field of an Account object. -pub fn is_initialized_account(account_data: &[u8]) -> bool { - *account_data - .get(ACCOUNT_INITIALIZED_INDEX) - .unwrap_or(&(AccountState::Uninitialized as u8)) - != AccountState::Uninitialized as u8 -} - -impl GenericTokenAccount for Account { - fn valid_account_data(account_data: &[u8]) -> bool { - account_data.len() == Account::LEN && is_initialized_account(account_data) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_mint_unpack_from_slice() { - let src: [u8; 82] = [0; 82]; - let mint = Mint::unpack_from_slice(&src).unwrap(); - assert!(!mint.is_initialized); - - let mut src: [u8; 82] = [0; 82]; - src[45] = 2; - let mint = Mint::unpack_from_slice(&src).unwrap_err(); - assert_eq!(mint, ProgramError::InvalidAccountData); - } - - #[test] - fn test_account_state() { - let account_state = AccountState::default(); - assert_eq!(account_state, AccountState::Uninitialized); - } - - #[test] - fn test_multisig_unpack_from_slice() { - let src: [u8; 355] = [0; 355]; - let multisig = Multisig::unpack_from_slice(&src).unwrap(); - assert_eq!(multisig.m, 0); - assert_eq!(multisig.n, 0); - assert!(!multisig.is_initialized); - - let mut src: [u8; 355] = [0; 355]; - src[0] = 1; - src[1] = 1; - src[2] = 1; - let multisig = Multisig::unpack_from_slice(&src).unwrap(); - assert_eq!(multisig.m, 1); - assert_eq!(multisig.n, 1); - assert!(multisig.is_initialized); - - let mut src: [u8; 355] = [0; 355]; - src[2] = 2; - let multisig = Multisig::unpack_from_slice(&src).unwrap_err(); - assert_eq!(multisig, ProgramError::InvalidAccountData); - } - - #[test] - fn test_unpack_coption_key() { - let src: [u8; 36] = [0; 36]; - let result = unpack_coption_key(&src).unwrap(); - assert_eq!(result, COption::None); - - let mut src: [u8; 36] = [0; 36]; - src[1] = 1; - let result = unpack_coption_key(&src).unwrap_err(); - assert_eq!(result, ProgramError::InvalidAccountData); - } - - #[test] - fn test_unpack_coption_u64() { - let src: [u8; 12] = [0; 12]; - let result = unpack_coption_u64(&src).unwrap(); - assert_eq!(result, COption::None); - - let mut src: [u8; 12] = [0; 12]; - src[0] = 1; - let result = unpack_coption_u64(&src).unwrap(); - assert_eq!(result, COption::Some(0)); - - let mut src: [u8; 12] = [0; 12]; - src[1] = 1; - let result = unpack_coption_u64(&src).unwrap_err(); - assert_eq!(result, ProgramError::InvalidAccountData); - } - - #[test] - fn test_unpack_token_owner() { - // Account data length < Account::LEN, unpack will not return a key - let src: [u8; 12] = [0; 12]; - let result = Account::unpack_account_owner(&src); - assert_eq!(result, Option::None); - - // The right account data size and initialized, unpack will return some key - let mut src: [u8; Account::LEN] = [0; Account::LEN]; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - let result = Account::unpack_account_owner(&src); - assert!(result.is_some()); - - // The right account data size and frozen, unpack will return some key - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8; - let result = Account::unpack_account_owner(&src); - assert!(result.is_some()); - - // The right account data size and uninitialized, unpack will return None - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8; - let result = Account::unpack_account_mint(&src); - assert_eq!(result, Option::None); - - // Account data length > account data size, unpack will not return a key - let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5]; - let result = Account::unpack_account_owner(&src); - assert_eq!(result, Option::None); - } - - #[test] - fn test_unpack_token_mint() { - // Account data length < Account::LEN, unpack will not return a key - let src: [u8; 12] = [0; 12]; - let result = Account::unpack_account_mint(&src); - assert_eq!(result, Option::None); - - // The right account data size and initialized, unpack will return some key - let mut src: [u8; Account::LEN] = [0; Account::LEN]; - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8; - let result = Account::unpack_account_mint(&src); - assert!(result.is_some()); - - // The right account data size and frozen, unpack will return some key - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8; - let result = Account::unpack_account_mint(&src); - assert!(result.is_some()); - - // The right account data size and uninitialized, unpack will return None - src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8; - let result = Account::unpack_account_mint(&src); - assert_eq!(result, Option::None); - - // Account data length > account data size, unpack will not return a key - let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5]; - let result = Account::unpack_account_mint(&src); - assert_eq!(result, Option::None); - } -} diff --git a/token/program/tests/action.rs b/token/program/tests/action.rs deleted file mode 100644 index 0a67538b0ff..00000000000 --- a/token/program/tests/action.rs +++ /dev/null @@ -1,140 +0,0 @@ -use { - solana_program_test::BanksClient, - solana_sdk::{ - hash::Hash, - program_pack::Pack, - pubkey::Pubkey, - signature::{Keypair, Signer}, - system_instruction, - transaction::Transaction, - transport::TransportError, - }, - spl_token::{ - id, instruction, - state::{Account, Mint}, - }, -}; - -pub async fn create_mint( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - pool_mint: &Keypair, - manager: &Pubkey, - decimals: u8, -) -> Result<(), TransportError> { - let rent = banks_client.get_rent().await.unwrap(); - let mint_rent = rent.minimum_balance(Mint::LEN); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &pool_mint.pubkey(), - mint_rent, - Mint::LEN as u64, - &id(), - ), - instruction::initialize_mint(&id(), &pool_mint.pubkey(), manager, None, decimals) - .unwrap(), - ], - Some(&payer.pubkey()), - &[payer, pool_mint], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} - -pub async fn create_account( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - account: &Keypair, - pool_mint: &Pubkey, - owner: &Pubkey, -) -> Result<(), TransportError> { - let rent = banks_client.get_rent().await.unwrap(); - let account_rent = rent.minimum_balance(Account::LEN); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &payer.pubkey(), - &account.pubkey(), - account_rent, - Account::LEN as u64, - &id(), - ), - instruction::initialize_account(&id(), &account.pubkey(), pool_mint, owner).unwrap(), - ], - Some(&payer.pubkey()), - &[payer, account], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} - -pub async fn mint_to( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - mint: &Pubkey, - account: &Pubkey, - mint_authority: &Keypair, - amount: u64, -) -> Result<(), TransportError> { - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::mint_to(&id(), mint, account, &mint_authority.pubkey(), &[], amount) - .unwrap(), - ], - Some(&payer.pubkey()), - &[payer, mint_authority], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} - -pub async fn transfer( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - source: &Pubkey, - destination: &Pubkey, - authority: &Keypair, - amount: u64, -) -> Result<(), TransportError> { - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::transfer(&id(), source, destination, &authority.pubkey(), &[], amount) - .unwrap(), - ], - Some(&payer.pubkey()), - &[payer, authority], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} - -pub async fn burn( - banks_client: &mut BanksClient, - payer: &Keypair, - recent_blockhash: Hash, - mint: &Pubkey, - account: &Pubkey, - authority: &Keypair, - amount: u64, -) -> Result<(), TransportError> { - let transaction = Transaction::new_signed_with_payer( - &[instruction::burn(&id(), account, mint, &authority.pubkey(), &[], amount).unwrap()], - Some(&payer.pubkey()), - &[payer, authority], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await?; - Ok(()) -} diff --git a/token/program/tests/assert_instruction_count.rs b/token/program/tests/assert_instruction_count.rs deleted file mode 100644 index 009fb1edb28..00000000000 --- a/token/program/tests/assert_instruction_count.rs +++ /dev/null @@ -1,332 +0,0 @@ -#![cfg(feature = "test-sbf")] - -mod action; -use { - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - program_pack::Pack, - pubkey::Pubkey, - signature::{Keypair, Signer}, - system_instruction, - transaction::Transaction, - }, - spl_token::{ - id, instruction, - processor::Processor, - state::{Account, Mint}, - }, -}; - -const TRANSFER_AMOUNT: u64 = 1_000_000_000_000_000; - -#[tokio::test] -async fn initialize_mint() { - let mut pt = ProgramTest::new("spl_token", id(), processor!(Processor::process)); - pt.set_compute_max_units(5_000); // last known 2252 - let (banks_client, payer, recent_blockhash) = pt.start().await; - - let owner_key = Pubkey::new_unique(); - let mint = Keypair::new(); - let decimals = 9; - - let rent = banks_client.get_rent().await.unwrap(); - let mint_rent = rent.minimum_balance(Mint::LEN); - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::create_account( - &payer.pubkey(), - &mint.pubkey(), - mint_rent, - Mint::LEN as u64, - &id(), - )], - Some(&payer.pubkey()), - &[&payer, &mint], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[ - instruction::initialize_mint(&id(), &mint.pubkey(), &owner_key, None, decimals) - .unwrap(), - ], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -#[tokio::test] -async fn initialize_account() { - let mut pt = ProgramTest::new("spl_token", id(), processor!(Processor::process)); - pt.set_compute_max_units(6_000); // last known 3284 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let account = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - let rent = banks_client.get_rent().await.unwrap(); - let account_rent = rent.minimum_balance(Account::LEN); - let transaction = Transaction::new_signed_with_payer( - &[system_instruction::create_account( - &payer.pubkey(), - &account.pubkey(), - account_rent, - Account::LEN as u64, - &id(), - )], - Some(&payer.pubkey()), - &[&payer, &account], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::initialize_account( - &id(), - &account.pubkey(), - &mint.pubkey(), - &owner.pubkey(), - ) - .unwrap()], - Some(&payer.pubkey()), - &[&payer], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -#[tokio::test] -async fn mint_to() { - let mut pt = ProgramTest::new("spl_token", id(), processor!(Processor::process)); - pt.set_compute_max_units(6_000); // last known 2668 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let account = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &account, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::mint_to( - &id(), - &mint.pubkey(), - &account.pubkey(), - &owner.pubkey(), - &[], - TRANSFER_AMOUNT, - ) - .unwrap()], - Some(&payer.pubkey()), - &[&payer, &owner], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} - -#[tokio::test] -async fn transfer() { - let mut pt = ProgramTest::new("spl_token", id(), processor!(Processor::process)); - pt.set_compute_max_units(7_000); // last known 2972 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let source = Keypair::new(); - let destination = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &source, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &destination, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - - action::mint_to( - &mut banks_client, - &payer, - recent_blockhash, - &mint.pubkey(), - &source.pubkey(), - &owner, - TRANSFER_AMOUNT, - ) - .await - .unwrap(); - - action::transfer( - &mut banks_client, - &payer, - recent_blockhash, - &source.pubkey(), - &destination.pubkey(), - &owner, - TRANSFER_AMOUNT, - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn burn() { - let mut pt = ProgramTest::new("spl_token", id(), processor!(Processor::process)); - pt.set_compute_max_units(6_000); // last known 2655 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let account = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &account, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - - action::mint_to( - &mut banks_client, - &payer, - recent_blockhash, - &mint.pubkey(), - &account.pubkey(), - &owner, - TRANSFER_AMOUNT, - ) - .await - .unwrap(); - - action::burn( - &mut banks_client, - &payer, - recent_blockhash, - &mint.pubkey(), - &account.pubkey(), - &owner, - TRANSFER_AMOUNT, - ) - .await - .unwrap(); -} - -#[tokio::test] -async fn close_account() { - let mut pt = ProgramTest::new("spl_token", id(), processor!(Processor::process)); - pt.set_compute_max_units(6_000); // last known 1783 - let (mut banks_client, payer, recent_blockhash) = pt.start().await; - - let owner = Keypair::new(); - let mint = Keypair::new(); - let account = Keypair::new(); - let decimals = 9; - - action::create_mint( - &mut banks_client, - &payer, - recent_blockhash, - &mint, - &owner.pubkey(), - decimals, - ) - .await - .unwrap(); - action::create_account( - &mut banks_client, - &payer, - recent_blockhash, - &account, - &mint.pubkey(), - &owner.pubkey(), - ) - .await - .unwrap(); - - let transaction = Transaction::new_signed_with_payer( - &[instruction::close_account( - &id(), - &account.pubkey(), - &owner.pubkey(), - &owner.pubkey(), - &[], - ) - .unwrap()], - Some(&payer.pubkey()), - &[&payer, &owner], - recent_blockhash, - ); - banks_client.process_transaction(transaction).await.unwrap(); -} diff --git a/token/program/tests/close_account.rs b/token/program/tests/close_account.rs deleted file mode 100644 index 8425fe206bf..00000000000 --- a/token/program/tests/close_account.rs +++ /dev/null @@ -1,200 +0,0 @@ -#![cfg(feature = "test-sbf")] - -use { - solana_program_test::{processor, tokio, ProgramTest, ProgramTestContext}, - solana_sdk::{ - instruction::InstructionError, - program_pack::Pack, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - system_instruction, - transaction::{Transaction, TransactionError}, - }, - spl_token::{ - instruction, - processor::Processor, - state::{Account, Mint}, - }, -}; - -async fn setup_mint_and_account( - context: &mut ProgramTestContext, - mint: &Keypair, - token_account: &Keypair, - owner: &Pubkey, - token_program_id: &Pubkey, -) { - let rent = context.banks_client.get_rent().await.unwrap(); - let mint_authority_pubkey = Pubkey::new_unique(); - - let space = Mint::LEN; - let tx = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &mint.pubkey(), - rent.minimum_balance(space), - space as u64, - token_program_id, - ), - instruction::initialize_mint( - token_program_id, - &mint.pubkey(), - &mint_authority_pubkey, - None, - 9, - ) - .unwrap(), - ], - Some(&context.payer.pubkey()), - &[&context.payer, mint], - context.last_blockhash, - ); - context.banks_client.process_transaction(tx).await.unwrap(); - let space = Account::LEN; - let tx = Transaction::new_signed_with_payer( - &[ - system_instruction::create_account( - &context.payer.pubkey(), - &token_account.pubkey(), - rent.minimum_balance(space), - space as u64, - token_program_id, - ), - instruction::initialize_account( - token_program_id, - &token_account.pubkey(), - &mint.pubkey(), - owner, - ) - .unwrap(), - ], - Some(&context.payer.pubkey()), - &[&context.payer, token_account], - context.last_blockhash, - ); - context.banks_client.process_transaction(tx).await.unwrap(); -} - -#[tokio::test] -async fn success_init_after_close_account() { - let program_test = - ProgramTest::new("spl_token", spl_token::id(), processor!(Processor::process)); - let mut context = program_test.start_with_context().await; - let mint = Keypair::new(); - let token_account = Keypair::new(); - let owner = Keypair::new(); - let token_program_id = spl_token::id(); - setup_mint_and_account( - &mut context, - &mint, - &token_account, - &owner.pubkey(), - &token_program_id, - ) - .await; - - let destination = Pubkey::new_unique(); - let tx = Transaction::new_signed_with_payer( - &[ - instruction::close_account( - &token_program_id, - &token_account.pubkey(), - &destination, - &owner.pubkey(), - &[], - ) - .unwrap(), - system_instruction::create_account( - &context.payer.pubkey(), - &token_account.pubkey(), - 1_000_000_000, - Account::LEN as u64, - &token_program_id, - ), - instruction::initialize_account( - &token_program_id, - &token_account.pubkey(), - &mint.pubkey(), - &owner.pubkey(), - ) - .unwrap(), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &owner, &token_account], - context.last_blockhash, - ); - context.banks_client.process_transaction(tx).await.unwrap(); - let destination = context - .banks_client - .get_account(destination) - .await - .unwrap() - .unwrap(); - assert!(destination.lamports > 0); -} - -#[tokio::test] -async fn fail_init_after_close_account() { - let program_test = - ProgramTest::new("spl_token", spl_token::id(), processor!(Processor::process)); - let mut context = program_test.start_with_context().await; - let mint = Keypair::new(); - let token_account = Keypair::new(); - let owner = Keypair::new(); - let token_program_id = spl_token::id(); - setup_mint_and_account( - &mut context, - &mint, - &token_account, - &owner.pubkey(), - &token_program_id, - ) - .await; - - let destination = Pubkey::new_unique(); - let tx = Transaction::new_signed_with_payer( - &[ - instruction::close_account( - &token_program_id, - &token_account.pubkey(), - &destination, - &owner.pubkey(), - &[], - ) - .unwrap(), - system_instruction::transfer( - &context.payer.pubkey(), - &token_account.pubkey(), - 1_000_000_000, - ), - instruction::initialize_account( - &token_program_id, - &token_account.pubkey(), - &mint.pubkey(), - &owner.pubkey(), - ) - .unwrap(), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &owner], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(tx) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(2, InstructionError::InvalidAccountData) - ); - assert!(context - .banks_client - .get_account(destination) - .await - .unwrap() - .is_none()); -} diff --git a/token/transfer-hook/cli/Cargo.toml b/token/transfer-hook/cli/Cargo.toml deleted file mode 100644 index a3dffbbbae6..00000000000 --- a/token/transfer-hook/cli/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -authors = ["Solana Labs Maintainers "] -description = "SPL Transfer Hook Command-line Utility" -edition = "2021" -homepage = "https://spl.solana.com/token" -license = "Apache-2.0" -name = "spl-transfer-hook-cli" -repository = "https://github.com/solana-labs/solana-program-library" -version = "0.2.0" - -[dependencies] -clap = { version = "3", features = ["cargo"] } -futures-util = "0.3.31" -solana-clap-v3-utils = "2.1.0" -solana-cli-config = "2.1.0" -solana-client = "2.1.0" -solana-logger = "2.1.0" -solana-remote-wallet = "2.1.0" -solana-sdk = "2.1.0" -spl-tlv-account-resolution = { version = "0.9.0", path = "../../../libraries/tlv-account-resolution", features = ["serde-traits"] } -spl-transfer-hook-interface = { version = "0.9.0", path = "../interface" } -strum = "0.26" -strum_macros = "0.26" -tokio = { version = "1", features = ["full"] } -serde = { version = "1.0.217", features = ["derive"] } -serde_json = "1.0.135" -serde_yaml = "0.9.34" - -[dev-dependencies] -solana-test-validator = "2.1.0" -spl-token-2022 = { version = "6.0.0", path = "../../program-2022", features = ["no-entrypoint"] } -spl-token-client = { version = "0.13.0", path = "../../client" } -spl-transfer-hook-example = { version = "0.6.0", path = "../example" } - -[[bin]] -name = "spl-transfer-hook" -path = "src/main.rs" diff --git a/token/transfer-hook/cli/src/main.rs b/token/transfer-hook/cli/src/main.rs deleted file mode 100644 index bf7273b1766..00000000000 --- a/token/transfer-hook/cli/src/main.rs +++ /dev/null @@ -1,636 +0,0 @@ -pub mod meta; - -use { - crate::meta::parse_transfer_hook_account_arg, - clap::{crate_description, crate_name, crate_version, Arg, ArgAction, Command}, - solana_clap_v3_utils::{ - input_parsers::{ - parse_url_or_moniker, - signer::{SignerSource, SignerSourceParserBuilder}, - }, - input_validators::normalize_to_url_if_moniker, - keypair::signer_from_path, - }, - solana_client::nonblocking::rpc_client::RpcClient, - solana_remote_wallet::remote_wallet::RemoteWalletManager, - solana_sdk::{ - commitment_config::CommitmentConfig, - instruction::Instruction, - pubkey::Pubkey, - signature::{Signature, Signer}, - system_instruction, system_program, - transaction::Transaction, - }, - spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList}, - spl_transfer_hook_interface::{ - get_extra_account_metas_address, - instruction::{initialize_extra_account_meta_list, update_extra_account_meta_list}, - }, - std::{process::exit, rc::Rc}, -}; - -// Helper function to calculate the required lamports for rent -async fn calculate_rent_lamports( - rpc_client: &RpcClient, - account_address: &Pubkey, - account_size: usize, -) -> Result> { - let required_lamports = rpc_client - .get_minimum_balance_for_rent_exemption(account_size) - .await - .map_err(|err| format!("error: unable to fetch rent-exemption: {err}"))?; - let account_info = rpc_client.get_account(account_address).await; - let current_lamports = account_info.map(|a| a.lamports).unwrap_or(0); - Ok(required_lamports.saturating_sub(current_lamports)) -} - -async fn build_transaction_with_rent_transfer( - rpc_client: &RpcClient, - payer: &dyn Signer, - extra_account_metas_address: &Pubkey, - extra_account_metas: &[ExtraAccountMeta], - instruction: Instruction, -) -> Result> { - let account_size = ExtraAccountMetaList::size_of(extra_account_metas.len())?; - let transfer_lamports = - calculate_rent_lamports(rpc_client, extra_account_metas_address, account_size).await?; - - let mut instructions = vec![]; - if transfer_lamports > 0 { - instructions.push(system_instruction::transfer( - &payer.pubkey(), - extra_account_metas_address, - transfer_lamports, - )); - } - - instructions.push(instruction); - - let transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey())); - - Ok(transaction) -} - -async fn sign_and_send_transaction( - transaction: &mut Transaction, - rpc_client: &RpcClient, - payer: &dyn Signer, - mint_authority: &dyn Signer, -) -> Result> { - let mut signers = vec![payer]; - if payer.pubkey() != mint_authority.pubkey() { - signers.push(mint_authority); - } - - let blockhash = rpc_client - .get_latest_blockhash() - .await - .map_err(|err| format!("error: unable to get latest blockhash: {err}"))?; - - transaction - .try_sign(&signers, blockhash) - .map_err(|err| format!("error: failed to sign transaction: {err}"))?; - - rpc_client - .send_and_confirm_transaction_with_spinner(transaction) - .await - .map_err(|err| format!("error: send transaction: {err}").into()) -} - -struct Config { - commitment_config: CommitmentConfig, - default_signer: Box, - json_rpc_url: String, - verbose: bool, -} - -async fn process_create_extra_account_metas( - rpc_client: &RpcClient, - program_id: &Pubkey, - token: &Pubkey, - extra_account_metas: Vec, - mint_authority: &dyn Signer, - payer: &dyn Signer, -) -> Result> { - let extra_account_metas_address = get_extra_account_metas_address(token, program_id); - - // Check if the extra meta account has already been initialized - let extra_account_metas_account = rpc_client.get_account(&extra_account_metas_address).await; - if let Ok(account) = &extra_account_metas_account { - if account.owner != system_program::id() { - return Err(format!("error: extra account metas for mint {token} and program {program_id} already exists").into()); - } - } - - let instruction = initialize_extra_account_meta_list( - program_id, - &extra_account_metas_address, - token, - &mint_authority.pubkey(), - &extra_account_metas, - ); - - let mut transaction = build_transaction_with_rent_transfer( - rpc_client, - payer, - &extra_account_metas_address, - &extra_account_metas, - instruction, - ) - .await?; - - sign_and_send_transaction(&mut transaction, rpc_client, payer, mint_authority).await -} - -async fn process_update_extra_account_metas( - rpc_client: &RpcClient, - program_id: &Pubkey, - token: &Pubkey, - extra_account_metas: Vec, - mint_authority: &dyn Signer, - payer: &dyn Signer, -) -> Result> { - let extra_account_metas_address = get_extra_account_metas_address(token, program_id); - - // Check if the extra meta account has been initialized first - let extra_account_metas_account = rpc_client.get_account(&extra_account_metas_address).await; - if extra_account_metas_account.is_err() { - return Err(format!( - "error: extra account metas for mint {token} and program {program_id} does not exist" - ) - .into()); - } - - let instruction = update_extra_account_meta_list( - program_id, - &extra_account_metas_address, - token, - &mint_authority.pubkey(), - &extra_account_metas, - ); - - let mut transaction = build_transaction_with_rent_transfer( - rpc_client, - payer, - &extra_account_metas_address, - &extra_account_metas, - instruction, - ) - .await?; - - sign_and_send_transaction(&mut transaction, rpc_client, payer, mint_authority).await -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - let app_matches = Command::new(crate_name!()) - .about(crate_description!()) - .version(crate_version!()) - .subcommand_required(true) - .arg_required_else_help(true) - .arg({ - let arg = Arg::new("config_file") - .short('C') - .long("config") - .value_name("PATH") - .takes_value(true) - .global(true) - .help("Configuration file to use"); - if let Some(ref config_file) = *solana_cli_config::CONFIG_FILE { - arg.default_value(config_file) - } else { - arg - } - }) - .arg( - Arg::new("fee_payer") - .long("fee-payer") - .value_name("KEYPAIR") - .value_parser(SignerSourceParserBuilder::default().allow_all().build()) - .takes_value(true) - .global(true) - .help("Filepath or URL to a keypair to pay transaction fee [default: client keypair]"), - ) - .arg( - Arg::new("verbose") - .long("verbose") - .short('v') - .takes_value(false) - .global(true) - .help("Show additional information"), - ) - .arg( - Arg::new("json_rpc_url") - .short('u') - .long("url") - .value_name("URL") - .takes_value(true) - .global(true) - .value_parser(parse_url_or_moniker) - .help("JSON RPC URL for the cluster [default: value from configuration file]"), - ) - .subcommand( - Command::new("create-extra-metas") - .about("Create the extra account metas account for a transfer hook program") - .arg( - Arg::new("program_id") - .value_parser(SignerSourceParserBuilder::default().allow_pubkey().allow_file_path().build()) - .value_name("TRANSFER_HOOK_PROGRAM") - .takes_value(true) - .index(1) - .required(true) - .help("The transfer hook program id"), - ) - .arg( - Arg::new("token") - .value_parser(SignerSourceParserBuilder::default().allow_pubkey().allow_file_path().build()) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(2) - .required(true) - .help("The token mint address for the transfer hook"), - ) - .arg( - Arg::new("transfer_hook_accounts") - .value_parser(parse_transfer_hook_account_arg) - .value_name("TRANSFER_HOOK_ACCOUNTS") - .takes_value(true) - .action(ArgAction::Append) - .min_values(0) - .index(3) - .help(r#"Additional account(s) required for a transfer hook and their respective configurations, whether they are a fixed address or PDA. - -Additional accounts with known fixed addresses can be passed at the command line in the format ":". The role must be "readonly", "writable". "readonlySigner", or "writableSigner". - -Additional accounts requiring seed configurations can be defined in a configuration file using either JSON or YAML. The format is as follows: - -```json -{ - "extraMetas": [ - { - "pubkey": "39UhV...", - "role": "readonlySigner" - }, - { - "seeds": [ - { - "literal": { - "bytes": [1, 2, 3, 4, 5, 6] - } - }, - { - "accountKey": { - "index": 0 - } - } - ], - "role": "writable" - } - ] -} -``` - -```yaml -extraMetas: - - pubkey: "39UhV..." - role: "readonlySigner" - - seeds: - - literal: - bytes: [1, 2, 3, 4, 5, 6] - - accountKey: - index: 0 - role: "writable" -``` -"#) - ) - .arg( - Arg::new("mint_authority") - .long("mint-authority") - .value_name("KEYPAIR") - .value_parser(SignerSourceParserBuilder::default().allow_all().build()) - .takes_value(true) - .global(true) - .help("Filepath or URL to mint-authority keypair [default: client keypair]"), - ) - ) - .subcommand( - Command::new("update-extra-metas") - .about("Update the extra account metas account for a transfer hook program") - .arg( - Arg::new("program_id") - .value_parser(SignerSourceParserBuilder::default().allow_pubkey().allow_file_path().build()) - .value_name("TRANSFER_HOOK_PROGRAM") - .takes_value(true) - .index(1) - .required(true) - .help("The transfer hook program id"), - ) - .arg( - Arg::new("token") - .value_parser(SignerSourceParserBuilder::default().allow_pubkey().allow_file_path().build()) - .value_name("TOKEN_MINT_ADDRESS") - .takes_value(true) - .index(2) - .required(true) - .help("The token mint address for the transfer hook"), - ) - .arg( - Arg::new("transfer_hook_accounts") - .value_parser(parse_transfer_hook_account_arg) - .value_name("TRANSFER_HOOK_ACCOUNTS") - .takes_value(true) - .action(ArgAction::Append) - .min_values(0) - .index(3) - .help(r#"Additional account(s) required for a transfer hook and their respective configurations, whether they are a fixed address or PDA. - -Additional accounts with known fixed addresses can be passed at the command line in the format ":". The role must be "readonly", "writable". "readonlySigner", or "writableSigner". - -Additional accounts requiring seed configurations can be defined in a configuration file using either JSON or YAML. The format is as follows: - -```json -{ - "extraMetas": [ - { - "pubkey": "39UhV...", - "role": "readonlySigner" - }, - { - "seeds": [ - { - "literal": { - "bytes": [1, 2, 3, 4, 5, 6] - } - }, - { - "accountKey": { - "index": 0 - } - } - ], - "role": "writable" - } - ] -} -``` - -```yaml -extraMetas: - - pubkey: "39UhV..." - role: "readonlySigner" - - seeds: - - literal: - bytes: [1, 2, 3, 4, 5, 6] - - accountKey: - index: 0 - role: "writable" -``` -"#) - ) - .arg( - Arg::new("mint_authority") - .long("mint-authority") - .value_name("KEYPAIR") - .value_parser(SignerSourceParserBuilder::default().allow_all().build()) - .takes_value(true) - .global(true) - .help("Filepath or URL to mint-authority keypair [default: client keypair]"), - ) - ).get_matches(); - - let (command, matches) = app_matches.subcommand().unwrap(); - let mut wallet_manager: Option> = None; - - let cli_config = if let Some(config_file) = matches.get_one::("config_file") { - solana_cli_config::Config::load(config_file).unwrap_or_default() - } else { - solana_cli_config::Config::default() - }; - - let config = { - let default_signer = if let Some((signer, _)) = - SignerSource::try_get_signer(matches, "fee_payer", &mut wallet_manager)? - { - signer - } else { - signer_from_path( - matches, - &cli_config.keypair_path, - "fee_payer", - &mut wallet_manager, - )? - }; - - let json_rpc_url = normalize_to_url_if_moniker( - matches - .get_one::("json_rpc_url") - .unwrap_or(&cli_config.json_rpc_url), - ); - - Config { - commitment_config: CommitmentConfig::confirmed(), - default_signer, - json_rpc_url, - verbose: matches.try_contains_id("verbose")?, - } - }; - solana_logger::setup_with_default("solana=info"); - - if config.verbose { - println!("JSON RPC URL: {}", config.json_rpc_url); - } - let rpc_client = - RpcClient::new_with_commitment(config.json_rpc_url.clone(), config.commitment_config); - - match (command, matches) { - ("create-extra-metas", arg_matches) => { - let program_id = - SignerSource::try_get_pubkey(arg_matches, "program_id", &mut wallet_manager)? - .unwrap(); - let token = - SignerSource::try_get_pubkey(arg_matches, "token", &mut wallet_manager)?.unwrap(); - - let transfer_hook_accounts = arg_matches - .get_many::>("transfer_hook_accounts") - .unwrap_or_default() - .flatten() - .cloned() - .collect(); - let mint_authority = if let Some((signer, _)) = - SignerSource::try_get_signer(matches, "mint_authority", &mut wallet_manager)? - { - signer - } else { - signer_from_path( - matches, - &cli_config.keypair_path, - "mint_authority", - &mut wallet_manager, - )? - }; - let signature = process_create_extra_account_metas( - &rpc_client, - &program_id, - &token, - transfer_hook_accounts, - mint_authority.as_ref(), - config.default_signer.as_ref(), - ) - .await - .unwrap_or_else(|err| { - eprintln!("error: send transaction: {err}"); - exit(1); - }); - println!("Signature: {signature}"); - } - ("update-extra-metas", arg_matches) => { - let program_id = - SignerSource::try_get_pubkey(arg_matches, "program_id", &mut wallet_manager)? - .unwrap(); - let token = - SignerSource::try_get_pubkey(arg_matches, "token", &mut wallet_manager)?.unwrap(); - - let transfer_hook_accounts = arg_matches - .get_many::>("transfer_hook_accounts") - .unwrap_or_default() - .flatten() - .cloned() - .collect(); - let mint_authority = if let Some((signer, _)) = - SignerSource::try_get_signer(matches, "mint_authority", &mut wallet_manager)? - { - signer - } else { - signer_from_path( - matches, - &cli_config.keypair_path, - "mint_authority", - &mut wallet_manager, - )? - }; - let signature = process_update_extra_account_metas( - &rpc_client, - &program_id, - &token, - transfer_hook_accounts, - mint_authority.as_ref(), - config.default_signer.as_ref(), - ) - .await - .unwrap_or_else(|err| { - eprintln!("error: send transaction: {err}"); - exit(1); - }); - println!("Signature: {signature}"); - } - _ => unreachable!(), - }; - - Ok(()) -} - -#[cfg(test)] -mod test { - use { - super::*, - solana_sdk::{ - account::Account, bpf_loader_upgradeable, instruction::AccountMeta, - program_option::COption, signer::keypair::Keypair, - }, - solana_test_validator::{TestValidator, TestValidatorGenesis, UpgradeableProgramInfo}, - spl_token_2022::{ - extension::{ExtensionType, StateWithExtensionsMut}, - state::Mint, - }, - spl_token_client::{ - client::{ProgramRpcClient, ProgramRpcClientSendTransaction}, - token::Token, - }, - std::{path::PathBuf, sync::Arc}, - }; - - async fn new_validator_for_test( - program_id: Pubkey, - mint_authority: &Pubkey, - decimals: u8, - ) -> (TestValidator, Keypair) { - solana_logger::setup(); - let mut test_validator_genesis = TestValidatorGenesis::default(); - test_validator_genesis.add_upgradeable_programs_with_path(&[UpgradeableProgramInfo { - program_id, - loader: bpf_loader_upgradeable::id(), - program_path: PathBuf::from("../../../target/deploy/spl_transfer_hook_example.so"), - upgrade_authority: Pubkey::new_unique(), - }]); - - let mint_size = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let mut mint_data = vec![0; mint_size]; - let mut state = - StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data).unwrap(); - let token_amount = 1_000_000_000_000; - state.base = Mint { - mint_authority: COption::Some(*mint_authority), - supply: token_amount, - decimals, - is_initialized: true, - freeze_authority: COption::None, - }; - state.pack_base(); - test_validator_genesis.add_account( - spl_transfer_hook_example::mint::id(), - Account { - lamports: 1_000_000_000, - data: mint_data, - owner: spl_token_2022::id(), - ..Account::default() - } - .into(), - ); - test_validator_genesis.start_async().await - } - - #[tokio::test] - async fn test_create() { - let program_id = Pubkey::new_unique(); - - let decimals = 2; - let mint_authority = Keypair::new(); - let (test_validator, payer) = - new_validator_for_test(program_id, &mint_authority.pubkey(), decimals).await; - let payer: Arc = Arc::new(payer); - let rpc_client = Arc::new(test_validator.get_async_rpc_client()); - let client = Arc::new(ProgramRpcClient::new( - rpc_client.clone(), - ProgramRpcClientSendTransaction, - )); - - let token = Token::new( - client.clone(), - &spl_token_2022::id(), - &spl_transfer_hook_example::mint::id(), - Some(decimals), - payer.clone(), - ); - - let required_address = Pubkey::new_unique(); - let accounts = [AccountMeta::new_readonly(required_address, false)]; - process_create_extra_account_metas( - &rpc_client, - &program_id, - token.get_address(), - accounts.iter().map(|a| a.into()).collect(), - &mint_authority, - payer.as_ref(), - ) - .await - .unwrap(); - - let extra_account_metas_address = - get_extra_account_metas_address(token.get_address(), &program_id); - let account = rpc_client - .get_account(&extra_account_metas_address) - .await - .unwrap(); - assert_eq!(account.owner, program_id); - } -} diff --git a/token/transfer-hook/cli/src/meta.rs b/token/transfer-hook/cli/src/meta.rs deleted file mode 100644 index b72dfa80686..00000000000 --- a/token/transfer-hook/cli/src/meta.rs +++ /dev/null @@ -1,322 +0,0 @@ -use { - serde::{Deserialize, Serialize}, - solana_sdk::pubkey::Pubkey, - spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}, - std::{path::Path, str::FromStr}, - strum_macros::{EnumString, IntoStaticStr}, -}; - -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Access { - is_signer: bool, - is_writable: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, EnumString, IntoStaticStr, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[strum(serialize_all = "camelCase")] -enum Role { - Readonly, - Writable, - ReadonlySigner, - WritableSigner, -} - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -enum AddressConfig { - Pubkey(String), - Seeds(Vec), -} - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Config { - #[serde(flatten)] - address_config: AddressConfig, - role: Role, -} - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ConfigFile { - extra_metas: Vec, -} - -impl From<&Role> for Access { - fn from(role: &Role) -> Self { - match role { - Role::Readonly => Access { - is_signer: false, - is_writable: false, - }, - Role::Writable => Access { - is_signer: false, - is_writable: true, - }, - Role::ReadonlySigner => Access { - is_signer: true, - is_writable: false, - }, - Role::WritableSigner => Access { - is_signer: true, - is_writable: true, - }, - } - } -} - -impl From<&Config> for ExtraAccountMeta { - fn from(config: &Config) -> Self { - let Access { - is_signer, - is_writable, - } = Access::from(&config.role); - match &config.address_config { - AddressConfig::Pubkey(pubkey_string) => ExtraAccountMeta::new_with_pubkey( - &Pubkey::from_str(pubkey_string).unwrap(), - is_signer, - is_writable, - ) - .unwrap(), - AddressConfig::Seeds(seeds) => { - ExtraAccountMeta::new_with_seeds(seeds, is_signer, is_writable).unwrap() - } - } - } -} - -type ParseFn = fn(&str) -> Result; - -fn get_parse_function(path: &Path) -> Result { - match path.extension().and_then(|s| s.to_str()) { - Some("json") => Ok(|v: &str| { - serde_json::from_str::(v).map_err(|e| format!("Unable to parse file: {e}")) - }), - Some("yaml") | Some("yml") => Ok(|v: &str| { - serde_yaml::from_str::(v).map_err(|e| format!("Unable to parse file: {e}")) - }), - _ => Err(format!( - "Unsupported file extension: {}. Only JSON and YAML files are supported", - path.display() - )), - } -} - -fn parse_config_file_arg(path_str: &str) -> Result, String> { - let path = Path::new(path_str); - let parse_fn = get_parse_function(path)?; - let file = - std::fs::read_to_string(path).map_err(|err| format!("Unable to read file: {err}"))?; - let parsed_config_file = parse_fn(&file)?; - Ok(parsed_config_file - .extra_metas - .iter() - .map(ExtraAccountMeta::from) - .collect()) -} - -fn parse_pubkey_role_arg(pubkey_string: &str, role: &str) -> Result, String> { - let pubkey = Pubkey::from_str(pubkey_string).map_err(|e| format!("{e}"))?; - let role = &Role::from_str(role).map_err(|e| format!("{e}"))?; - let Access { - is_signer, - is_writable, - } = role.into(); - ExtraAccountMeta::new_with_pubkey(&pubkey, is_signer, is_writable) - .map(|meta| vec![meta]) - .map_err(|e| format!("{e}")) -} - -pub fn parse_transfer_hook_account_arg(arg: &str) -> Result, String> { - match arg.split(':').collect::>().as_slice() { - [pubkey_str, role] => parse_pubkey_role_arg(pubkey_str, role), - _ => parse_config_file_arg(arg), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_json() { - let config = r#"{ - "extraMetas": [ - { - "pubkey": "39UhVsxAmJwzPnoWhBSHsZ6nBDtdzt9D8rfDa8zGHrP6", - "role": "readonlySigner" - }, - { - "pubkey": "6WEvW9B9jTKc3EhP1ewGEJPrxw5d8vD9eMYCf2snNYsV", - "role": "readonly" - }, - { - "seeds": [ - { - "literal": { - "bytes": [1, 2, 3, 4, 5, 6] - } - }, - { - "instructionData": { - "index": 0, - "length": 8 - } - }, - { - "accountKey": { - "index": 0 - } - } - ], - "role": "writable" - }, - { - "seeds": [ - { - "accountData": { - "accountIndex": 1, - "dataIndex": 4, - "length": 4 - } - }, - { - "accountKey": { - "index": 1 - } - } - ], - "role": "readonly" - } - ] - }"#; - let parsed_config_file = serde_json::from_str::(config).unwrap(); - let parsed_extra_metas: Vec = parsed_config_file - .extra_metas - .iter() - .map(|config| config.into()) - .collect::>(); - let expected = vec![ - ExtraAccountMeta::new_with_pubkey( - &Pubkey::from_str("39UhVsxAmJwzPnoWhBSHsZ6nBDtdzt9D8rfDa8zGHrP6").unwrap(), - true, - false, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey( - &Pubkey::from_str("6WEvW9B9jTKc3EhP1ewGEJPrxw5d8vD9eMYCf2snNYsV").unwrap(), - false, - false, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: vec![1, 2, 3, 4, 5, 6], - }, - Seed::InstructionData { - index: 0, - length: 8, - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::AccountData { - account_index: 1, - data_index: 4, - length: 4, - }, - Seed::AccountKey { index: 1 }, - ], - false, - false, - ) - .unwrap(), - ]; - assert_eq!(parsed_extra_metas, expected); - } - - #[test] - fn test_parse_yaml() { - let config = r#" - extraMetas: - - pubkey: "39UhVsxAmJwzPnoWhBSHsZ6nBDtdzt9D8rfDa8zGHrP6" - role: "readonlySigner" - - pubkey: "6WEvW9B9jTKc3EhP1ewGEJPrxw5d8vD9eMYCf2snNYsV" - role: "readonly" - - seeds: - - literal: - bytes: [1, 2, 3, 4, 5, 6] - - instructionData: - index: 0 - length: 8 - - accountKey: - index: 0 - role: "writable" - - seeds: - - accountData: - accountIndex: 1 - dataIndex: 4 - length: 4 - - accountKey: - index: 1 - role: "readonly" - "#; - let parsed_config_file = serde_yaml::from_str::(config).unwrap(); - let parsed_extra_metas: Vec = parsed_config_file - .extra_metas - .iter() - .map(|config| config.into()) - .collect::>(); - let expected = vec![ - ExtraAccountMeta::new_with_pubkey( - &Pubkey::from_str("39UhVsxAmJwzPnoWhBSHsZ6nBDtdzt9D8rfDa8zGHrP6").unwrap(), - true, - false, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey( - &Pubkey::from_str("6WEvW9B9jTKc3EhP1ewGEJPrxw5d8vD9eMYCf2snNYsV").unwrap(), - false, - false, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: vec![1, 2, 3, 4, 5, 6], - }, - Seed::InstructionData { - index: 0, - length: 8, - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::AccountData { - account_index: 1, - data_index: 4, - length: 4, - }, - Seed::AccountKey { index: 1 }, - ], - false, - false, - ) - .unwrap(), - ]; - assert_eq!(parsed_extra_metas, expected); - } -} diff --git a/token/transfer-hook/example/Cargo.toml b/token/transfer-hook/example/Cargo.toml deleted file mode 100644 index ee6d4759e2a..00000000000 --- a/token/transfer-hook/example/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "spl-transfer-hook-example" -version = "0.6.0" -description = "Solana Program Library Transfer Hook Example Program" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[features] -default = ["forbid-additional-mints"] -no-entrypoint = [] -test-sbf = [] -forbid-additional-mints = [] - -[dependencies] -arrayref = "0.3.9" -solana-program = "2.1.0" -spl-tlv-account-resolution = { version = "0.9.0", path = "../../../libraries/tlv-account-resolution" } -spl-token-2022 = { version = "6.0.0", path = "../../program-2022", features = ["no-entrypoint"] } -spl-transfer-hook-interface = { version = "0.9.0", path = "../interface" } -spl-type-length-value = { version = "0.7.0", path = "../../../libraries/type-length-value" } - -[dev-dependencies] -solana-program-test = "2.1.0" -solana-sdk = "2.1.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token/transfer-hook/example/README.md b/token/transfer-hook/example/README.md deleted file mode 100644 index 4f490729e9e..00000000000 --- a/token/transfer-hook/example/README.md +++ /dev/null @@ -1,66 +0,0 @@ -## Transfer-Hook Example - -Full example program and tests implementing the `spl-transfer-hook-interface`, -to be used for testing a program that calls into the `spl-transfer-hook-interface`. - -See the -[SPL Transfer Hook Interface](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/interface) -code for more information. - -### Example usage of example - -When testing your program that uses `spl-transfer-hook-interface`, you can also -import this crate, and then use it with `solana-program-test`: - -```rust -use { - solana_program_test::{processor, ProgramTest}, - solana_sdk::{account::Account, instruction::AccountMeta}, - spl_transfer_hook_example::state::example_data, - spl_transfer_hook_interface::get_extra_account_metas_address, -}; - -#[test] -fn my_program_test() { - let mut program_test = ProgramTest::new( - "my_program", - my_program_id, - processor!(my_program_processor), - ); - - let transfer_hook_program_id = Pubkey::new_unique(); - program_test.prefer_bpf(false); // BPF won't work, unless you've built this from scratch! - program_test.add_program( - "spl_transfer_hook_example", - transfer_hook_program_id, - processor!(spl_transfer_hook_example::processor::process), - ); - - let mint = Pubkey::new_unique(); - let extra_accounts_address = get_extra_account_metas_address(&mint, &transfer_hook_program_id); - let account_metas = vec![ - AccountMeta { - pubkey: Pubkey::new_unique(), - is_signer: false, - is_writable: false, - }, - AccountMeta { - pubkey: Pubkey::new_unique(), - is_signer: false, - is_writable: false, - }, - ]; - let data = example_data(&account_metas); - program_test.add_account( - extra_accounts_address, - Account { - lamports: 1_000_000_000, // a lot, just to be safe - data, - owner: transfer_hook_program_id, - ..Account::default() - }, - ); - - // run your test logic! -} -``` diff --git a/token/transfer-hook/example/src/entrypoint.rs b/token/transfer-hook/example/src/entrypoint.rs deleted file mode 100644 index b2046037275..00000000000 --- a/token/transfer-hook/example/src/entrypoint.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Program entrypoint - -use { - crate::processor, - solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError, - pubkey::Pubkey, - }, - spl_transfer_hook_interface::error::TransferHookError, -}; - -solana_program::entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - if let Err(error) = processor::process(program_id, accounts, instruction_data) { - // catch the error so we can print it - error.print::(); - return Err(error); - } - Ok(()) -} diff --git a/token/transfer-hook/example/src/lib.rs b/token/transfer-hook/example/src/lib.rs deleted file mode 100644 index aa49d80c431..00000000000 --- a/token/transfer-hook/example/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Crate defining an example program for performing a hook on transfer, where -//! the token program calls into a separate program with additional accounts -//! after all other logic, to be sure that a transfer has accomplished all -//! required preconditions. - -#![allow(clippy::arithmetic_side_effects)] -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -pub mod processor; -pub mod state; - -#[cfg(not(feature = "no-entrypoint"))] -mod entrypoint; - -// Export current sdk types for downstream users building with a different sdk -// version -pub use solana_program; - -/// Place the mint id that you want to target with your transfer hook program. -/// Any other mint will fail to initialize, protecting the transfer hook program -/// from rogue mints trying to get access to accounts. -/// -/// There are many situations where it's reasonable to support multiple mints -/// with one transfer-hook program, but because it's easy to make something -/// unsafe, this simple example implementation only allows for one mint. -#[cfg(feature = "forbid-additional-mints")] -pub mod mint { - solana_program::declare_id!("Mint111111111111111111111111111111111111111"); -} diff --git a/token/transfer-hook/example/src/processor.rs b/token/transfer-hook/example/src/processor.rs deleted file mode 100644 index 0a5f4c1144f..00000000000 --- a/token/transfer-hook/example/src/processor.rs +++ /dev/null @@ -1,228 +0,0 @@ -//! Program state processor - -use { - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - msg, - program::invoke_signed, - program_error::ProgramError, - pubkey::Pubkey, - system_instruction, - }, - spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList}, - spl_token_2022::{ - extension::{ - transfer_hook::TransferHookAccount, BaseStateWithExtensions, StateWithExtensions, - }, - state::{Account, Mint}, - }, - spl_transfer_hook_interface::{ - collect_extra_account_metas_signer_seeds, - error::TransferHookError, - get_extra_account_metas_address, get_extra_account_metas_address_and_bump_seed, - instruction::{ExecuteInstruction, TransferHookInstruction}, - }, -}; - -fn check_token_account_is_transferring(account_info: &AccountInfo) -> Result<(), ProgramError> { - let account_data = account_info.try_borrow_data()?; - let token_account = StateWithExtensions::::unpack(&account_data)?; - let extension = token_account.get_extension::()?; - if bool::from(extension.transferring) { - Ok(()) - } else { - Err(TransferHookError::ProgramCalledOutsideOfTransfer.into()) - } -} - -/// Processes an [Execute](enum.TransferHookInstruction.html) instruction. -pub fn process_execute( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let source_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let destination_account_info = next_account_info(account_info_iter)?; - let _authority_info = next_account_info(account_info_iter)?; - let extra_account_metas_info = next_account_info(account_info_iter)?; - - // Check that the accounts are properly in "transferring" mode - check_token_account_is_transferring(source_account_info)?; - check_token_account_is_transferring(destination_account_info)?; - - // For the example program, we just check that the correct pda and validation - // pubkeys are provided - let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id); - if expected_validation_address != *extra_account_metas_info.key { - return Err(ProgramError::InvalidSeeds); - } - - let data = extra_account_metas_info.try_borrow_data()?; - - ExtraAccountMetaList::check_account_infos::( - accounts, - &TransferHookInstruction::Execute { amount }.pack(), - program_id, - &data, - )?; - - Ok(()) -} - -/// Processes a -/// [`InitializeExtraAccountMetaList`](enum.TransferHookInstruction.html) -/// instruction. -pub fn process_initialize_extra_account_meta_list( - program_id: &Pubkey, - accounts: &[AccountInfo], - extra_account_metas: &[ExtraAccountMeta], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let extra_account_metas_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let _system_program_info = next_account_info(account_info_iter)?; - - // check that the one mint we want to target is trying to create extra - // account metas - #[cfg(feature = "forbid-additional-mints")] - if *mint_info.key != crate::mint::id() { - return Err(ProgramError::InvalidArgument); - } - - // check that the mint authority is valid without fully deserializing - let mint_data = mint_info.try_borrow_data()?; - let mint = StateWithExtensions::::unpack(&mint_data)?; - let mint_authority = mint - .base - .mint_authority - .ok_or(TransferHookError::MintHasNoMintAuthority)?; - - // Check signers - if !authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - if *authority_info.key != mint_authority { - return Err(TransferHookError::IncorrectMintAuthority.into()); - } - - // Check validation account - let (expected_validation_address, bump_seed) = - get_extra_account_metas_address_and_bump_seed(mint_info.key, program_id); - if expected_validation_address != *extra_account_metas_info.key { - return Err(ProgramError::InvalidSeeds); - } - - // Create the account - let bump_seed = [bump_seed]; - let signer_seeds = collect_extra_account_metas_signer_seeds(mint_info.key, &bump_seed); - let length = extra_account_metas.len(); - let account_size = ExtraAccountMetaList::size_of(length)?; - invoke_signed( - &system_instruction::allocate(extra_account_metas_info.key, account_size as u64), - &[extra_account_metas_info.clone()], - &[&signer_seeds], - )?; - invoke_signed( - &system_instruction::assign(extra_account_metas_info.key, program_id), - &[extra_account_metas_info.clone()], - &[&signer_seeds], - )?; - - // Write the data - let mut data = extra_account_metas_info.try_borrow_mut_data()?; - ExtraAccountMetaList::init::(&mut data, extra_account_metas)?; - - Ok(()) -} - -/// Processes a -/// [`UpdateExtraAccountMetaList`](enum.TransferHookInstruction.html) -/// instruction. -pub fn process_update_extra_account_meta_list( - program_id: &Pubkey, - accounts: &[AccountInfo], - extra_account_metas: &[ExtraAccountMeta], -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let extra_account_metas_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - - // check that the mint authority is valid without fully deserializing - let mint_data = mint_info.try_borrow_data()?; - let mint = StateWithExtensions::::unpack(&mint_data)?; - let mint_authority = mint - .base - .mint_authority - .ok_or(TransferHookError::MintHasNoMintAuthority)?; - - // Check signers - if !authority_info.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } - if *authority_info.key != mint_authority { - return Err(TransferHookError::IncorrectMintAuthority.into()); - } - - // Check validation account - let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id); - if expected_validation_address != *extra_account_metas_info.key { - return Err(ProgramError::InvalidSeeds); - } - - // Check if the extra metas have been initialized - let min_account_size = ExtraAccountMetaList::size_of(0)?; - let original_account_size = extra_account_metas_info.data_len(); - if program_id != extra_account_metas_info.owner || original_account_size < min_account_size { - return Err(ProgramError::UninitializedAccount); - } - - // If the new extra_account_metas length is different, resize the account and - // update - let length = extra_account_metas.len(); - let account_size = ExtraAccountMetaList::size_of(length)?; - if account_size >= original_account_size { - extra_account_metas_info.realloc(account_size, false)?; - let mut data = extra_account_metas_info.try_borrow_mut_data()?; - ExtraAccountMetaList::update::(&mut data, extra_account_metas)?; - } else { - { - let mut data = extra_account_metas_info.try_borrow_mut_data()?; - ExtraAccountMetaList::update::(&mut data, extra_account_metas)?; - } - extra_account_metas_info.realloc(account_size, false)?; - } - - Ok(()) -} - -/// Processes an [Instruction](enum.Instruction.html). -pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { - let instruction = TransferHookInstruction::unpack(input)?; - - match instruction { - TransferHookInstruction::Execute { amount } => { - msg!("Instruction: Execute"); - process_execute(program_id, accounts, amount) - } - TransferHookInstruction::InitializeExtraAccountMetaList { - extra_account_metas, - } => { - msg!("Instruction: InitializeExtraAccountMetaList"); - process_initialize_extra_account_meta_list(program_id, accounts, &extra_account_metas) - } - TransferHookInstruction::UpdateExtraAccountMetaList { - extra_account_metas, - } => { - msg!("Instruction: UpdateExtraAccountMetaList"); - process_update_extra_account_meta_list(program_id, accounts, &extra_account_metas) - } - } -} diff --git a/token/transfer-hook/example/src/state.rs b/token/transfer-hook/example/src/state.rs deleted file mode 100644 index b2c962c5072..00000000000 --- a/token/transfer-hook/example/src/state.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! State helpers for working with the example program - -use { - solana_program::program_error::ProgramError, - spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList}, - spl_transfer_hook_interface::instruction::ExecuteInstruction, -}; - -/// Generate example data to be used directly in an account for testing -pub fn example_data(account_metas: &[ExtraAccountMeta]) -> Result, ProgramError> { - let account_size = ExtraAccountMetaList::size_of(account_metas.len())?; - let mut data = vec![0; account_size]; - ExtraAccountMetaList::init::(&mut data, account_metas)?; - Ok(data) -} diff --git a/token/transfer-hook/example/tests/functional.rs b/token/transfer-hook/example/tests/functional.rs deleted file mode 100644 index 5e0aa098270..00000000000 --- a/token/transfer-hook/example/tests/functional.rs +++ /dev/null @@ -1,1464 +0,0 @@ -// Mark this test as SBF-only due to current `ProgramTest` limitations when -// CPIing into the system program -#![cfg(feature = "test-sbf")] - -use { - solana_program_test::{processor, tokio, ProgramTest}, - solana_sdk::{ - account::Account as SolanaAccount, - account_info::AccountInfo, - entrypoint::ProgramResult, - instruction::{AccountMeta, InstructionError}, - program_error::ProgramError, - program_option::COption, - pubkey::Pubkey, - signature::Signer, - signer::keypair::Keypair, - system_instruction, sysvar, - transaction::{Transaction, TransactionError}, - }, - spl_tlv_account_resolution::{ - account::ExtraAccountMeta, error::AccountResolutionError, seeds::Seed, - state::ExtraAccountMetaList, - }, - spl_token_2022::{ - extension::{ - transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, ExtensionType, - StateWithExtensionsMut, - }, - state::{Account, AccountState, Mint}, - }, - spl_transfer_hook_interface::{ - error::TransferHookError, - get_extra_account_metas_address, - instruction::{ - execute_with_extra_account_metas, initialize_extra_account_meta_list, - update_extra_account_meta_list, - }, - onchain, - }, -}; - -fn setup(program_id: &Pubkey) -> ProgramTest { - let mut program_test = ProgramTest::new( - "spl_transfer_hook_example", - *program_id, - processor!(spl_transfer_hook_example::processor::process), - ); - - program_test.prefer_bpf(false); // simplicity in the build - - program_test.add_program( - "spl_token_2022", - spl_token_2022::id(), - processor!(spl_token_2022::processor::Processor::process), - ); - - program_test -} - -#[allow(clippy::too_many_arguments)] -fn setup_token_accounts( - program_test: &mut ProgramTest, - program_id: &Pubkey, - mint_address: &Pubkey, - mint_authority: &Pubkey, - source: &Pubkey, - destination: &Pubkey, - owner: &Pubkey, - decimals: u8, - transferring: bool, -) { - // add mint, source, and destination accounts by hand to always force - // the "transferring" flag to true - let mint_size = ExtensionType::try_calculate_account_len::(&[]).unwrap(); - let mut mint_data = vec![0; mint_size]; - let mut state = StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data).unwrap(); - let token_amount = 1_000_000_000_000; - state.base = Mint { - mint_authority: COption::Some(*mint_authority), - supply: token_amount, - decimals, - is_initialized: true, - freeze_authority: COption::None, - }; - state.pack_base(); - program_test.add_account( - *mint_address, - SolanaAccount { - lamports: 1_000_000_000, - data: mint_data, - owner: *program_id, - ..SolanaAccount::default() - }, - ); - - let account_size = - ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferHookAccount]) - .unwrap(); - let mut account_data = vec![0; account_size]; - let mut state = - StateWithExtensionsMut::::unpack_uninitialized(&mut account_data).unwrap(); - let extension = state.init_extension::(true).unwrap(); - extension.transferring = transferring.into(); - let token_amount = 1_000_000_000_000; - state.base = Account { - mint: *mint_address, - owner: *owner, - amount: token_amount, - delegate: COption::None, - state: AccountState::Initialized, - is_native: COption::None, - delegated_amount: 0, - close_authority: COption::None, - }; - state.pack_base(); - state.init_account_type().unwrap(); - - program_test.add_account( - *source, - SolanaAccount { - lamports: 1_000_000_000, - data: account_data.clone(), - owner: *program_id, - ..SolanaAccount::default() - }, - ); - program_test.add_account( - *destination, - SolanaAccount { - lamports: 1_000_000_000, - data: account_data, - owner: *program_id, - ..SolanaAccount::default() - }, - ); -} - -#[tokio::test] -async fn success_execute() { - let program_id = Pubkey::new_unique(); - let mut program_test = setup(&program_id); - - let token_program_id = spl_token_2022::id(); - let wallet = Keypair::new(); - let mint_address = spl_transfer_hook_example::mint::id(); - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - let source = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let decimals = 2; - let amount = 0u64; - - setup_token_accounts( - &mut program_test, - &token_program_id, - &mint_address, - &mint_authority_pubkey, - &source, - &destination, - &wallet.pubkey(), - decimals, - true, - ); - - let extra_account_metas_address = get_extra_account_metas_address(&mint_address, &program_id); - - let writable_pubkey = Pubkey::new_unique(); - - let init_extra_account_metas = [ - ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"seed-prefix".to_vec(), - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 8, // After instruction discriminator - length: 8, // `u64` (amount) - }, - Seed::AccountKey { index: 2 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), - ]; - - let extra_pda_1 = Pubkey::find_program_address( - &[ - b"seed-prefix", // Literal prefix - source.as_ref(), // Account at index 0 - ], - &program_id, - ) - .0; - let extra_pda_2 = Pubkey::find_program_address( - &[ - &amount.to_le_bytes(), // Instruction data bytes 8 to 16 - destination.as_ref(), // Account at index 2 - ], - &program_id, - ) - .0; - - let extra_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, true), - AccountMeta::new(extra_pda_1, false), - AccountMeta::new(extra_pda_2, false), - AccountMeta::new(writable_pubkey, false), - ]; - - let context = program_test.start_with_context().await; - let rent = context.banks_client.get_rent().await.unwrap(); - let rent_lamports = rent - .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - &extra_account_metas_address, - rent_lamports, - ), - initialize_extra_account_meta_list( - &program_id, - &extra_account_metas_address, - &mint_address, - &mint_authority_pubkey, - &init_extra_account_metas, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // fail with missing account - { - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas[..2], - amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), - ) - ); - } - - // fail with wrong account - { - let extra_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, true), - AccountMeta::new(extra_pda_1, false), - AccountMeta::new(extra_pda_2, false), - AccountMeta::new(Pubkey::new_unique(), false), - ]; - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas, - amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), - ) - ); - } - - // fail with wrong PDA - let wrong_pda_2 = Pubkey::find_program_address( - &[ - &99u64.to_le_bytes(), // Wrong data - destination.as_ref(), - ], - &program_id, - ) - .0; - { - let extra_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, true), - AccountMeta::new(extra_pda_1, false), - AccountMeta::new(wrong_pda_2, false), - AccountMeta::new(writable_pubkey, false), - ]; - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas, - amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), - ) - ); - } - - // fail with not signer - { - let extra_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, false), - AccountMeta::new(extra_pda_1, false), - AccountMeta::new(extra_pda_2, false), - AccountMeta::new(writable_pubkey, false), - ]; - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas, - amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), - ) - ); - } - - // success with correct params - { - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas, - amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - } -} - -#[tokio::test] -async fn fail_incorrect_derivation() { - let program_id = Pubkey::new_unique(); - let mut program_test = setup(&program_id); - - let token_program_id = spl_token_2022::id(); - let wallet = Keypair::new(); - let mint_address = spl_transfer_hook_example::mint::id(); - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - let source = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let decimals = 2; - setup_token_accounts( - &mut program_test, - &token_program_id, - &mint_address, - &mint_authority_pubkey, - &source, - &destination, - &wallet.pubkey(), - decimals, - true, - ); - - // wrong derivation - let extra_account_metas = get_extra_account_metas_address(&program_id, &mint_address); - - let context = program_test.start_with_context().await; - let rent = context.banks_client.get_rent().await.unwrap(); - let rent_lamports = rent.minimum_balance(ExtraAccountMetaList::size_of(0).unwrap()); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - &extra_account_metas, - rent_lamports, - ), - initialize_extra_account_meta_list( - &program_id, - &extra_account_metas, - &mint_address, - &mint_authority_pubkey, - &[], - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(1, InstructionError::InvalidSeeds) - ); -} - -#[tokio::test] -async fn fail_incorrect_mint() { - let program_id = Pubkey::new_unique(); - let mut program_test = setup(&program_id); - - let token_program_id = spl_token_2022::id(); - let wallet = Keypair::new(); - // wrong mint, only `spl_transfer_hook_example::mint::id()` allowed - let mint_address = Pubkey::new_unique(); - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - let source = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let decimals = 2; - setup_token_accounts( - &mut program_test, - &token_program_id, - &mint_address, - &mint_authority_pubkey, - &source, - &destination, - &wallet.pubkey(), - decimals, - true, - ); - - let extra_account_metas = get_extra_account_metas_address(&mint_address, &program_id); - - let context = program_test.start_with_context().await; - let rent = context.banks_client.get_rent().await.unwrap(); - let rent_lamports = rent.minimum_balance(ExtraAccountMetaList::size_of(0).unwrap()); - - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - &extra_account_metas, - rent_lamports, - ), - initialize_extra_account_meta_list( - &program_id, - &extra_account_metas, - &mint_address, - &mint_authority_pubkey, - &[], - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError(1, InstructionError::InvalidArgument) - ); -} - -/// Test program to CPI into default transfer-hook-interface program -pub fn process_instruction( - _program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - let amount = input - .get(8..16) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(ProgramError::InvalidInstructionData)?; - onchain::invoke_execute( - accounts[0].key, - accounts[1].clone(), - accounts[2].clone(), - accounts[3].clone(), - accounts[4].clone(), - &accounts[5..], - amount, - ) -} - -#[tokio::test] -async fn success_on_chain_invoke() { - let hook_program_id = Pubkey::new_unique(); - let mut program_test = setup(&hook_program_id); - let program_id = Pubkey::new_unique(); - program_test.add_program( - "test_cpi_program", - program_id, - processor!(process_instruction), - ); - - let token_program_id = spl_token_2022::id(); - let wallet = Keypair::new(); - let mint_address = spl_transfer_hook_example::mint::id(); - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - let source = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let decimals = 2; - let amount = 0u64; - - setup_token_accounts( - &mut program_test, - &token_program_id, - &mint_address, - &mint_authority_pubkey, - &source, - &destination, - &wallet.pubkey(), - decimals, - true, - ); - - let extra_account_metas_address = - get_extra_account_metas_address(&mint_address, &hook_program_id); - let writable_pubkey = Pubkey::new_unique(); - - let init_extra_account_metas = [ - ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"seed-prefix".to_vec(), - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 8, // After instruction discriminator - length: 8, // `u64` (amount) - }, - Seed::AccountKey { index: 2 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), - ]; - - let extra_pda_1 = Pubkey::find_program_address( - &[ - b"seed-prefix", // Literal prefix - source.as_ref(), // Account at index 0 - ], - &hook_program_id, - ) - .0; - let extra_pda_2 = Pubkey::find_program_address( - &[ - &amount.to_le_bytes(), // Instruction data bytes 8 to 16 - destination.as_ref(), // Account at index 2 - ], - &hook_program_id, - ) - .0; - - let extra_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, true), - AccountMeta::new(extra_pda_1, false), - AccountMeta::new(extra_pda_2, false), - AccountMeta::new(writable_pubkey, false), - ]; - - let context = program_test.start_with_context().await; - let rent = context.banks_client.get_rent().await.unwrap(); - let rent_lamports = rent - .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - &extra_account_metas_address, - rent_lamports, - ), - initialize_extra_account_meta_list( - &hook_program_id, - &extra_account_metas_address, - &mint_address, - &mint_authority_pubkey, - &init_extra_account_metas, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - // easier to hack this up! - let mut test_instruction = execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas, - amount, - ); - test_instruction - .accounts - .insert(0, AccountMeta::new_readonly(hook_program_id, false)); - let transaction = Transaction::new_signed_with_payer( - &[test_instruction], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} - -#[tokio::test] -async fn fail_without_transferring_flag() { - let program_id = Pubkey::new_unique(); - let mut program_test = setup(&program_id); - - let token_program_id = spl_token_2022::id(); - let wallet = Keypair::new(); - let mint_address = spl_transfer_hook_example::mint::id(); - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - let source = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let decimals = 2; - - setup_token_accounts( - &mut program_test, - &token_program_id, - &mint_address, - &mint_authority_pubkey, - &source, - &destination, - &wallet.pubkey(), - decimals, - false, - ); - - let extra_account_metas_address = get_extra_account_metas_address(&mint_address, &program_id); - let extra_account_metas = []; - let init_extra_account_metas = []; - let context = program_test.start_with_context().await; - let rent = context.banks_client.get_rent().await.unwrap(); - let rent_lamports = rent - .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - &extra_account_metas_address, - rent_lamports, - ), - initialize_extra_account_meta_list( - &program_id, - &extra_account_metas_address, - &mint_address, - &mint_authority_pubkey, - &init_extra_account_metas, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas, - 0, - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(TransferHookError::ProgramCalledOutsideOfTransfer as u32) - ) - ); -} - -#[tokio::test] -async fn success_on_chain_invoke_with_updated_extra_account_metas() { - let hook_program_id = Pubkey::new_unique(); - let mut program_test = setup(&hook_program_id); - let program_id = Pubkey::new_unique(); - program_test.add_program( - "test_cpi_program", - program_id, - processor!(process_instruction), - ); - - let token_program_id = spl_token_2022::id(); - let wallet = Keypair::new(); - let mint_address = spl_transfer_hook_example::mint::id(); - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - let source = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let decimals = 2; - let amount = 0u64; - - setup_token_accounts( - &mut program_test, - &token_program_id, - &mint_address, - &mint_authority_pubkey, - &source, - &destination, - &wallet.pubkey(), - decimals, - true, - ); - - let extra_account_metas_address = - get_extra_account_metas_address(&mint_address, &hook_program_id); - let writable_pubkey = Pubkey::new_unique(); - - // Create an initial account metas list - let init_extra_account_metas = [ - ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"init-seed-prefix".to_vec(), - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 8, // After instruction discriminator - length: 8, // `u64` (amount) - }, - Seed::AccountKey { index: 2 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), - ]; - - let context = program_test.start_with_context().await; - let rent = context.banks_client.get_rent().await.unwrap(); - let rent_lamports = rent - .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); - let init_transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - &extra_account_metas_address, - rent_lamports, - ), - initialize_extra_account_meta_list( - &hook_program_id, - &extra_account_metas_address, - &mint_address, - &mint_authority_pubkey, - &init_extra_account_metas, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(init_transaction) - .await - .unwrap(); - - // Create an updated account metas list - let updated_extra_account_metas = [ - ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"updated-seed-prefix".to_vec(), - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 8, // After instruction discriminator - length: 8, // `u64` (amount) - }, - Seed::AccountKey { index: 2 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), - ]; - - let rent = context.banks_client.get_rent().await.unwrap(); - let rent_lamports = rent - .minimum_balance(ExtraAccountMetaList::size_of(updated_extra_account_metas.len()).unwrap()); - let update_transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - &extra_account_metas_address, - rent_lamports, - ), - update_extra_account_meta_list( - &hook_program_id, - &extra_account_metas_address, - &mint_address, - &mint_authority_pubkey, - &updated_extra_account_metas, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(update_transaction) - .await - .unwrap(); - - let updated_extra_pda_1 = Pubkey::find_program_address( - &[ - b"updated-seed-prefix", // Literal prefix - source.as_ref(), // Account at index 0 - ], - &hook_program_id, - ) - .0; - let extra_pda_2 = Pubkey::find_program_address( - &[ - &amount.to_le_bytes(), // Instruction data bytes 8 to 16 - destination.as_ref(), // Account at index 2 - ], - &hook_program_id, - ) - .0; - - let test_updated_extra_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, true), - AccountMeta::new(updated_extra_pda_1, false), - AccountMeta::new(extra_pda_2, false), - AccountMeta::new(writable_pubkey, false), - ]; - - // Use updated account metas list - let mut test_instruction = execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &test_updated_extra_account_metas, - amount, - ); - test_instruction - .accounts - .insert(0, AccountMeta::new_readonly(hook_program_id, false)); - let transaction = Transaction::new_signed_with_payer( - &[test_instruction], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} - -#[tokio::test] -async fn success_execute_with_updated_extra_account_metas() { - let program_id = Pubkey::new_unique(); - let mut program_test = setup(&program_id); - - let token_program_id = spl_token_2022::id(); - let wallet = Keypair::new(); - let mint_address = spl_transfer_hook_example::mint::id(); - let mint_authority = Keypair::new(); - let mint_authority_pubkey = mint_authority.pubkey(); - let source = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let decimals = 2; - let amount = 0u64; - - setup_token_accounts( - &mut program_test, - &token_program_id, - &mint_address, - &mint_authority_pubkey, - &source, - &destination, - &wallet.pubkey(), - decimals, - true, - ); - - let extra_account_metas_address = get_extra_account_metas_address(&mint_address, &program_id); - - let writable_pubkey = Pubkey::new_unique(); - - let init_extra_account_metas = [ - ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"seed-prefix".to_vec(), - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 8, // After instruction discriminator - length: 8, // `u64` (amount) - }, - Seed::AccountKey { index: 2 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), - ]; - - let extra_pda_1 = Pubkey::find_program_address( - &[ - b"seed-prefix", // Literal prefix - source.as_ref(), // Account at index 0 - ], - &program_id, - ) - .0; - - let extra_pda_2 = Pubkey::find_program_address( - &[ - &amount.to_le_bytes(), // Instruction data bytes 8 to 16 - destination.as_ref(), // Account at index 2 - ], - &program_id, - ) - .0; - - let init_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, true), - AccountMeta::new(extra_pda_1, false), - AccountMeta::new(extra_pda_2, false), - AccountMeta::new(writable_pubkey, false), - ]; - - let context = program_test.start_with_context().await; - let rent = context.banks_client.get_rent().await.unwrap(); - let rent_lamports = rent - .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); - let transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - &extra_account_metas_address, - rent_lamports, - ), - initialize_extra_account_meta_list( - &program_id, - &extra_account_metas_address, - &mint_address, - &mint_authority_pubkey, - &init_extra_account_metas, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - - let updated_amount = 1u64; - let updated_writable_pubkey = Pubkey::new_unique(); - - // Create updated extra account metas - let updated_extra_account_metas = [ - ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"updated-seed-prefix".to_vec(), - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 8, // After instruction discriminator - length: 8, // `u64` (amount) - }, - Seed::AccountKey { index: 2 }, - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_pubkey(&updated_writable_pubkey, false, true).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::Literal { - bytes: b"new-seed-prefix".to_vec(), - }, - Seed::AccountKey { index: 0 }, - ], - false, - true, - ) - .unwrap(), - ]; - - let updated_extra_pda_1 = Pubkey::find_program_address( - &[ - b"updated-seed-prefix", // Literal prefix - source.as_ref(), // Account at index 0 - ], - &program_id, - ) - .0; - - let updated_extra_pda_2 = Pubkey::find_program_address( - &[ - &updated_amount.to_le_bytes(), // Instruction data bytes 8 to 16 - destination.as_ref(), // Account at index 2 - ], - &program_id, - ) - .0; - - // add another PDA - let new_extra_pda = Pubkey::find_program_address( - &[ - b"new-seed-prefix", // Literal prefix - source.as_ref(), // Account at index 0 - ], - &program_id, - ) - .0; - - let updated_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, true), - AccountMeta::new(updated_extra_pda_1, false), - AccountMeta::new(updated_extra_pda_2, false), - AccountMeta::new(updated_writable_pubkey, false), - AccountMeta::new(new_extra_pda, false), - ]; - - let update_transaction = Transaction::new_signed_with_payer( - &[ - system_instruction::transfer( - &context.payer.pubkey(), - &extra_account_metas_address, - rent_lamports, - ), - update_extra_account_meta_list( - &program_id, - &extra_account_metas_address, - &mint_address, - &mint_authority_pubkey, - &updated_extra_account_metas, - ), - ], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - - context - .banks_client - .process_transaction(update_transaction) - .await - .unwrap(); - - // fail with initial account metas list - { - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &init_account_metas, - updated_amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), - ) - ); - } - - // fail with missing account - { - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &updated_account_metas[..2], - updated_amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), - ) - ); - } - - // fail with wrong account - { - let extra_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, true), - AccountMeta::new(updated_extra_pda_1, false), - AccountMeta::new(updated_extra_pda_2, false), - AccountMeta::new(Pubkey::new_unique(), false), - ]; - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas, - updated_amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), - ) - ); - } - - // fail with wrong PDA - let wrong_pda_2 = Pubkey::find_program_address( - &[ - &99u64.to_le_bytes(), // Wrong data - destination.as_ref(), - ], - &program_id, - ) - .0; - { - let extra_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, true), - AccountMeta::new(updated_extra_pda_1, false), - AccountMeta::new(wrong_pda_2, false), - AccountMeta::new(writable_pubkey, false), - ]; - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas, - updated_amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), - ) - ); - } - - // fail with not signer - { - let extra_account_metas = [ - AccountMeta::new_readonly(sysvar::instructions::id(), false), - AccountMeta::new_readonly(mint_authority_pubkey, false), - AccountMeta::new(updated_extra_pda_1, false), - AccountMeta::new(updated_extra_pda_2, false), - AccountMeta::new(writable_pubkey, false), - ]; - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &extra_account_metas, - updated_amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - let error = context - .banks_client - .process_transaction(transaction) - .await - .unwrap_err() - .unwrap(); - assert_eq!( - error, - TransactionError::InstructionError( - 0, - InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), - ) - ); - } - - // success with correct params - { - let transaction = Transaction::new_signed_with_payer( - &[execute_with_extra_account_metas( - &program_id, - &source, - &mint_address, - &destination, - &wallet.pubkey(), - &extra_account_metas_address, - &updated_account_metas, - updated_amount, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &mint_authority], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); - } -} diff --git a/token/transfer-hook/interface/Cargo.toml b/token/transfer-hook/interface/Cargo.toml deleted file mode 100644 index c8b70cad60d..00000000000 --- a/token/transfer-hook/interface/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "spl-transfer-hook-interface" -version = "0.9.0" -description = "Solana Program Library Transfer Hook Interface" -authors = ["Solana Labs Maintainers "] -repository = "https://github.com/solana-labs/solana-program-library" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -arrayref = "0.3.9" -bytemuck = { version = "1.21.0", features = ["derive"] } -num-derive = "0.4" -num-traits = "0.2" -solana-account-info = "2.1.0" -solana-cpi = "2.1.0" -solana-decode-error = "2.1.0" -solana-instruction = { version = "2.1.0", features = ["std"] } -solana-msg = "2.1.0" -solana-program-error = "2.1.0" -solana-pubkey = { version = "2.1.0", features = ["curve25519"] } -spl-discriminator = { version = "0.4.0" , path = "../../../libraries/discriminator" } -spl-program-error = { version = "0.6.0", path = "../../../libraries/program-error" } -spl-tlv-account-resolution = { version = "0.9.0", path = "../../../libraries/tlv-account-resolution" } -spl-type-length-value = { version = "0.7.0", path = "../../../libraries/type-length-value" } -spl-pod = { version = "0.5.0", path = "../../../libraries/pod" } -thiserror = "2.0" - -[lib] -crate-type = ["cdylib", "lib"] - -[dev-dependencies] -solana-program = "2.1.0" -tokio = { version = "1.42.0", features = ["full"] } - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] diff --git a/token/transfer-hook/interface/README.md b/token/transfer-hook/interface/README.md deleted file mode 100644 index 17c320bbc29..00000000000 --- a/token/transfer-hook/interface/README.md +++ /dev/null @@ -1,149 +0,0 @@ -## Transfer-Hook Interface - -### Example program - -Here is an example program that only implements the required "execute" instruction, -assuming that the proper account data is already written to the appropriate -program-derived address defined by the interface. - -```rust -use { - solana_program::{entrypoint::ProgramResult, program_error::ProgramError}, - spl_tlv_account_resolution::state::ExtraAccountMetaList, - spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction}, - spl_type_length_value::state::TlvStateBorrowed, -}; -pub fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - input: &[u8], -) -> ProgramResult { - let instruction = TransferHookInstruction::unpack(input)?; - let _amount = match instruction { - TransferHookInstruction::Execute { amount } => amount, - _ => return Err(ProgramError::InvalidInstructionData), - }; - let account_info_iter = &mut accounts.iter(); - - // Pull out the accounts in order, none are validated in this test program - let _source_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let _destination_account_info = next_account_info(account_info_iter)?; - let _authority_info = next_account_info(account_info_iter)?; - let extra_account_metas_info = next_account_info(account_info_iter)?; - - // Only check that the correct pda and account are provided - let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id); - if expected_validation_address != *extra_account_metas_info.key { - return Err(ProgramError::InvalidSeeds); - } - - // Load the extra required accounts from the validation account - let data = extra_account_metas_info.try_borrow_data()?; - - // Check the provided accounts against the validation data - ExtraAccountMetaList::check_account_infos::( - accounts, - &TransferHookInstruction::Execute { amount }.pack(), - program_id, - &data, - )?; - - Ok(()) -} -``` - -### Motivation - -Token creators may need more control over how their token is transferred. The -most prominent use case revolves around NFT royalties. Whenever a token is moved, -the creator should be entitled to royalties, but due to the design of the current -token program, it's impossible to stop a transfer at the protocol level. - -Current solutions typically resort to perpetually freezing tokens, which requires -a whole proxy layer to interact with the token. Wallets and marketplaces need -to be aware of the proxy layer in order to properly use the token. - -Worse still, different royalty systems have different proxy layers for using -their token. All in all, these systems harm composability and make development -harder. - -### Solution - -To improve the situation, Token-2022 introduces the concept of the transfer-hook -interface and extension. A token creator must develop and deploy a program that -implements the interface and then configure their token mint to use their program. - -During transfer, Token-2022 calls into the program with the accounts specified -at a well-defined program-derived address for that mint and program id. This -call happens after all other transfer logic, so the accounts reflect the *end* -state of the transfer. - -### How to Use - -Developers must implement the `Execute` instruction, and optionally the -`InitializeExtraAccountMetaList` instruction to write the required additional account -pubkeys into the program-derived address defined by the mint and program id. - -Note: it's technically not required to implement `InitializeExtraAccountMetaList` -at that instruction discriminator. Your program may implement multiple interfaces, -so any other instruction in your program can create the account at the program-derived -address! - -When your program stores configurations for extra required accounts in the -well-defined program-derived address, it's possible to send an instruction - -such as `Execute` (transfer) - to your program with only accounts required -for the interface instruction, and all extra required accounts are -automatically resolved! - -### Account Resolution - -Implementations of the transfer-hook interface are encouraged to make use of the -[spl-tlv-account-resolution](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution/README.md) -library to manage the additional required accounts for their transfer hook -program. - -TLV Account Resolution is capable of powering on-chain account resolution -when an instruction that requires extra accounts is invoked. -Read more about how account resolution works in the repository's -[README file](https://github.com/solana-labs/solana-program-library/tree/master/libraries/tlv-account-resolution/README.md). - -### An Example - -You have created a DAO to govern a community. Your DAO's authority is a -multisig account, and you want to ensure that any transfer of your token is -approved by the DAO. You also want to make sure that someone who intends to -transfer your token has the proper permissions to do so. - -Let's assume the DAO multisig has some **fixed address**. And let's assume that -in order to have the `can_transfer` permission, a user must have this -**dynamic program-derived address** associated with their wallet via the -following seeds: `"can_transfer" + `. - -Using the transfer-hook interface, you can store these configurations in the -well-defined program-derived address for your mint and program id. - -When a user attempts to transfer your token, they might provide to Token-2022: - -```rust -[source, mint, destination, owner/delegate] -``` - -Token-2022 will then call into your program, -**resolving the extra required accounts automatically** from your stored -configurations, to result in the following accounts being provided to your -program: - -```rust -[source, mint, destination, owner/delegate, dao_authority, can_transfer_pda] -``` - -### Utilities - -The `spl-transfer-hook-interface` library provides offchain and onchain helpers -for resolving the additional accounts required. See -[onchain.rs](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/interface/src/onchain.rs) -for usage on-chain, and -[offchain.rs](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/interface/src/offchain.rs) -for fetching the additional required account metas with any async off-chain client -like `BanksClient` or `RpcClient`. diff --git a/token/transfer-hook/interface/src/error.rs b/token/transfer-hook/interface/src/error.rs deleted file mode 100644 index de50ec827c7..00000000000 --- a/token/transfer-hook/interface/src/error.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Error types - -use { - solana_decode_error::DecodeError, - solana_msg::msg, - solana_program_error::{PrintProgramError, ProgramError}, -}; - -/// Errors that may be returned by the interface. -#[repr(u32)] -#[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] -pub enum TransferHookError { - /// Incorrect account provided - #[error("Incorrect account provided")] - IncorrectAccount = 2_110_272_652, - /// Mint has no mint authority - #[error("Mint has no mint authority")] - MintHasNoMintAuthority, - /// Incorrect mint authority has signed the instruction - #[error("Incorrect mint authority has signed the instruction")] - IncorrectMintAuthority, - /// Program called outside of a token transfer - #[error("Program called outside of a token transfer")] - ProgramCalledOutsideOfTransfer, -} - -impl From for ProgramError { - fn from(e: TransferHookError) -> Self { - ProgramError::Custom(e as u32) - } -} - -impl DecodeError for TransferHookError { - fn type_of() -> &'static str { - "TransferHookError" - } -} - -impl PrintProgramError for TransferHookError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - TransferHookError::IncorrectAccount => { - msg!("Incorrect account provided") - } - TransferHookError::MintHasNoMintAuthority => { - msg!("Mint has no mint authority") - } - TransferHookError::IncorrectMintAuthority => { - msg!("Incorrect mint authority has signed the instruction") - } - TransferHookError::ProgramCalledOutsideOfTransfer => { - msg!("Program called outside of a token transfer") - } - } - } -} diff --git a/token/transfer-hook/interface/src/instruction.rs b/token/transfer-hook/interface/src/instruction.rs deleted file mode 100644 index 68485bcbbce..00000000000 --- a/token/transfer-hook/interface/src/instruction.rs +++ /dev/null @@ -1,310 +0,0 @@ -//! Instruction types - -use { - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, - spl_pod::{bytemuck::pod_slice_to_bytes, slice::PodSlice}, - spl_tlv_account_resolution::account::ExtraAccountMeta, - std::convert::TryInto, -}; - -const SYSTEM_PROGRAM_ID: Pubkey = Pubkey::from_str_const("11111111111111111111111111111111"); - -/// Instructions supported by the transfer hook interface. -#[repr(C)] -#[derive(Clone, Debug, PartialEq)] -pub enum TransferHookInstruction { - /// Runs additional transfer logic. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[]` Source account - /// 1. `[]` Token mint - /// 2. `[]` Destination account - /// 3. `[]` Source account's owner/delegate - /// 4. `[]` (Optional) Validation account - /// 5. ..`5+M` `[]` `M` optional additional accounts, written in - /// validation account data - Execute { - /// Amount of tokens to transfer - amount: u64, - }, - - /// Initializes the extra account metas on an account, writing into the - /// first open TLV space. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Account with extra account metas - /// 1. `[]` Mint - /// 2. `[s]` Mint authority - /// 3. `[]` System program - InitializeExtraAccountMetaList { - /// List of `ExtraAccountMeta`s to write into the account - extra_account_metas: Vec, - }, - /// Updates the extra account metas on an account by overwriting the - /// existing list. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[w]` Account with extra account metas - /// 1. `[]` Mint - /// 2. `[s]` Mint authority - UpdateExtraAccountMetaList { - /// The new list of `ExtraAccountMetas` to overwrite the existing entry - /// in the account. - extra_account_metas: Vec, - }, -} -/// TLV instruction type only used to define the discriminator. The actual data -/// is entirely managed by `ExtraAccountMetaList`, and it is the only data -/// contained by this type. -#[derive(SplDiscriminate)] -#[discriminator_hash_input("spl-transfer-hook-interface:execute")] -pub struct ExecuteInstruction; - -/// TLV instruction type used to initialize extra account metas -/// for the transfer hook -#[derive(SplDiscriminate)] -#[discriminator_hash_input("spl-transfer-hook-interface:initialize-extra-account-metas")] -pub struct InitializeExtraAccountMetaListInstruction; - -/// TLV instruction type used to update extra account metas -/// for the transfer hook -#[derive(SplDiscriminate)] -#[discriminator_hash_input("spl-transfer-hook-interface:update-extra-account-metas")] -pub struct UpdateExtraAccountMetaListInstruction; - -impl TransferHookInstruction { - /// Unpacks a byte buffer into a - /// [`TransferHookInstruction`](enum.TransferHookInstruction.html). - pub fn unpack(input: &[u8]) -> Result { - if input.len() < ArrayDiscriminator::LENGTH { - return Err(ProgramError::InvalidInstructionData); - } - let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH); - Ok(match discriminator { - ExecuteInstruction::SPL_DISCRIMINATOR_SLICE => { - let amount = rest - .get(..8) - .and_then(|slice| slice.try_into().ok()) - .map(u64::from_le_bytes) - .ok_or(ProgramError::InvalidInstructionData)?; - Self::Execute { amount } - } - InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE => { - let pod_slice = PodSlice::::unpack(rest)?; - let extra_account_metas = pod_slice.data().to_vec(); - Self::InitializeExtraAccountMetaList { - extra_account_metas, - } - } - UpdateExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE => { - let pod_slice = PodSlice::::unpack(rest)?; - let extra_account_metas = pod_slice.data().to_vec(); - Self::UpdateExtraAccountMetaList { - extra_account_metas, - } - } - _ => return Err(ProgramError::InvalidInstructionData), - }) - } - - /// Packs a [`TransferHookInstruction`](enum.TransferHookInstruction.html) - /// into a byte buffer. - pub fn pack(&self) -> Vec { - let mut buf = vec![]; - match self { - Self::Execute { amount } => { - buf.extend_from_slice(ExecuteInstruction::SPL_DISCRIMINATOR_SLICE); - buf.extend_from_slice(&amount.to_le_bytes()); - } - Self::InitializeExtraAccountMetaList { - extra_account_metas, - } => { - buf.extend_from_slice( - InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE, - ); - buf.extend_from_slice(&(extra_account_metas.len() as u32).to_le_bytes()); - buf.extend_from_slice(pod_slice_to_bytes(extra_account_metas)); - } - Self::UpdateExtraAccountMetaList { - extra_account_metas, - } => { - buf.extend_from_slice( - UpdateExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE, - ); - buf.extend_from_slice(&(extra_account_metas.len() as u32).to_le_bytes()); - buf.extend_from_slice(pod_slice_to_bytes(extra_account_metas)); - } - }; - buf - } -} - -/// Creates an `Execute` instruction, provided all of the additional required -/// account metas -#[allow(clippy::too_many_arguments)] -pub fn execute_with_extra_account_metas( - program_id: &Pubkey, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - validate_state_pubkey: &Pubkey, - additional_accounts: &[AccountMeta], - amount: u64, -) -> Instruction { - let mut instruction = execute( - program_id, - source_pubkey, - mint_pubkey, - destination_pubkey, - authority_pubkey, - amount, - ); - instruction - .accounts - .push(AccountMeta::new_readonly(*validate_state_pubkey, false)); - instruction.accounts.extend_from_slice(additional_accounts); - instruction -} - -/// Creates an `Execute` instruction, without the additional accounts -#[allow(clippy::too_many_arguments)] -pub fn execute( - program_id: &Pubkey, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - amount: u64, -) -> Instruction { - let data = TransferHookInstruction::Execute { amount }.pack(); - let accounts = vec![ - AccountMeta::new_readonly(*source_pubkey, false), - AccountMeta::new_readonly(*mint_pubkey, false), - AccountMeta::new_readonly(*destination_pubkey, false), - AccountMeta::new_readonly(*authority_pubkey, false), - ]; - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -/// Creates a `InitializeExtraAccountMetaList` instruction. -pub fn initialize_extra_account_meta_list( - program_id: &Pubkey, - extra_account_metas_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - extra_account_metas: &[ExtraAccountMeta], -) -> Instruction { - let data = TransferHookInstruction::InitializeExtraAccountMetaList { - extra_account_metas: extra_account_metas.to_vec(), - } - .pack(); - - let accounts = vec![ - AccountMeta::new(*extra_account_metas_pubkey, false), - AccountMeta::new_readonly(*mint_pubkey, false), - AccountMeta::new_readonly(*authority_pubkey, true), - AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false), - ]; - - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -/// Creates a `UpdateExtraAccountMetaList` instruction. -pub fn update_extra_account_meta_list( - program_id: &Pubkey, - extra_account_metas_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - extra_account_metas: &[ExtraAccountMeta], -) -> Instruction { - let data = TransferHookInstruction::UpdateExtraAccountMetaList { - extra_account_metas: extra_account_metas.to_vec(), - } - .pack(); - - let accounts = vec![ - AccountMeta::new(*extra_account_metas_pubkey, false), - AccountMeta::new_readonly(*mint_pubkey, false), - AccountMeta::new_readonly(*authority_pubkey, true), - ]; - - Instruction { - program_id: *program_id, - accounts, - data, - } -} - -#[cfg(test)] -mod test { - use {super::*, crate::NAMESPACE, solana_program::hash, spl_pod::bytemuck::pod_from_bytes}; - - #[test] - fn system_program_id() { - assert_eq!(solana_program::system_program::id(), SYSTEM_PROGRAM_ID); - } - - #[test] - fn validate_packing() { - let amount = 111_111_111; - let check = TransferHookInstruction::Execute { amount }; - let packed = check.pack(); - // Please use ExecuteInstruction::SPL_DISCRIMINATOR in your program, the - // following is just for test purposes - let preimage = hash::hashv(&[format!("{NAMESPACE}:execute").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - let mut expect = vec![]; - expect.extend_from_slice(discriminator.as_ref()); - expect.extend_from_slice(&amount.to_le_bytes()); - assert_eq!(packed, expect); - let unpacked = TransferHookInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - } - - #[test] - fn initialize_validation_pubkeys_packing() { - let extra_meta_len_bytes = &[ - 1, 0, 0, 0, // `1u32` - ]; - let extra_meta_bytes = &[ - 0, // `AccountMeta` - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, // pubkey - 0, // is_signer - 0, // is_writable - ]; - let extra_account_metas = - vec![*pod_from_bytes::(extra_meta_bytes).unwrap()]; - let check = TransferHookInstruction::InitializeExtraAccountMetaList { - extra_account_metas, - }; - let packed = check.pack(); - // Please use INITIALIZE_EXTRA_ACCOUNT_METAS_DISCRIMINATOR in your program, - // the following is just for test purposes - let preimage = - hash::hashv(&[format!("{NAMESPACE}:initialize-extra-account-metas").as_bytes()]); - let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; - let mut expect = vec![]; - expect.extend_from_slice(discriminator.as_ref()); - expect.extend_from_slice(extra_meta_len_bytes); - expect.extend_from_slice(extra_meta_bytes); - assert_eq!(packed, expect); - let unpacked = TransferHookInstruction::unpack(&expect).unwrap(); - assert_eq!(unpacked, check); - } -} diff --git a/token/transfer-hook/interface/src/lib.rs b/token/transfer-hook/interface/src/lib.rs deleted file mode 100644 index 0e4f382ebc5..00000000000 --- a/token/transfer-hook/interface/src/lib.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Crate defining an interface for performing a hook on transfer, where the -//! token program calls into a separate program with additional accounts after -//! all other logic, to be sure that a transfer has accomplished all required -//! preconditions. - -#![allow(clippy::arithmetic_side_effects)] -#![deny(missing_docs)] -#![cfg_attr(not(test), forbid(unsafe_code))] - -pub mod error; -pub mod instruction; -pub mod offchain; -pub mod onchain; - -// Export current sdk types for downstream users building with a different sdk -// version -use solana_pubkey::Pubkey; -pub use { - solana_account_info, solana_cpi, solana_decode_error, solana_instruction, solana_msg, - solana_program_error, solana_pubkey, -}; - -/// Namespace for all programs implementing transfer-hook -pub const NAMESPACE: &str = "spl-transfer-hook-interface"; - -/// Seed for the state -const EXTRA_ACCOUNT_METAS_SEED: &[u8] = b"extra-account-metas"; - -/// Get the state address PDA -pub fn get_extra_account_metas_address(mint: &Pubkey, program_id: &Pubkey) -> Pubkey { - get_extra_account_metas_address_and_bump_seed(mint, program_id).0 -} - -/// Function used by programs implementing the interface, when creating the PDA, -/// to also get the bump seed -pub fn get_extra_account_metas_address_and_bump_seed( - mint: &Pubkey, - program_id: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address(&collect_extra_account_metas_seeds(mint), program_id) -} - -/// Function used by programs implementing the interface, when creating the PDA, -/// to get all of the PDA seeds -pub fn collect_extra_account_metas_seeds(mint: &Pubkey) -> [&[u8]; 2] { - [EXTRA_ACCOUNT_METAS_SEED, mint.as_ref()] -} - -/// Function used by programs implementing the interface, when creating the PDA, -/// to sign for the PDA -pub fn collect_extra_account_metas_signer_seeds<'a>( - mint: &'a Pubkey, - bump_seed: &'a [u8], -) -> [&'a [u8]; 3] { - [EXTRA_ACCOUNT_METAS_SEED, mint.as_ref(), bump_seed] -} diff --git a/token/transfer-hook/interface/src/offchain.rs b/token/transfer-hook/interface/src/offchain.rs deleted file mode 100644 index 46e078e38f8..00000000000 --- a/token/transfer-hook/interface/src/offchain.rs +++ /dev/null @@ -1,260 +0,0 @@ -//! Offchain helper for fetching required accounts to build instructions - -pub use spl_tlv_account_resolution::state::{AccountDataResult, AccountFetchError}; -use { - crate::{ - error::TransferHookError, - get_extra_account_metas_address, - instruction::{execute, ExecuteInstruction}, - }, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - spl_tlv_account_resolution::state::ExtraAccountMetaList, - std::future::Future, -}; - -/// Offchain helper to get all additional required account metas for an execute -/// instruction, based on a validation state account. -/// -/// The instruction being provided to this function must contain at least the -/// same account keys as the ones being provided, in order. Specifically: -/// 1. source -/// 2. mint -/// 3. destination -/// 4. authority -/// -/// The `program_id` should be the program ID of the program that the -/// created `ExecuteInstruction` is for. -/// -/// To be client-agnostic and to avoid pulling in the full solana-sdk, this -/// simply takes a function that will return its data as `Future>` for -/// the given address. Can be called in the following way: -/// -/// ```rust,ignore -/// add_extra_account_metas_for_execute( -/// &mut instruction, -/// &program_id, -/// &source, -/// &mint, -/// &destination, -/// &authority, -/// amount, -/// |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)), -/// ) -/// .await?; -/// ``` -#[allow(clippy::too_many_arguments)] -pub async fn add_extra_account_metas_for_execute( - instruction: &mut Instruction, - program_id: &Pubkey, - source_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - destination_pubkey: &Pubkey, - authority_pubkey: &Pubkey, - amount: u64, - fetch_account_data_fn: F, -) -> Result<(), AccountFetchError> -where - F: Fn(Pubkey) -> Fut, - Fut: Future, -{ - let validate_state_pubkey = get_extra_account_metas_address(mint_pubkey, program_id); - let validate_state_data = fetch_account_data_fn(validate_state_pubkey) - .await? - .ok_or(ProgramError::InvalidAccountData)?; - - // Check to make sure the provided keys are in the instruction - if [ - source_pubkey, - mint_pubkey, - destination_pubkey, - authority_pubkey, - ] - .iter() - .any(|&key| !instruction.accounts.iter().any(|meta| meta.pubkey == *key)) - { - Err(TransferHookError::IncorrectAccount)?; - } - - let mut execute_instruction = execute( - program_id, - source_pubkey, - mint_pubkey, - destination_pubkey, - authority_pubkey, - amount, - ); - execute_instruction - .accounts - .push(AccountMeta::new_readonly(validate_state_pubkey, false)); - - ExtraAccountMetaList::add_to_instruction::( - &mut execute_instruction, - fetch_account_data_fn, - &validate_state_data, - ) - .await?; - - // Add only the extra accounts resolved from the validation state - instruction - .accounts - .extend_from_slice(&execute_instruction.accounts[5..]); - - // Add the program id and validation state account - instruction - .accounts - .push(AccountMeta::new_readonly(*program_id, false)); - instruction - .accounts - .push(AccountMeta::new_readonly(validate_state_pubkey, false)); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use { - super::*, - spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}, - tokio, - }; - - const PROGRAM_ID: Pubkey = Pubkey::new_from_array([1u8; 32]); - const EXTRA_META_1: Pubkey = Pubkey::new_from_array([2u8; 32]); - const EXTRA_META_2: Pubkey = Pubkey::new_from_array([3u8; 32]); - - // Mock to return the validation state account data - async fn mock_fetch_account_data_fn(_address: Pubkey) -> AccountDataResult { - let extra_metas = vec![ - ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::AccountKey { index: 0 }, // source - Seed::AccountKey { index: 2 }, // destination - Seed::AccountKey { index: 4 }, // validation state - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 8, - length: 8, - }, // amount - Seed::AccountKey { index: 2 }, // destination - Seed::AccountKey { index: 5 }, // extra meta 1 - Seed::AccountKey { index: 7 }, // extra meta 3 (PDA) - ], - false, - true, - ) - .unwrap(), - ]; - let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap(); - let mut data = vec![0u8; account_size]; - ExtraAccountMetaList::init::(&mut data, &extra_metas)?; - Ok(Some(data)) - } - - #[tokio::test] - async fn test_add_extra_account_metas_for_execute() { - let source = Pubkey::new_unique(); - let mint = Pubkey::new_unique(); - let destination = Pubkey::new_unique(); - let authority = Pubkey::new_unique(); - let amount = 100u64; - - let validate_state_pubkey = get_extra_account_metas_address(&mint, &PROGRAM_ID); - let extra_meta_3_pubkey = Pubkey::find_program_address( - &[ - source.as_ref(), - destination.as_ref(), - validate_state_pubkey.as_ref(), - ], - &PROGRAM_ID, - ) - .0; - let extra_meta_4_pubkey = Pubkey::find_program_address( - &[ - amount.to_le_bytes().as_ref(), - destination.as_ref(), - EXTRA_META_1.as_ref(), - extra_meta_3_pubkey.as_ref(), - ], - &PROGRAM_ID, - ) - .0; - - // Fail missing key - let mut instruction = Instruction::new_with_bytes( - PROGRAM_ID, - &[], - vec![ - // source missing - AccountMeta::new_readonly(mint, false), - AccountMeta::new(destination, false), - AccountMeta::new_readonly(authority, true), - ], - ); - assert_eq!( - add_extra_account_metas_for_execute( - &mut instruction, - &PROGRAM_ID, - &source, - &mint, - &destination, - &authority, - amount, - mock_fetch_account_data_fn, - ) - .await - .unwrap_err() - .downcast::() - .unwrap(), - Box::new(TransferHookError::IncorrectAccount) - ); - - // Success - let mut instruction = Instruction::new_with_bytes( - PROGRAM_ID, - &[], - vec![ - AccountMeta::new(source, false), - AccountMeta::new_readonly(mint, false), - AccountMeta::new(destination, false), - AccountMeta::new_readonly(authority, true), - ], - ); - add_extra_account_metas_for_execute( - &mut instruction, - &PROGRAM_ID, - &source, - &mint, - &destination, - &authority, - amount, - mock_fetch_account_data_fn, - ) - .await - .unwrap(); - - let check_metas = [ - AccountMeta::new(source, false), - AccountMeta::new_readonly(mint, false), - AccountMeta::new(destination, false), - AccountMeta::new_readonly(authority, true), - AccountMeta::new_readonly(EXTRA_META_1, true), - AccountMeta::new_readonly(EXTRA_META_2, true), - AccountMeta::new(extra_meta_3_pubkey, false), - AccountMeta::new(extra_meta_4_pubkey, false), - AccountMeta::new_readonly(PROGRAM_ID, false), - AccountMeta::new_readonly(validate_state_pubkey, false), - ]; - - assert_eq!(instruction.accounts, check_metas); - } -} diff --git a/token/transfer-hook/interface/src/onchain.rs b/token/transfer-hook/interface/src/onchain.rs deleted file mode 100644 index 7bd8eff3925..00000000000 --- a/token/transfer-hook/interface/src/onchain.rs +++ /dev/null @@ -1,521 +0,0 @@ -//! On-chain program invoke helper to perform on-chain `execute` with correct -//! accounts - -use { - crate::{error::TransferHookError, get_extra_account_metas_address, instruction}, - solana_account_info::AccountInfo, - solana_cpi::invoke, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramResult, - solana_pubkey::Pubkey, - spl_tlv_account_resolution::state::ExtraAccountMetaList, -}; -/// Helper to CPI into a transfer-hook program on-chain, looking through the -/// additional account infos to create the proper instruction -pub fn invoke_execute<'a>( - program_id: &Pubkey, - source_info: AccountInfo<'a>, - mint_info: AccountInfo<'a>, - destination_info: AccountInfo<'a>, - authority_info: AccountInfo<'a>, - additional_accounts: &[AccountInfo<'a>], - amount: u64, -) -> ProgramResult { - let mut cpi_instruction = instruction::execute( - program_id, - source_info.key, - mint_info.key, - destination_info.key, - authority_info.key, - amount, - ); - - let validation_pubkey = get_extra_account_metas_address(mint_info.key, program_id); - - let mut cpi_account_infos = vec![source_info, mint_info, destination_info, authority_info]; - - if let Some(validation_info) = additional_accounts - .iter() - .find(|&x| *x.key == validation_pubkey) - { - cpi_instruction - .accounts - .push(AccountMeta::new_readonly(validation_pubkey, false)); - cpi_account_infos.push(validation_info.clone()); - - ExtraAccountMetaList::add_to_cpi_instruction::( - &mut cpi_instruction, - &mut cpi_account_infos, - &validation_info.try_borrow_data()?, - additional_accounts, - )?; - } - - invoke(&cpi_instruction, &cpi_account_infos) -} - -/// Helper to add accounts required for an `ExecuteInstruction` on-chain, -/// looking through the additional account infos to add the proper accounts. -/// -/// Note this helper is designed to add the extra accounts that will be -/// required for a CPI to a transfer hook program. However, the instruction -/// being provided to this helper is for the program that will CPI to the -/// transfer hook program. Because of this, we must resolve the extra accounts -/// for the `ExecuteInstruction` CPI, then add those extra resolved accounts to -/// the provided instruction. -#[allow(clippy::too_many_arguments)] -pub fn add_extra_accounts_for_execute_cpi<'a>( - cpi_instruction: &mut Instruction, - cpi_account_infos: &mut Vec>, - program_id: &Pubkey, - source_info: AccountInfo<'a>, - mint_info: AccountInfo<'a>, - destination_info: AccountInfo<'a>, - authority_info: AccountInfo<'a>, - amount: u64, - additional_accounts: &[AccountInfo<'a>], -) -> ProgramResult { - let validate_state_pubkey = get_extra_account_metas_address(mint_info.key, program_id); - - let program_info = additional_accounts - .iter() - .find(|&x| x.key == program_id) - .ok_or(TransferHookError::IncorrectAccount)?; - - if let Some(validate_state_info) = additional_accounts - .iter() - .find(|&x| *x.key == validate_state_pubkey) - { - let mut execute_instruction = instruction::execute( - program_id, - source_info.key, - mint_info.key, - destination_info.key, - authority_info.key, - amount, - ); - execute_instruction - .accounts - .push(AccountMeta::new_readonly(validate_state_pubkey, false)); - let mut execute_account_infos = vec![ - source_info, - mint_info, - destination_info, - authority_info, - validate_state_info.clone(), - ]; - - ExtraAccountMetaList::add_to_cpi_instruction::( - &mut execute_instruction, - &mut execute_account_infos, - &validate_state_info.try_borrow_data()?, - additional_accounts, - )?; - - // Add only the extra accounts resolved from the validation state - cpi_instruction - .accounts - .extend_from_slice(&execute_instruction.accounts[5..]); - cpi_account_infos.extend_from_slice(&execute_account_infos[5..]); - - // Add the validation state account - cpi_instruction - .accounts - .push(AccountMeta::new_readonly(validate_state_pubkey, false)); - cpi_account_infos.push(validate_state_info.clone()); - } - - // Add the program id - cpi_instruction - .accounts - .push(AccountMeta::new_readonly(*program_id, false)); - cpi_account_infos.push(program_info.clone()); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::instruction::ExecuteInstruction, - solana_program::{bpf_loader_upgradeable, system_program}, - spl_tlv_account_resolution::{ - account::ExtraAccountMeta, error::AccountResolutionError, seeds::Seed, - }, - }; - - const EXTRA_META_1: Pubkey = Pubkey::new_from_array([2u8; 32]); - const EXTRA_META_2: Pubkey = Pubkey::new_from_array([3u8; 32]); - - fn setup_validation_data() -> Vec { - let extra_metas = vec![ - ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(), - ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::AccountKey { index: 0 }, // source - Seed::AccountKey { index: 2 }, // destination - Seed::AccountKey { index: 4 }, // validation state - ], - false, - true, - ) - .unwrap(), - ExtraAccountMeta::new_with_seeds( - &[ - Seed::InstructionData { - index: 8, - length: 8, - }, // amount - Seed::AccountKey { index: 2 }, // destination - Seed::AccountKey { index: 5 }, // extra meta 1 - Seed::AccountKey { index: 7 }, // extra meta 3 (PDA) - ], - false, - true, - ) - .unwrap(), - ]; - let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap(); - let mut data = vec![0u8; account_size]; - ExtraAccountMetaList::init::(&mut data, &extra_metas).unwrap(); - data - } - - #[test] - fn test_add_extra_accounts_for_execute_cpi() { - let spl_token_2022_program_id = Pubkey::new_unique(); // Mock - let transfer_hook_program_id = Pubkey::new_unique(); - - let amount = 100u64; - - let source_pubkey = Pubkey::new_unique(); - let mut source_data = vec![0; 165]; // Mock - let mut source_lamports = 0; // Mock - let source_account_info = AccountInfo::new( - &source_pubkey, - false, - true, - &mut source_lamports, - &mut source_data, - &spl_token_2022_program_id, - false, - 0, - ); - - let mint_pubkey = Pubkey::new_unique(); - let mut mint_data = vec![0; 165]; // Mock - let mut mint_lamports = 0; // Mock - let mint_account_info = AccountInfo::new( - &mint_pubkey, - false, - true, - &mut mint_lamports, - &mut mint_data, - &spl_token_2022_program_id, - false, - 0, - ); - - let destination_pubkey = Pubkey::new_unique(); - let mut destination_data = vec![0; 165]; // Mock - let mut destination_lamports = 0; // Mock - let destination_account_info = AccountInfo::new( - &destination_pubkey, - false, - true, - &mut destination_lamports, - &mut destination_data, - &spl_token_2022_program_id, - false, - 0, - ); - - let authority_pubkey = Pubkey::new_unique(); - let mut authority_data = vec![]; // Mock - let mut authority_lamports = 0; // Mock - let authority_account_info = AccountInfo::new( - &authority_pubkey, - false, - true, - &mut authority_lamports, - &mut authority_data, - &system_program::ID, - false, - 0, - ); - - let validate_state_pubkey = - get_extra_account_metas_address(&mint_pubkey, &transfer_hook_program_id); - - let extra_meta_1_pubkey = EXTRA_META_1; - let mut extra_meta_1_data = vec![]; // Mock - let mut extra_meta_1_lamports = 0; // Mock - let extra_meta_1_account_info = AccountInfo::new( - &extra_meta_1_pubkey, - true, - false, - &mut extra_meta_1_lamports, - &mut extra_meta_1_data, - &system_program::ID, - false, - 0, - ); - - let extra_meta_2_pubkey = EXTRA_META_2; - let mut extra_meta_2_data = vec![]; // Mock - let mut extra_meta_2_lamports = 0; // Mock - let extra_meta_2_account_info = AccountInfo::new( - &extra_meta_2_pubkey, - true, - false, - &mut extra_meta_2_lamports, - &mut extra_meta_2_data, - &system_program::ID, - false, - 0, - ); - - let extra_meta_3_pubkey = Pubkey::find_program_address( - &[ - &source_pubkey.to_bytes(), - &destination_pubkey.to_bytes(), - &validate_state_pubkey.to_bytes(), - ], - &transfer_hook_program_id, - ) - .0; - let mut extra_meta_3_data = vec![]; // Mock - let mut extra_meta_3_lamports = 0; // Mock - let extra_meta_3_account_info = AccountInfo::new( - &extra_meta_3_pubkey, - false, - true, - &mut extra_meta_3_lamports, - &mut extra_meta_3_data, - &transfer_hook_program_id, - false, - 0, - ); - - let extra_meta_4_pubkey = Pubkey::find_program_address( - &[ - &amount.to_le_bytes(), - &destination_pubkey.to_bytes(), - &extra_meta_1_pubkey.to_bytes(), - &extra_meta_3_pubkey.to_bytes(), - ], - &transfer_hook_program_id, - ) - .0; - let mut extra_meta_4_data = vec![]; // Mock - let mut extra_meta_4_lamports = 0; // Mock - let extra_meta_4_account_info = AccountInfo::new( - &extra_meta_4_pubkey, - false, - true, - &mut extra_meta_4_lamports, - &mut extra_meta_4_data, - &transfer_hook_program_id, - false, - 0, - ); - - let mut validate_state_data = setup_validation_data(); - let mut validate_state_lamports = 0; // Mock - let validate_state_account_info = AccountInfo::new( - &validate_state_pubkey, - false, - true, - &mut validate_state_lamports, - &mut validate_state_data, - &transfer_hook_program_id, - false, - 0, - ); - - let mut transfer_hook_program_data = vec![]; // Mock - let mut transfer_hook_program_lamports = 0; // Mock - let transfer_hook_program_account_info = AccountInfo::new( - &transfer_hook_program_id, - false, - true, - &mut transfer_hook_program_lamports, - &mut transfer_hook_program_data, - &bpf_loader_upgradeable::ID, - false, - 0, - ); - - let mut cpi_instruction = Instruction::new_with_bytes( - spl_token_2022_program_id, - &[], - vec![ - AccountMeta::new(source_pubkey, false), - AccountMeta::new_readonly(mint_pubkey, false), - AccountMeta::new(destination_pubkey, false), - AccountMeta::new_readonly(authority_pubkey, true), - ], - ); - let mut cpi_account_infos = vec![ - source_account_info.clone(), - mint_account_info.clone(), - destination_account_info.clone(), - authority_account_info.clone(), - ]; - let additional_account_infos = vec![ - extra_meta_1_account_info.clone(), - extra_meta_2_account_info.clone(), - extra_meta_3_account_info.clone(), - extra_meta_4_account_info.clone(), - transfer_hook_program_account_info.clone(), - validate_state_account_info.clone(), - ]; - - // Allow missing validation info from additional account infos - { - let additional_account_infos_missing_infos = vec![ - extra_meta_1_account_info.clone(), - extra_meta_2_account_info.clone(), - extra_meta_3_account_info.clone(), - extra_meta_4_account_info.clone(), - // validate state missing - transfer_hook_program_account_info.clone(), - ]; - let mut cpi_instruction = cpi_instruction.clone(); - let mut cpi_account_infos = cpi_account_infos.clone(); - add_extra_accounts_for_execute_cpi( - &mut cpi_instruction, - &mut cpi_account_infos, - &transfer_hook_program_id, - source_account_info.clone(), - mint_account_info.clone(), - destination_account_info.clone(), - authority_account_info.clone(), - amount, - &additional_account_infos_missing_infos, - ) - .unwrap(); - let check_metas = [ - AccountMeta::new(source_pubkey, false), - AccountMeta::new_readonly(mint_pubkey, false), - AccountMeta::new(destination_pubkey, false), - AccountMeta::new_readonly(authority_pubkey, true), - AccountMeta::new_readonly(transfer_hook_program_id, false), - ]; - - let check_account_infos = vec![ - source_account_info.clone(), - mint_account_info.clone(), - destination_account_info.clone(), - authority_account_info.clone(), - transfer_hook_program_account_info.clone(), - ]; - - assert_eq!(cpi_instruction.accounts, check_metas); - for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) { - assert_eq!(a.key, b.key); - assert_eq!(a.is_signer, b.is_signer); - assert_eq!(a.is_writable, b.is_writable); - } - } - - // Fail missing program info from additional account infos - let additional_account_infos_missing_infos = vec![ - extra_meta_1_account_info.clone(), - extra_meta_2_account_info.clone(), - extra_meta_3_account_info.clone(), - extra_meta_4_account_info.clone(), - validate_state_account_info.clone(), - // transfer hook program missing - ]; - assert_eq!( - add_extra_accounts_for_execute_cpi( - &mut cpi_instruction, - &mut cpi_account_infos, - &transfer_hook_program_id, - source_account_info.clone(), - mint_account_info.clone(), - destination_account_info.clone(), - authority_account_info.clone(), - amount, - &additional_account_infos_missing_infos, // Missing account info - ) - .unwrap_err(), - TransferHookError::IncorrectAccount.into() - ); - - // Fail missing extra meta info from additional account infos - let additional_account_infos_missing_infos = vec![ - extra_meta_1_account_info.clone(), - extra_meta_2_account_info.clone(), - // extra meta 3 missing - extra_meta_4_account_info.clone(), - validate_state_account_info.clone(), - transfer_hook_program_account_info.clone(), - ]; - assert_eq!( - add_extra_accounts_for_execute_cpi( - &mut cpi_instruction, - &mut cpi_account_infos, - &transfer_hook_program_id, - source_account_info.clone(), - mint_account_info.clone(), - destination_account_info.clone(), - authority_account_info.clone(), - amount, - &additional_account_infos_missing_infos, // Missing account info - ) - .unwrap_err(), - AccountResolutionError::IncorrectAccount.into() // Note the error - ); - - // Success - add_extra_accounts_for_execute_cpi( - &mut cpi_instruction, - &mut cpi_account_infos, - &transfer_hook_program_id, - source_account_info.clone(), - mint_account_info.clone(), - destination_account_info.clone(), - authority_account_info.clone(), - amount, - &additional_account_infos, - ) - .unwrap(); - - let check_metas = [ - AccountMeta::new(source_pubkey, false), - AccountMeta::new_readonly(mint_pubkey, false), - AccountMeta::new(destination_pubkey, false), - AccountMeta::new_readonly(authority_pubkey, true), - AccountMeta::new_readonly(EXTRA_META_1, true), - AccountMeta::new_readonly(EXTRA_META_2, true), - AccountMeta::new(extra_meta_3_pubkey, false), - AccountMeta::new(extra_meta_4_pubkey, false), - AccountMeta::new_readonly(validate_state_pubkey, false), - AccountMeta::new_readonly(transfer_hook_program_id, false), - ]; - - let check_account_infos = vec![ - source_account_info, - mint_account_info, - destination_account_info, - authority_account_info, - extra_meta_1_account_info, - extra_meta_2_account_info, - extra_meta_3_account_info, - extra_meta_4_account_info, - validate_state_account_info, - transfer_hook_program_account_info, - ]; - - assert_eq!(cpi_instruction.accounts, check_metas); - for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) { - assert_eq!(a.key, b.key); - assert_eq!(a.is_signer, b.is_signer); - assert_eq!(a.is_writable, b.is_writable); - } - } -} diff --git a/token/zk-token-protocol-paper/part1.pdf b/token/zk-token-protocol-paper/part1.pdf deleted file mode 100644 index 41384d32d03..00000000000 Binary files a/token/zk-token-protocol-paper/part1.pdf and /dev/null differ diff --git a/token/zk-token-protocol-paper/part2.pdf b/token/zk-token-protocol-paper/part2.pdf deleted file mode 100644 index 09ed0e81232..00000000000 Binary files a/token/zk-token-protocol-paper/part2.pdf and /dev/null differ diff --git a/utils/test-client/Cargo.toml b/utils/test-client/Cargo.toml index ffc54b1163c..12f53134e6b 100644 --- a/utils/test-client/Cargo.toml +++ b/utils/test-client/Cargo.toml @@ -10,5 +10,5 @@ edition = "2021" [dependencies] solana-sdk = "2.1.0" spl-memo = { version = "6.0.0", features = [ "no-entrypoint" ] } -spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] } +spl-token = { version = "7.0.0", features = [ "no-entrypoint" ] } spl-token-swap = { path = "../../token-swap/program", features = [ "no-entrypoint" ] }