From cace777f78f0a382a02baf33f689ccd6ad730a8a Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:47:55 +0100 Subject: [PATCH 01/17] CI: Remove CI files for removed programs --- .../pull-request-feature-proposal.yml | 64 ---- .../pull-request-instruction-padding.yml | 62 ---- .github/workflows/pull-request-record.yml | 60 ---- .../workflows/pull-request-single-pool.yml | 132 ------- .github/workflows/pull-request-stake-pool.yml | 129 ------- .../workflows/pull-request-token-group.yml | 91 ----- .../workflows/pull-request-token-metadata.yml | 92 ----- .github/workflows/pull-request-token.yml | 337 ------------------ ci/js-test-libraries.sh | 13 - ci/js-test-single-pool.sh | 18 - ci/js-test-stake-pool.sh | 12 - ci/js-test-token-group.sh | 13 - ci/js-test-token-metadata.sh | 13 - ci/js-test-token.sh | 15 - ci/py-test-stake-pool.sh | 23 -- 15 files changed, 1074 deletions(-) delete mode 100644 .github/workflows/pull-request-feature-proposal.yml delete mode 100644 .github/workflows/pull-request-instruction-padding.yml delete mode 100644 .github/workflows/pull-request-record.yml delete mode 100644 .github/workflows/pull-request-single-pool.yml delete mode 100644 .github/workflows/pull-request-stake-pool.yml delete mode 100644 .github/workflows/pull-request-token-group.yml delete mode 100644 .github/workflows/pull-request-token-metadata.yml delete mode 100644 .github/workflows/pull-request-token.yml delete mode 100755 ci/js-test-libraries.sh delete mode 100755 ci/js-test-single-pool.sh delete mode 100755 ci/js-test-stake-pool.sh delete mode 100755 ci/js-test-token-group.sh delete mode 100755 ci/js-test-token-metadata.sh delete mode 100755 ci/js-test-token.sh delete mode 100755 ci/py-test-stake-pool.sh 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-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/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 From b5cb27b7d8a41495af8e094541585fda28bcee07 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:48:31 +0100 Subject: [PATCH 02/17] docs: Point everything at the new repos --- docs/src/associated-token-account.md | 4 ++-- docs/src/feature-proposal.md | 4 ++-- docs/src/single-pool.mdx | 8 +++---- docs/src/stake-pool.md | 20 ++++++++--------- docs/src/token-2022.md | 16 +++++++------- docs/src/token-2022/extensions.mdx | 22 +++++++++---------- docs/src/token.mdx | 6 ++--- docs/src/transfer-hook-interface.md | 2 +- .../configuring-extra-accounts.md | 14 ++++++------ docs/src/transfer-hook-interface/examples.md | 14 ++++++------ 10 files changed, 55 insertions(+), 55 deletions(-) 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 From 5ab996e03f22c8126c32fe9c7e58dd8e0645f243 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:49:11 +0100 Subject: [PATCH 03/17] Remove from CI (more) --- .github/workflows/pull-request-js.yml | 22 -------------------- .github/workflows/pull-request-libraries.yml | 21 ------------------- coverage.sh | 1 - 3 files changed, 44 deletions(-) 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/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 ) From bd61a20aca15c4733567eaf9ca206b9824716126 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:49:26 +0100 Subject: [PATCH 04/17] README: Update internal documentation --- README.md | 70 ++++++++++++++++++++++++++----------------- SECURITY.md | 86 ++++++++--------------------------------------------- 2 files changed, 56 insertions(+), 100 deletions(-) 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) From c4a84e44955819e4b0701d39628c9fceb680434e Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:50:00 +0100 Subject: [PATCH 05/17] ATA: Remove program, add README explaining --- associated-token-account/README.md | 2 + associated-token-account/client/Cargo.toml | 18 - .../client/src/address.rs | 70 -- .../client/src/instruction.rs | 116 --- associated-token-account/client/src/lib.rs | 11 - .../program-test/Cargo.toml | 20 - .../program-test/tests/create_idempotent.rs | 244 ------- .../program-test/tests/extended_mint.rs | 216 ------ .../tests/fixtures/token-mint-data.bin | Bin 82 -> 0 bytes ...process_create_associated_token_account.rs | 318 -------- .../program-test/tests/program_test.rs | 84 --- .../program-test/tests/recover_nested.rs | 678 ------------------ .../program-test/tests/spl_token_create.rs | 112 --- associated-token-account/program/Cargo.toml | 32 - associated-token-account/program/Xargo.toml | 2 - .../program/program-id.md | 1 - associated-token-account/program/run-tests.sh | 14 - .../program/src/entrypoint.rs | 14 - associated-token-account/program/src/error.rs | 26 - .../program/src/instruction.rs | 50 -- associated-token-account/program/src/lib.rs | 66 -- .../program/src/processor.rs | 309 -------- .../program/src/tools/account.rs | 100 --- .../program/src/tools/mod.rs | 3 - 24 files changed, 2 insertions(+), 2504 deletions(-) create mode 100644 associated-token-account/README.md delete mode 100644 associated-token-account/client/Cargo.toml delete mode 100644 associated-token-account/client/src/address.rs delete mode 100644 associated-token-account/client/src/instruction.rs delete mode 100644 associated-token-account/client/src/lib.rs delete mode 100644 associated-token-account/program-test/Cargo.toml delete mode 100644 associated-token-account/program-test/tests/create_idempotent.rs delete mode 100644 associated-token-account/program-test/tests/extended_mint.rs delete mode 100644 associated-token-account/program-test/tests/fixtures/token-mint-data.bin delete mode 100644 associated-token-account/program-test/tests/process_create_associated_token_account.rs delete mode 100644 associated-token-account/program-test/tests/program_test.rs delete mode 100644 associated-token-account/program-test/tests/recover_nested.rs delete mode 100644 associated-token-account/program-test/tests/spl_token_create.rs delete mode 100644 associated-token-account/program/Cargo.toml delete mode 100644 associated-token-account/program/Xargo.toml delete mode 100644 associated-token-account/program/program-id.md delete mode 100755 associated-token-account/program/run-tests.sh delete mode 100644 associated-token-account/program/src/entrypoint.rs delete mode 100644 associated-token-account/program/src/error.rs delete mode 100644 associated-token-account/program/src/instruction.rs delete mode 100644 associated-token-account/program/src/lib.rs delete mode 100644 associated-token-account/program/src/processor.rs delete mode 100644 associated-token-account/program/src/tools/account.rs delete mode 100644 associated-token-account/program/src/tools/mod.rs 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 4a48512c02af880b699bc2e6ede5e3e4a2048a99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82 zcmV-Y0ImN40000S<5}%m0WJjk6f2x{8XR7S&(NS28=Qsz(;Ilr{Mhz@hD3Ix4FCWJ o0RaF204knd+qFCdXONixdlF?A6hlwIj8-a|JBASj=5o{`bN`JWZ~y=R 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; From c1f61ac149d11feb88a075ae12fa6d9d061f7fc9 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:50:57 +0100 Subject: [PATCH 06/17] feature-proposal: Remove program --- feature-proposal/README.md | 2 + feature-proposal/cli/Cargo.toml | 25 - feature-proposal/cli/src/main.rs | 486 ------------------- feature-proposal/program/Cargo.toml | 32 -- feature-proposal/program/Xargo.toml | 2 - feature-proposal/program/program-id.md | 1 - feature-proposal/program/run-tests.sh | 15 - feature-proposal/program/src/entrypoint.rs | 14 - feature-proposal/program/src/instruction.rs | 227 --------- feature-proposal/program/src/lib.rs | 78 --- feature-proposal/program/src/processor.rs | 371 -------------- feature-proposal/program/src/state.rs | 117 ----- feature-proposal/program/tests/functional.rs | 206 -------- 13 files changed, 2 insertions(+), 1574 deletions(-) create mode 100644 feature-proposal/README.md delete mode 100644 feature-proposal/cli/Cargo.toml delete mode 100644 feature-proposal/cli/src/main.rs delete mode 100644 feature-proposal/program/Cargo.toml delete mode 100644 feature-proposal/program/Xargo.toml delete mode 100644 feature-proposal/program/program-id.md delete mode 100755 feature-proposal/program/run-tests.sh delete mode 100644 feature-proposal/program/src/entrypoint.rs delete mode 100644 feature-proposal/program/src/instruction.rs delete mode 100644 feature-proposal/program/src/lib.rs delete mode 100644 feature-proposal/program/src/processor.rs delete mode 100644 feature-proposal/program/src/state.rs delete mode 100644 feature-proposal/program/tests/functional.rs 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) - )); -} From 269cb0f957e0dae67f76eb8f3f4a9e183047be24 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:51:33 +0100 Subject: [PATCH 07/17] instruction-padding: Remove program --- instruction-padding/README.md | 2 + instruction-padding/program/Cargo.toml | 34 ---- instruction-padding/program/README.md | 29 --- instruction-padding/program/src/entrypoint.rs | 16 -- .../program/src/instruction.rs | 169 ------------------ instruction-padding/program/src/lib.rs | 9 - instruction-padding/program/src/processor.rs | 54 ------ instruction-padding/program/tests/noop.rs | 38 ---- instruction-padding/program/tests/system.rs | 62 ------- 9 files changed, 2 insertions(+), 411 deletions(-) create mode 100644 instruction-padding/README.md delete mode 100644 instruction-padding/program/Cargo.toml delete mode 100644 instruction-padding/program/README.md delete mode 100644 instruction-padding/program/src/entrypoint.rs delete mode 100644 instruction-padding/program/src/instruction.rs delete mode 100644 instruction-padding/program/src/lib.rs delete mode 100644 instruction-padding/program/src/processor.rs delete mode 100644 instruction-padding/program/tests/noop.rs delete mode 100644 instruction-padding/program/tests/system.rs 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 - ); -} From 89eb27600b4203e5fb20264cbb8e283c33969643 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:54:28 +0100 Subject: [PATCH 08/17] libraries: Remove ported libraries --- libraries/README.md | 13 + libraries/discriminator/Cargo.toml | 24 - libraries/discriminator/README.md | 57 - libraries/discriminator/derive/Cargo.toml | 19 - libraries/discriminator/derive/src/lib.rs | 20 - libraries/discriminator/src/discriminator.rs | 83 - libraries/discriminator/src/lib.rs | 144 -- libraries/discriminator/syn/Cargo.toml | 18 - libraries/discriminator/syn/src/error.rs | 12 - libraries/discriminator/syn/src/lib.rs | 108 -- libraries/discriminator/syn/src/parser.rs | 43 - libraries/pod/Cargo.toml | 37 - libraries/pod/README.md | 3 - libraries/pod/src/bytemuck.rs | 53 - libraries/pod/src/error.rs | 56 - libraries/pod/src/lib.rs | 14 - libraries/pod/src/option.rs | 188 -- libraries/pod/src/optional_keys.rs | 360 ---- libraries/pod/src/primitives.rs | 269 --- libraries/pod/src/slice.rs | 220 --- libraries/program-error/Cargo.toml | 26 - libraries/program-error/README.md | 293 --- libraries/program-error/derive/Cargo.toml | 17 - libraries/program-error/derive/src/lib.rs | 85 - .../program-error/derive/src/macro_impl.rs | 207 -- libraries/program-error/derive/src/parser.rs | 145 -- libraries/program-error/src/lib.rs | 17 - libraries/program-error/tests/bench.rs | 50 - libraries/program-error/tests/decode.rs | 29 - libraries/program-error/tests/into.rs | 22 - libraries/program-error/tests/mod.rs | 141 -- libraries/program-error/tests/print.rs | 30 - libraries/program-error/tests/spl.rs | 91 - libraries/tlv-account-resolution/Cargo.toml | 42 - libraries/tlv-account-resolution/README.md | 198 -- .../tlv-account-resolution/src/account.rs | 296 --- libraries/tlv-account-resolution/src/error.rs | 165 -- libraries/tlv-account-resolution/src/lib.rs | 21 - .../tlv-account-resolution/src/pubkey_data.rs | 185 -- libraries/tlv-account-resolution/src/seeds.rs | 524 ----- libraries/tlv-account-resolution/src/state.rs | 1726 ----------------- .../type-length-value-derive-test/Cargo.toml | 16 - .../type-length-value-derive-test/src/lib.rs | 59 - libraries/type-length-value/Cargo.toml | 31 - libraries/type-length-value/README.md | 196 -- libraries/type-length-value/derive/Cargo.toml | 16 - .../type-length-value/derive/src/builder.rs | 91 - libraries/type-length-value/derive/src/lib.rs | 22 - libraries/type-length-value/js/.eslintignore | 5 - libraries/type-length-value/js/.eslintrc | 34 - libraries/type-length-value/js/.gitignore | 13 - libraries/type-length-value/js/.mocharc.json | 5 - libraries/type-length-value/js/.nojekyll | 0 libraries/type-length-value/js/LICENSE | 202 -- libraries/type-length-value/js/README.md | 18 - libraries/type-length-value/js/package.json | 65 - libraries/type-length-value/js/src/errors.ts | 11 - libraries/type-length-value/js/src/index.ts | 3 - .../js/src/splDiscriminate.ts | 8 - .../type-length-value/js/src/tlvState.ts | 109 -- .../js/test/splDiscriminate.test.ts | 38 - .../type-length-value/js/test/tlvData.test.ts | 147 -- .../type-length-value/js/tsconfig.all.json | 11 - .../type-length-value/js/tsconfig.base.json | 14 - .../type-length-value/js/tsconfig.cjs.json | 10 - .../type-length-value/js/tsconfig.esm.json | 13 - libraries/type-length-value/js/tsconfig.json | 8 - .../type-length-value/js/tsconfig.root.json | 6 - libraries/type-length-value/js/typedoc.json | 5 - libraries/type-length-value/src/error.rs | 50 - libraries/type-length-value/src/length.rs | 25 - libraries/type-length-value/src/lib.rs | 18 - libraries/type-length-value/src/state.rs | 1365 ------------- .../src/variable_len_pack.rs | 27 - 74 files changed, 13 insertions(+), 8679 deletions(-) create mode 100644 libraries/README.md delete mode 100644 libraries/discriminator/Cargo.toml delete mode 100644 libraries/discriminator/README.md delete mode 100644 libraries/discriminator/derive/Cargo.toml delete mode 100644 libraries/discriminator/derive/src/lib.rs delete mode 100644 libraries/discriminator/src/discriminator.rs delete mode 100644 libraries/discriminator/src/lib.rs delete mode 100644 libraries/discriminator/syn/Cargo.toml delete mode 100644 libraries/discriminator/syn/src/error.rs delete mode 100644 libraries/discriminator/syn/src/lib.rs delete mode 100644 libraries/discriminator/syn/src/parser.rs delete mode 100644 libraries/pod/Cargo.toml delete mode 100644 libraries/pod/README.md delete mode 100644 libraries/pod/src/bytemuck.rs delete mode 100644 libraries/pod/src/error.rs delete mode 100644 libraries/pod/src/lib.rs delete mode 100644 libraries/pod/src/option.rs delete mode 100644 libraries/pod/src/optional_keys.rs delete mode 100644 libraries/pod/src/primitives.rs delete mode 100644 libraries/pod/src/slice.rs delete mode 100644 libraries/program-error/Cargo.toml delete mode 100644 libraries/program-error/README.md delete mode 100644 libraries/program-error/derive/Cargo.toml delete mode 100644 libraries/program-error/derive/src/lib.rs delete mode 100644 libraries/program-error/derive/src/macro_impl.rs delete mode 100644 libraries/program-error/derive/src/parser.rs delete mode 100644 libraries/program-error/src/lib.rs delete mode 100644 libraries/program-error/tests/bench.rs delete mode 100644 libraries/program-error/tests/decode.rs delete mode 100644 libraries/program-error/tests/into.rs delete mode 100644 libraries/program-error/tests/mod.rs delete mode 100644 libraries/program-error/tests/print.rs delete mode 100644 libraries/program-error/tests/spl.rs delete mode 100644 libraries/tlv-account-resolution/Cargo.toml delete mode 100644 libraries/tlv-account-resolution/README.md delete mode 100644 libraries/tlv-account-resolution/src/account.rs delete mode 100644 libraries/tlv-account-resolution/src/error.rs delete mode 100644 libraries/tlv-account-resolution/src/lib.rs delete mode 100644 libraries/tlv-account-resolution/src/pubkey_data.rs delete mode 100644 libraries/tlv-account-resolution/src/seeds.rs delete mode 100644 libraries/tlv-account-resolution/src/state.rs delete mode 100644 libraries/type-length-value-derive-test/Cargo.toml delete mode 100644 libraries/type-length-value-derive-test/src/lib.rs delete mode 100644 libraries/type-length-value/Cargo.toml delete mode 100644 libraries/type-length-value/README.md delete mode 100644 libraries/type-length-value/derive/Cargo.toml delete mode 100644 libraries/type-length-value/derive/src/builder.rs delete mode 100644 libraries/type-length-value/derive/src/lib.rs delete mode 100644 libraries/type-length-value/js/.eslintignore delete mode 100644 libraries/type-length-value/js/.eslintrc delete mode 100644 libraries/type-length-value/js/.gitignore delete mode 100644 libraries/type-length-value/js/.mocharc.json delete mode 100644 libraries/type-length-value/js/.nojekyll delete mode 100644 libraries/type-length-value/js/LICENSE delete mode 100644 libraries/type-length-value/js/README.md delete mode 100644 libraries/type-length-value/js/package.json delete mode 100644 libraries/type-length-value/js/src/errors.ts delete mode 100644 libraries/type-length-value/js/src/index.ts delete mode 100644 libraries/type-length-value/js/src/splDiscriminate.ts delete mode 100644 libraries/type-length-value/js/src/tlvState.ts delete mode 100644 libraries/type-length-value/js/test/splDiscriminate.test.ts delete mode 100644 libraries/type-length-value/js/test/tlvData.test.ts delete mode 100644 libraries/type-length-value/js/tsconfig.all.json delete mode 100644 libraries/type-length-value/js/tsconfig.base.json delete mode 100644 libraries/type-length-value/js/tsconfig.cjs.json delete mode 100644 libraries/type-length-value/js/tsconfig.esm.json delete mode 100644 libraries/type-length-value/js/tsconfig.json delete mode 100644 libraries/type-length-value/js/tsconfig.root.json delete mode 100644 libraries/type-length-value/js/typedoc.json delete mode 100644 libraries/type-length-value/src/error.rs delete mode 100644 libraries/type-length-value/src/length.rs delete mode 100644 libraries/type-length-value/src/lib.rs delete mode 100644 libraries/type-length-value/src/state.rs delete mode 100644 libraries/type-length-value/src/variable_len_pack.rs 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/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) - } -} From de50b68f69649199a3c4ad5f5881462230f28f35 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:55:03 +0100 Subject: [PATCH 09/17] record: Remove program --- record/README.md | 2 + record/program/Cargo.toml | 40 -- record/program/README.md | 9 - record/program/program-id.md | 1 - record/program/src/entrypoint.rs | 16 - record/program/src/error.rs | 28 -- record/program/src/instruction.rs | 260 ----------- record/program/src/lib.rs | 17 - record/program/src/processor.rs | 181 -------- record/program/src/state.rs | 71 --- record/program/tests/functional.rs | 698 ----------------------------- 11 files changed, 2 insertions(+), 1321 deletions(-) create mode 100644 record/README.md delete mode 100644 record/program/Cargo.toml delete mode 100644 record/program/README.md delete mode 100644 record/program/program-id.md delete mode 100644 record/program/src/entrypoint.rs delete mode 100644 record/program/src/error.rs delete mode 100644 record/program/src/instruction.rs delete mode 100644 record/program/src/lib.rs delete mode 100644 record/program/src/processor.rs delete mode 100644 record/program/src/state.rs delete mode 100644 record/program/tests/functional.rs 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) - ); -} From a72b3ab01700168546fa2ac616847058228db93f Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:56:00 +0100 Subject: [PATCH 10/17] single-pool: Remove program / cli / js --- single-pool/README.md | 11 +- single-pool/cli/Cargo.toml | 46 - single-pool/cli/src/cli.rs | 473 ----- single-pool/cli/src/config.rs | 129 -- single-pool/cli/src/main.rs | 876 ---------- single-pool/cli/src/output.rs | 307 ---- single-pool/cli/src/quarantine.rs | 78 - single-pool/cli/tests/test.rs | 429 ----- single-pool/js/LICENSE | 202 --- single-pool/js/packages/classic/.eslintignore | 4 - single-pool/js/packages/classic/.eslintrc.cjs | 21 - single-pool/js/packages/classic/.gitignore | 1 - .../js/packages/classic/.prettierrc.cjs | 7 - single-pool/js/packages/classic/README.md | 11 - single-pool/js/packages/classic/package.json | 43 - .../js/packages/classic/src/addresses.ts | 72 - single-pool/js/packages/classic/src/index.ts | 19 - .../js/packages/classic/src/instructions.ts | 82 - .../js/packages/classic/src/internal.ts | 71 - .../js/packages/classic/src/mpl_metadata.ts | 12 - .../js/packages/classic/src/transactions.ts | 115 -- .../tests/fixtures/mpl_token_metadata.so | 1 - .../classic/tests/fixtures/spl_single_pool.so | 1 - .../classic/tests/transactions.test.ts | 432 ----- .../packages/classic/tests/vote_account.json | 14 - single-pool/js/packages/classic/ts-fixup.sh | 1 - .../js/packages/classic/tsconfig-base.json | 1 - .../js/packages/classic/tsconfig-cjs.json | 1 - single-pool/js/packages/classic/tsconfig.json | 1 - single-pool/js/packages/modern/.eslintignore | 4 - single-pool/js/packages/modern/.eslintrc.cjs | 21 - single-pool/js/packages/modern/.gitignore | 1 - .../js/packages/modern/.prettierrc.cjs | 7 - single-pool/js/packages/modern/README.md | 11 - single-pool/js/packages/modern/package.json | 29 - .../js/packages/modern/src/addresses.ts | 117 -- single-pool/js/packages/modern/src/index.ts | 19 - .../js/packages/modern/src/instructions.ts | 381 ---- .../js/packages/modern/src/internal.ts | 3 - .../js/packages/modern/src/quarantine.ts | 236 --- .../js/packages/modern/src/transactions.ts | 347 ---- single-pool/js/packages/modern/ts-fixup.sh | 1 - .../js/packages/modern/tsconfig-base.json | 1 - .../js/packages/modern/tsconfig-cjs.json | 1 - single-pool/js/packages/modern/tsconfig.json | 1 - single-pool/js/ts-fixup.sh | 11 - single-pool/js/tsconfig-base.json | 25 - single-pool/js/tsconfig-cjs.json | 8 - single-pool/js/tsconfig.json | 8 - single-pool/program/Cargo.toml | 44 - single-pool/program/program-id.md | 1 - single-pool/program/src/entrypoint.rs | 42 - single-pool/program/src/error.rs | 157 -- .../program/src/inline_mpl_token_metadata.rs | 1 - single-pool/program/src/instruction.rs | 473 ----- single-pool/program/src/lib.rs | 125 -- single-pool/program/src/processor.rs | 1539 ----------------- single-pool/program/src/state.rs | 57 - single-pool/program/tests/accounts.rs | 304 ---- .../tests/create_pool_token_metadata.rs | 57 - single-pool/program/tests/deposit.rs | 477 ----- .../tests/fixtures/mpl_token_metadata.so | 1 - single-pool/program/tests/helpers/mod.rs | 450 ----- single-pool/program/tests/helpers/stake.rs | 140 -- single-pool/program/tests/helpers/token.rs | 76 - single-pool/program/tests/initialize.rs | 116 -- single-pool/program/tests/reactivate.rs | 156 -- .../tests/update_pool_token_metadata.rs | 127 -- single-pool/program/tests/withdraw.rs | 257 --- 69 files changed, 2 insertions(+), 9291 deletions(-) delete mode 100644 single-pool/cli/Cargo.toml delete mode 100644 single-pool/cli/src/cli.rs delete mode 100644 single-pool/cli/src/config.rs delete mode 100644 single-pool/cli/src/main.rs delete mode 100644 single-pool/cli/src/output.rs delete mode 100644 single-pool/cli/src/quarantine.rs delete mode 100644 single-pool/cli/tests/test.rs delete mode 100644 single-pool/js/LICENSE delete mode 100644 single-pool/js/packages/classic/.eslintignore delete mode 100644 single-pool/js/packages/classic/.eslintrc.cjs delete mode 100644 single-pool/js/packages/classic/.gitignore delete mode 100644 single-pool/js/packages/classic/.prettierrc.cjs delete mode 100644 single-pool/js/packages/classic/README.md delete mode 100644 single-pool/js/packages/classic/package.json delete mode 100644 single-pool/js/packages/classic/src/addresses.ts delete mode 100644 single-pool/js/packages/classic/src/index.ts delete mode 100644 single-pool/js/packages/classic/src/instructions.ts delete mode 100644 single-pool/js/packages/classic/src/internal.ts delete mode 100644 single-pool/js/packages/classic/src/mpl_metadata.ts delete mode 100644 single-pool/js/packages/classic/src/transactions.ts delete mode 120000 single-pool/js/packages/classic/tests/fixtures/mpl_token_metadata.so delete mode 120000 single-pool/js/packages/classic/tests/fixtures/spl_single_pool.so delete mode 100644 single-pool/js/packages/classic/tests/transactions.test.ts delete mode 100644 single-pool/js/packages/classic/tests/vote_account.json delete mode 120000 single-pool/js/packages/classic/ts-fixup.sh delete mode 120000 single-pool/js/packages/classic/tsconfig-base.json delete mode 120000 single-pool/js/packages/classic/tsconfig-cjs.json delete mode 120000 single-pool/js/packages/classic/tsconfig.json delete mode 100644 single-pool/js/packages/modern/.eslintignore delete mode 100644 single-pool/js/packages/modern/.eslintrc.cjs delete mode 100644 single-pool/js/packages/modern/.gitignore delete mode 100644 single-pool/js/packages/modern/.prettierrc.cjs delete mode 100644 single-pool/js/packages/modern/README.md delete mode 100644 single-pool/js/packages/modern/package.json delete mode 100644 single-pool/js/packages/modern/src/addresses.ts delete mode 100644 single-pool/js/packages/modern/src/index.ts delete mode 100644 single-pool/js/packages/modern/src/instructions.ts delete mode 100644 single-pool/js/packages/modern/src/internal.ts delete mode 100644 single-pool/js/packages/modern/src/quarantine.ts delete mode 100644 single-pool/js/packages/modern/src/transactions.ts delete mode 120000 single-pool/js/packages/modern/ts-fixup.sh delete mode 120000 single-pool/js/packages/modern/tsconfig-base.json delete mode 120000 single-pool/js/packages/modern/tsconfig-cjs.json delete mode 120000 single-pool/js/packages/modern/tsconfig.json delete mode 100755 single-pool/js/ts-fixup.sh delete mode 100644 single-pool/js/tsconfig-base.json delete mode 100644 single-pool/js/tsconfig-cjs.json delete mode 100644 single-pool/js/tsconfig.json delete mode 100644 single-pool/program/Cargo.toml delete mode 100644 single-pool/program/program-id.md delete mode 100644 single-pool/program/src/entrypoint.rs delete mode 100644 single-pool/program/src/error.rs delete mode 120000 single-pool/program/src/inline_mpl_token_metadata.rs delete mode 100644 single-pool/program/src/instruction.rs delete mode 100644 single-pool/program/src/lib.rs delete mode 100644 single-pool/program/src/processor.rs delete mode 100644 single-pool/program/src/state.rs delete mode 100644 single-pool/program/tests/accounts.rs delete mode 100644 single-pool/program/tests/create_pool_token_metadata.rs delete mode 100644 single-pool/program/tests/deposit.rs delete mode 120000 single-pool/program/tests/fixtures/mpl_token_metadata.so delete mode 100644 single-pool/program/tests/helpers/mod.rs delete mode 100644 single-pool/program/tests/helpers/stake.rs delete mode 100644 single-pool/program/tests/helpers/token.rs delete mode 100644 single-pool/program/tests/initialize.rs delete mode 100644 single-pool/program/tests/reactivate.rs delete mode 100644 single-pool/program/tests/update_pool_token_metadata.rs delete mode 100644 single-pool/program/tests/withdraw.rs 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/vNxdDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA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", - "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); -} From fbe28f3e5db561cad017945da31aff0820abe74f Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:56:31 +0100 Subject: [PATCH 11/17] slashing: Remove program --- slashing/README.md | 2 + slashing/program/Cargo.toml | 48 - slashing/program/program-id.md | 1 - slashing/program/src/duplicate_block_proof.rs | 872 ------------------ slashing/program/src/entrypoint.rs | 14 - slashing/program/src/error.rs | 83 -- slashing/program/src/instruction.rs | 144 --- slashing/program/src/lib.rs | 16 - slashing/program/src/processor.rs | 166 ---- slashing/program/src/shred.rs | 564 ----------- slashing/program/src/state.rs | 82 -- .../program/tests/duplicate_block_proof.rs | 390 -------- 12 files changed, 2 insertions(+), 2380 deletions(-) delete mode 100644 slashing/program/Cargo.toml delete mode 100644 slashing/program/program-id.md delete mode 100644 slashing/program/src/duplicate_block_proof.rs delete mode 100644 slashing/program/src/entrypoint.rs delete mode 100644 slashing/program/src/error.rs delete mode 100644 slashing/program/src/instruction.rs delete mode 100644 slashing/program/src/lib.rs delete mode 100644 slashing/program/src/processor.rs delete mode 100644 slashing/program/src/shred.rs delete mode 100644 slashing/program/src/state.rs delete mode 100644 slashing/program/tests/duplicate_block_proof.rs 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); -} From fb56d8df83b68b8db3f582325c9bd0c7946ca8b3 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 17:58:24 +0100 Subject: [PATCH 12/17] stake-pool: Remove program and clients --- stake-pool/README.md | 16 +- stake-pool/cli/Cargo.toml | 41 - stake-pool/cli/README.md | 7 - stake-pool/cli/scripts/add-validators.sh | 25 - stake-pool/cli/scripts/deposit.sh | 71 - stake-pool/cli/scripts/rebalance.sh | 27 - stake-pool/cli/scripts/setup-stake-pool.sh | 86 - .../cli/scripts/setup-test-validator.sh | 67 - stake-pool/cli/scripts/withdraw.sh | 75 - stake-pool/cli/src/client.rs | 184 - stake-pool/cli/src/main.rs | 3479 --------------- stake-pool/cli/src/output.rs | 505 --- stake-pool/js/.eslintignore | 4 - stake-pool/js/.eslintrc.js | 21 - stake-pool/js/.gitignore | 22 - stake-pool/js/.prettierrc.js | 7 - stake-pool/js/README.md | 54 - stake-pool/js/package.json | 90 - stake-pool/js/rollup.config.mjs | 113 - stake-pool/js/src/codecs.ts | 159 - stake-pool/js/src/constants.ts | 24 - stake-pool/js/src/index.ts | 1228 ------ stake-pool/js/src/instructions.ts | 1095 ----- stake-pool/js/src/layouts.ts | 246 -- stake-pool/js/src/types/buffer-layout.d.ts | 29 - stake-pool/js/src/utils/index.ts | 12 - stake-pool/js/src/utils/instruction.ts | 46 - stake-pool/js/src/utils/math.ts | 27 - stake-pool/js/src/utils/program-address.ts | 89 - stake-pool/js/src/utils/stake.ts | 229 - stake-pool/js/test/calculation.test.ts | 15 - stake-pool/js/test/equal.ts | 37 - stake-pool/js/test/instructions.test.ts | 541 --- stake-pool/js/test/layouts.test.ts | 35 - stake-pool/js/test/mocks.ts | 217 - stake-pool/js/tsconfig.json | 22 - stake-pool/program/Cargo.toml | 49 - stake-pool/program/Xargo.toml | 2 - stake-pool/program/program-id.md | 1 - .../program/proptest-regressions/state.txt | 1 - stake-pool/program/src/big_vec.rs | 302 -- stake-pool/program/src/entrypoint.rs | 42 - stake-pool/program/src/error.rs | 177 - .../program/src/inline_mpl_token_metadata.rs | 138 - stake-pool/program/src/instruction.rs | 2648 ------------ stake-pool/program/src/lib.rs | 175 - stake-pool/program/src/processor.rs | 3715 ----------------- stake-pool/program/src/state.rs | 1465 ------- .../tests/create_pool_token_metadata.rs | 273 -- stake-pool/program/tests/decrease.rs | 661 --- stake-pool/program/tests/deposit.rs | 905 ---- stake-pool/program/tests/deposit_authority.rs | 222 - .../program/tests/deposit_edge_cases.rs | 335 -- stake-pool/program/tests/deposit_sol.rs | 605 --- .../tests/fixtures/mpl_token_metadata.so | Bin 693904 -> 0 bytes stake-pool/program/tests/force_destake.rs | 343 -- stake-pool/program/tests/helpers/mod.rs | 2678 ------------ stake-pool/program/tests/huge_pool.rs | 710 ---- stake-pool/program/tests/increase.rs | 595 --- stake-pool/program/tests/initialize.rs | 1632 -------- stake-pool/program/tests/set_deposit_fee.rs | 279 -- stake-pool/program/tests/set_epoch_fee.rs | 245 -- .../program/tests/set_funding_authority.rs | 264 -- stake-pool/program/tests/set_manager.rs | 288 -- stake-pool/program/tests/set_preferred.rs | 263 -- stake-pool/program/tests/set_referral_fee.rs | 263 -- stake-pool/program/tests/set_staker.rs | 181 - .../program/tests/set_withdrawal_fee.rs | 913 ---- .../tests/update_pool_token_metadata.rs | 191 - .../tests/update_stake_pool_balance.rs | 413 -- .../tests/update_validator_list_balance.rs | 737 ---- .../update_validator_list_balance_hijack.rs | 547 --- stake-pool/program/tests/vsa_add.rs | 720 ---- stake-pool/program/tests/vsa_remove.rs | 845 ---- stake-pool/program/tests/withdraw.rs | 840 ---- .../program/tests/withdraw_edge_cases.rs | 877 ---- stake-pool/program/tests/withdraw_sol.rs | 394 -- stake-pool/program/tests/withdraw_with_fee.rs | 223 - stake-pool/py/.flake8 | 2 - stake-pool/py/.gitignore | 7 - stake-pool/py/LICENSE | 13 - stake-pool/py/README.md | 68 - stake-pool/py/bot/__init__.py | 0 stake-pool/py/bot/rebalance.py | 132 - stake-pool/py/optional-requirements.txt | 11 - stake-pool/py/requirements.txt | 14 - stake-pool/py/spl_token/__init__.py | 0 stake-pool/py/spl_token/actions.py | 62 - stake-pool/py/stake/__init__.py | 0 stake-pool/py/stake/actions.py | 91 - stake-pool/py/stake/constants.py | 18 - stake-pool/py/stake/instructions.py | 178 - stake-pool/py/stake/state.py | 130 - stake-pool/py/stake_pool/__init__.py | 0 stake-pool/py/stake_pool/actions.py | 753 ---- stake-pool/py/stake_pool/constants.py | 121 - stake-pool/py/stake_pool/instructions.py | 1277 ------ stake-pool/py/stake_pool/state.py | 327 -- stake-pool/py/system/__init__.py | 0 stake-pool/py/system/actions.py | 8 - stake-pool/py/tests/conftest.py | 121 - stake-pool/py/tests/test_a_time_sensitive.py | 103 - stake-pool/py/tests/test_add_remove.py | 35 - stake-pool/py/tests/test_bot_rebalance.py | 86 - stake-pool/py/tests/test_create.py | 71 - .../test_create_update_token_metadata.py | 63 - .../py/tests/test_deposit_withdraw_sol.py | 28 - .../py/tests/test_deposit_withdraw_stake.py | 52 - stake-pool/py/tests/test_stake.py | 33 - stake-pool/py/tests/test_system.py | 14 - stake-pool/py/tests/test_token.py | 16 - stake-pool/py/tests/test_vote.py | 15 - stake-pool/py/vote/__init__.py | 0 stake-pool/py/vote/actions.py | 47 - stake-pool/py/vote/constants.py | 8 - stake-pool/py/vote/instructions.py | 98 - 116 files changed, 2 insertions(+), 39102 deletions(-) delete mode 100644 stake-pool/cli/Cargo.toml delete mode 100644 stake-pool/cli/README.md delete mode 100755 stake-pool/cli/scripts/add-validators.sh delete mode 100755 stake-pool/cli/scripts/deposit.sh delete mode 100755 stake-pool/cli/scripts/rebalance.sh delete mode 100755 stake-pool/cli/scripts/setup-stake-pool.sh delete mode 100755 stake-pool/cli/scripts/setup-test-validator.sh delete mode 100755 stake-pool/cli/scripts/withdraw.sh delete mode 100644 stake-pool/cli/src/client.rs delete mode 100644 stake-pool/cli/src/main.rs delete mode 100644 stake-pool/cli/src/output.rs delete mode 100644 stake-pool/js/.eslintignore delete mode 100644 stake-pool/js/.eslintrc.js delete mode 100644 stake-pool/js/.gitignore delete mode 100644 stake-pool/js/.prettierrc.js delete mode 100644 stake-pool/js/README.md delete mode 100644 stake-pool/js/package.json delete mode 100644 stake-pool/js/rollup.config.mjs delete mode 100644 stake-pool/js/src/codecs.ts delete mode 100644 stake-pool/js/src/constants.ts delete mode 100644 stake-pool/js/src/index.ts delete mode 100644 stake-pool/js/src/instructions.ts delete mode 100644 stake-pool/js/src/layouts.ts delete mode 100644 stake-pool/js/src/types/buffer-layout.d.ts delete mode 100644 stake-pool/js/src/utils/index.ts delete mode 100644 stake-pool/js/src/utils/instruction.ts delete mode 100644 stake-pool/js/src/utils/math.ts delete mode 100644 stake-pool/js/src/utils/program-address.ts delete mode 100644 stake-pool/js/src/utils/stake.ts delete mode 100644 stake-pool/js/test/calculation.test.ts delete mode 100644 stake-pool/js/test/equal.ts delete mode 100644 stake-pool/js/test/instructions.test.ts delete mode 100644 stake-pool/js/test/layouts.test.ts delete mode 100644 stake-pool/js/test/mocks.ts delete mode 100644 stake-pool/js/tsconfig.json delete mode 100644 stake-pool/program/Cargo.toml delete mode 100644 stake-pool/program/Xargo.toml delete mode 100644 stake-pool/program/program-id.md delete mode 100644 stake-pool/program/proptest-regressions/state.txt delete mode 100644 stake-pool/program/src/big_vec.rs delete mode 100644 stake-pool/program/src/entrypoint.rs delete mode 100644 stake-pool/program/src/error.rs delete mode 100644 stake-pool/program/src/inline_mpl_token_metadata.rs delete mode 100644 stake-pool/program/src/instruction.rs delete mode 100644 stake-pool/program/src/lib.rs delete mode 100644 stake-pool/program/src/processor.rs delete mode 100644 stake-pool/program/src/state.rs delete mode 100644 stake-pool/program/tests/create_pool_token_metadata.rs delete mode 100644 stake-pool/program/tests/decrease.rs delete mode 100644 stake-pool/program/tests/deposit.rs delete mode 100644 stake-pool/program/tests/deposit_authority.rs delete mode 100644 stake-pool/program/tests/deposit_edge_cases.rs delete mode 100644 stake-pool/program/tests/deposit_sol.rs delete mode 100755 stake-pool/program/tests/fixtures/mpl_token_metadata.so delete mode 100644 stake-pool/program/tests/force_destake.rs delete mode 100644 stake-pool/program/tests/helpers/mod.rs delete mode 100644 stake-pool/program/tests/huge_pool.rs delete mode 100644 stake-pool/program/tests/increase.rs delete mode 100644 stake-pool/program/tests/initialize.rs delete mode 100644 stake-pool/program/tests/set_deposit_fee.rs delete mode 100644 stake-pool/program/tests/set_epoch_fee.rs delete mode 100644 stake-pool/program/tests/set_funding_authority.rs delete mode 100644 stake-pool/program/tests/set_manager.rs delete mode 100644 stake-pool/program/tests/set_preferred.rs delete mode 100644 stake-pool/program/tests/set_referral_fee.rs delete mode 100644 stake-pool/program/tests/set_staker.rs delete mode 100644 stake-pool/program/tests/set_withdrawal_fee.rs delete mode 100644 stake-pool/program/tests/update_pool_token_metadata.rs delete mode 100644 stake-pool/program/tests/update_stake_pool_balance.rs delete mode 100644 stake-pool/program/tests/update_validator_list_balance.rs delete mode 100644 stake-pool/program/tests/update_validator_list_balance_hijack.rs delete mode 100644 stake-pool/program/tests/vsa_add.rs delete mode 100644 stake-pool/program/tests/vsa_remove.rs delete mode 100644 stake-pool/program/tests/withdraw.rs delete mode 100644 stake-pool/program/tests/withdraw_edge_cases.rs delete mode 100644 stake-pool/program/tests/withdraw_sol.rs delete mode 100644 stake-pool/program/tests/withdraw_with_fee.rs delete mode 100644 stake-pool/py/.flake8 delete mode 100644 stake-pool/py/.gitignore delete mode 100644 stake-pool/py/LICENSE delete mode 100644 stake-pool/py/README.md delete mode 100644 stake-pool/py/bot/__init__.py delete mode 100644 stake-pool/py/bot/rebalance.py delete mode 100644 stake-pool/py/optional-requirements.txt delete mode 100644 stake-pool/py/requirements.txt delete mode 100644 stake-pool/py/spl_token/__init__.py delete mode 100644 stake-pool/py/spl_token/actions.py delete mode 100644 stake-pool/py/stake/__init__.py delete mode 100644 stake-pool/py/stake/actions.py delete mode 100644 stake-pool/py/stake/constants.py delete mode 100644 stake-pool/py/stake/instructions.py delete mode 100644 stake-pool/py/stake/state.py delete mode 100644 stake-pool/py/stake_pool/__init__.py delete mode 100644 stake-pool/py/stake_pool/actions.py delete mode 100644 stake-pool/py/stake_pool/constants.py delete mode 100644 stake-pool/py/stake_pool/instructions.py delete mode 100644 stake-pool/py/stake_pool/state.py delete mode 100644 stake-pool/py/system/__init__.py delete mode 100644 stake-pool/py/system/actions.py delete mode 100644 stake-pool/py/tests/conftest.py delete mode 100644 stake-pool/py/tests/test_a_time_sensitive.py delete mode 100644 stake-pool/py/tests/test_add_remove.py delete mode 100644 stake-pool/py/tests/test_bot_rebalance.py delete mode 100644 stake-pool/py/tests/test_create.py delete mode 100644 stake-pool/py/tests/test_create_update_token_metadata.py delete mode 100644 stake-pool/py/tests/test_deposit_withdraw_sol.py delete mode 100644 stake-pool/py/tests/test_deposit_withdraw_stake.py delete mode 100644 stake-pool/py/tests/test_stake.py delete mode 100644 stake-pool/py/tests/test_system.py delete mode 100644 stake-pool/py/tests/test_token.py delete mode 100644 stake-pool/py/tests/test_vote.py delete mode 100644 stake-pool/py/vote/__init__.py delete mode 100644 stake-pool/py/vote/actions.py delete mode 100644 stake-pool/py/vote/constants.py delete mode 100644 stake-pool/py/vote/instructions.py 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 399c584c5c130c92e588bd4834bc3fd168a55d2b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 693904 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

sn*}wGLX;1k1FQK6d`=&BJVX2+(xfCf^ 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 (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, - ) From 161d3d2ff14be0a19d11e145093c28c60bd9612f Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 18:00:41 +0100 Subject: [PATCH 13/17] token / token-2022 / transfer-hook: Remove --- token/README.md | 9 + token/cli/Cargo.toml | 61 - token/cli/README.md | 42 - token/cli/build.rs | 79 - token/cli/examples/confidential-transfer.sh | 77 - token/cli/src/bench.rs | 381 - token/cli/src/clap_app.rs | 2691 ------ token/cli/src/command.rs | 4734 ---------- token/cli/src/config.rs | 619 -- token/cli/src/encryption_keypair.rs | 75 - token/cli/src/lib.rs | 7 - token/cli/src/main.rs | 39 - token/cli/src/output.rs | 1006 -- token/cli/src/sort.rs | 124 - token/cli/tests/command.rs | 4330 --------- token/cli/tests/config.rs | 10 - token/client/Cargo.toml | 39 - token/client/src/client.rs | 426 - token/client/src/lib.rs | 6 - token/client/src/output.rs | 59 - token/client/src/token.rs | 3623 ------- token/client/tests/program-test.rs | 311 - .../ciphertext-arithmetic/Cargo.toml | 21 - .../ciphertext-arithmetic/src/lib.rs | 334 - .../elgamal-registry/Cargo.toml | 28 - .../elgamal-registry/src/entrypoint.rs | 14 - .../elgamal-registry/src/instruction.rs | 194 - .../elgamal-registry/src/lib.rs | 28 - .../elgamal-registry/src/processor.rs | 166 - .../elgamal-registry/src/state.rs | 18 - .../proof-extraction/Cargo.toml | 19 - .../proof-extraction/src/burn.rs | 130 - .../proof-extraction/src/encryption.rs | 69 - .../proof-extraction/src/errors.rs | 17 - .../proof-extraction/src/instruction.rs | 231 - .../proof-extraction/src/lib.rs | 8 - .../proof-extraction/src/mint.rs | 130 - .../proof-extraction/src/transfer.rs | 137 - .../proof-extraction/src/transfer_with_fee.rs | 322 - .../proof-extraction/src/withdraw.rs | 53 - .../proof-generation/Cargo.toml | 21 - .../proof-generation/src/burn.rs | 169 - .../proof-generation/src/encryption.rs | 140 - .../proof-generation/src/errors.rs | 15 - .../proof-generation/src/lib.rs | 108 - .../proof-generation/src/mint.rs | 162 - .../proof-generation/src/transfer.rs | 178 - .../proof-generation/src/transfer_with_fee.rs | 361 - .../proof-generation/src/withdraw.rs | 63 - .../proof-tests/Cargo.toml | 15 - .../proof-tests/tests/proof_test.rs | 311 - token/js/.eslintignore | 5 - token/js/.eslintrc | 34 - token/js/.gitignore | 13 - token/js/.mocharc.json | 5 - token/js/.nojekyll | 0 token/js/LICENSE | 202 - token/js/README.md | 116 - token/js/examples/cpiGuard.ts | 64 - .../examples/createMintAndTransferTokens.ts | 58 - token/js/examples/createMintCloseAuthority.ts | 59 - token/js/examples/defaultAccountState.ts | 70 - token/js/examples/immutableOwner.ts | 61 - token/js/examples/interestBearing.ts | 41 - token/js/examples/memoTransfer.ts | 84 - token/js/examples/metadata.ts | 109 - token/js/examples/nonTransferable.ts | 45 - token/js/examples/permanentDelegate.ts | 101 - token/js/examples/reallocate.ts | 62 - token/js/examples/transferFee.ts | 167 - token/js/examples/transferHook.ts | 98 - token/js/package.json | 86 - token/js/src/actions/amountToUiAmount.ts | 271 - token/js/src/actions/approve.ts | 40 - token/js/src/actions/approveChecked.ts | 54 - token/js/src/actions/burn.ts | 40 - token/js/src/actions/burnChecked.ts | 42 - token/js/src/actions/closeAccount.ts | 38 - token/js/src/actions/createAccount.ts | 53 - .../actions/createAssociatedTokenAccount.ts | 53 - .../createAssociatedTokenAccountIdempotent.ts | 54 - token/js/src/actions/createMint.ts | 47 - token/js/src/actions/createMultisig.ts | 45 - token/js/src/actions/createNativeMint.ts | 26 - .../src/actions/createWrappedNativeAccount.ts | 91 - token/js/src/actions/freezeAccount.ts | 38 - .../getOrCreateAssociatedTokenAccount.ts | 89 - token/js/src/actions/index.ts | 25 - token/js/src/actions/internal.ts | 9 - token/js/src/actions/mintTo.ts | 40 - token/js/src/actions/mintToChecked.ts | 50 - token/js/src/actions/recoverNested.ts | 69 - token/js/src/actions/revoke.ts | 36 - token/js/src/actions/setAuthority.ts | 48 - token/js/src/actions/syncNative.ts | 27 - token/js/src/actions/thawAccount.ts | 38 - token/js/src/actions/transfer.ts | 40 - token/js/src/actions/transferChecked.ts | 53 - token/js/src/actions/uiAmountToAmount.ts | 32 - token/js/src/constants.ts | 25 - token/js/src/errors.ts | 86 - token/js/src/extensions/accountType.ts | 6 - token/js/src/extensions/cpiGuard/actions.ts | 67 - token/js/src/extensions/cpiGuard/index.ts | 3 - .../src/extensions/cpiGuard/instructions.ts | 83 - token/js/src/extensions/cpiGuard/state.ts | 24 - .../extensions/defaultAccountState/actions.ts | 67 - .../extensions/defaultAccountState/index.ts | 3 - .../defaultAccountState/instructions.ts | 94 - .../extensions/defaultAccountState/state.ts | 24 - token/js/src/extensions/extensionType.ts | 304 - .../extensions/groupMemberPointer/index.ts | 2 - .../groupMemberPointer/instructions.ts | 98 - .../extensions/groupMemberPointer/state.ts | 36 - token/js/src/extensions/groupPointer/index.ts | 2 - .../extensions/groupPointer/instructions.ts | 98 - token/js/src/extensions/groupPointer/state.ts | 36 - token/js/src/extensions/immutableOwner.ts | 20 - token/js/src/extensions/index.ts | 17 - .../extensions/interestBearingMint/actions.ts | 87 - .../extensions/interestBearingMint/index.ts | 3 - .../interestBearingMint/instructions.ts | 107 - .../extensions/interestBearingMint/state.ts | 31 - .../js/src/extensions/memoTransfer/actions.ts | 70 - token/js/src/extensions/memoTransfer/index.ts | 3 - .../extensions/memoTransfer/instructions.ts | 86 - token/js/src/extensions/memoTransfer/state.ts | 24 - .../src/extensions/metadataPointer/index.ts | 2 - .../metadataPointer/instructions.ts | 98 - .../src/extensions/metadataPointer/state.ts | 36 - token/js/src/extensions/mintCloseAuthority.ts | 24 - token/js/src/extensions/nonTransferable.ts | 34 - token/js/src/extensions/permanentDelegate.ts | 24 - token/js/src/extensions/tokenGroup/actions.ts | 281 - token/js/src/extensions/tokenGroup/index.ts | 2 - token/js/src/extensions/tokenGroup/state.ts | 45 - .../src/extensions/tokenMetadata/actions.ts | 382 - .../js/src/extensions/tokenMetadata/index.ts | 2 - .../js/src/extensions/tokenMetadata/state.ts | 84 - .../js/src/extensions/transferFee/actions.ts | 203 - token/js/src/extensions/transferFee/index.ts | 3 - .../extensions/transferFee/instructions.ts | 983 -- token/js/src/extensions/transferFee/state.ts | 108 - .../js/src/extensions/transferHook/actions.ts | 176 - token/js/src/extensions/transferHook/index.ts | 4 - .../extensions/transferHook/instructions.ts | 364 - token/js/src/extensions/transferHook/seeds.ts | 127 - token/js/src/extensions/transferHook/state.ts | 140 - token/js/src/index.ts | 6 - token/js/src/instructions/amountToUiAmount.ts | 128 - token/js/src/instructions/approve.ts | 153 - token/js/src/instructions/approveChecked.ts | 170 - .../instructions/associatedTokenAccount.ts | 164 - token/js/src/instructions/burn.ts | 153 - token/js/src/instructions/burnChecked.ts | 163 - token/js/src/instructions/closeAccount.ts | 141 - token/js/src/instructions/createNativeMint.ts | 44 - token/js/src/instructions/decode.ts | 249 - token/js/src/instructions/freezeAccount.ts | 141 - token/js/src/instructions/index.ts | 47 - .../js/src/instructions/initializeAccount.ts | 136 - .../js/src/instructions/initializeAccount2.ts | 135 - .../js/src/instructions/initializeAccount3.ts | 130 - .../instructions/initializeImmutableOwner.ts | 124 - token/js/src/instructions/initializeMint.ts | 159 - token/js/src/instructions/initializeMint2.ts | 150 - .../initializeMintCloseAuthority.ts | 139 - .../js/src/instructions/initializeMultisig.ts | 151 - .../src/instructions/initializeMultisig2.ts | 1 - .../initializeNonTransferableMint.ts | 44 - .../initializePermanentDelegate.ts | 139 - token/js/src/instructions/internal.ts | 23 - token/js/src/instructions/mintTo.ts | 153 - token/js/src/instructions/mintToChecked.ts | 163 - token/js/src/instructions/reallocate.ts | 54 - token/js/src/instructions/revoke.ts | 128 - token/js/src/instructions/setAuthority.ts | 176 - token/js/src/instructions/syncNative.ts | 112 - token/js/src/instructions/thawAccount.ts | 141 - token/js/src/instructions/transfer.ts | 153 - token/js/src/instructions/transferChecked.ts | 170 - token/js/src/instructions/types.ts | 45 - token/js/src/instructions/uiAmountToAmount.ts | 136 - token/js/src/serialization.ts | 39 - token/js/src/state/account.ts | 196 - token/js/src/state/index.ts | 3 - token/js/src/state/mint.ts | 205 - token/js/src/state/multisig.ts | 110 - token/js/test/common.ts | 22 - token/js/test/e2e-2022/closeMint.test.ts | 108 - token/js/test/e2e-2022/cpiGuard.test.ts | 126 - .../test/e2e-2022/defaultAccountState.test.ts | 105 - .../test/e2e-2022/groupMemberPointer.test.ts | 144 - token/js/test/e2e-2022/groupPointer.test.ts | 144 - token/js/test/e2e-2022/immutableOwner.test.ts | 78 - .../test/e2e-2022/interestBearingMint.test.ts | 103 - token/js/test/e2e-2022/memoTransfer.test.ts | 123 - .../js/test/e2e-2022/metadataPointer.test.ts | 97 - .../test/e2e-2022/nonTransferableMint.test.ts | 100 - .../test/e2e-2022/permanentDelegate.test.ts | 124 - token/js/test/e2e-2022/reallocate.test.ts | 55 - token/js/test/e2e-2022/tlv.test.ts | 93 - token/js/test/e2e-2022/tokenGroup.test.ts | 240 - .../js/test/e2e-2022/tokenGroupMember.test.ts | 145 - token/js/test/e2e-2022/tokenMetadata.test.ts | 524 -- token/js/test/e2e-2022/transferFee.test.ts | 373 - token/js/test/e2e-2022/transferHook.test.ts | 221 - token/js/test/e2e/amount.test.ts | 38 - token/js/test/e2e/burn.test.ts | 61 - token/js/test/e2e/close.test.ts | 67 - token/js/test/e2e/create.test.ts | 210 - token/js/test/e2e/freeze.test.ts | 61 - token/js/test/e2e/initialize.test.ts | 144 - token/js/test/e2e/mint.test.ts | 66 - token/js/test/e2e/multisig.test.ts | 118 - token/js/test/e2e/native.test.ts | 109 - token/js/test/e2e/recoverNested.test.ts | 122 - token/js/test/e2e/setAuthority.test.ts | 135 - token/js/test/e2e/transfer.test.ts | 165 - token/js/test/unit/decode.test.ts | 28 - token/js/test/unit/groupMemberPointer.test.ts | 142 - token/js/test/unit/groupPointer.test.ts | 142 - token/js/test/unit/index.test.ts | 308 - token/js/test/unit/interestBearing.test.ts | 327 - token/js/test/unit/metadataPointer.test.ts | 142 - token/js/test/unit/programId.test.ts | 99 - token/js/test/unit/tokenMetadata.test.ts | 221 - token/js/test/unit/transferHook.test.ts | 540 -- token/js/test/unit/transferfee.test.ts | 167 - token/js/tsconfig.all.json | 11 - token/js/tsconfig.base.json | 14 - token/js/tsconfig.cjs.json | 10 - token/js/tsconfig.esm.json | 13 - token/js/tsconfig.json | 8 - token/js/tsconfig.root.json | 6 - token/js/typedoc.json | 5 - token/program-2022-test/Cargo.toml | 49 - token/program-2022-test/README.md | 26 - token/program-2022-test/build.rs | 67 - token/program-2022-test/tests/burn.rs | 300 - .../program-2022-test/tests/close_account.rs | 170 - .../tests/confidential_transfer.rs | 3331 ------- .../tests/confidential_transfer_fee.rs | 1224 --- token/program-2022-test/tests/cpi_guard.rs | 814 -- .../tests/default_account_state.rs | 324 - token/program-2022-test/tests/delegate.rs | 297 - .../tests/fixtures/spl_instruction_padding.so | Bin 24464 -> 0 bytes .../fixtures/spl_transfer_hook_example.so | Bin 111920 -> 0 bytes .../spl_transfer_hook_example_downgrade.so | Bin 19320 -> 0 bytes .../spl_transfer_hook_example_fail.so | Bin 18768 -> 0 bytes .../spl_transfer_hook_example_success.so | Bin 17696 -> 0 bytes .../spl_transfer_hook_example_swap.so | Bin 69920 -> 0 bytes ...spl_transfer_hook_example_swap_with_fee.so | Bin 72712 -> 0 bytes token/program-2022-test/tests/freeze.rs | 45 - .../tests/group_member_pointer.rs | 294 - .../program-2022-test/tests/group_pointer.rs | 253 - .../tests/initialize_account.rs | 262 - .../tests/initialize_mint.rs | 654 -- .../tests/interest_bearing_mint.rs | 350 - .../program-2022-test/tests/memo_transfer.rs | 247 - .../tests/metadata_pointer.rs | 257 - .../tests/mint_close_authority.rs | 287 - .../tests/non_transferable.rs | 334 - token/program-2022-test/tests/pausable.rs | 356 - .../tests/permanent_delegate.rs | 276 - token/program-2022-test/tests/program_test.rs | 376 - token/program-2022-test/tests/reallocate.rs | 287 - .../tests/scaled_ui_amount.rs | 379 - token/program-2022-test/tests/sync_native.rs | 86 - .../tests/token_group_initialize.rs | 271 - .../tests/token_group_initialize_member.rs | 490 - .../tests/token_group_update_authority.rs | 202 - .../tests/token_group_update_max_size.rs | 249 - .../tests/token_metadata_emit.rs | 141 - .../tests/token_metadata_initialize.rs | 290 - .../tests/token_metadata_remove_key.rs | 258 - .../tests/token_metadata_update_authority.rs | 228 - .../tests/token_metadata_update_field.rs | 206 - token/program-2022-test/tests/transfer.rs | 379 - token/program-2022-test/tests/transfer_fee.rs | 1715 ---- .../program-2022-test/tests/transfer_hook.rs | 1112 --- .../downgrade/Cargo.toml | 21 - .../downgrade/src/lib.rs | 42 - .../fail/Cargo.toml | 21 - .../fail/src/lib.rs | 16 - .../success/Cargo.toml | 21 - .../success/src/lib.rs | 14 - .../swap-with-fee/Cargo.toml | 22 - .../swap-with-fee/src/lib.rs | 59 - .../swap/Cargo.toml | 22 - .../swap/src/lib.rs | 57 - token/program-2022/Cargo.toml | 62 - token/program-2022/README.md | 13 - token/program-2022/Xargo.toml | 2 - token/program-2022/inc/token.h | 687 -- token/program-2022/program-id.md | 1 - token/program-2022/src/entrypoint.rs | 39 - token/program-2022/src/error.rs | 502 - .../confidential_mint_burn/account_info.rs | 208 - .../confidential_mint_burn/instruction.rs | 574 -- .../extension/confidential_mint_burn/mod.rs | 45 - .../confidential_mint_burn/processor.rs | 445 - .../confidential_mint_burn/verify_proof.rs | 114 - .../confidential_transfer/account_info.rs | 332 - .../confidential_transfer/instruction.rs | 1801 ---- .../extension/confidential_transfer/mod.rs | 201 - .../confidential_transfer/processor.rs | 1416 --- .../confidential_transfer/verify_proof.rs | 197 - .../confidential_transfer_fee/account_info.rs | 60 - .../confidential_transfer_fee/instruction.rs | 579 -- .../confidential_transfer_fee/mod.rs | 77 - .../confidential_transfer_fee/processor.rs | 499 - .../src/extension/cpi_guard/instruction.rs | 104 - .../src/extension/cpi_guard/mod.rs | 43 - .../src/extension/cpi_guard/processor.rs | 74 - .../default_account_state/instruction.rs | 127 - .../extension/default_account_state/mod.rs | 27 - .../default_account_state/processor.rs | 96 - .../group_member_pointer/instruction.rs | 128 - .../src/extension/group_member_pointer/mod.rs | 28 - .../group_member_pointer/processor.rs | 103 - .../extension/group_pointer/instruction.rs | 128 - .../src/extension/group_pointer/mod.rs | 28 - .../src/extension/group_pointer/processor.rs | 103 - .../src/extension/immutable_owner.rs | 17 - .../interest_bearing_mint/instruction.rs | 117 - .../extension/interest_bearing_mint/mod.rs | 464 - .../interest_bearing_mint/processor.rs | 107 - .../extension/memo_transfer/instruction.rs | 98 - .../src/extension/memo_transfer/mod.rs | 55 - .../src/extension/memo_transfer/processor.rs | 69 - .../extension/metadata_pointer/instruction.rs | 128 - .../src/extension/metadata_pointer/mod.rs | 28 - .../extension/metadata_pointer/processor.rs | 100 - .../src/extension/mint_close_authority.rs | 20 - token/program-2022/src/extension/mod.rs | 3131 ------- .../src/extension/non_transferable.rs | 29 - .../src/extension/pausable/instruction.rs | 136 - .../src/extension/pausable/mod.rs | 39 - .../src/extension/pausable/processor.rs | 91 - .../src/extension/permanent_delegate.rs | 32 - .../program-2022/src/extension/reallocate.rs | 116 - .../extension/scaled_ui_amount/instruction.rs | 143 - .../src/extension/scaled_ui_amount/mod.rs | 345 - .../extension/scaled_ui_amount/processor.rs | 126 - .../src/extension/token_group/mod.rs | 15 - .../src/extension/token_group/processor.rs | 232 - .../src/extension/token_metadata/mod.rs | 11 - .../src/extension/token_metadata/processor.rs | 239 - .../src/extension/transfer_fee/instruction.rs | 500 - .../src/extension/transfer_fee/mod.rs | 475 - .../src/extension/transfer_fee/processor.rs | 326 - .../extension/transfer_hook/instruction.rs | 128 - .../src/extension/transfer_hook/mod.rs | 80 - .../src/extension/transfer_hook/processor.rs | 111 - .../program-2022/src/generic_token_account.rs | 65 - token/program-2022/src/instruction.rs | 2829 ------ token/program-2022/src/lib.rs | 167 - token/program-2022/src/native_mint.rs | 36 - token/program-2022/src/offchain.rs | 486 - token/program-2022/src/onchain.rs | 148 - token/program-2022/src/pod.rs | 315 - token/program-2022/src/pod_instruction.rs | 227 - token/program-2022/src/processor.rs | 8295 ----------------- token/program-2022/src/serialization.rs | 212 - token/program-2022/src/state.rs | 553 -- token/program-2022/tests/action.rs | 173 - .../tests/assert_instruction_count.rs | 402 - token/program-2022/tests/serialization.rs | 168 - token/program/Cargo.toml | 38 - token/program/README.md | 13 - token/program/Xargo.toml | 2 - token/program/inc/token.h | 687 -- token/program/program-id.md | 1 - token/program/src/entrypoint.rs | 23 - token/program/src/error.rs | 141 - token/program/src/instruction.rs | 1705 ---- token/program/src/lib.rs | 93 - token/program/src/native_mint.rs | 23 - token/program/src/processor.rs | 7026 -------------- token/program/src/state.rs | 492 - token/program/tests/action.rs | 140 - .../program/tests/assert_instruction_count.rs | 332 - token/program/tests/close_account.rs | 200 - token/transfer-hook/cli/Cargo.toml | 37 - token/transfer-hook/cli/src/main.rs | 636 -- token/transfer-hook/cli/src/meta.rs | 322 - token/transfer-hook/example/Cargo.toml | 32 - token/transfer-hook/example/README.md | 66 - token/transfer-hook/example/src/entrypoint.rs | 24 - token/transfer-hook/example/src/lib.rs | 30 - token/transfer-hook/example/src/processor.rs | 228 - token/transfer-hook/example/src/state.rs | 15 - .../transfer-hook/example/tests/functional.rs | 1464 --- token/transfer-hook/interface/Cargo.toml | 37 - token/transfer-hook/interface/README.md | 149 - token/transfer-hook/interface/src/error.rs | 63 - .../interface/src/instruction.rs | 310 - token/transfer-hook/interface/src/lib.rs | 56 - token/transfer-hook/interface/src/offchain.rs | 260 - token/transfer-hook/interface/src/onchain.rs | 521 -- token/zk-token-protocol-paper/part1.pdf | Bin 318514 -> 0 bytes token/zk-token-protocol-paper/part2.pdf | Bin 440391 -> 0 bytes 403 files changed, 9 insertions(+), 103809 deletions(-) create mode 100644 token/README.md delete mode 100644 token/cli/Cargo.toml delete mode 100644 token/cli/README.md delete mode 100644 token/cli/build.rs delete mode 100755 token/cli/examples/confidential-transfer.sh delete mode 100644 token/cli/src/bench.rs delete mode 100644 token/cli/src/clap_app.rs delete mode 100644 token/cli/src/command.rs delete mode 100644 token/cli/src/config.rs delete mode 100644 token/cli/src/encryption_keypair.rs delete mode 100644 token/cli/src/lib.rs delete mode 100644 token/cli/src/main.rs delete mode 100644 token/cli/src/output.rs delete mode 100644 token/cli/src/sort.rs delete mode 100644 token/cli/tests/command.rs delete mode 100644 token/cli/tests/config.rs delete mode 100644 token/client/Cargo.toml delete mode 100644 token/client/src/client.rs delete mode 100644 token/client/src/lib.rs delete mode 100644 token/client/src/output.rs delete mode 100644 token/client/src/token.rs delete mode 100644 token/client/tests/program-test.rs delete mode 100644 token/confidential-transfer/ciphertext-arithmetic/Cargo.toml delete mode 100644 token/confidential-transfer/ciphertext-arithmetic/src/lib.rs delete mode 100644 token/confidential-transfer/elgamal-registry/Cargo.toml delete mode 100644 token/confidential-transfer/elgamal-registry/src/entrypoint.rs delete mode 100644 token/confidential-transfer/elgamal-registry/src/instruction.rs delete mode 100644 token/confidential-transfer/elgamal-registry/src/lib.rs delete mode 100644 token/confidential-transfer/elgamal-registry/src/processor.rs delete mode 100644 token/confidential-transfer/elgamal-registry/src/state.rs delete mode 100644 token/confidential-transfer/proof-extraction/Cargo.toml delete mode 100644 token/confidential-transfer/proof-extraction/src/burn.rs delete mode 100644 token/confidential-transfer/proof-extraction/src/encryption.rs delete mode 100644 token/confidential-transfer/proof-extraction/src/errors.rs delete mode 100644 token/confidential-transfer/proof-extraction/src/instruction.rs delete mode 100644 token/confidential-transfer/proof-extraction/src/lib.rs delete mode 100644 token/confidential-transfer/proof-extraction/src/mint.rs delete mode 100644 token/confidential-transfer/proof-extraction/src/transfer.rs delete mode 100644 token/confidential-transfer/proof-extraction/src/transfer_with_fee.rs delete mode 100644 token/confidential-transfer/proof-extraction/src/withdraw.rs delete mode 100644 token/confidential-transfer/proof-generation/Cargo.toml delete mode 100644 token/confidential-transfer/proof-generation/src/burn.rs delete mode 100644 token/confidential-transfer/proof-generation/src/encryption.rs delete mode 100644 token/confidential-transfer/proof-generation/src/errors.rs delete mode 100644 token/confidential-transfer/proof-generation/src/lib.rs delete mode 100644 token/confidential-transfer/proof-generation/src/mint.rs delete mode 100644 token/confidential-transfer/proof-generation/src/transfer.rs delete mode 100644 token/confidential-transfer/proof-generation/src/transfer_with_fee.rs delete mode 100644 token/confidential-transfer/proof-generation/src/withdraw.rs delete mode 100644 token/confidential-transfer/proof-tests/Cargo.toml delete mode 100644 token/confidential-transfer/proof-tests/tests/proof_test.rs delete mode 100644 token/js/.eslintignore delete mode 100644 token/js/.eslintrc delete mode 100644 token/js/.gitignore delete mode 100644 token/js/.mocharc.json delete mode 100644 token/js/.nojekyll delete mode 100644 token/js/LICENSE delete mode 100644 token/js/README.md delete mode 100644 token/js/examples/cpiGuard.ts delete mode 100644 token/js/examples/createMintAndTransferTokens.ts delete mode 100644 token/js/examples/createMintCloseAuthority.ts delete mode 100644 token/js/examples/defaultAccountState.ts delete mode 100644 token/js/examples/immutableOwner.ts delete mode 100644 token/js/examples/interestBearing.ts delete mode 100644 token/js/examples/memoTransfer.ts delete mode 100644 token/js/examples/metadata.ts delete mode 100644 token/js/examples/nonTransferable.ts delete mode 100644 token/js/examples/permanentDelegate.ts delete mode 100644 token/js/examples/reallocate.ts delete mode 100644 token/js/examples/transferFee.ts delete mode 100644 token/js/examples/transferHook.ts delete mode 100644 token/js/package.json delete mode 100644 token/js/src/actions/amountToUiAmount.ts delete mode 100644 token/js/src/actions/approve.ts delete mode 100644 token/js/src/actions/approveChecked.ts delete mode 100644 token/js/src/actions/burn.ts delete mode 100644 token/js/src/actions/burnChecked.ts delete mode 100644 token/js/src/actions/closeAccount.ts delete mode 100644 token/js/src/actions/createAccount.ts delete mode 100644 token/js/src/actions/createAssociatedTokenAccount.ts delete mode 100644 token/js/src/actions/createAssociatedTokenAccountIdempotent.ts delete mode 100644 token/js/src/actions/createMint.ts delete mode 100644 token/js/src/actions/createMultisig.ts delete mode 100644 token/js/src/actions/createNativeMint.ts delete mode 100644 token/js/src/actions/createWrappedNativeAccount.ts delete mode 100644 token/js/src/actions/freezeAccount.ts delete mode 100644 token/js/src/actions/getOrCreateAssociatedTokenAccount.ts delete mode 100644 token/js/src/actions/index.ts delete mode 100644 token/js/src/actions/internal.ts delete mode 100644 token/js/src/actions/mintTo.ts delete mode 100644 token/js/src/actions/mintToChecked.ts delete mode 100644 token/js/src/actions/recoverNested.ts delete mode 100644 token/js/src/actions/revoke.ts delete mode 100644 token/js/src/actions/setAuthority.ts delete mode 100644 token/js/src/actions/syncNative.ts delete mode 100644 token/js/src/actions/thawAccount.ts delete mode 100644 token/js/src/actions/transfer.ts delete mode 100644 token/js/src/actions/transferChecked.ts delete mode 100644 token/js/src/actions/uiAmountToAmount.ts delete mode 100644 token/js/src/constants.ts delete mode 100644 token/js/src/errors.ts delete mode 100644 token/js/src/extensions/accountType.ts delete mode 100644 token/js/src/extensions/cpiGuard/actions.ts delete mode 100644 token/js/src/extensions/cpiGuard/index.ts delete mode 100644 token/js/src/extensions/cpiGuard/instructions.ts delete mode 100644 token/js/src/extensions/cpiGuard/state.ts delete mode 100644 token/js/src/extensions/defaultAccountState/actions.ts delete mode 100644 token/js/src/extensions/defaultAccountState/index.ts delete mode 100644 token/js/src/extensions/defaultAccountState/instructions.ts delete mode 100644 token/js/src/extensions/defaultAccountState/state.ts delete mode 100644 token/js/src/extensions/extensionType.ts delete mode 100644 token/js/src/extensions/groupMemberPointer/index.ts delete mode 100644 token/js/src/extensions/groupMemberPointer/instructions.ts delete mode 100644 token/js/src/extensions/groupMemberPointer/state.ts delete mode 100644 token/js/src/extensions/groupPointer/index.ts delete mode 100644 token/js/src/extensions/groupPointer/instructions.ts delete mode 100644 token/js/src/extensions/groupPointer/state.ts delete mode 100644 token/js/src/extensions/immutableOwner.ts delete mode 100644 token/js/src/extensions/index.ts delete mode 100644 token/js/src/extensions/interestBearingMint/actions.ts delete mode 100644 token/js/src/extensions/interestBearingMint/index.ts delete mode 100644 token/js/src/extensions/interestBearingMint/instructions.ts delete mode 100644 token/js/src/extensions/interestBearingMint/state.ts delete mode 100644 token/js/src/extensions/memoTransfer/actions.ts delete mode 100644 token/js/src/extensions/memoTransfer/index.ts delete mode 100644 token/js/src/extensions/memoTransfer/instructions.ts delete mode 100644 token/js/src/extensions/memoTransfer/state.ts delete mode 100644 token/js/src/extensions/metadataPointer/index.ts delete mode 100644 token/js/src/extensions/metadataPointer/instructions.ts delete mode 100644 token/js/src/extensions/metadataPointer/state.ts delete mode 100644 token/js/src/extensions/mintCloseAuthority.ts delete mode 100644 token/js/src/extensions/nonTransferable.ts delete mode 100644 token/js/src/extensions/permanentDelegate.ts delete mode 100644 token/js/src/extensions/tokenGroup/actions.ts delete mode 100644 token/js/src/extensions/tokenGroup/index.ts delete mode 100644 token/js/src/extensions/tokenGroup/state.ts delete mode 100644 token/js/src/extensions/tokenMetadata/actions.ts delete mode 100644 token/js/src/extensions/tokenMetadata/index.ts delete mode 100644 token/js/src/extensions/tokenMetadata/state.ts delete mode 100644 token/js/src/extensions/transferFee/actions.ts delete mode 100644 token/js/src/extensions/transferFee/index.ts delete mode 100644 token/js/src/extensions/transferFee/instructions.ts delete mode 100644 token/js/src/extensions/transferFee/state.ts delete mode 100644 token/js/src/extensions/transferHook/actions.ts delete mode 100644 token/js/src/extensions/transferHook/index.ts delete mode 100644 token/js/src/extensions/transferHook/instructions.ts delete mode 100644 token/js/src/extensions/transferHook/seeds.ts delete mode 100644 token/js/src/extensions/transferHook/state.ts delete mode 100644 token/js/src/index.ts delete mode 100644 token/js/src/instructions/amountToUiAmount.ts delete mode 100644 token/js/src/instructions/approve.ts delete mode 100644 token/js/src/instructions/approveChecked.ts delete mode 100644 token/js/src/instructions/associatedTokenAccount.ts delete mode 100644 token/js/src/instructions/burn.ts delete mode 100644 token/js/src/instructions/burnChecked.ts delete mode 100644 token/js/src/instructions/closeAccount.ts delete mode 100644 token/js/src/instructions/createNativeMint.ts delete mode 100644 token/js/src/instructions/decode.ts delete mode 100644 token/js/src/instructions/freezeAccount.ts delete mode 100644 token/js/src/instructions/index.ts delete mode 100644 token/js/src/instructions/initializeAccount.ts delete mode 100644 token/js/src/instructions/initializeAccount2.ts delete mode 100644 token/js/src/instructions/initializeAccount3.ts delete mode 100644 token/js/src/instructions/initializeImmutableOwner.ts delete mode 100644 token/js/src/instructions/initializeMint.ts delete mode 100644 token/js/src/instructions/initializeMint2.ts delete mode 100644 token/js/src/instructions/initializeMintCloseAuthority.ts delete mode 100644 token/js/src/instructions/initializeMultisig.ts delete mode 100644 token/js/src/instructions/initializeMultisig2.ts delete mode 100644 token/js/src/instructions/initializeNonTransferableMint.ts delete mode 100644 token/js/src/instructions/initializePermanentDelegate.ts delete mode 100644 token/js/src/instructions/internal.ts delete mode 100644 token/js/src/instructions/mintTo.ts delete mode 100644 token/js/src/instructions/mintToChecked.ts delete mode 100644 token/js/src/instructions/reallocate.ts delete mode 100644 token/js/src/instructions/revoke.ts delete mode 100644 token/js/src/instructions/setAuthority.ts delete mode 100644 token/js/src/instructions/syncNative.ts delete mode 100644 token/js/src/instructions/thawAccount.ts delete mode 100644 token/js/src/instructions/transfer.ts delete mode 100644 token/js/src/instructions/transferChecked.ts delete mode 100644 token/js/src/instructions/types.ts delete mode 100644 token/js/src/instructions/uiAmountToAmount.ts delete mode 100644 token/js/src/serialization.ts delete mode 100644 token/js/src/state/account.ts delete mode 100644 token/js/src/state/index.ts delete mode 100644 token/js/src/state/mint.ts delete mode 100644 token/js/src/state/multisig.ts delete mode 100644 token/js/test/common.ts delete mode 100644 token/js/test/e2e-2022/closeMint.test.ts delete mode 100644 token/js/test/e2e-2022/cpiGuard.test.ts delete mode 100644 token/js/test/e2e-2022/defaultAccountState.test.ts delete mode 100644 token/js/test/e2e-2022/groupMemberPointer.test.ts delete mode 100644 token/js/test/e2e-2022/groupPointer.test.ts delete mode 100644 token/js/test/e2e-2022/immutableOwner.test.ts delete mode 100644 token/js/test/e2e-2022/interestBearingMint.test.ts delete mode 100644 token/js/test/e2e-2022/memoTransfer.test.ts delete mode 100644 token/js/test/e2e-2022/metadataPointer.test.ts delete mode 100644 token/js/test/e2e-2022/nonTransferableMint.test.ts delete mode 100644 token/js/test/e2e-2022/permanentDelegate.test.ts delete mode 100644 token/js/test/e2e-2022/reallocate.test.ts delete mode 100644 token/js/test/e2e-2022/tlv.test.ts delete mode 100644 token/js/test/e2e-2022/tokenGroup.test.ts delete mode 100644 token/js/test/e2e-2022/tokenGroupMember.test.ts delete mode 100644 token/js/test/e2e-2022/tokenMetadata.test.ts delete mode 100644 token/js/test/e2e-2022/transferFee.test.ts delete mode 100644 token/js/test/e2e-2022/transferHook.test.ts delete mode 100644 token/js/test/e2e/amount.test.ts delete mode 100644 token/js/test/e2e/burn.test.ts delete mode 100644 token/js/test/e2e/close.test.ts delete mode 100644 token/js/test/e2e/create.test.ts delete mode 100644 token/js/test/e2e/freeze.test.ts delete mode 100644 token/js/test/e2e/initialize.test.ts delete mode 100644 token/js/test/e2e/mint.test.ts delete mode 100644 token/js/test/e2e/multisig.test.ts delete mode 100644 token/js/test/e2e/native.test.ts delete mode 100644 token/js/test/e2e/recoverNested.test.ts delete mode 100644 token/js/test/e2e/setAuthority.test.ts delete mode 100644 token/js/test/e2e/transfer.test.ts delete mode 100644 token/js/test/unit/decode.test.ts delete mode 100644 token/js/test/unit/groupMemberPointer.test.ts delete mode 100644 token/js/test/unit/groupPointer.test.ts delete mode 100644 token/js/test/unit/index.test.ts delete mode 100644 token/js/test/unit/interestBearing.test.ts delete mode 100644 token/js/test/unit/metadataPointer.test.ts delete mode 100644 token/js/test/unit/programId.test.ts delete mode 100644 token/js/test/unit/tokenMetadata.test.ts delete mode 100644 token/js/test/unit/transferHook.test.ts delete mode 100644 token/js/test/unit/transferfee.test.ts delete mode 100644 token/js/tsconfig.all.json delete mode 100644 token/js/tsconfig.base.json delete mode 100644 token/js/tsconfig.cjs.json delete mode 100644 token/js/tsconfig.esm.json delete mode 100644 token/js/tsconfig.json delete mode 100644 token/js/tsconfig.root.json delete mode 100644 token/js/typedoc.json delete mode 100644 token/program-2022-test/Cargo.toml delete mode 100644 token/program-2022-test/README.md delete mode 100644 token/program-2022-test/build.rs delete mode 100644 token/program-2022-test/tests/burn.rs delete mode 100644 token/program-2022-test/tests/close_account.rs delete mode 100644 token/program-2022-test/tests/confidential_transfer.rs delete mode 100644 token/program-2022-test/tests/confidential_transfer_fee.rs delete mode 100644 token/program-2022-test/tests/cpi_guard.rs delete mode 100644 token/program-2022-test/tests/default_account_state.rs delete mode 100644 token/program-2022-test/tests/delegate.rs delete mode 100755 token/program-2022-test/tests/fixtures/spl_instruction_padding.so delete mode 100755 token/program-2022-test/tests/fixtures/spl_transfer_hook_example.so delete mode 100755 token/program-2022-test/tests/fixtures/spl_transfer_hook_example_downgrade.so delete mode 100755 token/program-2022-test/tests/fixtures/spl_transfer_hook_example_fail.so delete mode 100755 token/program-2022-test/tests/fixtures/spl_transfer_hook_example_success.so delete mode 100755 token/program-2022-test/tests/fixtures/spl_transfer_hook_example_swap.so delete mode 100755 token/program-2022-test/tests/fixtures/spl_transfer_hook_example_swap_with_fee.so delete mode 100644 token/program-2022-test/tests/freeze.rs delete mode 100644 token/program-2022-test/tests/group_member_pointer.rs delete mode 100644 token/program-2022-test/tests/group_pointer.rs delete mode 100644 token/program-2022-test/tests/initialize_account.rs delete mode 100644 token/program-2022-test/tests/initialize_mint.rs delete mode 100644 token/program-2022-test/tests/interest_bearing_mint.rs delete mode 100644 token/program-2022-test/tests/memo_transfer.rs delete mode 100644 token/program-2022-test/tests/metadata_pointer.rs delete mode 100644 token/program-2022-test/tests/mint_close_authority.rs delete mode 100644 token/program-2022-test/tests/non_transferable.rs delete mode 100644 token/program-2022-test/tests/pausable.rs delete mode 100644 token/program-2022-test/tests/permanent_delegate.rs delete mode 100644 token/program-2022-test/tests/program_test.rs delete mode 100644 token/program-2022-test/tests/reallocate.rs delete mode 100644 token/program-2022-test/tests/scaled_ui_amount.rs delete mode 100644 token/program-2022-test/tests/sync_native.rs delete mode 100644 token/program-2022-test/tests/token_group_initialize.rs delete mode 100644 token/program-2022-test/tests/token_group_initialize_member.rs delete mode 100644 token/program-2022-test/tests/token_group_update_authority.rs delete mode 100644 token/program-2022-test/tests/token_group_update_max_size.rs delete mode 100644 token/program-2022-test/tests/token_metadata_emit.rs delete mode 100644 token/program-2022-test/tests/token_metadata_initialize.rs delete mode 100644 token/program-2022-test/tests/token_metadata_remove_key.rs delete mode 100644 token/program-2022-test/tests/token_metadata_update_authority.rs delete mode 100644 token/program-2022-test/tests/token_metadata_update_field.rs delete mode 100644 token/program-2022-test/tests/transfer.rs delete mode 100644 token/program-2022-test/tests/transfer_fee.rs delete mode 100644 token/program-2022-test/tests/transfer_hook.rs delete mode 100644 token/program-2022-test/transfer-hook-test-programs/downgrade/Cargo.toml delete mode 100644 token/program-2022-test/transfer-hook-test-programs/downgrade/src/lib.rs delete mode 100644 token/program-2022-test/transfer-hook-test-programs/fail/Cargo.toml delete mode 100644 token/program-2022-test/transfer-hook-test-programs/fail/src/lib.rs delete mode 100644 token/program-2022-test/transfer-hook-test-programs/success/Cargo.toml delete mode 100644 token/program-2022-test/transfer-hook-test-programs/success/src/lib.rs delete mode 100644 token/program-2022-test/transfer-hook-test-programs/swap-with-fee/Cargo.toml delete mode 100644 token/program-2022-test/transfer-hook-test-programs/swap-with-fee/src/lib.rs delete mode 100644 token/program-2022-test/transfer-hook-test-programs/swap/Cargo.toml delete mode 100644 token/program-2022-test/transfer-hook-test-programs/swap/src/lib.rs delete mode 100644 token/program-2022/Cargo.toml delete mode 100644 token/program-2022/README.md delete mode 100644 token/program-2022/Xargo.toml delete mode 100644 token/program-2022/inc/token.h delete mode 100644 token/program-2022/program-id.md delete mode 100644 token/program-2022/src/entrypoint.rs delete mode 100644 token/program-2022/src/error.rs delete mode 100644 token/program-2022/src/extension/confidential_mint_burn/account_info.rs delete mode 100644 token/program-2022/src/extension/confidential_mint_burn/instruction.rs delete mode 100644 token/program-2022/src/extension/confidential_mint_burn/mod.rs delete mode 100644 token/program-2022/src/extension/confidential_mint_burn/processor.rs delete mode 100644 token/program-2022/src/extension/confidential_mint_burn/verify_proof.rs delete mode 100644 token/program-2022/src/extension/confidential_transfer/account_info.rs delete mode 100644 token/program-2022/src/extension/confidential_transfer/instruction.rs delete mode 100644 token/program-2022/src/extension/confidential_transfer/mod.rs delete mode 100644 token/program-2022/src/extension/confidential_transfer/processor.rs delete mode 100644 token/program-2022/src/extension/confidential_transfer/verify_proof.rs delete mode 100644 token/program-2022/src/extension/confidential_transfer_fee/account_info.rs delete mode 100644 token/program-2022/src/extension/confidential_transfer_fee/instruction.rs delete mode 100644 token/program-2022/src/extension/confidential_transfer_fee/mod.rs delete mode 100644 token/program-2022/src/extension/confidential_transfer_fee/processor.rs delete mode 100644 token/program-2022/src/extension/cpi_guard/instruction.rs delete mode 100644 token/program-2022/src/extension/cpi_guard/mod.rs delete mode 100644 token/program-2022/src/extension/cpi_guard/processor.rs delete mode 100644 token/program-2022/src/extension/default_account_state/instruction.rs delete mode 100644 token/program-2022/src/extension/default_account_state/mod.rs delete mode 100644 token/program-2022/src/extension/default_account_state/processor.rs delete mode 100644 token/program-2022/src/extension/group_member_pointer/instruction.rs delete mode 100644 token/program-2022/src/extension/group_member_pointer/mod.rs delete mode 100644 token/program-2022/src/extension/group_member_pointer/processor.rs delete mode 100644 token/program-2022/src/extension/group_pointer/instruction.rs delete mode 100644 token/program-2022/src/extension/group_pointer/mod.rs delete mode 100644 token/program-2022/src/extension/group_pointer/processor.rs delete mode 100644 token/program-2022/src/extension/immutable_owner.rs delete mode 100644 token/program-2022/src/extension/interest_bearing_mint/instruction.rs delete mode 100644 token/program-2022/src/extension/interest_bearing_mint/mod.rs delete mode 100644 token/program-2022/src/extension/interest_bearing_mint/processor.rs delete mode 100644 token/program-2022/src/extension/memo_transfer/instruction.rs delete mode 100644 token/program-2022/src/extension/memo_transfer/mod.rs delete mode 100644 token/program-2022/src/extension/memo_transfer/processor.rs delete mode 100644 token/program-2022/src/extension/metadata_pointer/instruction.rs delete mode 100644 token/program-2022/src/extension/metadata_pointer/mod.rs delete mode 100644 token/program-2022/src/extension/metadata_pointer/processor.rs delete mode 100644 token/program-2022/src/extension/mint_close_authority.rs delete mode 100644 token/program-2022/src/extension/mod.rs delete mode 100644 token/program-2022/src/extension/non_transferable.rs delete mode 100644 token/program-2022/src/extension/pausable/instruction.rs delete mode 100644 token/program-2022/src/extension/pausable/mod.rs delete mode 100644 token/program-2022/src/extension/pausable/processor.rs delete mode 100644 token/program-2022/src/extension/permanent_delegate.rs delete mode 100644 token/program-2022/src/extension/reallocate.rs delete mode 100644 token/program-2022/src/extension/scaled_ui_amount/instruction.rs delete mode 100644 token/program-2022/src/extension/scaled_ui_amount/mod.rs delete mode 100644 token/program-2022/src/extension/scaled_ui_amount/processor.rs delete mode 100644 token/program-2022/src/extension/token_group/mod.rs delete mode 100644 token/program-2022/src/extension/token_group/processor.rs delete mode 100644 token/program-2022/src/extension/token_metadata/mod.rs delete mode 100644 token/program-2022/src/extension/token_metadata/processor.rs delete mode 100644 token/program-2022/src/extension/transfer_fee/instruction.rs delete mode 100644 token/program-2022/src/extension/transfer_fee/mod.rs delete mode 100644 token/program-2022/src/extension/transfer_fee/processor.rs delete mode 100644 token/program-2022/src/extension/transfer_hook/instruction.rs delete mode 100644 token/program-2022/src/extension/transfer_hook/mod.rs delete mode 100644 token/program-2022/src/extension/transfer_hook/processor.rs delete mode 100644 token/program-2022/src/generic_token_account.rs delete mode 100644 token/program-2022/src/instruction.rs delete mode 100644 token/program-2022/src/lib.rs delete mode 100644 token/program-2022/src/native_mint.rs delete mode 100644 token/program-2022/src/offchain.rs delete mode 100644 token/program-2022/src/onchain.rs delete mode 100644 token/program-2022/src/pod.rs delete mode 100644 token/program-2022/src/pod_instruction.rs delete mode 100644 token/program-2022/src/processor.rs delete mode 100644 token/program-2022/src/serialization.rs delete mode 100644 token/program-2022/src/state.rs delete mode 100644 token/program-2022/tests/action.rs delete mode 100644 token/program-2022/tests/assert_instruction_count.rs delete mode 100644 token/program-2022/tests/serialization.rs delete mode 100644 token/program/Cargo.toml delete mode 100644 token/program/README.md delete mode 100644 token/program/Xargo.toml delete mode 100644 token/program/inc/token.h delete mode 100644 token/program/program-id.md delete mode 100644 token/program/src/entrypoint.rs delete mode 100644 token/program/src/error.rs delete mode 100644 token/program/src/instruction.rs delete mode 100644 token/program/src/lib.rs delete mode 100644 token/program/src/native_mint.rs delete mode 100644 token/program/src/processor.rs delete mode 100644 token/program/src/state.rs delete mode 100644 token/program/tests/action.rs delete mode 100644 token/program/tests/assert_instruction_count.rs delete mode 100644 token/program/tests/close_account.rs delete mode 100644 token/transfer-hook/cli/Cargo.toml delete mode 100644 token/transfer-hook/cli/src/main.rs delete mode 100644 token/transfer-hook/cli/src/meta.rs delete mode 100644 token/transfer-hook/example/Cargo.toml delete mode 100644 token/transfer-hook/example/README.md delete mode 100644 token/transfer-hook/example/src/entrypoint.rs delete mode 100644 token/transfer-hook/example/src/lib.rs delete mode 100644 token/transfer-hook/example/src/processor.rs delete mode 100644 token/transfer-hook/example/src/state.rs delete mode 100644 token/transfer-hook/example/tests/functional.rs delete mode 100644 token/transfer-hook/interface/Cargo.toml delete mode 100644 token/transfer-hook/interface/README.md delete mode 100644 token/transfer-hook/interface/src/error.rs delete mode 100644 token/transfer-hook/interface/src/instruction.rs delete mode 100644 token/transfer-hook/interface/src/lib.rs delete mode 100644 token/transfer-hook/interface/src/offchain.rs delete mode 100644 token/transfer-hook/interface/src/onchain.rs delete mode 100644 token/zk-token-protocol-paper/part1.pdf delete mode 100644 token/zk-token-protocol-paper/part2.pdf 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 16a50fee6e7efb4cfe0da100c7272600b03d1678..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24464 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 Wdf(+SPpkLv@!=f92OHiT`+ookrdK8a 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 c3ad4a44acc942d1af07a7f893f303d187626616..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111920 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 Rj}u?xIn`SO;kTUs{{`FP&Y%DQ 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 fdda8fb327b988206e18bb1f4e0e5b925b382d5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19320 zcmbtceT-GtaX;+tWq~Aqu*)^eCS>0l@NPDK`0e*w8rLkC;JB%RKVFt3qRV4Scsgx?NqEslVDw;nklz-|fRg2Q3aor@_ z`OU|D_uVx%Nl(n1J9FmDnKLtI&d0r*FYf;2$Lm(Ca8~Nw&mFLml6GiC=@Z7`+zwZA z4fMO(tzzDQs`AR8g>T(2?F_oLf`fe}{c9N(JPX&>3*BJ4T?18Je?aQ9umzP=3|hN7 zqM^S|ZI^|wq7fB?s|0m^ojiPr^u9?teO2jklRU(W0vl;SuPrStfv=gKTj<{!^ zzjAd6sK4|E!}y?7hK)PDz@q#=td_j%VmG z?h!m}9B?pYND9I<0b^9J{X_m$rgzhCE&V3pVdFMxQ%i@B!^V%YJ$(gSG_H_=^WzG4 z2;8(6qjK&F387{rORVBUv9RAEaR1)_75jv1ncwdaxPR|kOh>ThSNo;I%j@XyV2v8~(zrMn#^9My=zgcj^5BMXEpK`tE={L)`tMzIRod<{` zZr3hy%60$Adiy4Am(&X$xF3t(L_9abqgQ|M{5>hMKo9Fh|F}vDi7H5TB z!f$bNMNn4`RE~tZdsf`-M%zVB3*n-n#8aV390|G==o8ue7q=&NHT%X}h1>E3;?oL` zQ}ap-lF~`3;c;p%j_(v&ToKqdEHD`-`4u7y6%l-eaFi>MN5%YYa-M~7Dx(7ep-;^? zE33I8t7lXZ{=#F@PrO@j(<^>daOAx~(jh(|xY;3UJ;#1F`$w%e#P1}|az0u_t&$(X zhn}KV$&*p*b>>gU|4qba>$OBqUR2|bqgILMH2*o~m-&>YUnY8do|`$&gM?!oGvsT( z_$tfy=U-z8I#n;(xPX6$`ZYWr6ghFHLiJy`O7q>YSUX%Hf#Y|Q04n7=sSg_^Kg6@z z{waaz=MSmhP#3bmgzvo`OR;ljzC>uUieUtvQ^hjj0>4ylIWO9 z@jPms>mZ~^Fl>e|O#1PfPyKlrm%pg}*!p;0+C{dGs{YXy)pLC(KfvGIPhXz#oaXnaPv(b> z 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 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 6e46894064dcf4d211ca07817de69180011bc561..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18768 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 M+G7<88`SCk4+nw{g#Z8m 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 258c11432a29edd85528c2d37da9336fc00c0c48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17696 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 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 b8184a8aa7f6ee2214e06481c8eb154a5adadab0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72712 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 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 41384d32d03a5a1fdd4904fe9195159f65cc5d00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 318514 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%0DkkrjPnQ-sAM# zFGyvg_wrwTw+&G1-m<~6t%-`yf zX-O@zV4TMi7AhVbrI`yHNc7M@Q<$B&2^S}(T^w1#sRtZ#ijmT^`;DN{G+JdRBgXLMZqSRh1Rc0m{MK|xS#=kp z12@mhLn?F^z0aBAufG-nD(aB<3trU^vcvvoU}JnyK|Ma~85&N{*Kw9oURr@Fi~l13 z8r^ub$gV3Q7LaLadw;t9ReCrz%D&51*`5LshFACTszY#mN&Vje-Bp=EAeI^=d`)oe zgtQl2fgtL#CRr4UzP}#;L*0V+2$Okz4r{j(sA_V^pP*Fx0DNNTfMqEz2=*>I*~jhK zOck*;mQh;2b_$Z5@cIhwK?G4wVsKjqOLcrbtj;lOR^T&98_@?@_aPqhh;oU6x~R+8 zO`Ga9(K+u<9CX+>DS0fH%$%lUv9_{1_tF7>gM0J+&1YBwR20 zt=ko~4)`FskznkY*cG%TXMg*YKT%TG++Sxf5VQuIxXM~mcDHxqkN~ph_$jC%I#g8k zhqvIWO^?$M%(JFs@S5ofiduGNbqPgk%}9xE^W{;YXf^Hum)dk$2MD=*y3~cFt1((5 zL<5BHMm#eBvRqQW=&`%!T%0!$sADtGJ|Fy85~O!=bH0qq6t|7B28s0L;bLw7ok1w_ zzCWpL?(sm{qi#ItYtm~8t*{;cQq<$+zExI;i#Wmfyi6H9>EwY{(bhD)iFrBqJI5wN zL!?M@i?PNwblgSYld8o2 zSjtYypIW&H4JfdB(}&=S18(G|T%XG8cfurv;6&(5W36=z@z{6@96c;tAEV1W$Q^Q7 z=X8K;m=y)V4ud9o^V(~}ZiAraitFdb=$(41S)%t|lj4Ty`?8H8Q22bjQi&B=@A zpd~XXzncfKg0*Xkv^qAM#%EU(>N2UvHLb9l3uc{t=vV~^__p-C&J5UNV@!l{TVEp1}G{>$Xcbo?|*E z26rl(b+7gDIF7Z`%*x^(r?3;`QFBVBFwT7gEU0dhOiMf(6QkA&j*TCFR`=06JMEs_ zDeV)(wlS~H9X|;+=1`2=tVN!UZ94m#+cYFAxgZ~c*<*|7D*F;YH&#{VK;%qkiM-O+ z#pUH_(;Xnr&(KZmMIOknS&#pI?8pVP!HD(i+#%SHhZ*YGO_`IyYJqYNCDW`mLbd5O zzV5Hbx(0GQJ#M}0{jc7g9#XW$je8B40&P-!^mo1l(9kkn(GY)W47Sbq3c%s`yBkiw z*tJY8y%GWFk_Lf>itAR+M_#-yBSUs^!A!O1kHVhw{^3~VFPhz>l8}_i3|nEfH1xV{ zh42!G2ZpDdwVK--!VbG*3>`$W|sX;)5B^}TSFs&GXi_wfp8St->~ zbq%A0EhcCW8hKC7F#@|~75MXk$Hl5o$PGf?4)5%x`2u{?ba$wc>zUv@4|!VBo=Co8 z9(l2FD`n(LrJ~$Td{0sa?k9)D5>45NBV`l0eT;YB9n`Oia;0f>c}#6W$F_r_BOYg2 z;`CQzkXoei0MwvCAQnio|3xN+#{j)2e6K0zn>cQ;iv5&?{9P_f>)pnkJ*BOm^iFpp z8+sM)qKDmdWKAg@-G=6sDC8Q_Psn->k3C`B1-_Rdd2>Q$2$3L3aNP2#JbTgpa~Pu# zyoxPIj+-bm3_hVK$O*6gqBzsL{xtJ>{2R5wKvrV~ohSSchW%#p7(*zIHYHT2$_Dwj zBux&+creaakxtg*o`ANuzCYeK?4o&!>EcvI)0^WorAg7TW+~y@K^7c3xbVlU;J95w zTtXHk7qN2BL?R->=P|q|K=xl-^xZnW-T|NXc2VW0GlLxd#|Mtim;@dDmB*lWXl1t) zAD^uv_ieDol;I?@be!&GFzSH{*HpC_O56<`raQ$GsgsW2#Wri<+9p@>jF_u0SXq9Yp5hZo=KFKptf? zT~vAFQ@CE}b8~=Ls$c#Ew%y!CJ^M13rTP|2J%thu0N`#xWmzGxuxG!TF0P`Hs2J&W zNTxnH0+*VL#TVKWD!9@nHiIabhrq&CFh57POYg>RVLb1cnRw&DLQLp{ z;!$n2MQW98#R$wwG}_c6nDfyKUGF}dv@ASSK2!CqB!z>k{u)B=<(*}m{Mu9u0pwI! zjk#GYwD66YKSIEZzx{$UrC;v8o4)u93wn`ur)Ez@ta)}}PVQr)SBZtAO5}iP%Ynj(FE_Q6J+^p_-pd7*$OwFXy( zl~a7WnT!+BQkyu_l+V1yd&4D{peoJCvuLYi!{j#RUIcz<`1aA3uFyI2Jk0AWtB;`s zdCw3X9yfvt*rSk)kC@!b#h-^nD~ZPN0pMiHHj;lib-5uZj6})tM&)1emqH${?bI!= z2<2OKzbi9Myo2j$Z>{yUn~boq!{EPJefOroHVQ%HL(I+L)DY#VHW=|O*)7jW?kF!% zb%cv0Ebauo)&MKA?>n*J*Rlo7Azy=Hb$BZ~GchmuE(oiD?E696kouT7|0o}r6kK$s zmC8pUfYsm(@M80~V;~$v%{Zz^-ez_T{p*?zL&^zDpm=g6e{1(~XhhCs`;JqL?`i8C`%-CYWGzlCcvW zmJLmCc6ZFq!Z)M`}~9a^c;nF37R;I|5!wrwAJ z!I=fpp}GI>(;t_Sx-vud*+e_0AT-XsNae2U^giC!!7%(i00n=u7<(VoHGLOW?Ks4j zJ`~3n3;A{1`I#7c}c;8S9yz<~r zn$V7TV|Q1~AC>5H_J0CB;(GLE^zWEawF)8h^}*x#aC1M#tc7VCMgikRl?um@Hup24);V> zTII{DcI{Aj4C`8!h@EQSzRofiMBs#Pm@p4eTuzRq2a!=y6O;-nBFP>^NYNOsZs-61UD=+$bBDyUKq$fy;KB!y_uW>8i$%aTJ$lzn|Ofh_@%buHE+iQ>C3e zb<6zt)RW%zPQU85CqlUbv0y%s`!W(9#cNMVbs?du(=LP{#0ttd4fF`y<1~O>qKs*I zp5V7DE8btxvcTl`3JTpYC(&oQJS*&U8`JlIKW~Of-yn!waF@sup@w=oKrK8Y3ut|H zO#{d8y71<$Y6XY7MhiiGqiKy3ysda~W5F`ql2;Pi^}`mTdTY9BEkCJI3C)-E@aSE1hkXmenGXEqJ;HG(VRLT_aI92HlPS4(>>mc42=CwBLXBFeSd|Ngs z>Vze0N=%|vk`-ET({89&?ryPGd7V{2l}=;DqlS4zPM%(`YAS!yuKSO<^fxOH{R#3r zbShfq*T_@qKV*mUEevk_xTl6My9W$xAdJ6!I&^}Ih$+~XAZ5<*4lq>_Rf?KGk z$U!dVW!Mo#`u0S{2b|>C66+@rS)L6K8n@fN7nyvz1R7oAL32N)!S8RJCpFjNxYT8* zA5F@Am+S&Dg#TWx!pN&6RF~~a zzbluQvI|IMA-~wMR54d4gmc)Rr4~2|%w(9$n-$Z zKS=ag)8UtSGdpDq79BMFTn=$f!1&4<9xQPz%@wPe*i!weVO(#er`=6y&)vHtN)m4< znzFnP9}col1iv7OfCmo6|3OXyNMvL_G?bIwYt7Auy+wb8XP1EIGljh~okODaV4!k! zP6>@Ae$h#7`&WS2ZQ6U*D?wjPkOhDLQ%Lh^L-20Mcg|e^*v2MrWG2PGg-#6FS4CoR zn^;7RVsqo<`>}!=6j;9XP3*39RuU9q+5i#J3Hu>q2e)nFd6h2u`;;0~UtpeZ*Ie7= zJirsjw`iO%I$Aq$V9F_Ah6y!oNl4$7X?ra+J=<n} zl$wIxp~Jt&x!rb>d}VYLu&!ZeccN3zSQILtdupV-ME7t^YsfFr0Jm&Rv#i~XB<2W7 z!1#Xgk+IcH3c$lX>q*eEcl(W>ViSvwV^$4n5vpPWC=fBf@#-RO)VnEhQexBil|@6( zQ|Fx&f01LUhWCR3r@Q|pcl3Q6QP+cEaxZ&9IP0$&18ToydEg{!Iu)}-4noIl(0m6s zZtoH`s>3^n&kvPAj8BD%RCgsJ9*(V{v6NsfqlvAe3uix2j#*vBb?dZ?-~zx!Es-lM z%-RXK+3}v#HcXVWE<-&P18j#mh6izh@~$ZX$}~MgD=}P)Pp&ij+NWe>;@TsCGK1eO zQoVaO=;3ALXymF0?}Vesq(|Oviqm`=!U!C97V6?&)f&2E;vHX5-|0g3?!ZDb#mfc` z{PJd7*x%gnEW}$IVHF(x9k%s$S+s1?lwsv?et19CNTNh`pK1Y%MiC0tJK;gK&#$EX zXq`7L%B1C$Jk3~z(l-k((|5pDTl|Y2Z|d#sh20&|u;ehW3hTkT9$$c}lu6JZF18Rb z%D1QCSmNVn&K+86?5G4#S2yk-vPmB#2exw_ZwY0ih^m~C*%SPd9uwPitPWptcpu6~ z0nfrtIJ&A2#H#2fW@F{cLhid#qj}96w62pNN>CH*h=@@+dNP}7qD%owy6XHe>h}Xp zYQ82aQ#CtN0x9^Fy~3p&Nons|E&Uc?hSGP%T<1dWW%2iZ*PYO?;vG3W#86OPubSET z(sx3ytJd4o#_V(qZE}sEj7>iD_62#z=QJVZQeAzE6oRnHiZbD2D!y6q>JpK9{SD_y z9+4yqfv2sDU6V4OX6sUBeLAbOX$M&6L})j#Io2m|*bcTP@J@ai1?AQ{9WE96)2zcu zKBds_u-_t-E_-0Tcb-|AYQwLOxyXeGZ$TXY6GPQepI+3xNlJD5tLrIG*#b6LH zA}FLqcbjl-$PK7UA;HECVb4&WXQi}I&OL1m{NTu3>yzocA*iy zS1j=m;jS|THKJOxnF1| zZpzki<>BF@%cLv-A?VR4H#g3H+Dy?5Tx7QuLcZhZwS-SqBr$C|9B_Eae|8VBtCSu| zgh8D57)OisP`nz{kHfW$T33jBn?3^VhcyXPo~TAyX^PW3a!*kC2Tqe`Yn9O;DPaSp zcCaNQsq5wZoLbNhE&6E zKR=v{dSdZO^uMZtu6&v6PmefI;J@SYM1rvB*=1f!mFHYk@T0wtJ)9y|0l*qr;6pQ* zK_1ER;BMq4mK^j&WJUsh0idfW9K2YSkIT;-v(Rv^uBpA6lWd@#Dr6aP{^m7lsoLbS z9b=z43XMe~2X3j^AO<`)OJNYw@|~-u7+;;afz^~~;NslhG@8;Tfke4b4SbMXr&fA+ z8bbFhon*9e8!0pe@d}WxCBHWI8cT%(X1Ic_m(cN}5>*zfOlle*-Nq`{ADy4Q9fTYB z{lK!TRjkXK=sH8FjZox^3COA$xKrKPt(Fb1hVjGp zccCHup|&VJZ3^#=PklD<C(8j69O11R%G2+pdSRXz&O>(Er+Oer&?`r+| zdCMfNJh!wxFf)qe5GT)7%H=tXLpgf?f^{fq87 zh8M7G*;@b90hfuc+-8r=e?!*7HXu6oAS0@`Yl5@DMQ7?L13eUD>p;9JWke>-pJb9~7oYefMj^z*+u})@i0=xw zpelxea)L3bTI~IE)aImw)toi8@oyRCW^2a-25^$3w)Y8ad}ic2JDQHGq$DNmiWhNq z)T8z4f`U=l7*<;4xr`F7COunGLu{R_mUt_7>Lda-e)lr8j2MS$#Q3eXPY|n@>=Rm4 z!&cD8oJ{t-jlY7PLv^fc}aVWy(Xgc;g;os{BN9^SeJ^HlsM8f_eonDEp5P@t)aMm!0e0ZcIRBgS1|HhJ|v0zn)RK50kEkq@Qq(-7fEFUKb)$^--(6v zsVW=N7*P3%*x>1c6@IX*KK)moT)?OK@+Bu^XNPIn4(^3Uj z)2_C6P#HkMK&J0A3ozhy;yqCxhwAjb`l3R^kUriV93JxRh6Qcs7@|?sua0k|6>+oF ztr!l>g`ogci`ShGLG<)kH<-#B!l|KFeWUj z;&QJr>)UQL_q+7II}Ns4+bhp)MG<{uc60ayK&S?9oewlen}uQ{KhgIK^|wUflER9w zaSHeJ6RCzpiAGH`Y?C{dWOlqMJwF*HWcQY};(p|BxEy&T7~Hpgj?}|XE@?r>H5CJ! z0z8?3tF_edcBs5L#MHkO&`7mo+ujeovPDI^D3nL!S z+IvoO^F88UfYH+JRN~H*KUgmo(Xj%g?M?rp?UXVBpDbWHN!^a07FM#f$1&s#qEyE^ zVMI5lC))Ae!yd!b^u%Qshdt1jYK&{-|GktSEPO;j35~5Aa1ij#5WmU$t3Io7z6+Np2QfM@!9U%%MCHMo7TzUw@hTEf3Q`1e<);M_$SbkL$sg)!6{~8cJQm z-9#k5VYO|6rm84VCoJOCiU0&k*ECE3@D$;HIrFNyaKRr?Gy>M63B?mWxEW!a^NLiMus39uRKH<9CX2_K0@sxg3lYD_ z+Ng0E0Ii&6RJr;{b2Q4?yY~!|-EM&9m8yL_q+xqz>|RKOVW4(G3C7xfE^i}QtUMou zYG}X$?$oAIdwoTJP}bwP%h1~9m;=Mz5V^MVz;|65Oz(K!IPJgC3NPH-7UPHY$ShuG+7RR z+`kdkqlfzMW5u8T4nwCnxK=y&w=!<7tp<5h0fka4x|EH0D~<`qKiyjJ=pa<*LK1QA z+?<{rhivt{g%OlnM4@!-Wisi%Cwj-x--x0mt~?qM?EjmG?jr_C=%L%q`6?hMM$um& zdk#3V$mG8_lV5rT=I1suGOWw8R)5&jrJG=wrI1YW?@lQ{h4T%|CS+WM0dnIRG=`P` znrlFjl*u8~b7m43E2Tn(tbiCiOZ|JGn^!lJ{y!$@G@)@4?%du5iDt(VnVZ;0{nakU zsfwAA@)$vOOjoF;oKakG?=@cM(<&%JJCj@96x2~i?U;8YKnnPiB#75%)G5x5$_Z?p zQ-_e4&nM+<-N}aE`vQOV*^G{VmPL$Y9Lz-cij6BXvMA89S2iw?#j{eX;It8&9HNcR zSl?V6@pw=NTPx9H43?*8jC&GUu(|LL_qAY(f{$bNd-2YGmrRlRlsBIFlijn^pZQP#B(+H3Ov7s>-NB4`=qjoh9s4}IRqTy@;pNj>jPt>3+=lF~5KNNY z*g)Mo-~VQp!+i1o7h~tJBn-C%$hK|Uw(YNN+qP}nwr$(CZQJhoH#cGyvzXnfCpZ;R z8JPj5G$x-gAO~bl_M@e(sNGF+oGh9@HECYS+25>+-~Vy|3*~0kybaMtmG<#Tv-4-H z345xv=p606?_IrF%yZ@O)X!=YXJlvE`W&sZvU3>jUwn=a_vI-b`dMQWrUt!Mpy@AC zGtF`jA&adn*cx57+qv^XMZx|w`DZ((O;-?R{PI~Gim6(RQ%3a5KEHYEQkcqidyd3l zdTPkH4JmRJoFw@iy+6$D)7a2w1O)R&;Rji6Jxxg0NjaCyBpMhfFOAuU zRn}6nL)+Bylro>7D9hl#35){GHI2M{X+%b?-yLH>>r{rDdS=!#qV(oTp)Y{+K^0Ko zVht_v4m|hk#7llD&->YJ{0@xQ$K3Bpz(z65GG%UXYy9F_&8)mG3*SQeCyn-bahSZ{ zuwCFbF%M7VkRG;8f0U||Z6hg5Xs(y+Kd>fe!t+$zc8$V)zM*!9wP1Lg_4ID=ZWcWj znk`^4z8V~|`idAyBk0ptVwP}$T2e9DmUmNzYIhWWM29y&xnt3L%tV41Ti#MfOx?Jg z?9o!3R@B!cfudxe-@*g{^~9&lV^g5|9A@7hvG;pC`@b|W_Eq{K3kh_g{r3gl9y9n$G>hTlH5?&ZWl^6rklbk@S(>s}o+1IOxCq1aSBsd(l-eSz6tG;#xazsxtWPtN4}EQ*yQ@&Xr-+cZ#%_kbo)G4^psG*df&&&Fc{LY4K6WaQJYFs zrCK2OCY3y2{wIZul8b{M8IodijBGgQlmvM<#i%M{QKm=NQ!MJ`4J!Z@Afd}^!liZY zm2rk8m@sDAQ2V)mjhqw%=PVc}7?D8POJ8b^$#kVqrAJ9*rB_xe->>W#boVj|C&29% zGj9C+P;92tr1B!2{taK#)v$gJY$uJu-7k4P?5lFY91DoW8Yz>LYKJYcvBVqH<|YBe zBfVvZXVz7S*M7oj;^zz?W6e(`av2f_LV1q*mCP)C3TLEg67hsFvbDs!=Pk%Bd2Kii zA0};^JsRK)ti<#1>oA`Irsm3&-lwK=WZctTnh%f0G9Xt`9SM|zD-?RSQ8Ddc8V$dS z1*8LUb$;anJ}4+VLmup!;H~fJ!Ye*-futhQS-$KCLxp!EpIl?ILhlBBYBwkee`^6# zzJGPfE7M-c*-&&fANLg@zj^iP_pO)SQdHd;>QOnV$I+nc_^aiv!LVqHV$#es!C}$y zP|f}$2B1}qH6y^l-kZIO%e{n%~dx5uYph1MvkCui}b*JbhY0#UF3>Cs+{ zMRphm6P4F#&B$M{d^`Sj4Z#?N3%WQQek&u30RJ{B*_%5%HdK5t8ooB`%fx%#R8|6D zWbBIZFs%A~oU!MQS1jtpT16bg$5ZUI*^KBC{0!0+1xLO*i zCgGe0kNL37)nI4_3e|aK7@Bw4Y#tX`_xH)$oWY9^I8r3JTl|=}_u9~yUKSYRbg*a&cQrhpENB^FHDbR81#YIby_G zamjWpifrCckh;7?y$Ha zT;F^Yr*e@pG~9Av2eZtV<+X~|Us)O=tPe^oQJ-&9`-Yyi29!ZU8va72DO%P9^kxgG z7@MS2Rak&W4VvNhbYC;IyU3sIy)O*Tbe|a6LnIziqAJJtZV>mwKKa^QGeaT_bxx_2 z6aGSqJFQ_gaq15ZbASlAD`Va&Q-7~s-WI8FXCCl#67(rRf(_kSQ%axeuQ6uA_U0pH zIw7*;>|@Zo?_I6rAf`F<-hOEfesri>jaLWXtCHx129~u?;$ zF_whhJZ?%fkwk;|XOb?_TCZzQKuH_;k|~s}v@=|2y_aULjv--YTz+Y&@p`#hypj^X z{q?{FN}tSHvpFD`m{uN*$K`MM;Ty?cL#f>%?7Pg)7Xjvu0uhIVFEPQuDO};_7h+2G z&*_s-j?|ISOwWy#L5S4}A0j~l1*|24&bge|Y<^S>Xu3X3NMW0n#bGTfO9=W)+dho; z9brgi9gR6>0hPBk-4>*cZ@`dTegm8irny5OQ!V<~sfI5~f;h|5z(H@w{}d*v#LC^+ zaw)DPovXX7`kqW8z?WNjYAyRx&2+j%g{ZH=3o~7q{DmpC-toyBNxl)8 zrTsJqbs9#AWYhNhwJW~`IC!K!VhjJo{UcTb&hWk9ROghv3Zq6IVef=jmzOici}pUAL5>?f;=k+B zw@jsMA|$))NN$``pA-*srWml)HtSkz%Ycff(^)rxLmCasbY~htqiyi?N~tp z6*Pt>Sm*C3KS&QnqjBQXn&J_TQn4Ql9Vp9lrSdm?AXM)3cTuYvv;1QU+y979rb--g zta=$?QoIb--ts%7AUYhKnLeB0RdPxNAeoZXFe9-dM!q9kfnLc|?vy;sGNRoSFUeA; z%JciUPVX<0J8wc3VJ|6S0L!pp@)J?CSAi~nL#fabIcrYiERsXmX|_EFAa+gA$*Z00=T>yfaHZ?@jPutca(oPtXRFr-_AO2aJh_l_JP~ z5EjGU9afvMKhAn~+GPf941Ku@Av?Hw3Qm*84 z*X9#au}M9p^i_iCRwoeA6F6DvY9%KkZ>FY}(mxhG!vUdZ4ucdpdIFOuET`Smm`{aa zpI)+?x$zs?>>XLp)BB8+Flaaw_UFvozFl=G(XI51h917*OUdy>fX9Wm19Em|N;D)& zGhy0GTTvGV9i|1}Y9j&2e)!w0P=xl)j{DgD|UP zn|*cJ^zA&d6T^vX%kfA&2n6L<2+ZP74INA=?%)*HBo#|nK%c##{HX}NoaREIDQ|+0 znqprIY~^1JT~oL=l?RX0Anv-Qm|!{PC!OSPM0?|-kuyri*cM$Zr&z5xJx%&n&DIz! zm>4;_7(cLJSbp|p+~GPy1(JGwmR4bG^K z3x6r;c9J{riiz{#r*t)FNQ5<&8Bw!iXe2-Rx`)H9xgqq-1*KF`(Al<^# zMxtj!yp-Pg-XD9_pM;(e=X@-+`s$MBU@S0ZPoC9=V=W#`9@3p52Vsn{rq{TY2fnPW%vY`;nz=5&KG{@@b+}YYYzJvQ z-$KB_)c1G=F0Y^Gofq;&t^NWKJYHgZ?bodm}Bi+cF$TjqzB@M4A z;YzJi=}%#BX77ltBaSmc_VW+q1_$X#eJ+qC7ydGaAu?MDy-DKUgH_l8;Z((kRi9n@ z@a@13h|Bg)rj{y1<`y82J2U^?jaVlJS6fsVr z&E{-s#lYOAWplgn2!%H#)|+;zvT~SOen&%kF6JP6Nyf9cf=XIEBI+MxQ&K)X}jwh&4)&60{8LSNwNr*5ugv_f>S>6 z&_cH)W@R7<>GwSOQc>^uTz11n*Ky~3{dyNNN(zfXEwq0dM|b5`rB+DY>PP-F1T+9G zXFct0+J{$Mrp6+qn&T!*FGCryV+yjpw7bF|yw1u{#e(umHqnw4L!b0;7*c$ND3?hi z&6IZ$R*tGXZm5tju6zB_O3ots4$++F0CuW+NtKH5ku;Vyjz;XEbg3x#QCdIdMycq! z+AoRe?a}8)$IC&X*+G{QeT^}N;lLU|QIGuqT+ctR!$7hf!U%Q0;lEPdYnbny*Np&j z7w%H_=Coc-wVjfYluY;*w@S?j*jV-+HIJ-Dp^es_u=)3GoxKFBK}l*PFbCZ zB1#MMUR%eYq&PRms2i<&M3Mn-rp5HaoOUEhHC`fZ*Uu;ioek@?jk9iLMHK_~(JkHgxecfaU0iu(TfskGPtc{WvWpI#(;4tzjHgf_|XZHSyi za#v#IeXXjQv#zhRD!Dt3N3Va#pxZ1Az$Jb#`a7u-iBNCW$UlVV;HLyY9|H4=^COTK zR_{|d58!TzlZpj^Tjl8zWpip?ni;98lh|H^1VNx0*}tt#ArB^nXX>Lp8i@xTwxf<) zdt87DuAKyPRj$OgCoK4#hh44J`67q>81YAj0ow5|<%>9CfsUmWN(O*8TEb!GtaC}P z$j$GzSHpkXJ9=7cld1#^NC;LZaght4MX8%la@|LV-his^QVsZr0(a`L$p7Ugzv1m< z>9=d+T{YBtB``X1F85yT;WH^YwGYnDN*EjI=(C+R7qUH)#`sZP;$cSbE!b>qYrs^@ zZc$WW_zo&ioFlr;3@AQe+njsc9p0UhhX31+^7j0_QS#H1Q#**qdco^r4G4UKn#ZFp zt14l@X}t62Fu{BrlR3)$*ugZasB~)zgDMWO2!ud_rbpu8MvOw~Jr?uJ!L*IUsrbP6 z7oTyzVGOo{N#s}2oeOk!29EsGVDsBCyJHdZhqoT7P_4LYfpFu{t#V9+FkQ%aofF~If71UCfpnY5$h%4Y^1a|6&!#u#O$)9c0}M=deX zpZO{`K-0+A_e=I7Ti@BGA8-?^4{t}IL+9`ZWzCT9%^`yjJ3l(n(UGgpHE@;|Io7)} z=#qgQB>!NS2mV-63B*P-s)fhbC$ls}26K6ekq3!zMO|1CF0s51qOT!z_~iEfr3CGj z`9AxeW;zZ*aWXNJ+ta>+4*=d7xh7!%dNCkgF{dub7tg(trJ!Hen{9#N3P03{gV>K@ zq8#UmoVH_~a7g*3uU@teuqM{Ra zKEYZ{e*xYVsXI-ZabK`Q0Ba(r-%#!Q@D_yR-_CDlI20=9&;8O{1tk5j`j~2#6Df9c z_dKJ%hZrg=&rX!%sxm%5XTp^8UuSEkeV%AjZi) z`)1sSPiUg)W-X49`=7JIf`UK81$|e1NmEpo4ePppH3=Xx9V*xR&I3}ebYiOK5y@xT zD=}tz&GpuDTgUSAMLmCreMC?PlOH>iJ{;vR%b42LbjY$B_@urd08@5o^e)Yw>4Y^> zx8)@cLK`py1WoaBX6%=E7$%p&4{`bleod;f>(wNVOxrb(@3Pg=Orv zLWYXAoaguSp7L=fw`IM&d&?*6(htOps(ja-E1y1eaXRVUWhVGoOagAQE!qN{N;zq0 zG+J|wqTFN_61N4kh$dRt9y%Y%h?J>*mjUtiP0HBrFbf`9s7}Bl(1lXJ5t5m z2LZeX=D!YKKpZ9;D|l$2wuXOVRS*cCM2#5Za)fR_q%U{dAWLW9V0(_=<%PmSYBqMV z#JSKMbHLWI3adojKPOlp;TmbNK}S^W|BklTxWm~b%9P-VLJC^nfIZ6SN-;&Ylv0`2 z>C#@fcMFd~G=DO(A^n$)s}VH#QLqq&u6={$qu=>pQW8>q$cH&zJ&)d%ql>R5*tu9y zN<@^xyX4~+7J@>js#aXsXTp&d!EmRiMm7^d57@J15~;Z5#jG|J#zd}UHpZL0iAt4= z;zC*3zAlF%o;qGKS&|%3zc8Fffo8iXX)U~n&otUX3D$(A8{6V+lqwv+_*2^NG-kr! zQabdoeDz5TWZrvZ{}!c+Q7Vkk8Cy$AO8O0$jy~u~5H#S7B&AhE2mJ=CM%$Qjtl3+X zbRTN5(T0}F%IayS;mK{0M)6otI8m)1*~G#E-Qc)Tc>&qeZFKQ0r;qd%0HdcaT!s%r za2kSi0{wSR8230?W&m8TB4TAWO%NtY!**D*WT3tFdC zB-fCWqD~eDEL4$~;2+&8S>gJr*-1=>J2ap+XmtlQw>}`IB1dIvBAbB$Ub}UK&iYcT z&tc^uxFyt1c3do+UTi5&jEVWLO0CjW%7MYEU}Jl4|8Zt`)DRVkj+-0AC}(ocrutGI z{dY`cY?PKNXtX2Lp*vgnOv%Rq`Scb8q&2CdHBN8e1ZT6Vw<^ppf?0s@oSVaB?Oqp@7m@XZdckQ~S2 zh5giDe)yT4*$XhXwx+bK(#ld4`b7OetTp)N!)NJI7GG5g*vj)Cz?Mid&N|LlkUQB6 zuJPmVnJZkF7=C}9hnt^rMR5-REfWX#m=<9Ww3@i@qx0FSttD5w54q&4E69j9bhpJE zt8s2TD@D;Y!+Fpk6yN}!w-~Ai9mXc}c!WuCp-x7PzNDmyY?YEj7x}X0#WxAE{yAL& zMBzPE}4(@xDF1#{GA4e`>{c8 zrN=K*f!+#~1Lh{sf%7i0wB;+6$)p#iEEwDBto=0pQ-FIw8<&Pl)QgVjPAF6Y4}T2Z z*mjT$VSzEuUDXB!hv%OVkolh4W{!wCT8124KZ*0lHB~3{8Tb`(tB}Wcq}qC(cZ(=; z`#|K2UN%qB9X)pJU?Pq?tG1JI(7hPX(@qAkNSJgh^&kT!0d+r=iPRdGmy{&v6vwx2 zK%<7SxFr~5#JrqsQczghB9XoFO5F6^r*2RYh#K{R5*9IO`3QhTNq(AqEwRdPJd6cQ zGS8LE64V!jdWB@av0WdZpNwa6^guS?KJBd==PQ*6qjV5*cs@nrS+<;MMQa~*B~BG8 zfCYp7qUd={ME2pct7WZBvt+PO7BhEPe{Nu~+1DpPl+2N{E36|QMlW4lJgv7?jR?eD z3r95AA)rMR7j~e>_02@TEMlr2>Jo@gTkgbWat$*ixhe_PKFn9dGCMODI zQ&ILe^?35sozt#J)K4k!Nn+<{@AWF3K^VANXJzs9im%&PoPG*t5CHW+9EVeHoD34?dV(podMVNOA06NRbR=9U>f_TnMI|_X-}>n*1}weZsNm|{NC+JPQAlc zIoisX$AA{kmO`!>B$D7F`mycw-(N_FJ>9QE$0r_NKh-A0`;^`L0bANoV8d6>`nG}V zX_IKoSSaXLmgoG8Dq_BO0J~JmqTitUE8WV-h-$@uGPSHHZS_koAoaNpY{#G$ivrRt z`ff!eIuD%1QN?uM@4yNv6KA6o5{RvIY1{d&3LZ1s-4iZ=k6UIju@J|1Noya4f+tKx z24k-(Gg&bV%Bs@$&UWaUK<{1pNiUxE_zdZeamQbMrBD=>}pDYC}i8{lR^c)DNA?^fsg zAM6QUF#!~daTY=w1u7Z)+H*|Li~CsVp4YJv6B*dd7LNdf!txW*?hA?)ojtUPdE*5% zT;(8zJAwx-f#f<31l(`2N~e+uipJnE7vhjsXz8*5`)j zkqyez&}5XcuFM@kDNYeA)KY*fOls)2-LSu6Da=u{RNtjG`BKxRkP?kU zPXCq?lS2wkNqnaqfEZ)TJxIZygGAlo?k*#CBAO6lT zm>!Zp(Z5>o-~#T4ph5yG&`E^ab@YjMQ^?h^+C132W5NW1rTBCeYJt+mnI7UyIZQ}3mA+`(bhW>KGtwyw=;9s_Qq&4QYPZE6 zNt*=4{_A0KKdu6PZ7}uPV)p=t3sP#Ur;g9dd}WX*nW-CS3&zI@MR0{RR7CpX(tvBP}3LnqHnYohD#%RYVhh+4?a z$+3#PLZj~Ad#x_&#)h)2X*D(mnWx?q4=mCj+Okt>j@t52{-Dd(70cBf@erYaT+J3; z%E27@_0bXwie0ixr1U4@IOPbTpQQn9{>F61uld`t++PW&{X!&LS$iogB)PuBL#9x` zPoU9k1>Bz4C3`neBQlS@;uI!WWbjn_-rrp6sibl7@tfNpBT55i`rEygw59F4*BM6` zmfd65B!1i$^CG>1^eo3q?yN4KILR}Mk2NqYUitC}iY_bX4^ydvupR!5RwAH=VJ3y5 z(kp{DSPiGF-&IKB9h0jPm>FniTezvzLn#4zz|?h3B{*Low5WZTZKqP^p1JqR2a{A0 z;d~D0?WR^P8

    rNbJ%fgLug1@~>V1YGl%_m}_`3N*~**&mBuQH=mv1=>FQ6f04e zU4S)<#7*`8crwEU37DLEd!2rdu`jGXi~@Pz<}fahf4LaS(Wae|eRf6{FRt@tosnN%Xqz^>8q)0JAyz3Y8kb0Nu0(GO(?to|5scb4 z1QRYz6+xRqY?Q{OfZ<$O0txJ< zQFBTVFAQCZaQ~ohG3Ul78$%0fa+m15kAEIear7#{z&7BnvWMW3nUGNCy>0=cwZ*L#~5yOjQ&4bHKSB!ia{l@{E#pwlp$i?_ zusKF_o1A{q35uLS`7~?Cb6YZs-xVOh$|SDqlR!k0`@N>DQ~G_8bk`+KbnLcObEGSu_#< z_qKhp+NZ$rS`Pxa(y(WYlKFuk@3digJVNduxpNEVm$|Mr3nzhd3v=_AZcE}kiCoi< zbH_V1xflE*N>gdR()qU89{<$|3v z=05Z%>CuXYqrK1RT&KW8))TJhG?~3@lWz-5b-oUB=)=WyYj^nfNX}kc5}DC{LvUsC z$)9QuuRUS$RM2V(C?}FSW-=^q8@$NQO*u-oNEGYj@`dVip4va4hI1?j|Id{8>X{TwI9f zzGQ(l6F&f3%Em~KHLy%R>goX#k#VptrCMqQ6un0oFfkQmFf@;U7_ix+r!QHhxGAMi zs^b(b%#jAat5g@WN_*@}N*Gh~**0^OgbEP%!jy(0Qam(f#-nV#<3u_OY6{GpEqx^s zv|n{cgrKcu#b*|+Km5t`$MlR9s#6K07~8BvvdfqUD;z`Sd6LneWvy-Wgs^nnr{%uFn%EN~f ziY^}t>Bwu0Wg0n!q(zZUdZchrGLx#>Vod_kuB)Qz3IaF~3gCsJXjRn6*7W_j2UGI5 ztg|pUqJW8|uSc{C_|2Q<12#7ioIVRM?at>_9zJ{z9=HaT*Er-vBk7m0sP?|Q^KJ!# zfEd|};(!d_=Uy zC|EMw{2wkP(e2gjv+0}%iMnFI0KE8}akhqwO zPrKRw>ikzF=(5;<8a#bCh?DOvY@et(Ybr)~D*HvWs^5Rp8w%82p}AtpucyGHj|AHE z5M&#<*BZTiF&;3g!%^K$_>`9#6b?U`0sqykd;E;JqP}_S3w5&-dCteH7?65VoL)v* zo`pcyj5qc6>t}tq>nDJZYZ899MRDXbE76_q@o;-wfq;BlXg3^La@~(6^Ze>h+HgrD z58E&AsN=taQZ;AXV)*!_=T1WVzS(H;F&X=`cIN7k<2UYIknJ%N`PABIn{G7dNiq_E;*bgE)c(SO%gO)YKPb;;-yO1H z>8j9AE1&uqh}gv0Y}2^Y6P|kRk9icu++v$t-TIx~yF=gV^u-uJ*Tygq46yhMO>d=@-Vt?rS3&?!t|#eQ*0Jlv%*JC^ehM_+K7 zg`!_Yh;6$+U&awiL6Ju<@1wU%mAVuRdzgImVbnzn2M5B2S!kR^Zbd9>`KH)yKFW*$ z{(mX+|8w9I*cns$4@ zs$MzsR?%Cj-TbQBfpOOC#93rxpsojkxttxCfkXiuue`E<0`M2ml_iJ=4+;>3KfDF` zor)gN2MFyF)R#~2Lo`0nkN+x?fjj;kR|*pZq{5R0u+;@<^M`25hd_YX2LuZF6Am5_ z1#l#g3x^fRLCyyX3fy79FgBp$vk%Yd`8s0y`v76kY655v2?5>sg8-M{4mv=vLO{uf zb8G|W=C3t}bp$#etR~cV%lnfYpfSC>y(JnPIXO8Qfpl~|3Ldy2QC|ntMR4=-hg*k* zc>~!3@MVXY2jC3)yN*T<0?apqclcVWg>G_r0~#3c7Y{-n#)5J2ioW;X^gr+)8v*Q` zv;>erXLy!t+{^=U?bq`H-akG5<=)X-=tCCF_X7#4t}&RUBPfRlu>`LV;RpcCgtE-O z-MzgAKv+IyM?5|T^NI%d2qcgts1hIi&kYU$wYUi|ZxrO$a%Kt}*52Ob&?%Vh+cy5f zJ#$1iC2V7CFgvSX-i1AS_jycUU;lE{`epy)(HJYAj=p?HuMZutx&CWAI64#}g%9QM z0zf_WBkI9o@NLut+y(p|AdsKWfDdpD1>mK%<@6(4cXkByJ2dgL^}R0Wp9eh+s{geP z!U13i?dxsu-Vwkv0N~aR&e4Cj7yBoMvAzAD-n@moAIcg$!0^q&93BMZ+YOBt;kb^<|1X$I*v0_hfIrG-yW|h$`eW^b`HvNw#{X}&1kekG7Nh?= zZd4}VTi|L8|Mt(UB;~b0{q$E3sqMT-kAWF<=OKao6t52!LB|wZFpD9PoCk&QthAD zcs7B2J=!$vjYABmH|i-Y@;TdfOKN%D0%l9K*9-@EF_9A0dzG;VtNw zS|e{qDxUrOk;~H<2T$&>-#xw?HpVq<>!`Z6zeZFjH%qXa&e}gEPls#i z$T_C#?s4OTd#zwMukFM9vWDf;(AuH8;w7=K2i4Y-4EmB(bH5MVR`vpM*J4Yz-{-z2 ziPenVi{bb0gySFOi2~;%gcl_$INl1KKfT&tFv8gJn{Zw#7|>cgAr4K^;5FT#$lVy> zl&K>`XztCf7NE*>w-jcahfjx6!V@Q$5LJ&SSAkB#KA}{}B z0EtQOMYI|$}_Wq&`*tk97vMB0God4|3idIY`Bb0_Tg_c_k@O`Oq#ajcY+ zx5wO-uNHe?O}Zc=L=FpnpCYI5VBNxN1#t}p4(lZ}?leiyx53Q$-zVrPB&IQKa>uUR zdH8?W0^dt;|Gor{=bsahL87x5s3Z+WZygu1$8UGZFix5h|RvEinyZ_N?c;43IY z_WTUbxUEzg+?jW5$(bG@-ZeVc>DG_Ll77=WhlJz8^1mCoOH}JJnQoQ^>iEVuODfV> z#%t3FUveiHLHwFZodc3%Dhyvt4)Ba2YM-Sn&6pC8?+IbC;Ab0*x6|qsM7XWsnGm6% zt$kI|M5uuvZx|*{h-CPSSV7S8Vp~l1>V0w=HCU54CCD=eQgLJ(sKGy)vnA44rL+7j zYMMo)x>3j`PS1G!iyJ~ldXxlTYDbVRsCRWTxEP`4H?kVBcM;*Lf86(d_ly0cuBFs0 zMo}Qsz9}axrMi4?@>u*>XS7ND3fA9x#i3e|q46)%k0Nq(6Pmlth}hu(E}U-BGVP zU5~hnR_-UqxiT6#8T@K$Ugm&nO3Aepv%5CzbI;UCrP>C7GMY zu=BjoH@0PWHh*KP!-Q_xXZN{zS56j@5g?dd0~uVaBx#qEz_go$YRRL=u@@T`e3F3B zD!zYR1fz{W!?5+>O~p@0pg7C_3I^Z_VPWzlP9zCFR3VC>_^8eLzr<6Lj)zgc_zQfe z#BqjHMk(PuVCx62KE6crCP}?avt_wuo&_5Shy)p5bMzhp(f8uCNQjktNEtdESox_$4E@nd9^5-e=vv5zTSDz6#x7=)>=PHf}Y@aR{B45_GB8hPS=*S{)P z_CEE(^yIJbk4o`^n zzshy2-tu+?cys>`iYj9W8eljsahLe-jL0^9&;X1OzqMbcMi&xRzDv$n_&t}Z&|`0! z)%HV}%G!?2kIf(qBfvemj<%pe5)_zR8;si3JGgfef<>=P5p8v5{ovZQEpoL+ro6(EcpSr*9DPyQO zPUH6`p&1IdE(?MxU8VE1ZsCI$Dc~u&nZI+rFocsugBc_Mj z+qrwp14E^EhlX~h580^)T zQHJ#B$CdUV9>56jM`3o3gi#tlux?Jr-cNdkP?&O%4(F2NrMK9CNLwuNhFSGM9O`EJ zqvoy*wL&hNcEHXyyx~2r z2(gp{N$F%c^PP4-N|GDgPUl8w%BsVB-|M8n{mT`(MH8l?Vb|X>tB&`gXBX6Iv9B6F zjA@Lk6e{XA+FCVZNJCUtZ|Km5rYJJ9c*U-zdhm#dBcF640lejwg3@#@OR8v(dxJ4{ zHjtWW_wcq%>tPE)w#jmm|3q%Ec5SVxk5td)@fz{AxLIGYJA)X%%q?MN!q-nCW{hoi zyEOOSx!3C7Rhgi5WU17_dVN<*waJ+G02%|iK|Nu+Sa{#2M5agh1p#TK7AbZTXjEb9 z{Yv#ahO*)=DWL)KaikuKk*3#EO2kPJ=pNBa7l2C9pk}LS7AU0C6>E@sz#E8w$I;vE zHE~}!#DqpAq1yO9fj^ne(bk5yFNH5Asfa}AU#`oWk9+7FShH4StK2VnV)kl~h&W(+ zuEdAq$9Okv=Q{KtOJy=aFCC4ecdx<`>BbpT{l+sx0@uCOQV(ZbuxCfwY#{bLE4MkY z-92?r(}3iLsa#G17qijVcH%ZJaaZ)!mM`SUn=mK9CC}3O#K2%%0cW9k}dPxsJrYfMH|+qTqi+X;dKZ6P1CZmX31* zBNphbU6b3W%85>Jx4_axLPXlr4wH4L&`)Xe3kai`poXCeim$z(vGA(P=Zwqt39+(g!b@(^2i%e zUjw4rJ<80tLJ*(XzaVPHU&L*1Gxt?Z3G}HG=3qS*Wzmn15+8(#w5#sM9MEeb>Wx#F zLmL0lQaGtYQO^`Db}PJV3w?@yqz`u96X$Fg>=7!&`V#EsZ#b6Bs1}KrA%CPiAd@(@ zP3iG~SwRcG6m9Mv&rqX5O|-3>8XqW>mARG}UGV4;WQ$t7|2oESioOnOAbVo6ch!zS zyRoY?0K*1WW9F5^`^e66yX(_Z=oG`Y;$b@N7Egs{-6hd1OH72UH29m&H|S{jHmL9N*`pz4hK*U) zA_xe6X(LHRIkFefrUT#Hk}%{9dJxPXd{u2HlSAbygWUl(FI4XFI~^8eF=3+W^vJa{ zz{O0EN0-IMYToaWBAANI-QW#sG=)GP+O~pNxq%94aROL=FKmKvk7<)JO6({blT{Q9 zWOCu{J1%lZ;EoJXPAlpKZ)t+pz_(n((Z34mCY;l(xI?gY7~;F=a&vGShzuJ)oi5dQ zlcR^01>-9Y3+LkPYk0yyEwJ7%DY0l4DQ}-&neAA_Mu36QsO`5N=xs>^k44M#U=Cr* z^$nD^T)BtEMxGb@fj`mwpeD_Q?!8NA4XAm?_X^FANw6v#M3%u_{g3e9eXCKu`5ebbBy-B2l7w&tC4#-iKmrP!)IqV47 zZ1L5`9#@qy@>@)-$vNrDe3dlqAG*7^qal zQp|7&6UpGWwgT^~-VJ`JUP*ZtM%xbwTq;L|Q5Fr}Wn4n`E(vTH6T;Vw8w$E27LR<# z1wIqK?Rq-y*3MOl;L2Y8SG3*h!(~xXWwdD+9H=zMc~sIM_$k%Ox8X1rR!?O=BYg1z z*n}{f9I8*;kL;Esm5~g#BR<*&_qkMZ+*KX~>C zJ>E(}(@vz8siyc#6`%x8uLlP1fa^j|0=(iWxu^G39ay82m)Q%+N85{mnWHQWDf79~ zJZ8t4t2*7=Fv0q^gF`n-|BsO8^@F=M*tK?)T+|4{Q~l*LZrivk8Uo#+FI)q9JAr0D zdodW~DU2)*eW$6U_m|W2NM;Lc@^Y)Cwfbu00O)7<{AksSRqE6bvxDm!ITuHlwVL~~ zJrB9ac6O;l7&daUo**GnFH%Uw*jf)Q{tQ>~Dw(iA_`8>a*Y2=s%-AZQHi(SLa6D!HYP9JKVE9&HTSyi&jqixDblg=iu`?!_l#*v#wAse63!I zfiFJN2ifT-nW#FUYn8qGy0}_c=ihlMJAv;$*l-Q@4we{U7ZlZ=YDTs90M<`M?MY0g zgzANUCw5iSR>|UtfSH)q43tK}Qk_BnErhkT0WZ2yg|O4-zq^;wqdiar!L((n+|)*S zo2E+9zEGqz-Ah#@>7>y-hUcBCt7M z_k7VC-T3RJHdu6O!w=z%)cEn-3IddW!3?!8TvcMs@`cvu(J%RCS%u;+k3Km3$j%8h zbQG%8Cw#v7zp;oU0Y1dQ$RKaLF}9`Xg8Uswn#zaYGPX4}@=QL%eB4RER~sbcLQzl^5o}b^X>-Sg zdz}oq$==2(@x>p=!!oUh&^|nlI)98KM>(BfW(AxBp=N+w=;tMwt+*_|qpBZ9$-rVx zE(7~ogsLTHXS2y&zx0fs{W|dP)8w5Huc(_`Cmki~8;)c|t0W@ZLAq0t6FoV;~+q9gA=59-H9mx-#@d2GK(UYjr`*c$Qw;FAoqaFY59UwqSOL=%!aF z-@j*G%`NeTEVU-qw1lo=SyhZ2lWfx_vWq=Y)gJ#TIK*u*3EptYUtwTWVvRga9c0`s z#12zkBHvh*-=zvfjHIFY-R_ApFJwGDq|}JTUdBJpUC!$4ZklY&df9rIy_ivY&+*5(VQy7ayadCc<9GZ5FPrjwsMf7UpGUQ*PHCq>3Igi}RI*0y$VO5t$Z5d1 zcf3=Vp*#Jkw@+ATce4Dvur%rSZ5Z;5QS?ATGV zJ;_)c#Q3X*w9DN&V|&jow0vBIyOx4V%myas^DPSXQSU_kGS90_z9Me^Y~S!%pkUT9 z?XSsrnLHmzlZnG|-ToERUwN5`vXI1N;29@Pnc5hyE$p{G;mVE?OVL7FJk~Ddzwkvl z#BBJ;wcwY7*lp_)vUy%$b~W#LUoeK3P|tDDU2WXq!$$W@M+Nd{3uAnMVcpMC>Ydrh zb6xDsbC9mJ<;6VGq1G!AaNQ^F!ox`tW;i1_Pqhg5gkRs78*@~>RAs21Et+(Id)!RE%Ax`4auEyYL+;g4)N z?b7+4VX9ej*vEJZV)6sfB-AOUP3sjc3`@@i<~)+A{3dBy?tI4)V{SBOoH4)#kv_m@ z*(ChF&KG}VRY>Z~yIwijmjPaa|61E==tE-jBmmFe!m-xW!VxZL>EguuBW6Xq58s>4 z1xb%B`I@Vo0W97(nYickPbIa^$B0$vVLr+j&U;C=p;M1%hG z(Q$}ZcQ41U3M~%Bf5DeS1hF|DflcFQ&mWOz+1+=Sb7Dt`slAN8Hu3#>YBQ%EH}w`W!-!pKsS!SiNThHm#oTg$B(R0Gn)L*!D1;@xIj(T|wys z?zHlDtePSTRl5EwI4e{Nv{ZKgHd`}zWQAGb+2>+b6w#~6M-MSpp_n;1@x!?rhS?xj zY}GK;RiyW!kaXB z!n2&SgG4tIISYyDPz8&Y6Q~{G%}oe~@F7Do@s>edM0E$1nQco;DZEaI-&NURTN6+_W zODa`~X2#NR>&eFv zJy@oCdCB`*UaZ55#=u*|44<+J5qe`C^&j@BE4_h&^RZZ5Y_0*t3rVa5{4d|Jr-^oF z!xgn78MM*LO|mPj`-By;LZxNQ?2S#eZw3V<5EvVhw+Gkdt0BqnYD^7E^`FAU>?rl% zq=e*X)ten;W0VALO%^+6!*=mG)=(rOu1bKael7wG2;hFY9YWGfDS+p%RxqK@;=+5d zx$b{yCmiuNtTRk3_74|BvZ1rdZsZ%JZQ|33^P;%6pY7Nn?TGOyAgC+s6;Uvw`ge6f z%AH&yHZxJ1T4VO;MHFV!?H@Rdv+~T&Y9s`9Cm0h0s8Ox0R$C1lEf?|NVz?d05PEGj zL;6#6Jyczgd;Iq(*t1mzo{3kNt-W@OC4MX%P0*#W4;cdPrNjzNc(--ndD7;jt`q}? z&&gurON%3{jzaV>a#NfU-TqP4Zr;_m8MqxxJp-!@Xt~ue6JAhNS`-Rf-LB5E9<7&Z z1(D?|XbxlEEwq+`Cq8+z8JN{#*Fq$4DhV>{h2&)Oy207u3s_E6*?b{{hi!BnT^OV= zv9HzvZUk0r9ZO?P70X)#+93$Ry7E30C3h*-`VCg0Mu`L?t{DiLwS`%ic3j_G7ozq9Fq4c1MVwUhC~c$F32OMR>ADCH-up6x%8i-w9S^V^H_xK z#qNDJT2oJRv2p`pPQ%Xgzm{LpLI7eq3EE6>*{ah8Q9q^I8UuqF8f~k1Rr@#!?A9)| zWg>Rq(Wr-ITJ(t(nUmb*n2Hq~g^U2$i!NbhG9Ut5yj7>ri48K^86zmVAQ#sN`HPDo zcx|!FlA&6ma?QGkuvSGBe2yprc55WjK`D7O%Mo7eHBMMGLne_OVgR z-p2e`Z}d!mYZk+e*Q0p9c9Gf<{!RN6E2_l!!}nSuE0MQDI7K&Rs=;9k^**D(%kSN9)d|+crhysAyp?sB0i|v1R z+({T9xY1p2Rlpfue>6L>Xk#9)zD3Er0Lk|RO1V0wt9Vnr;eKM#k^YD!1YE@C%X~a- zn0g-oq)o4SDw-q!Sb{!&ifJ4;b00YXYBBJ|6CWlcU^U8x+NS7$%Du3oHzBMvVs>g; zI9P{lB(wPsGG?{+v%|HMV&#t>cjZ@hw47$NavZpYO#H`TjUK}*lqo7>7S{Pze z5T&FzO>T_SLGfX;F6c-%I4)D;9v#3)-o)C6oW)wL?YZr;r!0t~3rOZ8C=o7hmLQTQ zA-PrUXSi%B>~lirm1e^7L7|2Zf7o^^Bi422#^E7pgZg~eHsONs7!725Zx?Q+<%5mc zJmpj5m4ai*txZj}yFSQ_>%nxw$$m&Y#xf{12Cq6J_P(HZ(j65b{DpHKM*ivo0RCU@k zdfDiGE?HoO9KU{qSzA9J2Ic!`dP`9?m5t@lSW*3r2Dp51t1){~l1m@IbhDbic!3~$ zRNt@thcvw}Ck(myo_AbkDD#MT)QC7L#W9T%o5jX95J4$wz?3K*vBvL>7PC_*$9eYk z7Ws6Q5@*Tda^{_t))oGAiIkOQ*^_QNDDc^wC1SHmon|gNiCc;G9DM z1mwpwC~5zWh=s07mjzcJ^AXN=&awu51Ny9NzH?x15#Fl!$i^WYOgy!#g9=ZT)Qw29 zgA(cniS`0>tU|e(SK`^y1sHv=?kc-MAIb0N0pG;jnpb+9(%p%>F5j!7M!m*JrpHVr z-F=$7WU^R^hFzub##HCbuuNN8i{7!5O6J#H$0L1?qvD=LZ2xxmz@3r8x2R~GRN%|H z#cVvG#4u^s>Y;J-qhKzPZLUEyp0y_-+wN`#;52bjs)na`irA8_p--)+SoGR9)5^h1 zdOuu`Z0ZtJmhlp|tk+>I6(?v`=aP83z9dqvCCCfJ8LTA76Q&+Yzx^zlX?}B5l({sD zOy@eG5izQOAADd%T8;n4>sbFAuVZE9_+L=>%j*~!82|U{|KoLx%nbi$Ugrv`oNTqi zCN3EyJ~9jY$IZ>{1OOO@@lTj@8C1A)5z9OwE|8?8Bzri^{8zzAC;9e|-o?jq!!plI z)5W&e&dRpcX*o;d8cB^osy-*iVoppFE(J)u>eBupI6%PWr2qjs7H$sQ;1=R%8AgmO z{N*`VfMCeC0l^g%SR4PivA}hlYb67Pz}Ns{?-=avA@cqqIuH<`wm|-_AB;h*5CCR8 z`5;#QIGlW-V4tnI^E`tYzFK%q&c^}ce`K8|BOnJT=$nRL2QW!aZ!H4#udIV|Yysxx z%{2#g1Ti0|CisX#2aj_xFb2oNaXj+BQS!>i>~--2P~*u$SkM&0wEY z82Mn1KtBqY+*r5*BXIlAVcOWH2iKsEpul*bu?W-*i&t|4z?QJCpgeeRv#QE~<=jC9 zei&7s2E9!fA(S^w{UxgA&E|A2KtPB8S2&qR z7Dv?BW74ee20)Jhafp5h>aPQ!Z%54?tGDOko!!@ylHD^Hhx+=#HINO+7ANBWMaZ^0 zf}Z&odlVVQ-t7tCtBi=?2#XCLQZ(GksZt7o%bIxSjRw)_fL!4Q4RPPvG1yWyfT3I zoznMRep6LcYuG#cce|QG?o$Uil7i+~zb3?eGVnVvEh~Y|n#*s334F&by+6kg0fDUg zw()vepuNM8Am3@%4)xVIcSLH!$T9`0B!Ao-n0I3!u$J2 zps$X6v4yOwAM9a(IG4Y$t_XU!j>pd*b`te!CFUL)h-1+&!j~A3!2izyJ;^BiF2Gj| z9&OFHP$y6v#PMtTvmZ%z+~py_5AZz@2f>ej99*p5zzE{5{}(9!+2~y>;ZMOapndB% z@I4^M<_{pgk;gYMpQqPX=#OPgN?6s!f#a*p_DJ^^_|LYm{y+9rC>vKSQ&JpLJ>+Fi z4N{5op=M4ZF2RQnD><|}?Vde7ApH|8Ch)7l-#`b3hPJyJE4gcnDztq;p5-Nm+7lgv(# z-8qZklO-W(%)h&C8VXSg@t47L)lq=Ax%?GweagyODh9f zS%gnK^^Yv&gfl&sjc7#VoHcX#$cnFU8?=b&jW=oNHa5;2TdND=VO+H5qfo6%I~=R4 z6#u6Dc865snnV&z>LX~{);VPCoJZKsQSwOlyt)Dx;2;x(6U1uwBjfRs(+Y|=izp#) z(dE3mpDMiFRfe0+m00szeb-zqFVK@>XltlC_XHO5zTMHuy^}-yG3)&6MjbkHd$Zyo zjp3AnUvo3UK^MxfBNSp=bk=V}yiNz^l9yg}<*jjw>A^;NT!Ob&;*tYSifl@s<# zO;D76DiGct9+_Z&hTRZuSn=-ww`vls`udIP>&X7q{=GPjDR=wK+3=FN<$l2=XUyAH zs8-K>EKn%k9NqhvK-hw}*KFqbvl_-N0`Vqz7Utd#e zJ$1>Q{*b!lo0ICiewHU^g-vhppa9Dilu`#qMFQk)%z8O}7^ng|FxQmn5;IFx`=;hM zz4X-^koQykak_3{(_t^1bhvaQ^$gc-8 zsBIACIb9#&(}yxRa?vvc9d9la(bO-&jLW7%q(O$|y`)P1o(xX=;yw)R_!uJu*l
    @_>^t9%jLBvauhz+vYP|~;D!<{1 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 MFfs}yB}Gq03OmrR8~^|S diff --git a/token/zk-token-protocol-paper/part2.pdf b/token/zk-token-protocol-paper/part2.pdf deleted file mode 100644 index 09ed0e812324b6fc38c274c24559e25aef940292..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 440391 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%0AxAwz>Bn=z>^%SaP4OBJfVpjTp}z2=2> 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 Date: Fri, 10 Jan 2025 18:01:54 +0100 Subject: [PATCH 14/17] token-group: Remove program and clients --- token-group/README.md | 2 + token-group/example/Cargo.toml | 32 -- token-group/example/src/entrypoint.rs | 23 -- token-group/example/src/lib.rs | 10 - token-group/example/src/processor.rs | 226 ------------- token-group/example/tests/initialize_group.rs | 149 --------- .../example/tests/initialize_member.rs | 260 --------------- token-group/example/tests/setup.rs | 61 ---- .../example/tests/update_group_authority.rs | 187 ----------- .../example/tests/update_group_max_size.rs | 283 ---------------- token-group/interface/Cargo.toml | 31 -- token-group/interface/README.md | 138 -------- token-group/interface/src/error.rs | 75 ----- token-group/interface/src/instruction.rs | 301 ------------------ token-group/interface/src/lib.rs | 11 - token-group/interface/src/state.rs | 195 ------------ token-group/js/.eslintignore | 5 - token-group/js/.eslintrc | 34 -- token-group/js/.gitignore | 13 - token-group/js/.mocharc.json | 5 - token-group/js/.nojekyll | 0 token-group/js/LICENSE | 202 ------------ token-group/js/README.md | 61 ---- token-group/js/package.json | 70 ---- token-group/js/src/errors.ts | 35 -- token-group/js/src/index.ts | 3 - token-group/js/src/instruction.ts | 144 --------- token-group/js/src/state/index.ts | 2 - token-group/js/src/state/tokenGroup.ts | 63 ---- token-group/js/src/state/tokenGroupMember.ts | 39 --- token-group/js/test/instruction.test.ts | 101 ------ token-group/js/test/state.test.ts | 47 --- token-group/js/tsconfig.all.json | 11 - token-group/js/tsconfig.base.json | 14 - token-group/js/tsconfig.cjs.json | 10 - token-group/js/tsconfig.esm.json | 13 - token-group/js/tsconfig.json | 8 - token-group/js/tsconfig.root.json | 6 - token-group/js/typedoc.json | 5 - 39 files changed, 2 insertions(+), 2873 deletions(-) create mode 100644 token-group/README.md delete mode 100644 token-group/example/Cargo.toml delete mode 100644 token-group/example/src/entrypoint.rs delete mode 100644 token-group/example/src/lib.rs delete mode 100644 token-group/example/src/processor.rs delete mode 100644 token-group/example/tests/initialize_group.rs delete mode 100644 token-group/example/tests/initialize_member.rs delete mode 100644 token-group/example/tests/setup.rs delete mode 100644 token-group/example/tests/update_group_authority.rs delete mode 100644 token-group/example/tests/update_group_max_size.rs delete mode 100644 token-group/interface/Cargo.toml delete mode 100644 token-group/interface/README.md delete mode 100644 token-group/interface/src/error.rs delete mode 100644 token-group/interface/src/instruction.rs delete mode 100644 token-group/interface/src/lib.rs delete mode 100644 token-group/interface/src/state.rs delete mode 100644 token-group/js/.eslintignore delete mode 100644 token-group/js/.eslintrc delete mode 100644 token-group/js/.gitignore delete mode 100644 token-group/js/.mocharc.json delete mode 100644 token-group/js/.nojekyll delete mode 100644 token-group/js/LICENSE delete mode 100644 token-group/js/README.md delete mode 100644 token-group/js/package.json delete mode 100644 token-group/js/src/errors.ts delete mode 100644 token-group/js/src/index.ts delete mode 100644 token-group/js/src/instruction.ts delete mode 100644 token-group/js/src/state/index.ts delete mode 100644 token-group/js/src/state/tokenGroup.ts delete mode 100644 token-group/js/src/state/tokenGroupMember.ts delete mode 100644 token-group/js/test/instruction.test.ts delete mode 100644 token-group/js/test/state.test.ts delete mode 100644 token-group/js/tsconfig.all.json delete mode 100644 token-group/js/tsconfig.base.json delete mode 100644 token-group/js/tsconfig.cjs.json delete mode 100644 token-group/js/tsconfig.esm.json delete mode 100644 token-group/js/tsconfig.json delete mode 100644 token-group/js/tsconfig.root.json delete mode 100644 token-group/js/typedoc.json 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" -} From d15a83b98429c6987cdcfcd4fe57b543a45563f2 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 18:02:37 +0100 Subject: [PATCH 15/17] token-metadata: Remove program and clients --- token-metadata/README.md | 2 + token-metadata/example/Cargo.toml | 31 -- token-metadata/example/src/entrypoint.rs | 24 - token-metadata/example/src/lib.rs | 10 - token-metadata/example/src/processor.rs | 222 -------- token-metadata/example/tests/emit.rs | 105 ---- token-metadata/example/tests/initialize.rs | 271 ---------- token-metadata/example/tests/program_test.rs | 167 ------ token-metadata/example/tests/remove_key.rs | 271 ---------- .../example/tests/update_authority.rs | 264 --------- token-metadata/example/tests/update_field.rs | 251 --------- token-metadata/interface/Cargo.toml | 39 -- token-metadata/interface/README.md | 107 ---- token-metadata/interface/src/error.rs | 75 --- token-metadata/interface/src/instruction.rs | 504 ------------------ token-metadata/interface/src/lib.rs | 18 - token-metadata/interface/src/state.rs | 219 -------- token-metadata/js/.eslintignore | 5 - token-metadata/js/.eslintrc | 34 -- token-metadata/js/.gitignore | 13 - token-metadata/js/.mocharc.json | 5 - token-metadata/js/.nojekyll | 0 token-metadata/js/LICENSE | 202 ------- token-metadata/js/README.md | 61 --- token-metadata/js/package.json | 70 --- token-metadata/js/src/errors.ts | 39 -- token-metadata/js/src/field.ts | 37 -- token-metadata/js/src/index.ts | 4 - token-metadata/js/src/instruction.ts | 201 ------- token-metadata/js/src/state.ts | 85 --- token-metadata/js/test/instruction.test.ts | 167 ------ token-metadata/js/test/state.test.ts | 61 --- token-metadata/js/tsconfig.all.json | 11 - token-metadata/js/tsconfig.base.json | 14 - token-metadata/js/tsconfig.cjs.json | 10 - token-metadata/js/tsconfig.esm.json | 13 - token-metadata/js/tsconfig.json | 8 - token-metadata/js/tsconfig.root.json | 6 - token-metadata/js/typedoc.json | 5 - 39 files changed, 2 insertions(+), 3629 deletions(-) create mode 100644 token-metadata/README.md delete mode 100644 token-metadata/example/Cargo.toml delete mode 100644 token-metadata/example/src/entrypoint.rs delete mode 100644 token-metadata/example/src/lib.rs delete mode 100644 token-metadata/example/src/processor.rs delete mode 100644 token-metadata/example/tests/emit.rs delete mode 100644 token-metadata/example/tests/initialize.rs delete mode 100644 token-metadata/example/tests/program_test.rs delete mode 100644 token-metadata/example/tests/remove_key.rs delete mode 100644 token-metadata/example/tests/update_authority.rs delete mode 100644 token-metadata/example/tests/update_field.rs delete mode 100644 token-metadata/interface/Cargo.toml delete mode 100644 token-metadata/interface/README.md delete mode 100644 token-metadata/interface/src/error.rs delete mode 100644 token-metadata/interface/src/instruction.rs delete mode 100644 token-metadata/interface/src/lib.rs delete mode 100644 token-metadata/interface/src/state.rs delete mode 100644 token-metadata/js/.eslintignore delete mode 100644 token-metadata/js/.eslintrc delete mode 100644 token-metadata/js/.gitignore delete mode 100644 token-metadata/js/.mocharc.json delete mode 100644 token-metadata/js/.nojekyll delete mode 100644 token-metadata/js/LICENSE delete mode 100644 token-metadata/js/README.md delete mode 100644 token-metadata/js/package.json delete mode 100644 token-metadata/js/src/errors.ts delete mode 100644 token-metadata/js/src/field.ts delete mode 100644 token-metadata/js/src/index.ts delete mode 100644 token-metadata/js/src/instruction.ts delete mode 100644 token-metadata/js/src/state.ts delete mode 100644 token-metadata/js/test/instruction.test.ts delete mode 100644 token-metadata/js/test/state.test.ts delete mode 100644 token-metadata/js/tsconfig.all.json delete mode 100644 token-metadata/js/tsconfig.base.json delete mode 100644 token-metadata/js/tsconfig.cjs.json delete mode 100644 token-metadata/js/tsconfig.esm.json delete mode 100644 token-metadata/js/tsconfig.json delete mode 100644 token-metadata/js/tsconfig.root.json delete mode 100644 token-metadata/js/typedoc.json 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" -} From aa96e9d92266a59f10e442883f1d5a5b8ed4965c Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 18:13:03 +0100 Subject: [PATCH 16/17] Update existing code to use non-local dependencies --- Anchor.toml | 6 - Cargo.lock | 888 +------- Cargo.toml | 44 +- binary-option/program/Cargo.toml | 2 +- binary-oracle-pair/program/Cargo.toml | 2 +- examples/rust/transfer-tokens/Cargo.toml | 2 +- governance/addin-mock/program/Cargo.toml | 2 +- governance/chat/program/Cargo.toml | 2 +- governance/program/Cargo.toml | 2 +- governance/test-sdk/Cargo.toml | 2 +- governance/tools/Cargo.toml | 2 +- managed-token/program/Cargo.toml | 6 +- package.json | 6 - pnpm-lock.yaml | 2014 ++---------------- stateless-asks/program/Cargo.toml | 4 +- token-collection/program/Cargo.toml | 18 +- token-lending/cli/Cargo.toml | 2 +- token-lending/flash_loan_receiver/Cargo.toml | 2 +- token-lending/program/Cargo.toml | 2 +- token-swap/program/Cargo.toml | 4 +- token-swap/program/fuzz/Cargo.toml | 2 +- token-upgrade/cli/Cargo.toml | 8 +- token-upgrade/program/Cargo.toml | 6 +- token-wrap/program/Cargo.toml | 6 +- utils/test-client/Cargo.toml | 2 +- 25 files changed, 240 insertions(+), 2796 deletions(-) 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/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/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/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/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/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-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-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/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" ] } From 2726d050792f33a0cf63148e256231618ddcb83f Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 10 Jan 2025 18:35:22 +0100 Subject: [PATCH 17/17] math-example: Fix tests that have been broken --- .../math-example/tests/instruction_count.rs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) 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);