diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a81685dcc..a4342f844 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -109,6 +109,27 @@ jobs: - name: Lint Client Rust run: pnpm clients:rust:lint + format_and_lint_interface: + name: Format & Lint Interface + runs-on: ubuntu-latest + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup + with: + clippy: true + rustfmt: true + cargo-cache-key: cargo-interface-lint + cargo-cache-fallback-key: cargo-interface + + - name: Format + run: pnpm interface:format + + - name: Lint + run: pnpm interface:lint + format_and_lint_client_rust_legacy: name: Format & Lint Client Rust Legacy runs-on: ubuntu-latest @@ -171,6 +192,22 @@ jobs: - name: Lint Proof Tests run: pnpm confidential-transfer:proof-tests:lint + test_interface: + name: Test Interface + runs-on: ubuntu-latest + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup + with: + cargo-cache-key: cargo-interface-test + cargo-cache-fallback-key: cargo-interface + + - name: Test + run: pnpm interface:test + test_confidential_transfer_proofs: name: Test Confidential Transfer Proofs runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 2f5daa65f..ef49bc608 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9878,12 +9878,10 @@ dependencies = [ "solana-account-info", "solana-clock", "solana-cpi", - "solana-decode-error", "solana-hash", "solana-instruction", "solana-keypair", "solana-msg", - "solana-native-token", "solana-program-entrypoint", "solana-program-error", "solana-program-memory", @@ -9904,18 +9902,52 @@ dependencies = [ "spl-memo", "spl-pod", "spl-tlv-account-resolution", - "spl-token", + "spl-token-2022-interface", "spl-token-confidential-transfer-ciphertext-arithmetic 0.3.0", "spl-token-confidential-transfer-proof-extraction 0.4.0", "spl-token-confidential-transfer-proof-generation 0.4.0", "spl-token-group-interface", "spl-token-metadata-interface", "spl-transfer-hook-interface", - "spl-type-length-value", "test-case", "thiserror 2.0.12", ] +[[package]] +name = "spl-token-2022-interface" +version = "1.0.0" +dependencies = [ + "arrayref", + "base64 0.22.1", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "proptest", + "serde", + "serde_json", + "serde_with", + "solana-account-info", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-sdk-ids", + "solana-zk-sdk", + "spl-pod", + "spl-token", + "spl-token-2022-interface", + "spl-token-confidential-transfer-proof-extraction 0.4.0", + "spl-token-confidential-transfer-proof-generation 0.4.0", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-type-length-value", + "thiserror 2.0.12", +] + [[package]] name = "spl-token-cli" version = "5.3.0" diff --git a/Cargo.toml b/Cargo.toml index 8eaf938af..59baacb03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "confidential-transfer/proof-extraction", "confidential-transfer/proof-generation", "confidential-transfer/proof-tests", + "interface", "program", ] diff --git a/interface/Cargo.toml b/interface/Cargo.toml new file mode 100644 index 000000000..373f407aa --- /dev/null +++ b/interface/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "spl-token-2022-interface" +version = "1.0.0" +description = "Solana Program Library Token 2022 Interface" +documentation = "https://docs.rs/spl-token-2022-interface" +readme = "README.md" +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[features] +serde = ["dep:serde", "dep:serde_with", "dep:base64", "spl-pod/serde-traits"] + +[dependencies] +arrayref = "0.3.9" +bytemuck = { version = "1.23.1", features = ["derive"] } +num-derive = "0.4" +num-traits = "0.2" +num_enum = "0.7.4" +solana-account-info = "2.3.0" +solana-decode-error = "2.2.1" +solana-instruction = "2.2.1" +solana-msg = "2.2.1" +solana-program-error = "2.2.1" +solana-program-option = "2.2.1" +solana-program-pack = "2.2.1" +solana-pubkey = "2.2.1" +solana-sdk-ids = "2.2.1" +solana-zk-sdk = "2.3.4" +spl-token-confidential-transfer-proof-extraction = { version = "0.4.0", path = "../confidential-transfer/proof-extraction" } +spl-token-group-interface = { version = "0.6.0" } +spl-token-metadata-interface = { version = "0.7.0" } +spl-type-length-value = { version = "0.8.0" } +spl-pod = { version = "0.5.1" } +thiserror = "2.0" +serde = { version = "1.0.219", optional = true } +serde_with = { version = "3.14.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.4.0", path = "../confidential-transfer/proof-generation" } + +[dev-dependencies] +proptest = "1.7" +solana-pubkey = { version = "2.2.1", features = ["curve25519"] } +spl-token = { version = "8.0", features = ["no-entrypoint"] } +spl-token-2022-interface = { path = ".", features = ["serde"] } +serde_json = "1.0.141" + +[lib] +crate-type = ["lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lints] +workspace = true + +[package.metadata.solana] +program-id = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" diff --git a/interface/README.md b/interface/README.md new file mode 100644 index 000000000..c0c642e85 --- /dev/null +++ b/interface/README.md @@ -0,0 +1,14 @@ +# Token-2022 Interface + +A token program interface on the Solana blockchain, usable for fungible and +non-fungible tokens. + +This crate provides an interface that third parties can utilize to create and +use their tokens. + +Full documentation is available at [https://www.solana-program.com/docs/token-2022](https://www.solana-program.com/docs/token-2022) + +## Audit + +The repository [README](https://github.com/solana-labs/solana-program-library#audits) +contains information about program audits. diff --git a/program/idl.json b/interface/idl.json similarity index 100% rename from program/idl.json rename to interface/idl.json diff --git a/interface/src/error.rs b/interface/src/error.rs new file mode 100644 index 000000000..99f7783aa --- /dev/null +++ b/interface/src/error.rs @@ -0,0 +1,506 @@ +//! Error types + +#[cfg(not(target_os = "solana"))] +use spl_token_confidential_transfer_proof_generation::errors::TokenProofGenerationError; +use { + num_derive::FromPrimitive, + solana_decode_error::DecodeError, + solana_msg::msg, + solana_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, + /// Pending supply is not zero + #[error("Key rotation attempted while pending balance is not zero")] + PendingBalanceNonZero, +} +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") + } + TokenError::PendingBalanceNonZero => { + msg!("Key rotation attempted while pending balance is not zero") + } + } + } +} + +#[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/interface/src/extension/confidential_mint_burn/instruction.rs b/interface/src/extension/confidential_mint_burn/instruction.rs new file mode 100644 index 000000000..98a599e3a --- /dev/null +++ b/interface/src/extension/confidential_mint_burn/instruction.rs @@ -0,0 +1,599 @@ +#[cfg(feature = "serde")] +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_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + solana_zk_sdk::encryption::pod::{ + auth_encryption::PodAeCiphertext, + elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + }, +}; +#[cfg(not(target_os = "solana"))] +use { + solana_zk_sdk::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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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 + /// + /// The pending burn amount must be zero in order for this instruction + /// to be processed successfully. + /// + /// 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. `[writable]` The SPL Token mint. + /// 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. `[writable]` The SPL Token mint. + /// 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, + + /// Applies the pending burn amount to the confidential supply + /// + /// * Single authority + /// 0. `[writable]` The SPL token mint. + /// 1. `[signer]` The single mint authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The SPL token mint. + /// 1. `[]` The multisig account owner. + /// 2. .. `[signer]` Required M signer accounts for the SPL Token Multisig + /// account. + ApplyPendingBurn, +} + +/// Data expected by `ConfidentialMintBurnInstruction::InitializeMint` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", serde(with = "elgamalpubkey_fromstr"))] + pub supply_elgamal_pubkey: PodElGamalPubkey, + /// The initial 0 supply encrypted with the supply aes key + #[cfg_attr(feature = "serde", serde(with = "aeciphertext_fromstr"))] + pub decryptable_supply: PodAeCiphertext, +} + +/// Data expected by `ConfidentialMintBurnInstruction::RotateSupplyElGamalPubkey` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", serde(with = "elgamalpubkey_fromstr"))] + pub new_supply_elgamal_pubkey: PodElGamalPubkey, + /// The location of the + /// `ProofInstruction::VerifyCiphertextCiphertextEquality` instruction + /// relative to the `RotateSupplyElGamalPubkey` instruction in the transaction + pub proof_instruction_offset: i8, +} + +/// Data expected by `ConfidentialMintBurnInstruction::UpdateDecryptableSupply` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct UpdateDecryptableSupplyData { + /// The new decryptable supply + #[cfg_attr(feature = "serde", serde(with = "aeciphertext_fromstr"))] + pub new_decryptable_supply: PodAeCiphertext, +} + +/// Data expected by `ConfidentialMintBurnInstruction::ConfidentialMint` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", serde(with = "aeciphertext_fromstr"))] + pub new_decryptable_supply: PodAeCiphertext, + /// The transfer amount encrypted under the auditor ElGamal public key + #[cfg_attr(feature = "serde", 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", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", serde(with = "aeciphertext_fromstr"))] + pub new_decryptable_available_balance: DecryptableBalance, + /// The transfer amount encrypted under the auditor ElGamal public key + #[cfg_attr(feature = "serde", 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", 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 `RotateSupplyElGamalPubkey` 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 `UpdateDecryptableSupply` 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, + 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), + AccountMeta::new(*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, + 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), + AccountMeta::new(*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) +} + +/// Create a `ApplyPendingBurn` instruction +pub fn apply_pending_burn( + 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::ConfidentialMintBurnExtension, + ConfidentialMintBurnInstruction::ApplyPendingBurn, + &(), + )) +} diff --git a/interface/src/extension/confidential_mint_burn/mod.rs b/interface/src/extension/confidential_mint_burn/mod.rs new file mode 100644 index 000000000..75d255b13 --- /dev/null +++ b/interface/src/extension/confidential_mint_burn/mod.rs @@ -0,0 +1,29 @@ +use { + crate::extension::{Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, + solana_zk_sdk::encryption::pod::{ + auth_encryption::PodAeCiphertext, + elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + }, +}; + +/// Confidential Mint-Burn Extension instructions +pub mod instruction; + +/// 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, + /// The amount of burn amounts not yet aggregated into the confidential supply + pub pending_burn: PodElGamalCiphertext, +} + +impl Extension for ConfidentialMintBurn { + const TYPE: ExtensionType = ExtensionType::ConfidentialMintBurn; +} diff --git a/interface/src/extension/confidential_transfer/instruction.rs b/interface/src/extension/confidential_transfer/instruction.rs new file mode 100644 index 000000000..3ba435954 --- /dev/null +++ b/interface/src/extension/confidential_transfer/instruction.rs @@ -0,0 +1,1665 @@ +pub use solana_zk_sdk::zk_elgamal_proof_program::{ + instruction::ProofInstruction, proof_data::*, state::ProofContextState, +}; +#[cfg(feature = "serde")] +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_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + solana_sdk_ids::{system_program, sysvar}, + spl_token_confidential_transfer_proof_extraction::instruction::ProofLocation, +}; + +/// Confidential Transfer extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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. `[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. `[]` The multisig source account owner. + /// 4. .. `[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. `[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. `[]` The multisig account owner. + /// 3. .. `[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 context state account. + /// 4. `[]` (Optional) Range proof 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 context state account. + /// 4. `[]` (Optional) Range proof 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 context state account. + /// 6. `[]` (Optional) Ciphertext validity context state account. + /// 7. `[]` (Optional) Range proof 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 context state account. + /// 6. `[]` (Optional) Ciphertext validity proof context state account. + /// 7. `[]` (Optional) Range proof 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 context state account. + /// 6. `[]` (Optional) Transfer amount ciphertext validity proof context + /// state account. + /// 7. `[]` (Optional) Fee sigma proof context state account. + /// 8. `[]` (Optional) Fee ciphertext validity proof context state + /// account. + /// 9. `[]` (Optional) Range proof 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 context state account. + /// 6. `[]` (Optional) Transfer amount ciphertext validity proof context + /// state account. + /// 7. `[]` (Optional) Fee sigma proof context state account. + /// 8. `[]` (Optional) Fee ciphertext validity proof context state + /// account. + /// 9. `[]` (Optional) Range proof 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", 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", 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", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", serde(with = "aeciphertext_fromstr"))] + pub new_decryptable_available_balance: DecryptableBalance, +} + +/// Data expected by `ConfidentialTransferInstruction::TransferWithFee` +#[cfg_attr(feature = "serde", 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", 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", 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", 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, _) => { + accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), 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()); + } + instructions + .push(ProofInstruction::VerifyPubkeyValidity.encode_verify_proof(None, proof_data)); + } + + 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, _) => { + accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), 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()); + } + instructions + .push(ProofInstruction::VerifyZeroCiphertext.encode_verify_proof(None, proof_data)); + }; + + 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_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_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()); + } + instructions.push( + ProofInstruction::VerifyCiphertextCommitmentEquality + .encode_verify_proof(None, proof_data), + ); + 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()); + } + instructions.push( + ProofInstruction::VerifyBatchedRangeProofU64.encode_verify_proof(None, proof_data), + ); + }; + + 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_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_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_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()); + } + instructions.push( + ProofInstruction::VerifyCiphertextCommitmentEquality + .encode_verify_proof(None, proof_data), + ); + 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()); + } + instructions.push( + ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity + .encode_verify_proof(None, proof_data), + ); + 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()); + } + instructions.push( + ProofInstruction::VerifyBatchedRangeProofU128.encode_verify_proof(None, proof_data), + ); + } + + 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_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_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_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_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_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()); + } + instructions.push( + ProofInstruction::VerifyCiphertextCommitmentEquality + .encode_verify_proof(None, proof_data), + ); + 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()); + } + instructions.push( + ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity + .encode_verify_proof(None, proof_data), + ); + 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()); + } + instructions + .push(ProofInstruction::VerifyPercentageWithCap.encode_verify_proof(None, proof_data)); + 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()); + } + instructions.push( + ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity + .encode_verify_proof(None, proof_data), + ); + 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()); + } + instructions.push( + ProofInstruction::VerifyBatchedRangeProofU256.encode_verify_proof(None, proof_data), + ); + } + + 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/interface/src/extension/confidential_transfer/mod.rs b/interface/src/extension/confidential_transfer/mod.rs new file mode 100644 index 000000000..893c8fc06 --- /dev/null +++ b/interface/src/extension/confidential_transfer/mod.rs @@ -0,0 +1,190 @@ +use { + crate::{ + error::TokenError, + extension::{Extension, ExtensionType}, + }, + bytemuck::{Pod, Zeroable}, + solana_program_error::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; + +/// 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 32 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/interface/src/extension/confidential_transfer_fee/instruction.rs b/interface/src/extension/confidential_transfer_fee/instruction.rs new file mode 100644 index 000000000..0d8ecbf3c --- /dev/null +++ b/interface/src/extension/confidential_transfer_fee/instruction.rs @@ -0,0 +1,551 @@ +#[cfg(feature = "serde")] +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_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + solana_sdk_ids::sysvar, + spl_pod::optional_keys::OptionalNonZeroPubkey, + spl_token_confidential_transfer_proof_extraction::instruction::ProofLocation, + std::convert::TryFrom, +}; + +/// Confidential Transfer extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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. `[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. `[]` The mint's multisig `withdraw_withheld_authority`. + /// 4. ..`4+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. `[signer]` The mint's `withdraw_withheld_authority`. + /// 4. ..`4+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. `[]` The mint's multisig `withdraw_withheld_authority`. + /// 4. ..`4+M` `[signer]` M signer accounts. + /// 5. `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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", serde(with = "elgamalpubkey_fromstr"))] + pub withdraw_withheld_authority_elgamal_pubkey: PodElGamalPubkey, +} + +/// Data expected by +/// `ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromMint` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", serde(with = "aeciphertext_fromstr"))] + pub new_decryptable_available_balance: DecryptableBalance, +} + +/// Data expected by +/// `ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromAccounts` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", 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, _) => { + accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), 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()); + } + instructions.push( + ProofInstruction::VerifyCiphertextCiphertextEquality + .encode_verify_proof(None, proof_data), + ); + }; + + 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, _) => { + accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), 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()); + } + instructions.push( + ProofInstruction::VerifyCiphertextCiphertextEquality + .encode_verify_proof(None, proof_data), + ); + }; + + 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/interface/src/extension/confidential_transfer_fee/mod.rs b/interface/src/extension/confidential_transfer_fee/mod.rs new file mode 100644 index 000000000..1c0f1fd5e --- /dev/null +++ b/interface/src/extension/confidential_transfer_fee/mod.rs @@ -0,0 +1,69 @@ +use { + crate::{ + error::TokenError, + extension::{Extension, ExtensionType}, + }, + bytemuck::{Pod, Zeroable}, + solana_program_error::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; + +/// 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/interface/src/extension/cpi_guard/instruction.rs b/interface/src/extension/cpi_guard/instruction.rs new file mode 100644 index 000000000..752c69258 --- /dev/null +++ b/interface/src/extension/cpi_guard/instruction.rs @@ -0,0 +1,102 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, +}; + +/// CPI Guard extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/cpi_guard/mod.rs b/interface/src/extension/cpi_guard/mod.rs new file mode 100644 index 000000000..80d401a4b --- /dev/null +++ b/interface/src/extension/cpi_guard/mod.rs @@ -0,0 +1,34 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut}, + state::Account, + }, + bytemuck::{Pod, Zeroable}, + spl_pod::primitives::PodBool, +}; + +/// CPI Guard extension instructions +pub mod instruction; + +/// CPI Guard extension for Accounts +#[repr(C)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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 +} diff --git a/interface/src/extension/default_account_state/instruction.rs b/interface/src/extension/default_account_state/instruction.rs new file mode 100644 index 000000000..72d061d14 --- /dev/null +++ b/interface/src/extension/default_account_state/instruction.rs @@ -0,0 +1,125 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, error::TokenError, instruction::TokenInstruction, + state::AccountState, + }, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + std::convert::TryFrom, +}; + +/// Default Account State extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/default_account_state/mod.rs b/interface/src/extension/default_account_state/mod.rs new file mode 100644 index 000000000..9890ab14a --- /dev/null +++ b/interface/src/extension/default_account_state/mod.rs @@ -0,0 +1,24 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::extension::{Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, +}; + +/// Default Account state extension instructions +pub mod instruction; + +/// Default Account::state extension data for mints. +#[repr(C)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/group_member_pointer/instruction.rs b/interface/src/extension/group_member_pointer/instruction.rs new file mode 100644 index 000000000..f429c812c --- /dev/null +++ b/interface/src/extension/group_member_pointer/instruction.rs @@ -0,0 +1,126 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + spl_pod::optional_keys::OptionalNonZeroPubkey, + std::convert::TryInto, +}; + +/// Group member pointer extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/group_member_pointer/mod.rs b/interface/src/extension/group_member_pointer/mod.rs new file mode 100644 index 000000000..544369bc7 --- /dev/null +++ b/interface/src/extension/group_member_pointer/mod.rs @@ -0,0 +1,26 @@ +#[cfg(feature = "serde")] +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; + +/// Group member pointer extension data for mints. +#[repr(C)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/group_pointer/instruction.rs b/interface/src/extension/group_pointer/instruction.rs new file mode 100644 index 000000000..c79b325e6 --- /dev/null +++ b/interface/src/extension/group_pointer/instruction.rs @@ -0,0 +1,126 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + spl_pod::optional_keys::OptionalNonZeroPubkey, + std::convert::TryInto, +}; + +/// Group pointer extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/group_pointer/mod.rs b/interface/src/extension/group_pointer/mod.rs new file mode 100644 index 000000000..7f48e53f7 --- /dev/null +++ b/interface/src/extension/group_pointer/mod.rs @@ -0,0 +1,26 @@ +#[cfg(feature = "serde")] +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; + +/// Group pointer extension data for mints. +#[repr(C)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/immutable_owner.rs b/interface/src/extension/immutable_owner.rs new file mode 100644 index 000000000..a0294f34e --- /dev/null +++ b/interface/src/extension/immutable_owner.rs @@ -0,0 +1,17 @@ +#[cfg(feature = "serde")] +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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/interest_bearing_mint/instruction.rs b/interface/src/extension/interest_bearing_mint/instruction.rs new file mode 100644 index 000000000..7afe93339 --- /dev/null +++ b/interface/src/extension/interest_bearing_mint/instruction.rs @@ -0,0 +1,115 @@ +#[cfg(feature = "serde")] +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_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + spl_pod::optional_keys::OptionalNonZeroPubkey, + std::convert::TryInto, +}; + +/// Interest-bearing mint extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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_mint::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_mint::BasisPoints` + UpdateRate, +} + +/// Data expected by `InterestBearing::Initialize` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/interest_bearing_mint/mod.rs b/interface/src/extension/interest_bearing_mint/mod.rs new file mode 100644 index 000000000..0d14031bd --- /dev/null +++ b/interface/src/extension/interest_bearing_mint/mod.rs @@ -0,0 +1,461 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + extension::{Extension, ExtensionType}, + trim_ui_amount_string, + }, + bytemuck::{Pod, Zeroable}, + solana_program_error::ProgramError, + spl_pod::{ + optional_keys::OptionalNonZeroPubkey, + primitives::{PodI16, PodI64}, + }, + std::convert::TryInto, +}; + +/// Interest-bearing mint extension instructions +pub mod instruction; + +/// 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/memo_transfer/instruction.rs b/interface/src/extension/memo_transfer/instruction.rs new file mode 100644 index 000000000..83cfa9bd5 --- /dev/null +++ b/interface/src/extension/memo_transfer/instruction.rs @@ -0,0 +1,96 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, +}; + +/// Required Memo Transfers extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/memo_transfer/mod.rs b/interface/src/extension/memo_transfer/mod.rs new file mode 100644 index 000000000..1d0869b9a --- /dev/null +++ b/interface/src/extension/memo_transfer/mod.rs @@ -0,0 +1,31 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::extension::{BaseState, BaseStateWithExtensions, Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, + spl_pod::primitives::PodBool, +}; + +/// Memo Transfer extension instructions +pub mod instruction; + +/// Memo Transfer extension for Accounts +#[repr(C)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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 +} diff --git a/interface/src/extension/metadata_pointer/instruction.rs b/interface/src/extension/metadata_pointer/instruction.rs new file mode 100644 index 000000000..b9eb96473 --- /dev/null +++ b/interface/src/extension/metadata_pointer/instruction.rs @@ -0,0 +1,126 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + spl_pod::optional_keys::OptionalNonZeroPubkey, + std::convert::TryInto, +}; + +/// Metadata pointer extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/metadata_pointer/mod.rs b/interface/src/extension/metadata_pointer/mod.rs new file mode 100644 index 000000000..31ae02c7d --- /dev/null +++ b/interface/src/extension/metadata_pointer/mod.rs @@ -0,0 +1,26 @@ +#[cfg(feature = "serde")] +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; + +/// Metadata pointer extension data for mints. +#[repr(C)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/mint_close_authority.rs b/interface/src/extension/mint_close_authority.rs new file mode 100644 index 000000000..dce961437 --- /dev/null +++ b/interface/src/extension/mint_close_authority.rs @@ -0,0 +1,20 @@ +#[cfg(feature = "serde")] +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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/mod.rs b/interface/src/extension/mod.rs new file mode 100644 index 000000000..5c225e1d0 --- /dev/null +++ b/interface/src/extension/mod.rs @@ -0,0 +1,3132 @@ +//! Extensions available to token mints and accounts + +#[cfg(feature = "serde")] +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_account_info::AccountInfo, + solana_program_error::ProgramError, + solana_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; +/// 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; + 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 = u16::from_le_bytes( + tlv_data[tlv_indices.type_start..tlv_indices.length_start] + .try_into() + .map_err(|_| ProgramError::InvalidAccountData)?, + ); + if extension_type == u16::from(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 == u16::from(ExtensionType::Uninitialized) { + if init { + return Ok(tlv_indices); + } else { + return Err(TokenError::ExtensionNotFound.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 BaseStateWithExtensions for StateWithExtensions<'_, 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 BaseStateWithExtensions for PodStateWithExtensions<'_, 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 BaseStateWithExtensions for StateWithExtensionsMut<'_, S> { + fn get_tlv_data(&self) -> &[u8] { + self.tlv_data + } +} +impl BaseStateWithExtensionsMut for StateWithExtensionsMut<'_, 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 BaseStateWithExtensions for PodStateWithExtensionsMut<'_, S> { + fn get_tlv_data(&self) -> &[u8] { + self.tlv_data + } +} +impl BaseStateWithExtensionsMut for PodStateWithExtensionsMut<'_, 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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 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.resize(new_account_len)?; + } + 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 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.resize(new_account_len)?; + 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.resize(new_account_len)?; + } + } + 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_account_info::{ + Account as GetAccount, IntoAccountInfo, MAX_PERMITTED_DATA_INCREASE, + }, + solana_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_ACCOUNT_TYPE: &[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 + ]; + + 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_ACCOUNT_TYPE).unwrap(); + assert_eq!(state.base, &TEST_POD_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::InvalidAccountData) + ); + + // 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::InvalidAccountData), + ); + + // 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, u64) { + // 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, + u64::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/interface/src/extension/non_transferable.rs b/interface/src/extension/non_transferable.rs new file mode 100644 index 000000000..a47273b48 --- /dev/null +++ b/interface/src/extension/non_transferable.rs @@ -0,0 +1,29 @@ +#[cfg(feature = "serde")] +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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/pausable/instruction.rs b/interface/src/extension/pausable/instruction.rs new file mode 100644 index 000000000..bbd228b6b --- /dev/null +++ b/interface/src/extension/pausable/instruction.rs @@ -0,0 +1,134 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, +}; + +/// Pausable extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/pausable/mod.rs b/interface/src/extension/pausable/mod.rs new file mode 100644 index 000000000..f0a5fe717 --- /dev/null +++ b/interface/src/extension/pausable/mod.rs @@ -0,0 +1,37 @@ +#[cfg(feature = "serde")] +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; + +/// Indicates that the tokens from this mint can be paused +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/permanent_delegate.rs b/interface/src/extension/permanent_delegate.rs new file mode 100644 index 000000000..dd4d99295 --- /dev/null +++ b/interface/src/extension/permanent_delegate.rs @@ -0,0 +1,32 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::extension::{BaseState, BaseStateWithExtensions, Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, + solana_pubkey::Pubkey, + spl_pod::optional_keys::OptionalNonZeroPubkey, +}; + +/// Permanent delegate extension data for mints. +#[repr(C)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/scaled_ui_amount/instruction.rs b/interface/src/extension/scaled_ui_amount/instruction.rs new file mode 100644 index 000000000..0349826a3 --- /dev/null +++ b/interface/src/extension/scaled_ui_amount/instruction.rs @@ -0,0 +1,141 @@ +#[cfg(feature = "serde")] +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_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + spl_pod::optional_keys::OptionalNonZeroPubkey, + std::convert::TryInto, +}; + +/// Interesting-bearing mint extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/scaled_ui_amount/mod.rs b/interface/src/extension/scaled_ui_amount/mod.rs new file mode 100644 index 000000000..ce068d666 --- /dev/null +++ b/interface/src/extension/scaled_ui_amount/mod.rs @@ -0,0 +1,363 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + extension::{Extension, ExtensionType}, + trim_ui_amount_string, + }, + bytemuck::{Pod, Zeroable}, + solana_program_error::ProgramError, + spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodI64}, +}; + +/// Scaled UI amount extension instructions +pub mod instruction; + +/// `UnixTimestamp` expressed with an alignment-independent type +pub type UnixTimestamp = PodI64; + +/// `f64` type that can be used in `Pod`s +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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 current_multiplier(&self, unix_timestamp: i64) -> f64 { + if unix_timestamp >= self.new_multiplier_effective_timestamp.into() { + self.new_multiplier.into() + } else { + self.multiplier.into() + } + } + + fn total_multiplier(&self, decimals: u8, unix_timestamp: i64) -> f64 { + self.current_multiplier(unix_timestamp) / 10_f64.powi(decimals as i32) + } + + /// Convert a raw amount to its UI representation using the given decimals + /// field. + /// + /// The value is converted to a float and then truncated towards 0. 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.current_multiplier(unix_timestamp); + let truncated_amount = scaled_amount.trunc() / 10_f64.powi(decimals as i32); + let ui_amount = format!("{truncated_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. + /// + /// The string is parsed to a float, scaled, and then truncated towards 0 + /// before being converted to a fixed-point number. + 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 truncate earlier, you'll get wrong "inf" + // answers + Ok(amount.trunc() 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 { + multiplier: PodF64::from(multiplier), + new_multiplier: PodF64::from(new_multiplier), + new_multiplier_effective_timestamp: UnixTimestamp::from( + new_multiplier_effective_timestamp, + ), + ..Default::default() + }; + 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 { + 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 { + 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"); + + // truncation + let config = ScaledUiAmountConfig { + multiplier: PodF64::from(0.99), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() + }; + // This is really 0.99999... but it gets truncated + let ui_amount = config.amount_to_ui_amount(101, 2, 0).unwrap(); + assert_eq!(ui_amount, "0.99"); + } + + #[test] + fn specific_ui_amount_to_amount() { + // constant 5x + let config = ScaledUiAmountConfig { + 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 { + 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 { + 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 { + 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 { + 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 { + 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) + ); + } + + // truncation + let config = ScaledUiAmountConfig { + multiplier: PodF64::from(0.99), + new_multiplier_effective_timestamp: UnixTimestamp::from(1), + ..Default::default() + }; + // There are a few possibilities for what "0.99" means, it could be 101 + // or 100 underlying tokens, but the result gives the fewest possible + // tokens that give that UI amount. + let amount = config.try_ui_amount_into_amount("0.99", 2, 0).unwrap(); + assert_eq!(amount, 100); + } + + #[test] + fn specific_amount_to_ui_amount_no_scale() { + let config = ScaledUiAmountConfig { + 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 { + 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 { + 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/interface/src/extension/token_group/mod.rs b/interface/src/extension/token_group/mod.rs new file mode 100644 index 000000000..a974d63ca --- /dev/null +++ b/interface/src/extension/token_group/mod.rs @@ -0,0 +1,12 @@ +use { + crate::extension::{Extension, ExtensionType}, + spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, +}; + +impl Extension for TokenGroup { + const TYPE: ExtensionType = ExtensionType::TokenGroup; +} + +impl Extension for TokenGroupMember { + const TYPE: ExtensionType = ExtensionType::TokenGroupMember; +} diff --git a/interface/src/extension/token_metadata/mod.rs b/interface/src/extension/token_metadata/mod.rs new file mode 100644 index 000000000..fa294ef71 --- /dev/null +++ b/interface/src/extension/token_metadata/mod.rs @@ -0,0 +1,8 @@ +use { + crate::extension::{Extension, ExtensionType}, + spl_token_metadata_interface::state::TokenMetadata, +}; + +impl Extension for TokenMetadata { + const TYPE: ExtensionType = ExtensionType::TokenMetadata; +} diff --git a/interface/src/extension/transfer_fee/instruction.rs b/interface/src/extension/transfer_fee/instruction.rs new file mode 100644 index 000000000..4eced055f --- /dev/null +++ b/interface/src/extension/transfer_fee/instruction.rs @@ -0,0 +1,498 @@ +#[cfg(feature = "serde")] +use { + crate::serialization::coption_fromstr, + serde::{Deserialize, Serialize}, +}; +use { + crate::{check_program_account, error::TokenError, instruction::TokenInstruction}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_program_option::COption, + solana_pubkey::Pubkey, + std::convert::TryFrom, +}; + +/// Transfer Fee extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde", + 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", serde(with = "coption_fromstr"))] + transfer_fee_config_authority: COption, + /// Withdraw instructions must be signed by this key + #[cfg_attr(feature = "serde", 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/interface/src/extension/transfer_fee/mod.rs b/interface/src/extension/transfer_fee/mod.rs new file mode 100644 index 000000000..81e0ef26a --- /dev/null +++ b/interface/src/extension/transfer_fee/mod.rs @@ -0,0 +1,472 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + error::TokenError, + extension::{Extension, ExtensionType}, + }, + bytemuck::{Pod, Zeroable}, + solana_program_error::ProgramResult, + spl_pod::{ + optional_keys::OptionalNonZeroPubkey, + primitives::{PodU16, PodU64}, + }, + std::{ + cmp, + convert::{TryFrom, TryInto}, + }, +}; + +/// Transfer fee extension instructions +pub mod instruction; + +/// 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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: u64) -> &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: u64, 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: u64, 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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_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/interface/src/extension/transfer_hook/instruction.rs b/interface/src/extension/transfer_hook/instruction.rs new file mode 100644 index 000000000..29b8d6123 --- /dev/null +++ b/interface/src/extension/transfer_hook/instruction.rs @@ -0,0 +1,126 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + spl_pod::optional_keys::OptionalNonZeroPubkey, + std::convert::TryInto, +}; + +/// Transfer hook extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/extension/transfer_hook/mod.rs b/interface/src/extension/transfer_hook/mod.rs new file mode 100644 index 000000000..aaa870cab --- /dev/null +++ b/interface/src/extension/transfer_hook/mod.rs @@ -0,0 +1,80 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + extension::{ + BaseState, BaseStateWithExtensions, BaseStateWithExtensionsMut, Extension, + ExtensionType, PodStateWithExtensionsMut, + }, + pod::PodAccount, + }, + bytemuck::{Pod, Zeroable}, + solana_account_info::AccountInfo, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodBool}, +}; + +/// Instructions for the `TransferHook` extension +pub mod instruction; + +/// Transfer hook extension data for mints. +#[repr(C)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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/interface/src/generic_token_account.rs b/interface/src/generic_token_account.rs new file mode 100644 index 000000000..b34f90779 --- /dev/null +++ b/interface/src/generic_token_account.rs @@ -0,0 +1,65 @@ +//! 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_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/interface/src/instruction.rs b/interface/src/instruction.rs new file mode 100644 index 000000000..47da6d30d --- /dev/null +++ b/interface/src/instruction.rs @@ -0,0 +1,2679 @@ +//! Instruction types + +// Needed to avoid deprecation warning when generating serde implementation for +// TokenInstruction +#![allow(deprecated)] + +#[cfg(feature = "serde")] +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_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_program_option::COption, + solana_pubkey::{Pubkey, PUBKEY_BYTES}, + solana_sdk_ids::{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", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serde", + 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", serde(with = "As::"))] + mint_authority: Pubkey, + /// The freeze authority/multisignature of the mint. + #[cfg_attr(feature = "serde", 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", 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", 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", 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", serde(with = "As::"))] + mint_authority: Pubkey, + /// The freeze authority/multisignature of the mint. + #[cfg_attr(feature = "serde", 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`. + /// + /// WARNING: For mints using the interest-bearing or scaled-ui-amount + /// extensions, this instruction uses standard floating-point arithmetic to + /// convert values, which is not guaranteed to give consistent behavior. + /// + /// In particular, conversions will not always work in reverse. For example, + /// if you pass amount `A` to `AmountToUiAmount` and receive `B`, and pass + /// the result `B` to `UiAmountToAmount`, you will not always get back `A`. + /// + /// 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`. + /// + /// WARNING: For mints using the interest-bearing or scaled-ui-amount + /// extensions, this instruction uses standard floating-point arithmetic to + /// convert values, which is not guaranteed to give consistent behavior. + /// + /// In particular, conversions will not always work in reverse. For example, + /// if you pass amount `A` to `UiAmountToAmount` and receive `B`, and pass + /// the result `B` to `AmountToUiAmount`, you will not always get back `A`. + /// + /// 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", 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", 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", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", 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, + } + } + + /// Try to convert from a `u8` into the enum + pub 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, + mint_authority_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( + *mint_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 `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, + freeze_authority_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( + *freeze_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 `ThawAccount` instruction. +pub fn thaw_account( + token_program_id: &Pubkey, + account_pubkey: &Pubkey, + mint_pubkey: &Pubkey, + freeze_authority_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( + *freeze_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 `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, + mint_authority_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( + *mint_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 `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_interface::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::*, 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 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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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 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); + } + + #[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 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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + #[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); + } + + 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, + &freeze_authority_pubkey, + &signer_pubkeys, + )); + test_instruction!(thaw_account( + &token_program_id, + &account_pubkey, + &mint_pubkey, + &freeze_authority_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/interface/src/lib.rs b/interface/src/lib.rs new file mode 100644 index 000000000..dfeaa880f --- /dev/null +++ b/interface/src/lib.rs @@ -0,0 +1,66 @@ +#![allow(clippy::arithmetic_side_effects)] +#![deny(missing_docs)] +#![cfg_attr(not(test), warn(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 pod; +#[cfg(feature = "serde")] +pub mod serialization; +pub mod state; + +// Export current sdk types for downstream users building with a different sdk +// version +pub use solana_zk_sdk; +use { + solana_program_error::{ProgramError, ProgramResult}, + solana_pubkey::Pubkey, +}; + +solana_pubkey::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(()) +} + +/// In-lined spl token program id to avoid a dependency +pub mod inline_spl_token { + solana_pubkey::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); +} + +/// 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 != &inline_spl_token::id() { + return Err(ProgramError::IncorrectProgramId); + } + Ok(()) +} + +/// 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 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_inline_spl_token_program_id() { + assert_eq!(inline_spl_token::id(), spl_token::id()); + } +} diff --git a/interface/src/native_mint.rs b/interface/src/native_mint.rs new file mode 100644 index 000000000..50e42581c --- /dev/null +++ b/interface/src/native_mint.rs @@ -0,0 +1,22 @@ +//! 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_pubkey::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_pubkey::Pubkey}; + + #[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/interface/src/pod.rs b/interface/src/pod.rs new file mode 100644 index 000000000..37c581693 --- /dev/null +++ b/interface/src/pod.rs @@ -0,0 +1,317 @@ +//! 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_error::ProgramError, + solana_program_option::COption, + solana_program_pack::IsInitialized, + solana_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_sdk_ids::system_program::check_id(&self.owner) + || solana_sdk_ids::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, +{ + /// Whether a value is set + pub option: [u8; 4], + /// The value + pub 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(crate) 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/program/src/serialization.rs b/interface/src/serialization.rs similarity index 100% rename from program/src/serialization.rs rename to interface/src/serialization.rs diff --git a/interface/src/state.rs b/interface/src/state.rs new file mode 100644 index 000000000..e7501f492 --- /dev/null +++ b/interface/src/state.rs @@ -0,0 +1,551 @@ +//! 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_error::ProgramError, + solana_program_option::COption, + solana_program_pack::{IsInitialized, Pack, Sealed}, + solana_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_sdk_ids::system_program::check_id(&self.owner) + || solana_sdk_ids::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/program/tests/serialization.rs b/interface/tests/serialization.rs similarity index 94% rename from program/tests/serialization.rs rename to interface/tests/serialization.rs index 70d580ad1..6965f7da1 100644 --- a/program/tests/serialization.rs +++ b/interface/tests/serialization.rs @@ -1,11 +1,11 @@ -#![cfg(feature = "serde-traits")] +#![cfg(feature = "serde")] use { base64::{engine::general_purpose::STANDARD, Engine}, solana_program_option::COption, solana_pubkey::Pubkey, spl_pod::optional_keys::{OptionalNonZeroElGamalPubkey, OptionalNonZeroPubkey}, - spl_token_2022::{extension::confidential_transfer, instruction}, + spl_token_2022_interface::{extension::confidential_transfer, instruction}, std::str::FromStr, }; @@ -128,7 +128,7 @@ fn serde_instruction_decryptable_balance_podu64() { #[test] fn serde_instruction_elgamal_pubkey() { - use spl_token_2022::extension::confidential_transfer_fee::instruction::InitializeConfidentialTransferFeeConfigData; + use spl_token_2022_interface::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, @@ -153,7 +153,7 @@ fn serde_instruction_elgamal_pubkey() { #[test] fn serde_instruction_basis_points() { - use spl_token_2022::extension::interest_bearing_mint::instruction::InitializeInstructionData; + use spl_token_2022_interface::extension::interest_bearing_mint::instruction::InitializeInstructionData; let inst = InitializeInstructionData { rate_authority: OptionalNonZeroPubkey::default(), diff --git a/package.json b/package.json index 52fa19169..b10cf05e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { "private": true, "scripts": { + "interface:build-wasm": "zx ./scripts/rust/build-wasm.mjs interface", + "interface:test": "zx ./scripts/rust/test.mjs interface", + "interface:format": "zx ./scripts/rust/format.mjs interface", + "interface:lint": "zx ./scripts/rust/lint.mjs interface", "programs:build": "zx ./scripts/rust/build-sbf.mjs program", "programs:build-wasm": "zx ./scripts/rust/build-wasm.mjs program", "programs:test": "zx ./scripts/rust/test-sbf.mjs program", diff --git a/program/Cargo.toml b/program/Cargo.toml index ab25df9d3..62ddd1a06 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -13,7 +13,7 @@ edition = { workspace = true } [features] no-entrypoint = [] test-sbf = [] -serde-traits = ["dep:serde", "dep:serde_with", "dep:base64", "spl-pod/serde-traits"] +serde-traits = ["dep:serde", "dep:serde_with", "dep:base64", "spl-pod/serde-traits", "spl-token-2022-interface/serde"] default = ["zk-ops"] # Remove this feature once the underlying syscalls are released on all networks zk-ops = [] @@ -27,10 +27,8 @@ num_enum = "0.7.4" solana-account-info = "2.3.0" solana-clock = "2.2.1" solana-cpi = "2.2.1" -solana-decode-error = "2.2.1" solana-instruction = "2.2.1" solana-msg = "2.2.1" -solana-native-token = "2.2.1" solana-program-entrypoint = "2.3.0" solana-program-error = "2.2.1" solana-program-memory = "2.3.1" @@ -45,13 +43,12 @@ solana-system-interface = "1.0.0" solana-zk-sdk = "2.3.4" spl-elgamal-registry = { version = "0.3.0", path = "../confidential-transfer/elgamal-registry", features = ["no-entrypoint"] } spl-memo = { version = "6.0", features = ["no-entrypoint"] } -spl-token = { version = "8.0", features = ["no-entrypoint"] } +spl-token-2022-interface = { version = "1.0", path = "../interface" } spl-token-confidential-transfer-ciphertext-arithmetic = { version = "0.3.0", path = "../confidential-transfer/ciphertext-arithmetic" } spl-token-confidential-transfer-proof-extraction = { version = "0.4.0", path = "../confidential-transfer/proof-extraction" } spl-token-group-interface = { version = "0.6.0" } spl-token-metadata-interface = { version = "0.7.0" } spl-transfer-hook-interface = { version = "0.10.0" } -spl-type-length-value = { version = "0.8.0" } spl-pod = { version = "0.5.1" } thiserror = "2.0" serde = { version = "1.0.219", optional = true } diff --git a/program/src/error.rs b/program/src/error.rs index 99f7783aa..0fb478cd1 100644 --- a/program/src/error.rs +++ b/program/src/error.rs @@ -1,506 +1,2 @@ //! Error types - -#[cfg(not(target_os = "solana"))] -use spl_token_confidential_transfer_proof_generation::errors::TokenProofGenerationError; -use { - num_derive::FromPrimitive, - solana_decode_error::DecodeError, - solana_msg::msg, - solana_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, - /// Pending supply is not zero - #[error("Key rotation attempted while pending balance is not zero")] - PendingBalanceNonZero, -} -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") - } - TokenError::PendingBalanceNonZero => { - msg!("Key rotation attempted while pending balance is not zero") - } - } - } -} - -#[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, - } - } -} +pub use spl_token_2022_interface::error::*; diff --git a/program/src/extension/confidential_mint_burn/instruction.rs b/program/src/extension/confidential_mint_burn/instruction.rs index 019503171..28ff5310e 100644 --- a/program/src/extension/confidential_mint_burn/instruction.rs +++ b/program/src/extension/confidential_mint_burn/instruction.rs @@ -1,599 +1 @@ -#[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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - solana_zk_sdk::encryption::pod::{ - auth_encryption::PodAeCiphertext, - elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - }, -}; -#[cfg(not(target_os = "solana"))] -use { - solana_zk_sdk::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 - /// - /// The pending burn amount must be zero in order for this instruction - /// to be processed successfully. - /// - /// 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. `[writable]` The SPL Token mint. - /// 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. `[writable]` The SPL Token mint. - /// 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, - - /// Applies the pending burn amount to the confidential supply - /// - /// * Single authority - /// 0. `[writable]` The SPL token mint. - /// 1. `[signer]` The single mint authority. - /// - /// * Multisignature authority - /// 0. `[writable]` The SPL token mint. - /// 1. `[]` The multisig account owner. - /// 2. .. `[signer]` Required M signer accounts for the SPL Token Multisig - /// account. - ApplyPendingBurn, -} - -/// 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, - 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), - AccountMeta::new(*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, - 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), - AccountMeta::new(*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) -} - -/// Create a `ApplyPendingBurn` instruction -pub fn apply_pending_burn( - 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::ConfidentialMintBurnExtension, - ConfidentialMintBurnInstruction::ApplyPendingBurn, - &(), - )) -} +pub use spl_token_2022_interface::extension::confidential_mint_burn::instruction::*; diff --git a/program/src/extension/confidential_mint_burn/mod.rs b/program/src/extension/confidential_mint_burn/mod.rs index 58e3bd9e8..1a44d5c5a 100644 --- a/program/src/extension/confidential_mint_burn/mod.rs +++ b/program/src/extension/confidential_mint_burn/mod.rs @@ -1,12 +1,3 @@ -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, - solana_zk_sdk::encryption::pod::{ - auth_encryption::PodAeCiphertext, - elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - }, -}; - /// Confidential Mint-Burn Extension instructions pub mod instruction; @@ -21,19 +12,4 @@ pub mod verify_proof; 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, - /// The amount of burn amounts not yet aggregated into the confidential supply - pub pending_burn: PodElGamalCiphertext, -} - -impl Extension for ConfidentialMintBurn { - const TYPE: ExtensionType = ExtensionType::ConfidentialMintBurn; -} +pub use spl_token_2022_interface::extension::confidential_mint_burn::ConfidentialMintBurn; diff --git a/program/src/extension/confidential_transfer/instruction.rs b/program/src/extension/confidential_transfer/instruction.rs index 532247590..dae4743cc 100644 --- a/program/src/extension/confidential_transfer/instruction.rs +++ b/program/src/extension/confidential_transfer/instruction.rs @@ -1,1665 +1 @@ -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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - solana_sdk_ids::{system_program, sysvar}, - spl_token_confidential_transfer_proof_extraction::instruction::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. `[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. `[]` The multisig source account owner. - /// 4. .. `[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. `[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. `[]` The multisig account owner. - /// 3. .. `[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 context state account. - /// 4. `[]` (Optional) Range proof 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 context state account. - /// 4. `[]` (Optional) Range proof 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 context state account. - /// 6. `[]` (Optional) Ciphertext validity context state account. - /// 7. `[]` (Optional) Range proof 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 context state account. - /// 6. `[]` (Optional) Ciphertext validity proof context state account. - /// 7. `[]` (Optional) Range proof 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 context state account. - /// 6. `[]` (Optional) Transfer amount ciphertext validity proof context - /// state account. - /// 7. `[]` (Optional) Fee sigma proof context state account. - /// 8. `[]` (Optional) Fee ciphertext validity proof context state - /// account. - /// 9. `[]` (Optional) Range proof 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 context state account. - /// 6. `[]` (Optional) Transfer amount ciphertext validity proof context - /// state account. - /// 7. `[]` (Optional) Fee sigma proof context state account. - /// 8. `[]` (Optional) Fee ciphertext validity proof context state - /// account. - /// 9. `[]` (Optional) Range proof 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, _) => { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), 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()); - } - instructions - .push(ProofInstruction::VerifyPubkeyValidity.encode_verify_proof(None, proof_data)); - } - - 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, _) => { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), 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()); - } - instructions - .push(ProofInstruction::VerifyZeroCiphertext.encode_verify_proof(None, proof_data)); - }; - - 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_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_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()); - } - instructions.push( - ProofInstruction::VerifyCiphertextCommitmentEquality - .encode_verify_proof(None, proof_data), - ); - 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()); - } - instructions.push( - ProofInstruction::VerifyBatchedRangeProofU64.encode_verify_proof(None, proof_data), - ); - }; - - 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_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_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_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()); - } - instructions.push( - ProofInstruction::VerifyCiphertextCommitmentEquality - .encode_verify_proof(None, proof_data), - ); - 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()); - } - instructions.push( - ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity - .encode_verify_proof(None, proof_data), - ); - 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()); - } - instructions.push( - ProofInstruction::VerifyBatchedRangeProofU128.encode_verify_proof(None, proof_data), - ); - } - - 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_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_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_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_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_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()); - } - instructions.push( - ProofInstruction::VerifyCiphertextCommitmentEquality - .encode_verify_proof(None, proof_data), - ); - 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()); - } - instructions.push( - ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity - .encode_verify_proof(None, proof_data), - ); - 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()); - } - instructions - .push(ProofInstruction::VerifyPercentageWithCap.encode_verify_proof(None, proof_data)); - 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()); - } - instructions.push( - ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity - .encode_verify_proof(None, proof_data), - ); - 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()); - } - instructions.push( - ProofInstruction::VerifyBatchedRangeProofU256.encode_verify_proof(None, proof_data), - ); - } - - 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, - &(), - )) -} +pub use spl_token_2022_interface::extension::confidential_transfer::instruction::*; diff --git a/program/src/extension/confidential_transfer/mod.rs b/program/src/extension/confidential_transfer/mod.rs index bc1df377b..11336565a 100644 --- a/program/src/extension/confidential_transfer/mod.rs +++ b/program/src/extension/confidential_transfer/mod.rs @@ -1,30 +1,11 @@ -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; +pub use spl_token_2022_interface::extension::confidential_transfer::{ + ConfidentialTransferAccount, ConfidentialTransferMint, DecryptableBalance, EncryptedBalance, + DEFAULT_MAXIMUM_PENDING_BALANCE_CREDIT_COUNTER, MAXIMUM_DEPOSIT_TRANSFER_AMOUNT, + PENDING_BALANCE_LO_BIT_LENGTH, +}; /// Confidential Transfer Extension instructions pub mod instruction; @@ -39,163 +20,3 @@ 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 32 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/program/src/extension/confidential_transfer/processor.rs b/program/src/extension/confidential_transfer/processor.rs index 23df69c91..e98dea767 100644 --- a/program/src/extension/confidential_transfer/processor.rs +++ b/program/src/extension/confidential_transfer/processor.rs @@ -20,7 +20,7 @@ use { pausable::PausableConfig, set_account_type, transfer_fee::TransferFeeConfig, - transfer_hook, BaseStateWithExtensions, BaseStateWithExtensionsMut, + transfer_hook, BaseStateWithExtensions, BaseStateWithExtensionsMut, ExtensionType, PodStateWithExtensions, PodStateWithExtensionsMut, }, instruction::{decode_instruction_data, decode_instruction_type}, @@ -28,6 +28,7 @@ use { processor::Processor, state::Account, }, + bytemuck::Zeroable, solana_account_info::{next_account_info, AccountInfo}, solana_clock::Clock, solana_cpi::invoke, @@ -37,8 +38,15 @@ use { solana_rent::Rent, solana_system_interface::instruction as system_instruction, solana_sysvar::Sysvar, + solana_zk_sdk::encryption::pod::{ + auth_encryption::PodAeCiphertext, elgamal::PodElGamalCiphertext, + }, spl_elgamal_registry::state::ElGamalRegistry, - spl_pod::bytemuck::pod_from_bytes, + spl_pod::{ + bytemuck::pod_from_bytes, + optional_keys::{OptionalNonZeroElGamalPubkey, OptionalNonZeroPubkey}, + primitives::{PodBool, PodU64}, + }, spl_token_confidential_transfer_proof_extraction::{ instruction::verify_and_extract_context, transfer::TransferProofContext, transfer_with_fee::TransferWithFeeProofContext, diff --git a/program/src/extension/confidential_transfer_fee/instruction.rs b/program/src/extension/confidential_transfer_fee/instruction.rs index 526f59ff1..7548278eb 100644 --- a/program/src/extension/confidential_transfer_fee/instruction.rs +++ b/program/src/extension/confidential_transfer_fee/instruction.rs @@ -1,551 +1 @@ -#[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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_pubkey::Pubkey, - solana_sdk_ids::sysvar, - spl_pod::optional_keys::OptionalNonZeroPubkey, - spl_token_confidential_transfer_proof_extraction::instruction::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. `[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. `[]` The mint's multisig `withdraw_withheld_authority`. - /// 4. ..`4+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. `[signer]` The mint's `withdraw_withheld_authority`. - /// 4. ..`4+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. `[]` The mint's multisig `withdraw_withheld_authority`. - /// 4. ..`4+M` `[signer]` M signer accounts. - /// 5. `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, _) => { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), 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()); - } - instructions.push( - ProofInstruction::VerifyCiphertextCiphertextEquality - .encode_verify_proof(None, proof_data), - ); - }; - - 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, _) => { - accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), 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()); - } - instructions.push( - ProofInstruction::VerifyCiphertextCiphertextEquality - .encode_verify_proof(None, proof_data), - ); - }; - - 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, - &(), - )) -} +pub use spl_token_2022_interface::extension::confidential_transfer_fee::instruction::*; diff --git a/program/src/extension/confidential_transfer_fee/mod.rs b/program/src/extension/confidential_transfer_fee/mod.rs index 7e3554378..1456c41f2 100644 --- a/program/src/extension/confidential_transfer_fee/mod.rs +++ b/program/src/extension/confidential_transfer_fee/mod.rs @@ -1,15 +1,3 @@ -use { - crate::{ - error::TokenError, - extension::{Extension, ExtensionType}, - }, - bytemuck::{Pod, Zeroable}, - solana_program_error::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; @@ -21,57 +9,7 @@ pub mod processor; #[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()) - } - } -} +pub use spl_token_2022_interface::extension::confidential_transfer_fee::{ + ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, EncryptedFee, + EncryptedWithheldAmount, +}; diff --git a/program/src/extension/cpi_guard/instruction.rs b/program/src/extension/cpi_guard/instruction.rs index ccb5c69e7..ce5a33fe4 100644 --- a/program/src/extension/cpi_guard/instruction.rs +++ b/program/src/extension/cpi_guard/instruction.rs @@ -1,102 +1 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - instruction::{encode_instruction, TokenInstruction}, - }, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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, - &(), - )) -} +pub use spl_token_2022_interface::extension::cpi_guard::instruction::*; diff --git a/program/src/extension/cpi_guard/mod.rs b/program/src/extension/cpi_guard/mod.rs index baf258ea4..1d351f0fb 100644 --- a/program/src/extension/cpi_guard/mod.rs +++ b/program/src/extension/cpi_guard/mod.rs @@ -1,40 +1,10 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - extension::{BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsMut}, - state::Account, - }, - bytemuck::{Pod, Zeroable}, - 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 -} +pub use spl_token_2022_interface::extension::cpi_guard::{cpi_guard_enabled, CpiGuard}; /// Determine if we are in CPI pub fn in_cpi() -> bool { diff --git a/program/src/extension/default_account_state/instruction.rs b/program/src/extension/default_account_state/instruction.rs index d4ad868c0..f2f915522 100644 --- a/program/src/extension/default_account_state/instruction.rs +++ b/program/src/extension/default_account_state/instruction.rs @@ -1,125 +1 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, error::TokenError, instruction::TokenInstruction, - state::AccountState, - }, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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, - )) -} +pub use spl_token_2022_interface::extension::default_account_state::instruction::*; diff --git a/program/src/extension/default_account_state/mod.rs b/program/src/extension/default_account_state/mod.rs index 939bd16b0..a7970a8b7 100644 --- a/program/src/extension/default_account_state/mod.rs +++ b/program/src/extension/default_account_state/mod.rs @@ -1,27 +1,7 @@ -#[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; +pub use spl_token_2022_interface::extension::default_account_state::DefaultAccountState; diff --git a/program/src/extension/group_member_pointer/instruction.rs b/program/src/extension/group_member_pointer/instruction.rs index fdf0e2db8..39bb2366c 100644 --- a/program/src/extension/group_member_pointer/instruction.rs +++ b/program/src/extension/group_member_pointer/instruction.rs @@ -1,126 +1 @@ -#[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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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()?, - }, - )) -} +pub use spl_token_2022_interface::extension::group_member_pointer::instruction::*; diff --git a/program/src/extension/group_member_pointer/mod.rs b/program/src/extension/group_member_pointer/mod.rs index a74e4872d..e62d2c8b8 100644 --- a/program/src/extension/group_member_pointer/mod.rs +++ b/program/src/extension/group_member_pointer/mod.rs @@ -1,28 +1,7 @@ -#[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; -} +pub use spl_token_2022_interface::extension::group_member_pointer::GroupMemberPointer; diff --git a/program/src/extension/group_pointer/instruction.rs b/program/src/extension/group_pointer/instruction.rs index 4f552cab2..0e6f6b0f1 100644 --- a/program/src/extension/group_pointer/instruction.rs +++ b/program/src/extension/group_pointer/instruction.rs @@ -1,126 +1 @@ -#[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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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()?, - }, - )) -} +pub use spl_token_2022_interface::extension::group_pointer::instruction::*; diff --git a/program/src/extension/group_pointer/mod.rs b/program/src/extension/group_pointer/mod.rs index 27344d2d2..3d134f8ba 100644 --- a/program/src/extension/group_pointer/mod.rs +++ b/program/src/extension/group_pointer/mod.rs @@ -1,28 +1,7 @@ -#[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; -} +pub use spl_token_2022_interface::extension::group_pointer::GroupPointer; diff --git a/program/src/extension/immutable_owner.rs b/program/src/extension/immutable_owner.rs index cc3235994..2f08e7b14 100644 --- a/program/src/extension/immutable_owner.rs +++ b/program/src/extension/immutable_owner.rs @@ -1,17 +1 @@ -#[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; -} +pub use spl_token_2022_interface::extension::immutable_owner::ImmutableOwner; diff --git a/program/src/extension/interest_bearing_mint/instruction.rs b/program/src/extension/interest_bearing_mint/instruction.rs index 555bd5858..8917cdbba 100644 --- a/program/src/extension/interest_bearing_mint/instruction.rs +++ b/program/src/extension/interest_bearing_mint/instruction.rs @@ -1,115 +1 @@ -#[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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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), - )) -} +pub use spl_token_2022_interface::extension::interest_bearing_mint::instruction::*; diff --git a/program/src/extension/interest_bearing_mint/mod.rs b/program/src/extension/interest_bearing_mint/mod.rs index 864495c0b..07576ec5a 100644 --- a/program/src/extension/interest_bearing_mint/mod.rs +++ b/program/src/extension/interest_bearing_mint/mod.rs @@ -1,464 +1,9 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - extension::{Extension, ExtensionType}, - trim_ui_amount_string, - }, - bytemuck::{Pod, Zeroable}, - solana_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()); - } - } -} +pub use spl_token_2022_interface::extension::interest_bearing_mint::{ + BasisPoints, InterestBearingConfig, UnixTimestamp, +}; diff --git a/program/src/extension/memo_transfer/instruction.rs b/program/src/extension/memo_transfer/instruction.rs index eb77b588d..4bba05fa5 100644 --- a/program/src/extension/memo_transfer/instruction.rs +++ b/program/src/extension/memo_transfer/instruction.rs @@ -1,96 +1 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - check_program_account, - instruction::{encode_instruction, TokenInstruction}, - }, - num_enum::{IntoPrimitive, TryFromPrimitive}, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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, - &(), - )) -} +pub use spl_token_2022_interface::extension::memo_transfer::instruction::*; diff --git a/program/src/extension/memo_transfer/mod.rs b/program/src/extension/memo_transfer/mod.rs index 2e51c252e..ccc0c2fa9 100644 --- a/program/src/extension/memo_transfer/mod.rs +++ b/program/src/extension/memo_transfer/mod.rs @@ -1,15 +1,6 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; use { - crate::{ - error::TokenError, - extension::{BaseState, BaseStateWithExtensions, Extension, ExtensionType}, - }, - bytemuck::{Pod, Zeroable}, - solana_instruction::Instruction, - solana_program_error::ProgramError, + crate::error::TokenError, solana_instruction::Instruction, solana_program_error::ProgramError, solana_pubkey::Pubkey, - spl_pod::primitives::PodBool, }; /// Memo Transfer extension instructions @@ -18,26 +9,7 @@ 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 -} +pub use spl_token_2022_interface::extension::memo_transfer::{memo_required, MemoTransfer}; /// Check if the previous sibling instruction is a memo pub fn check_previous_sibling_instruction_is_memo() -> Result<(), ProgramError> { diff --git a/program/src/extension/metadata_pointer/instruction.rs b/program/src/extension/metadata_pointer/instruction.rs index ddbc47be8..07056f9cf 100644 --- a/program/src/extension/metadata_pointer/instruction.rs +++ b/program/src/extension/metadata_pointer/instruction.rs @@ -1,126 +1 @@ -#[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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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()?, - }, - )) -} +pub use spl_token_2022_interface::extension::metadata_pointer::instruction::*; diff --git a/program/src/extension/metadata_pointer/mod.rs b/program/src/extension/metadata_pointer/mod.rs index 639d92df4..aea3e300b 100644 --- a/program/src/extension/metadata_pointer/mod.rs +++ b/program/src/extension/metadata_pointer/mod.rs @@ -1,28 +1,6 @@ -#[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; -} +pub use spl_token_2022_interface::extension::metadata_pointer::MetadataPointer; diff --git a/program/src/extension/mint_close_authority.rs b/program/src/extension/mint_close_authority.rs index 7321b7f60..f378c6b89 100644 --- a/program/src/extension/mint_close_authority.rs +++ b/program/src/extension/mint_close_authority.rs @@ -1,20 +1 @@ -#[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; -} +pub use spl_token_2022_interface::extension::mint_close_authority::MintCloseAuthority; diff --git a/program/src/extension/mod.rs b/program/src/extension/mod.rs index 7a6be8c9c..d915f93ad 100644 --- a/program/src/extension/mod.rs +++ b/program/src/extension/mod.rs @@ -1,53 +1,5 @@ //! 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_account_info::AccountInfo, - solana_program_error::ProgramError, - solana_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 @@ -92,3044 +44,9 @@ 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; - 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 = u16::from_le_bytes( - tlv_data[tlv_indices.type_start..tlv_indices.length_start] - .try_into() - .map_err(|_| ProgramError::InvalidAccountData)?, - ); - if extension_type == u16::from(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 == u16::from(ExtensionType::Uninitialized) { - if init { - return Ok(tlv_indices); - } else { - return Err(TokenError::ExtensionNotFound.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 BaseStateWithExtensions for StateWithExtensions<'_, 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 BaseStateWithExtensions for PodStateWithExtensions<'_, 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 BaseStateWithExtensions for StateWithExtensionsMut<'_, S> { - fn get_tlv_data(&self) -> &[u8] { - self.tlv_data - } -} -impl BaseStateWithExtensionsMut for StateWithExtensionsMut<'_, 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 BaseStateWithExtensions for PodStateWithExtensionsMut<'_, S> { - fn get_tlv_data(&self) -> &[u8] { - self.tlv_data - } -} -impl BaseStateWithExtensionsMut for PodStateWithExtensionsMut<'_, 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.resize(new_account_len)?; - } - 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.resize(new_account_len)?; - 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.resize(new_account_len)?; - } - } - 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_account_info::{ - Account as GetAccount, IntoAccountInfo, MAX_PERMITTED_DATA_INCREASE, - }, - solana_clock::Epoch, - solana_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_ACCOUNT_TYPE: &[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 - ]; - - 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_ACCOUNT_TYPE).unwrap(); - assert_eq!(state.base, &TEST_POD_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::InvalidAccountData) - ); - - // 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::InvalidAccountData), - ); - - // 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()); - } -} +pub use spl_token_2022_interface::extension::{ + alloc_and_serialize, alloc_and_serialize_variable_len_extension, set_account_type, AccountType, + BaseState, BaseStateWithExtensions, BaseStateWithExtensionsMut, Extension, ExtensionType, + Length, PodStateWithExtensions, PodStateWithExtensionsMut, StateWithExtensions, + StateWithExtensionsMut, StateWithExtensionsOwned, +}; diff --git a/program/src/extension/non_transferable.rs b/program/src/extension/non_transferable.rs index f78b86142..4b0fa4778 100644 --- a/program/src/extension/non_transferable.rs +++ b/program/src/extension/non_transferable.rs @@ -1,29 +1,3 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, +pub use spl_token_2022_interface::extension::non_transferable::{ + NonTransferable, NonTransferableAccount, }; - -/// 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/program/src/extension/pausable/instruction.rs b/program/src/extension/pausable/instruction.rs index e3af219c0..926ae6fad 100644 --- a/program/src/extension/pausable/instruction.rs +++ b/program/src/extension/pausable/instruction.rs @@ -1,134 +1 @@ -#[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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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, - &(), - )) -} +pub use spl_token_2022_interface::extension::pausable::instruction::*; diff --git a/program/src/extension/pausable/mod.rs b/program/src/extension/pausable/mod.rs index 82a3878ab..0885202a1 100644 --- a/program/src/extension/pausable/mod.rs +++ b/program/src/extension/pausable/mod.rs @@ -1,39 +1,6 @@ -#[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; -} +pub use spl_token_2022_interface::extension::pausable::{PausableAccount, PausableConfig}; diff --git a/program/src/extension/permanent_delegate.rs b/program/src/extension/permanent_delegate.rs index a9ad8552b..575e6a37e 100644 --- a/program/src/extension/permanent_delegate.rs +++ b/program/src/extension/permanent_delegate.rs @@ -1,32 +1,3 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::extension::{BaseState, BaseStateWithExtensions, Extension, ExtensionType}, - bytemuck::{Pod, Zeroable}, - solana_pubkey::Pubkey, - spl_pod::optional_keys::OptionalNonZeroPubkey, +pub use spl_token_2022_interface::extension::permanent_delegate::{ + get_permanent_delegate, PermanentDelegate, }; - -/// 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/program/src/extension/scaled_ui_amount/instruction.rs b/program/src/extension/scaled_ui_amount/instruction.rs index bb0366385..16cc64cc6 100644 --- a/program/src/extension/scaled_ui_amount/instruction.rs +++ b/program/src/extension/scaled_ui_amount/instruction.rs @@ -1,141 +1 @@ -#[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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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(), - }, - )) -} +pub use spl_token_2022_interface::extension::scaled_ui_amount::instruction::*; diff --git a/program/src/extension/scaled_ui_amount/mod.rs b/program/src/extension/scaled_ui_amount/mod.rs index b40bbfa81..840cb0c32 100644 --- a/program/src/extension/scaled_ui_amount/mod.rs +++ b/program/src/extension/scaled_ui_amount/mod.rs @@ -1,366 +1,9 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - extension::{Extension, ExtensionType}, - trim_ui_amount_string, - }, - bytemuck::{Pod, Zeroable}, - solana_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 current_multiplier(&self, unix_timestamp: i64) -> f64 { - if unix_timestamp >= self.new_multiplier_effective_timestamp.into() { - self.new_multiplier.into() - } else { - self.multiplier.into() - } - } - - fn total_multiplier(&self, decimals: u8, unix_timestamp: i64) -> f64 { - self.current_multiplier(unix_timestamp) / 10_f64.powi(decimals as i32) - } - - /// Convert a raw amount to its UI representation using the given decimals - /// field. - /// - /// The value is converted to a float and then truncated towards 0. 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.current_multiplier(unix_timestamp); - let truncated_amount = scaled_amount.trunc() / 10_f64.powi(decimals as i32); - let ui_amount = format!("{truncated_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. - /// - /// The string is parsed to a float, scaled, and then truncated towards 0 - /// before being converted to a fixed-point number. - 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 truncate earlier, you'll get wrong "inf" - // answers - Ok(amount.trunc() 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 { - multiplier: PodF64::from(multiplier), - new_multiplier: PodF64::from(new_multiplier), - new_multiplier_effective_timestamp: UnixTimestamp::from( - new_multiplier_effective_timestamp, - ), - ..Default::default() - }; - 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 { - 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 { - 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"); - - // truncation - let config = ScaledUiAmountConfig { - multiplier: PodF64::from(0.99), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - // This is really 0.99999... but it gets truncated - let ui_amount = config.amount_to_ui_amount(101, 2, 0).unwrap(); - assert_eq!(ui_amount, "0.99"); - } - - #[test] - fn specific_ui_amount_to_amount() { - // constant 5x - let config = ScaledUiAmountConfig { - 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 { - 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 { - 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 { - 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 { - 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 { - 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) - ); - } - - // truncation - let config = ScaledUiAmountConfig { - multiplier: PodF64::from(0.99), - new_multiplier_effective_timestamp: UnixTimestamp::from(1), - ..Default::default() - }; - // There are a few possibilities for what "0.99" means, it could be 101 - // or 100 underlying tokens, but the result gives the fewest possible - // tokens that give that UI amount. - let amount = config.try_ui_amount_into_amount("0.99", 2, 0).unwrap(); - assert_eq!(amount, 100); - } - - #[test] - fn specific_amount_to_ui_amount_no_scale() { - let config = ScaledUiAmountConfig { - 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 { - 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 { - 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()); - } - } -} +pub use spl_token_2022_interface::extension::scaled_ui_amount::{ + PodF64, ScaledUiAmountConfig, UnixTimestamp, +}; diff --git a/program/src/extension/token_group/mod.rs b/program/src/extension/token_group/mod.rs index 32bb723a7..518a1fc94 100644 --- a/program/src/extension/token_group/mod.rs +++ b/program/src/extension/token_group/mod.rs @@ -1,15 +1,2 @@ -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/program/src/extension/token_metadata/mod.rs b/program/src/extension/token_metadata/mod.rs index 5fb94c42b..daa456836 100644 --- a/program/src/extension/token_metadata/mod.rs +++ b/program/src/extension/token_metadata/mod.rs @@ -1,11 +1,2 @@ -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/program/src/extension/transfer_fee/instruction.rs b/program/src/extension/transfer_fee/instruction.rs index 066eb82bb..9b5a5a0e6 100644 --- a/program/src/extension/transfer_fee/instruction.rs +++ b/program/src/extension/transfer_fee/instruction.rs @@ -1,498 +1 @@ -#[cfg(feature = "serde-traits")] -use { - crate::serialization::coption_fromstr, - serde::{Deserialize, Serialize}, -}; -use { - crate::{check_program_account, error::TokenError, instruction::TokenInstruction}, - solana_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_program_option::COption, - solana_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); - } -} +pub use spl_token_2022_interface::extension::transfer_fee::instruction::*; diff --git a/program/src/extension/transfer_fee/mod.rs b/program/src/extension/transfer_fee/mod.rs index ea972fec9..04dfa0e3e 100644 --- a/program/src/extension/transfer_fee/mod.rs +++ b/program/src/extension/transfer_fee/mod.rs @@ -1,476 +1,9 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - error::TokenError, - extension::{Extension, ExtensionType}, - }, - bytemuck::{Pod, Zeroable}, - solana_clock::Epoch, - solana_program_error::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_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); - } - } -} +pub use spl_token_2022_interface::extension::transfer_fee::{ + TransferFee, TransferFeeAmount, TransferFeeConfig, MAX_FEE_BASIS_POINTS, +}; diff --git a/program/src/extension/transfer_hook/instruction.rs b/program/src/extension/transfer_hook/instruction.rs index 7730f2a29..043828cdc 100644 --- a/program/src/extension/transfer_hook/instruction.rs +++ b/program/src/extension/transfer_hook/instruction.rs @@ -1,126 +1 @@ -#[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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_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()?, - }, - )) -} +pub use spl_token_2022_interface::extension::transfer_hook::instruction::*; diff --git a/program/src/extension/transfer_hook/mod.rs b/program/src/extension/transfer_hook/mod.rs index 13a3bf425..6982baacf 100644 --- a/program/src/extension/transfer_hook/mod.rs +++ b/program/src/extension/transfer_hook/mod.rs @@ -1,82 +1,9 @@ -#[cfg(feature = "serde-traits")] -use serde::{Deserialize, Serialize}; -use { - crate::{ - extension::{ - BaseState, BaseStateWithExtensions, BaseStateWithExtensionsMut, Extension, - ExtensionType, PodStateWithExtensionsMut, - }, - pod::PodAccount, - }, - bytemuck::{Pod, Zeroable}, - solana_account_info::AccountInfo, - solana_program_error::ProgramError, - solana_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(()) -} +pub use spl_token_2022_interface::extension::transfer_hook::{ + get_program_id, set_transferring, unset_transferring, TransferHook, TransferHookAccount, +}; diff --git a/program/src/generic_token_account.rs b/program/src/generic_token_account.rs index b34f90779..69af415af 100644 --- a/program/src/generic_token_account.rs +++ b/program/src/generic_token_account.rs @@ -1,65 +1,2 @@ //! 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_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 -} +pub use spl_token_2022_interface::generic_token_account::*; diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 18c763270..b4b072b75 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -1,2843 +1,2 @@ //! 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_instruction::{AccountMeta, Instruction}, - solana_program_error::ProgramError, - solana_program_option::COption, - solana_pubkey::{Pubkey, PUBKEY_BYTES}, - solana_sdk_ids::{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`. - /// - /// WARNING: For mints using the interest-bearing or scaled-ui-amount - /// extensions, this instruction uses standard floating-point arithmetic to - /// convert values, which is not guaranteed to give consistent behavior. - /// - /// In particular, conversions will not always work in reverse. For example, - /// if you pass amount `A` to `AmountToUiAmount` and receive `B`, and pass - /// the result `B` to `UiAmountToAmount`, you will not always get back `A`. - /// - /// 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`. - /// - /// WARNING: For mints using the interest-bearing or scaled-ui-amount - /// extensions, this instruction uses standard floating-point arithmetic to - /// convert values, which is not guaranteed to give consistent behavior. - /// - /// In particular, conversions will not always work in reverse. For example, - /// if you pass amount `A` to `UiAmountToAmount` and receive `B`, and pass - /// the result `B` to `AmountToUiAmount`, you will not always get back `A`. - /// - /// 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, - mint_authority_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( - *mint_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 `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, - freeze_authority_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( - *freeze_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 `ThawAccount` instruction. -pub fn thaw_account( - token_program_id: &Pubkey, - account_pubkey: &Pubkey, - mint_pubkey: &Pubkey, - freeze_authority_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( - *freeze_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 `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, - mint_authority_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( - *mint_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 `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, - &freeze_authority_pubkey, - &signer_pubkeys, - )); - test_instruction!(thaw_account( - &token_program_id, - &account_pubkey, - &mint_pubkey, - &freeze_authority_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); - } - } -} +pub use spl_token_2022_interface::instruction::*; diff --git a/program/src/lib.rs b/program/src/lib.rs index 251b598d0..5ea0cd92a 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -14,8 +14,6 @@ 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"))] @@ -24,6 +22,7 @@ mod entrypoint; // Export current sdk types for downstream users building with a different sdk // version pub use solana_zk_sdk; +pub use spl_token_2022_interface::{check_id, check_program_account, id, ID}; use { error::TokenError, solana_program_error::{ProgramError, ProgramResult}, @@ -101,25 +100,6 @@ pub fn try_ui_amount_into_amount(ui_amount: String, decimals: u8) -> Result 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( diff --git a/program/src/native_mint.rs b/program/src/native_mint.rs index 1e337a62a..ccc0c6b4c 100644 --- a/program/src/native_mint.rs +++ b/program/src/native_mint.rs @@ -1,37 +1,2 @@ //! 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_pubkey::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_native_token::{lamports_to_sol, sol_to_lamports}, - solana_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); - } -} +pub use spl_token_2022_interface::native_mint::*; diff --git a/program/src/onchain.rs b/program/src/onchain.rs index f5a9ac0aa..c8c81685c 100644 --- a/program/src/onchain.rs +++ b/program/src/onchain.rs @@ -14,11 +14,13 @@ use { solana_program_error::{ProgramError, ProgramResult}, solana_pubkey::Pubkey, spl_pod::bytemuck::pod_from_bytes, + spl_token_2022_interface::inline_spl_token, spl_transfer_hook_interface::onchain::add_extra_accounts_for_execute_cpi, }; fn is_multisig_account(account: &AccountInfo) -> bool { - let owned_by_token_program = account.owner == &crate::id() || account.owner == &spl_token::id(); + let owned_by_token_program = + account.owner == &crate::id() || account.owner == &inline_spl_token::id(); owned_by_token_program && account.data_len() == PodMultisig::SIZE_OF } diff --git a/program/src/pod.rs b/program/src/pod.rs index 0eade38c1..310929e31 100644 --- a/program/src/pod.rs +++ b/program/src/pod.rs @@ -1,315 +1,2 @@ //! 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_error::ProgramError, - solana_program_option::COption, - solana_program_pack::IsInitialized, - solana_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_sdk_ids::system_program::check_id(&self.owner) - || solana_sdk_ids::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(crate) 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)); - } -} +pub use spl_token_2022_interface::pod::*; diff --git a/program/src/pod_instruction.rs b/program/src/pod_instruction.rs index 8c64f0903..898fbcbc8 100644 --- a/program/src/pod_instruction.rs +++ b/program/src/pod_instruction.rs @@ -153,9 +153,12 @@ mod tests { super::*, crate::{ extension::ExtensionType, - instruction::{decode_instruction_data, decode_instruction_type}, + instruction::{ + decode_instruction_data, decode_instruction_type, AuthorityType, TokenInstruction, + }, }, proptest::prelude::*, + solana_program_option::COption, }; // Test function that mimics the "unpacking" in `Processor::process` by @@ -222,4 +225,402 @@ mod tests { let _no_panic = check_pod_instruction(&data); } } + + #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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); + } } diff --git a/program/src/state.rs b/program/src/state.rs index e7501f492..fdddac245 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -1,551 +1,2 @@ //! 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_error::ProgramError, - solana_program_option::COption, - solana_program_pack::{IsInitialized, Pack, Sealed}, - solana_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_sdk_ids::system_program::check_id(&self.owner) - || solana_sdk_ids::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()); - } -} +pub use spl_token_2022_interface::state::*; diff --git a/scripts/generate-clients.mjs b/scripts/generate-clients.mjs index da6e84900..ceb3a9c8f 100644 --- a/scripts/generate-clients.mjs +++ b/scripts/generate-clients.mjs @@ -7,7 +7,7 @@ import { workingDirectory } from './utils.mjs'; // Instanciate Codama. const codama = createFromRoot( - require(path.join(workingDirectory, 'program', 'idl.json')) + require(path.join(workingDirectory, 'interface', 'idl.json')) ); // Render JavaScript.