From 34f96582bc89218994fcbb50406efd430c83605c Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Fri, 24 Jul 2026 22:52:37 +0200 Subject: [PATCH] v0.1.0-pre.021 --- Cargo.toml | 4 +- config/example.config.json | 60 +- docs/KB_LIB_MIGRATION_AUDIT.md | 31 + kb-lib/Cargo.toml | 4 +- kb-lib/src/executor.rs | 79 +- kb-lib/src/executor/spl.rs | 76 +- kb-lib/src/executor/spl/token.rs | 126 +- kb-lib/src/executor/spl/token/builder.rs | 1214 +++++++++++++++++++ kb-lib/src/executor/spl/token/constants.rs | 15 + kb-lib/src/executor/spl/token/executor.rs | 244 ++++ kb-lib/src/executor/spl/token/intent.rs | 463 +++++++ kb-lib/src/lib.rs | 74 ++ kb-logging/src/tracing_runtime.rs | 3 +- scripts/audit_khadhroony_workspace_rules.py | 12 +- 14 files changed, 2317 insertions(+), 88 deletions(-) create mode 100644 docs/KB_LIB_MIGRATION_AUDIT.md create mode 100644 kb-lib/src/executor/spl/token/builder.rs create mode 100644 kb-lib/src/executor/spl/token/constants.rs create mode 100644 kb-lib/src/executor/spl/token/executor.rs create mode 100644 kb-lib/src/executor/spl/token/intent.rs diff --git a/Cargo.toml b/Cargo.toml index e62abd9..c9e99b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 1 +# version: 2 [workspace] resolver = "3" @@ -27,7 +27,7 @@ publish = false [workspace.dependencies] argon2 = { version = "^0.5", features = ["std", "zeroize"] } async-trait = { version = "^0.1", features = [] } -base64 = { version = "^0.22", features = [] } +base64 = { version = "^0.23", features = [] } bytemuck = { version = "^1.25", features = ["derive"] } borsh_0_10 = { package = "borsh", version = "^0.10" } borsh = { version = "^1.7", features = ["ascii", "bson", "bytes", "default", "derive", "de_strict_order", "borsh-derive", "indexmap", "std", "rc"] } diff --git a/config/example.config.json b/config/example.config.json index 88c927e..bd57546 100644 --- a/config/example.config.json +++ b/config/example.config.json @@ -39,7 +39,7 @@ "kb-lib.executor.spl.memo", "kb_executor_spl_noop", "kb_executor_spl_single_pool", - "kb_executor_spl_token", + "kb-lib.executor.spl.token", "kb-lib.executor.spl.token2022", "kb-logging", "kb-lib.materializer.admin", @@ -576,42 +576,42 @@ ] }, { - "name": "file_kb_executor_spl_token_debug", + "name": "file_kb_lib_executor_spl_token_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/devnet/kb_executor_spl_token/debug.log", + "path": "logs/devnet/kb_lib_executor_spl_token/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_executor_spl_token" + "kb-lib.executor.spl.token" ] }, { - "name": "file_kb_executor_spl_token_info", + "name": "file_kb_lib_executor_spl_token_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/devnet/kb_executor_spl_token/info.log", + "path": "logs/devnet/kb_lib_executor_spl_token/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_executor_spl_token" + "kb-lib.executor.spl.token" ] }, { - "name": "file_kb_executor_spl_token_error", + "name": "file_kb_lib_executor_spl_token_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/devnet/kb_executor_spl_token/error.jsonl", + "path": "logs/devnet/kb_lib_executor_spl_token/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb_executor_spl_token" + "kb-lib.executor.spl.token" ] }, { @@ -1680,7 +1680,7 @@ "kb-lib.executor.spl.memo", "kb_executor_spl_noop", "kb_executor_spl_single_pool", - "kb_executor_spl_token", + "kb-lib.executor.spl.token", "kb-lib.executor.spl.token2022", "kb-logging", "kb-lib.materializer.admin", @@ -2217,42 +2217,42 @@ ] }, { - "name": "file_kb_executor_spl_token_debug", + "name": "file_kb_lib_executor_spl_token_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet_research/kb_executor_spl_token/debug.log", + "path": "logs/mainnet_research/kb_lib_executor_spl_token/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_executor_spl_token" + "kb-lib.executor.spl.token" ] }, { - "name": "file_kb_executor_spl_token_info", + "name": "file_kb_lib_executor_spl_token_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet_research/kb_executor_spl_token/info.log", + "path": "logs/mainnet_research/kb_lib_executor_spl_token/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_executor_spl_token" + "kb-lib.executor.spl.token" ] }, { - "name": "file_kb_executor_spl_token_error", + "name": "file_kb_lib_executor_spl_token_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet_research/kb_executor_spl_token/error.jsonl", + "path": "logs/mainnet_research/kb_lib_executor_spl_token/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb_executor_spl_token" + "kb-lib.executor.spl.token" ] }, { @@ -3521,7 +3521,7 @@ "kb-lib.executor.spl.memo", "kb_executor_spl_noop", "kb_executor_spl_single_pool", - "kb_executor_spl_token", + "kb-lib.executor.spl.token", "kb-lib.executor.spl.token2022", "kb-logging", "kb-lib.materializer.admin", @@ -4058,42 +4058,42 @@ ] }, { - "name": "file_kb_executor_spl_token_debug", + "name": "file_kb_lib_executor_spl_token_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet/kb_executor_spl_token/debug.log", + "path": "logs/mainnet/kb_lib_executor_spl_token/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_executor_spl_token" + "kb-lib.executor.spl.token" ] }, { - "name": "file_kb_executor_spl_token_info", + "name": "file_kb_lib_executor_spl_token_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet/kb_executor_spl_token/info.log", + "path": "logs/mainnet/kb_lib_executor_spl_token/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_executor_spl_token" + "kb-lib.executor.spl.token" ] }, { - "name": "file_kb_executor_spl_token_error", + "name": "file_kb_lib_executor_spl_token_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet/kb_executor_spl_token/error.jsonl", + "path": "logs/mainnet/kb_lib_executor_spl_token/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb_executor_spl_token" + "kb-lib.executor.spl.token" ] }, { diff --git a/docs/KB_LIB_MIGRATION_AUDIT.md b/docs/KB_LIB_MIGRATION_AUDIT.md new file mode 100644 index 0000000..caefd9a --- /dev/null +++ b/docs/KB_LIB_MIGRATION_AUDIT.md @@ -0,0 +1,31 @@ +# Audit de migration de `kb-lib` + +## État après `0.1.0-pre.021` + +L’inventaire des anciennes crates métier de `khadhroony-bot2` a été comparé à l’arborescence consolidée de `khadhroony-bot3/kb-lib`. + +### Implémentations fonctionnelles migrées + +- modèles et contrats publics de décodage, matérialisation et exécution ; +- décodeur Solana Core ; +- décodeurs SPL Memo, Token classique, ATA, Token-2022 et ElGamal Registry ; +- décodeur Metaplex Token Metadata dans l’état intermédiaire atteint par bot2 `0.4.7` ; +- matérialisateurs transaction annotations, token accounts, admin, risk, fees, lifecycle, metadata, staking et compliance audit ; +- exécuteurs Solana Core, SPL Memo v4, SPL Token classique, ATA, Token-2022 et ElGamal Registry ; +- politique de sécurité commune des exécuteurs. + +### Éléments encore à migrer dans `kb-lib` + +Il reste une ancienne crate fonctionnelle : + +- `kb_execution_solana` : assemblage de transactions legacy, durable nonce, validation des comptes nonce, signature, preuves de simulation et contrôle de taille de paquet. + +Cette couche n’est pas un exécuteur de programme. Elle doit être portée sous la famille `executor/solana` avant la migration du transport on-chain. + +### Éléments volontairement réservés + +Les autres modules de décodeurs, matérialisateurs et exécuteurs présents dans bot3 correspondent aux placeholders réservés de bot2. Ils ne contiennent pas d’implémentation fonctionnelle perdue à migrer. + +### Étape suivante + +Migrer `kb_execution_solana`, vérifier ses contrats publics et ses tests, puis clôturer la parité de `kb-lib` avant de passer à la future crate de transport on-chain. diff --git a/kb-lib/Cargo.toml b/kb-lib/Cargo.toml index 17bcb4d..73f379f 100644 --- a/kb-lib/Cargo.toml +++ b/kb-lib/Cargo.toml @@ -1,5 +1,5 @@ # file: kb-lib/Cargo.toml -# version: 12 +# version: 13 [package] name = "kb-lib" @@ -39,6 +39,7 @@ spl-token-group-interface.workspace = true spl-token-metadata-interface.workspace = true spl-token-2022-interface.workspace = true spl-token-confidential-transfer-proof-extraction.workspace = true +spl-token-interface.workspace = true spl-memo-interface.workspace = true tracing.workspace = true ts-rs.workspace = true @@ -48,7 +49,6 @@ wincode.workspace = true solana-ed25519-program.workspace = true solana-secp256k1-program.workspace = true solana-secp256r1-program.workspace = true -spl-token-interface.workspace = true [lints] workspace = true diff --git a/kb-lib/src/executor.rs b/kb-lib/src/executor.rs index 090d92e..7682390 100644 --- a/kb-lib/src/executor.rs +++ b/kb-lib/src/executor.rs @@ -511,6 +511,56 @@ pub use self::spl::EX_SPL_MEMO_ADD_MEMO_OPERATION; pub use self::spl::EX_SPL_MEMO_MAX_MESSAGE_BYTES; /// Exposes the SPL Memo signer bound. pub use self::spl::EX_SPL_MEMO_MAX_SIGNERS; +/// Exposes `EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_APPROVE_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_APPROVE_OPERATION; +/// Exposes `EX_SPL_TOKEN_BATCH_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_BATCH_OPERATION; +/// Exposes `EX_SPL_TOKEN_BURN_CHECKED_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_BURN_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_BURN_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_BURN_OPERATION; +/// Exposes `EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION; +/// Exposes `EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_MINT_TO_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_MINT_TO_OPERATION; +/// Exposes `EX_SPL_TOKEN_REVOKE_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_REVOKE_OPERATION; +/// Exposes `EX_SPL_TOKEN_SET_AUTHORITY_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_SET_AUTHORITY_OPERATION; +/// Exposes `EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES`. +pub use self::spl::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES; +/// Exposes `EX_SPL_TOKEN_SYNC_NATIVE_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_SYNC_NATIVE_OPERATION; +/// Exposes `EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_TRANSFER_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_TRANSFER_OPERATION; +/// Exposes `EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION; +/// Exposes `EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION`. +pub use self::spl::EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION; /// Exposes `EX_SPL_TOKEN2022_AMOUNT_TO_UI_AMOUNT_OPERATION`. pub use self::spl::EX_SPL_TOKEN2022_AMOUNT_TO_UI_AMOUNT_OPERATION; /// Exposes `EX_SPL_TOKEN2022_APPLY_PENDING_CONFIDENTIAL_BALANCE_OPERATION`. @@ -721,6 +771,18 @@ pub use self::spl::ExSplAssociatedTokenAccountExecutor; pub use self::spl::ExSplAssociatedTokenAccountOperation; /// Exposes the exact Token Program used by ATA derivation. pub use self::spl::ExSplAssociatedTokenProgram; +/// Exposes `ExSplClassicTokenAmount`. +pub use self::spl::ExSplClassicTokenAmount; +/// Exposes `ExSplClassicTokenAuthority`. +pub use self::spl::ExSplClassicTokenAuthority; +/// Exposes `ExSplClassicTokenAuthorityType`. +pub use self::spl::ExSplClassicTokenAuthorityType; +/// Exposes `ExSplClassicTokenExecutionIntent`. +pub use self::spl::ExSplClassicTokenExecutionIntent; +/// Exposes `ExSplClassicTokenOperation`. +pub use self::spl::ExSplClassicTokenOperation; +/// Exposes `ExSplClassicTokenSingleOperation`. +pub use self::spl::ExSplClassicTokenSingleOperation; /// Exposes the typed SPL ElGamal registry execution intent. pub use self::spl::ExSplElgamalRegistryExecutionIntent; /// Exposes the typed SPL ElGamal registry executor. @@ -860,6 +922,14 @@ pub(crate) use self::solana::executor_solana_core_validate_seeded_address; pub(crate) use self::solana::executor_solana_core_vote_build_prepared_plan; /// Crate-root access to `build_prepared_plan` from `zk_elgamal`. pub(crate) use self::solana::executor_solana_core_zk_elgamal_build_prepared_plan; +/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS`. +pub(crate) use self::spl::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS`. +pub(crate) use self::spl::EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS`. +pub(crate) use self::spl::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES`. +pub(crate) use self::spl::EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES; /// Maximum ordered multisig signer occurrences for SPL Token-2022. pub(crate) use self::spl::EX_SPL_TOKEN2022_MAX_MULTISIG_SIGNERS; /// Maximum transfer-fee source account count for SPL Token-2022. @@ -872,6 +942,8 @@ pub(crate) use self::spl::TRACING_TARGET_EXECUTOR_SPL_ATA; pub(crate) use self::spl::TRACING_TARGET_EXECUTOR_SPL_ELGAMAL_REGISTRY; /// Canonical SPL Memo tracing target. pub(crate) use self::spl::TRACING_TARGET_EXECUTOR_SPL_MEMO; +/// Crate-root access to `TRACING_TARGET_EXECUTOR_SPL_TOKEN`. +pub(crate) use self::spl::TRACING_TARGET_EXECUTOR_SPL_TOKEN; /// Canonical SPL Token-2022 tracing target. pub(crate) use self::spl::TRACING_TARGET_EXECUTOR_SPL_TOKEN2022; /// Internal SPL ATA plan builder. @@ -880,6 +952,8 @@ pub(crate) use self::spl::executor_spl_ata_build_prepared_plan; pub(crate) use self::spl::executor_spl_elgamal_registry_build_prepared_plan; /// Internal SPL Memo plan builder. pub(crate) use self::spl::executor_spl_memo_build_prepared_plan; +/// Crate-root access to `executor_spl_token_build_prepared_plan`. +pub(crate) use self::spl::executor_spl_token_build_prepared_plan; /// Internal SPL Token-2022 prepared-plan builder. pub(crate) use self::spl::executor_spl_token2022_build_prepared_plan; @@ -887,7 +961,7 @@ pub(crate) use self::spl::executor_spl_token2022_build_prepared_plan; mod reserved_executor_tests { #[test] fn every_reserved_executor_is_registered_and_builds_only_an_empty_plan() { - let executors: [&dyn crate::ExApiInstructionExecutor; 99] = [ + let executors: [&dyn crate::ExApiInstructionExecutor; 98] = [ &crate::ExAdapterSaberDecimalWrapperExecutor, &crate::ExAdminJupiterLockExecutor, &crate::ExAdminPumpFeesExecutor, @@ -969,7 +1043,6 @@ mod reserved_executor_tests { &crate::ExSplNoopExecutor, &crate::ExSplSinglePoolExecutor, &crate::ExSplStakePoolExecutor, - &crate::ExSplTokenExecutor, &crate::ExStableSwapHyloExchangeExecutor, &crate::ExStableSwapJupiterStableExecutor, &crate::ExStableSwapNumeraireExecutor, @@ -988,7 +1061,7 @@ mod reserved_executor_tests { &crate::ExWalletJupiterApeproSmartWalletExecutor, &crate::ExWeightedSwapStabbleExecutor, ]; - assert_eq!(executors.len(), 99); + assert_eq!(executors.len(), 98); for executor in executors { assert!(!executor.program_ids().is_empty()); for program_id in executor.program_ids() { diff --git a/kb-lib/src/executor/spl.rs b/kb-lib/src/executor/spl.rs index 5b9d6e8..eacc138 100644 --- a/kb-lib/src/executor/spl.rs +++ b/kb-lib/src/executor/spl.rs @@ -65,7 +65,69 @@ pub use self::noop::ExSplNoopExecutor; pub use self::single_pool::ExSplSinglePoolExecutor; /// Exposes the reserved `spl/stake_pool` executor. pub use self::stake_pool::ExSplStakePoolExecutor; -/// Exposes the reserved `spl/token` executor. +/// Exposes `EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION`. +pub use self::token::EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION`. +pub use self::token::EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_APPROVE_OPERATION`. +pub use self::token::EX_SPL_TOKEN_APPROVE_OPERATION; +/// Exposes `EX_SPL_TOKEN_BATCH_OPERATION`. +pub use self::token::EX_SPL_TOKEN_BATCH_OPERATION; +/// Exposes `EX_SPL_TOKEN_BURN_CHECKED_OPERATION`. +pub use self::token::EX_SPL_TOKEN_BURN_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_BURN_OPERATION`. +pub use self::token::EX_SPL_TOKEN_BURN_OPERATION; +/// Exposes `EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION`. +pub use self::token::EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION`. +pub use self::token::EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION`. +pub use self::token::EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION`. +pub use self::token::EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION`. +pub use self::token::EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION`. +pub use self::token::EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION`. +pub use self::token::EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION; +/// Exposes `EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION`. +pub use self::token::EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_MINT_TO_OPERATION`. +pub use self::token::EX_SPL_TOKEN_MINT_TO_OPERATION; +/// Exposes `EX_SPL_TOKEN_REVOKE_OPERATION`. +pub use self::token::EX_SPL_TOKEN_REVOKE_OPERATION; +/// Exposes `EX_SPL_TOKEN_SET_AUTHORITY_OPERATION`. +pub use self::token::EX_SPL_TOKEN_SET_AUTHORITY_OPERATION; +/// Exposes `EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES`. +pub use self::token::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES; +/// Exposes `EX_SPL_TOKEN_SYNC_NATIVE_OPERATION`. +pub use self::token::EX_SPL_TOKEN_SYNC_NATIVE_OPERATION; +/// Exposes `EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION`. +pub use self::token::EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION`. +pub use self::token::EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_TRANSFER_OPERATION`. +pub use self::token::EX_SPL_TOKEN_TRANSFER_OPERATION; +/// Exposes `EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION`. +pub use self::token::EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION`. +pub use self::token::EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION; +/// Exposes `EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION`. +pub use self::token::EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION; +/// Exposes `ExSplClassicTokenAmount`. +pub use self::token::ExSplClassicTokenAmount; +/// Exposes `ExSplClassicTokenAuthority`. +pub use self::token::ExSplClassicTokenAuthority; +/// Exposes `ExSplClassicTokenAuthorityType`. +pub use self::token::ExSplClassicTokenAuthorityType; +/// Exposes `ExSplClassicTokenExecutionIntent`. +pub use self::token::ExSplClassicTokenExecutionIntent; +/// Exposes `ExSplClassicTokenOperation`. +pub use self::token::ExSplClassicTokenOperation; +/// Exposes `ExSplClassicTokenSingleOperation`. +pub use self::token::ExSplClassicTokenSingleOperation; +/// Exposes the SPL Token classic executor. pub use self::token::ExSplTokenExecutor; /// Exposes `EX_SPL_TOKEN2022_AMOUNT_TO_UI_AMOUNT_OPERATION`. pub use self::token2022::EX_SPL_TOKEN2022_AMOUNT_TO_UI_AMOUNT_OPERATION; @@ -320,6 +382,18 @@ pub(crate) use self::elgamal_registry::executor_spl_elgamal_registry_build_prepa pub(crate) use self::memo::TRACING_TARGET_EXECUTOR_SPL_MEMO; /// Internal SPL Memo plan builder. pub(crate) use self::memo::executor_spl_memo_build_prepared_plan; +/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS`. +pub(crate) use self::token::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS`. +pub(crate) use self::token::EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS`. +pub(crate) use self::token::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES`. +pub(crate) use self::token::EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES; +/// Crate-root access to `TRACING_TARGET_EXECUTOR_SPL_TOKEN`. +pub(crate) use self::token::TRACING_TARGET_EXECUTOR_SPL_TOKEN; +/// Crate-root access to `executor_spl_token_build_prepared_plan`. +pub(crate) use self::token::executor_spl_token_build_prepared_plan; /// Maximum ordered multisig signer occurrences for SPL Token-2022. pub(crate) use self::token2022::EX_SPL_TOKEN2022_MAX_MULTISIG_SIGNERS; /// Maximum transfer-fee source account count for SPL Token-2022. diff --git a/kb-lib/src/executor/spl/token.rs b/kb-lib/src/executor/spl/token.rs index 7654d42..d947684 100644 --- a/kb-lib/src/executor/spl/token.rs +++ b/kb-lib/src/executor/spl/token.rs @@ -1,49 +1,87 @@ // file: kb-lib/src/executor/spl/token.rs -// version: 2 +// version: 3 -//! Reserved executor for `spl_token`. +//! SPL Token classic executor. -/// Reserved executor for the `spl_token` program surface. -#[derive(Clone, Debug, Default)] -pub struct ExSplTokenExecutor; +mod builder; +mod constants; +mod executor; +mod intent; -impl crate::ExApiInstructionExecutor for crate::ExSplTokenExecutor { - fn executor_name(&self) -> &'static str { - return "kb_executor_spl_token"; - } +/// Exposes `ExSplTokenExecutor`. +pub use self::executor::ExSplTokenExecutor; +/// Exposes `EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_APPROVE_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_APPROVE_OPERATION; +/// Exposes `EX_SPL_TOKEN_BATCH_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_BATCH_OPERATION; +/// Exposes `EX_SPL_TOKEN_BURN_CHECKED_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_BURN_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_BURN_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_BURN_OPERATION; +/// Exposes `EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION; +/// Exposes `EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_MINT_TO_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_MINT_TO_OPERATION; +/// Exposes `EX_SPL_TOKEN_REVOKE_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_REVOKE_OPERATION; +/// Exposes `EX_SPL_TOKEN_SET_AUTHORITY_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_SET_AUTHORITY_OPERATION; +/// Exposes `EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES`. +pub use self::intent::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES; +/// Exposes `EX_SPL_TOKEN_SYNC_NATIVE_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_SYNC_NATIVE_OPERATION; +/// Exposes `EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_TRANSFER_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_TRANSFER_OPERATION; +/// Exposes `EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION; +/// Exposes `EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION`. +pub use self::intent::EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION; +/// Exposes `ExSplClassicTokenAmount`. +pub use self::intent::ExSplClassicTokenAmount; +/// Exposes `ExSplClassicTokenAuthority`. +pub use self::intent::ExSplClassicTokenAuthority; +/// Exposes `ExSplClassicTokenAuthorityType`. +pub use self::intent::ExSplClassicTokenAuthorityType; +/// Exposes `ExSplClassicTokenExecutionIntent`. +pub use self::intent::ExSplClassicTokenExecutionIntent; +/// Exposes `ExSplClassicTokenOperation`. +pub use self::intent::ExSplClassicTokenOperation; +/// Exposes `ExSplClassicTokenSingleOperation`. +pub use self::intent::ExSplClassicTokenSingleOperation; - fn executor_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn program_ids(&self) -> &'static [&'static str] { - return &[kb_program_ids::SPL_TOKEN_PROGRAM_ID]; - } - - fn supports_request( - &self, - request: &crate::ExApiExecutionRequest, - ) -> crate::ExApiExecutionSupport { - if crate::ExApiInstructionExecutor::handles_program_id(self, &request.program_id) { - return crate::ExApiExecutionSupport::Maybe; - } - return crate::ExApiExecutionSupport::No; - } - - fn build_plan( - &self, - _request: &crate::ExApiExecutionRequest, - ) -> kb_core::Result { - let payload_json = match crate::executor_api_serialize_payload_json( - &serde_json::json!({"status":"reserved_executor","surface":"spl_token"}), - ) { - std::result::Result::Ok(serialized) => serialized, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - return std::result::Result::Ok(crate::ExApiExecutionPlan { - executor_name: "kb_executor_spl_token".to_string(), - instruction_count: 0, - payload_json, - }); - } -} +/// Crate-root access to `executor_spl_token_build_prepared_plan`. +pub(crate) use self::builder::executor_spl_token_build_prepared_plan; +/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS`. +pub(crate) use self::constants::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS`. +pub(crate) use self::constants::EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS`. +pub(crate) use self::constants::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES`. +pub(crate) use self::constants::EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES; +/// Crate-root access to `TRACING_TARGET_EXECUTOR_SPL_TOKEN`. +pub(crate) use self::constants::TRACING_TARGET_EXECUTOR_SPL_TOKEN; diff --git a/kb-lib/src/executor/spl/token/builder.rs b/kb-lib/src/executor/spl/token/builder.rs new file mode 100644 index 0000000..05988e8 --- /dev/null +++ b/kb-lib/src/executor/spl/token/builder.rs @@ -0,0 +1,1214 @@ +// file: kb-lib/src/executor/spl/token/builder.rs +// version: 7 + +//! Official classic SPL Token builders and conservative plan validation. + +use std::str::FromStr; // rust-rules: trait-import + +struct ParsedAuthority { + authority: solana_pubkey::Pubkey, + signers: std::vec::Vec, +} + +pub(crate) fn executor_spl_token_build_prepared_plan( + intent: &crate::ExSplClassicTokenExecutionIntent, +) -> kb_core::Result { + if intent.intent_id.trim().is_empty() { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_intent_id_empty", + "SPL Token execution intent id must not be empty", + )); + } + let fee_payer = match parse_pubkey(&intent.fee_payer, "fee_payer") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + match validate_policy(intent) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + let program_id = match solana_pubkey::Pubkey::from_str(kb_program_ids::SPL_TOKEN_PROGRAM_ID) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_program_id_invalid", + error.to_string(), + )); + }, + }; + let instruction = match &intent.operation { + crate::ExSplClassicTokenOperation::Instruction { value } => { + match build_single(&program_id, value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + }, + crate::ExSplClassicTokenOperation::Batch { instructions } => { + match build_batch(&program_id, instructions.as_slice()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + }, + }; + let requested_spend_lamports = match declared_spend_lamports(&intent.operation) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let planned_instruction = planned_instruction(intent.operation.operation_code(), &instruction); + let required_signers = required_signers(&intent.fee_payer, &instruction.accounts); + tracing::debug!( + target: crate::TRACING_TARGET_EXECUTOR_SPL_TOKEN, + action = "executor_spl_token_build_prepared_plan", + intent_id = %intent.intent_id, + operation_code = intent.operation.operation_code(), + program_id = kb_program_ids::SPL_TOKEN_PROGRAM_ID, + account_occurrence_count = instruction.accounts.len(), + required_signer_count = required_signers.len(), + requested_spend_lamports, + dry_run = intent.policy.dry_run, + "built classic SPL Token execution plan" + ); + return std::result::Result::Ok(crate::ExApiPreparedExecutionPlan { + executor_name: std::string::String::from("kb_executor_spl_token"), + executor_version: std::string::String::from(env!("CARGO_PKG_VERSION")), + intent_id: intent.intent_id.clone(), + operation_code: std::string::String::from(intent.operation.operation_code()), + fee_payer: crate::MdPubkey(fee_payer.to_string()), + instructions: vec![planned_instruction], + required_signers, + policy: intent.policy.clone(), + requested_spend_lamports, + requested_compute_unit_price_micro_lamports: std::option::Option::None, + }); +} + +fn validate_policy(intent: &crate::ExSplClassicTokenExecutionIntent) -> kb_core::Result<()> { + if intent.policy.simulation != crate::ExApiExecutionSimulationPolicy::Required { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_simulation_required", + "SPL Token execution requires simulation before signing or sending", + )); + } + match intent.policy.cost_limit.max_fee_lamports { + std::option::Option::Some(limit) if limit > 0 => {}, + std::option::Option::Some(_) | std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_fee_limit_missing", + "SPL Token execution requires a positive transaction fee ceiling", + )); + }, + } + let validation = &intent.policy.post_execution_validation; + if !validation.canonical_insert_required + || !validation.core_extraction_required + || !validation.decode_replay_required + { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_post_validation_required", + "SPL Token execution requires canonical insertion, core extraction and decode replay validation", + )); + } + if intent.operation.requires_materialization() && !validation.materialization_required { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_materialization_validation_required", + "Mutating SPL Token execution requires materialization validation", + )); + } + if has_unbounded_lamport_movement(&intent.operation) && !intent.policy.dry_run { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_stateful_lamport_preflight_required", + "WithdrawExcessLamports and full-balance UnwrapLamports remain simulation-only until a stateful preflight bounds the movable lamports", + )); + } + return std::result::Result::Ok(()); +} + +macro_rules! parse_key { + ($value:expr, $field:expr) => { + match parse_pubkey($value, $field) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + }; +} + +macro_rules! parse_raw_amount { + ($value:expr, $field:expr) => { + match parse_amount($value, $field) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + }; +} + +macro_rules! parse_token_authority { + ($value:expr) => { + match parse_authority($value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + }; +} + +fn build_single( + program_id: &solana_pubkey::Pubkey, + operation: &crate::ExSplClassicTokenSingleOperation, +) -> kb_core::Result { + return match operation { + crate::ExSplClassicTokenSingleOperation::InitializeMint { + mint, + mint_authority, + freeze_authority, + decimals, + } => { + let mint = parse_key!(mint, "mint"); + let mint_authority = parse_key!(mint_authority, "mint_authority"); + let freeze_authority = match freeze_authority { + std::option::Option::Some(value) => { + std::option::Option::Some(parse_key!(value, "freeze_authority")) + }, + std::option::Option::None => std::option::Option::None, + }; + official_instruction(spl_token_interface::instruction::initialize_mint2( + program_id, + &mint, + &mint_authority, + freeze_authority.as_ref(), + *decimals, + )) + }, + crate::ExSplClassicTokenSingleOperation::InitializeAccount { account, mint, owner } => { + let account = parse_key!(account, "account"); + let mint = parse_key!(mint, "mint"); + let owner = parse_key!(owner, "owner"); + official_instruction(spl_token_interface::instruction::initialize_account3( + program_id, &account, &mint, &owner, + )) + }, + crate::ExSplClassicTokenSingleOperation::InitializeMultisig { + multisig, + members, + threshold, + } => { + if members.is_empty() + || members.len() > crate::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS + || *threshold == 0 + || usize::from(*threshold) > members.len() + { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_multisig_threshold_invalid", + format!( + "SPL Token multisig requires 1..={} members and threshold 1..=N; received M={} N={}", + crate::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS, + threshold, + members.len() + ), + )); + } + let multisig = parse_key!(multisig, "multisig"); + let mut parsed_members = std::vec::Vec::with_capacity(members.len()); + for member in members { + parsed_members.push(parse_key!(member, "multisig_member")); + } + let member_refs = parsed_members.iter().collect::>(); + official_instruction(spl_token_interface::instruction::initialize_multisig2( + program_id, + &multisig, + member_refs.as_slice(), + *threshold, + )) + }, + crate::ExSplClassicTokenSingleOperation::Transfer { + source, + destination, + authority, + amount, + } => { + let source = parse_key!(source, "source"); + let destination = parse_key!(destination, "destination"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + let amount = parse_raw_amount!(amount, "amount"); + official_instruction(spl_token_interface::instruction::transfer( + program_id, + &source, + &destination, + &authority.authority, + signer_refs.as_slice(), + amount, + )) + }, + crate::ExSplClassicTokenSingleOperation::Approve { + source, + delegate, + authority, + amount, + } => { + let source = parse_key!(source, "source"); + let delegate = parse_key!(delegate, "delegate"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + let amount = parse_raw_amount!(amount, "amount"); + official_instruction(spl_token_interface::instruction::approve( + program_id, + &source, + &delegate, + &authority.authority, + signer_refs.as_slice(), + amount, + )) + }, + crate::ExSplClassicTokenSingleOperation::Revoke { source, authority } => { + let source = parse_key!(source, "source"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + official_instruction(spl_token_interface::instruction::revoke( + program_id, + &source, + &authority.authority, + signer_refs.as_slice(), + )) + }, + crate::ExSplClassicTokenSingleOperation::SetAuthority { + owned, + authority_type, + new_authority, + current_authority, + } => { + let owned = parse_key!(owned, "owned"); + let new_authority = match new_authority { + std::option::Option::Some(value) => { + std::option::Option::Some(parse_key!(value, "new_authority")) + }, + std::option::Option::None => std::option::Option::None, + }; + let current_authority = parse_token_authority!(current_authority); + let signer_refs = current_authority.signers.iter().collect::>(); + let authority_type = match authority_type { + crate::ExSplClassicTokenAuthorityType::MintTokens => { + spl_token_interface::instruction::AuthorityType::MintTokens + }, + crate::ExSplClassicTokenAuthorityType::FreezeAccount => { + spl_token_interface::instruction::AuthorityType::FreezeAccount + }, + crate::ExSplClassicTokenAuthorityType::AccountOwner => { + spl_token_interface::instruction::AuthorityType::AccountOwner + }, + crate::ExSplClassicTokenAuthorityType::CloseAccount => { + spl_token_interface::instruction::AuthorityType::CloseAccount + }, + }; + official_instruction(spl_token_interface::instruction::set_authority( + program_id, + &owned, + new_authority.as_ref(), + authority_type, + ¤t_authority.authority, + signer_refs.as_slice(), + )) + }, + crate::ExSplClassicTokenSingleOperation::MintTo { + mint, + destination, + authority, + amount, + } => { + let mint = parse_key!(mint, "mint"); + let destination = parse_key!(destination, "destination"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + let amount = parse_raw_amount!(amount, "amount"); + official_instruction(spl_token_interface::instruction::mint_to( + program_id, + &mint, + &destination, + &authority.authority, + signer_refs.as_slice(), + amount, + )) + }, + crate::ExSplClassicTokenSingleOperation::Burn { source, mint, authority, amount } => { + let source = parse_key!(source, "source"); + let mint = parse_key!(mint, "mint"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + let amount = parse_raw_amount!(amount, "amount"); + official_instruction(spl_token_interface::instruction::burn( + program_id, + &source, + &mint, + &authority.authority, + signer_refs.as_slice(), + amount, + )) + }, + crate::ExSplClassicTokenSingleOperation::CloseAccount { + account, + destination, + authority, + } => { + let account = parse_key!(account, "account"); + let destination = parse_key!(destination, "destination"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + official_instruction(spl_token_interface::instruction::close_account( + program_id, + &account, + &destination, + &authority.authority, + signer_refs.as_slice(), + )) + }, + crate::ExSplClassicTokenSingleOperation::FreezeAccount { account, mint, authority } => { + let account = parse_key!(account, "account"); + let mint = parse_key!(mint, "mint"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + official_instruction(spl_token_interface::instruction::freeze_account( + program_id, + &account, + &mint, + &authority.authority, + signer_refs.as_slice(), + )) + }, + crate::ExSplClassicTokenSingleOperation::ThawAccount { account, mint, authority } => { + let account = parse_key!(account, "account"); + let mint = parse_key!(mint, "mint"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + official_instruction(spl_token_interface::instruction::thaw_account( + program_id, + &account, + &mint, + &authority.authority, + signer_refs.as_slice(), + )) + }, + crate::ExSplClassicTokenSingleOperation::TransferChecked { + source, + mint, + destination, + authority, + amount, + decimals, + } => { + let source = parse_key!(source, "source"); + let mint = parse_key!(mint, "mint"); + let destination = parse_key!(destination, "destination"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + let amount = parse_raw_amount!(amount, "amount"); + official_instruction(spl_token_interface::instruction::transfer_checked( + program_id, + &source, + &mint, + &destination, + &authority.authority, + signer_refs.as_slice(), + amount, + *decimals, + )) + }, + crate::ExSplClassicTokenSingleOperation::ApproveChecked { + source, + mint, + delegate, + authority, + amount, + decimals, + } => { + let source = parse_key!(source, "source"); + let mint = parse_key!(mint, "mint"); + let delegate = parse_key!(delegate, "delegate"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + let amount = parse_raw_amount!(amount, "amount"); + official_instruction(spl_token_interface::instruction::approve_checked( + program_id, + &source, + &mint, + &delegate, + &authority.authority, + signer_refs.as_slice(), + amount, + *decimals, + )) + }, + crate::ExSplClassicTokenSingleOperation::MintToChecked { + mint, + destination, + authority, + amount, + decimals, + } => { + let mint = parse_key!(mint, "mint"); + let destination = parse_key!(destination, "destination"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + let amount = parse_raw_amount!(amount, "amount"); + official_instruction(spl_token_interface::instruction::mint_to_checked( + program_id, + &mint, + &destination, + &authority.authority, + signer_refs.as_slice(), + amount, + *decimals, + )) + }, + crate::ExSplClassicTokenSingleOperation::BurnChecked { + source, + mint, + authority, + amount, + decimals, + } => { + let source = parse_key!(source, "source"); + let mint = parse_key!(mint, "mint"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + let amount = parse_raw_amount!(amount, "amount"); + official_instruction(spl_token_interface::instruction::burn_checked( + program_id, + &source, + &mint, + &authority.authority, + signer_refs.as_slice(), + amount, + *decimals, + )) + }, + crate::ExSplClassicTokenSingleOperation::SyncNative { account, include_rent_sysvar } => { + let account = parse_key!(account, "account"); + if *include_rent_sysvar { + official_instruction( + spl_token_interface::instruction::sync_native_with_rent_sysvar( + program_id, &account, + ), + ) + } else { + official_instruction(spl_token_interface::instruction::sync_native( + program_id, &account, + )) + } + }, + crate::ExSplClassicTokenSingleOperation::GetAccountDataSize { mint } => { + let mint = parse_key!(mint, "mint"); + official_instruction(spl_token_interface::instruction::get_account_data_size( + program_id, &mint, + )) + }, + crate::ExSplClassicTokenSingleOperation::InitializeImmutableOwner { account } => { + let account = parse_key!(account, "account"); + official_instruction(spl_token_interface::instruction::initialize_immutable_owner( + program_id, &account, + )) + }, + crate::ExSplClassicTokenSingleOperation::AmountToUiAmount { mint, amount } => { + let mint = parse_key!(mint, "mint"); + let amount = parse_raw_amount!(amount, "amount"); + official_instruction(spl_token_interface::instruction::amount_to_ui_amount( + program_id, &mint, amount, + )) + }, + crate::ExSplClassicTokenSingleOperation::UiAmountToAmount { mint, ui_amount } => { + if ui_amount.len() > crate::EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_ui_amount_too_large", + format!( + "SPL Token UI amount length {} exceeds {} bytes", + ui_amount.len(), + crate::EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES + ), + )); + } + let mint = parse_key!(mint, "mint"); + official_instruction(spl_token_interface::instruction::ui_amount_to_amount( + program_id, + &mint, + ui_amount.as_str(), + )) + }, + crate::ExSplClassicTokenSingleOperation::WithdrawExcessLamports { + account, + destination, + authority, + } => { + let account = parse_key!(account, "account"); + let destination = parse_key!(destination, "destination"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + official_instruction(spl_token_interface::instruction::withdraw_excess_lamports( + program_id, + &account, + &destination, + &authority.authority, + signer_refs.as_slice(), + )) + }, + crate::ExSplClassicTokenSingleOperation::UnwrapLamports { + account, + destination, + authority, + amount_lamports, + } => { + let account = parse_key!(account, "account"); + let destination = parse_key!(destination, "destination"); + let authority = parse_token_authority!(authority); + let signer_refs = authority.signers.iter().collect::>(); + let amount_lamports = match amount_lamports { + std::option::Option::Some(value) => { + std::option::Option::Some(parse_raw_amount!(value, "amount_lamports")) + }, + std::option::Option::None => std::option::Option::None, + }; + official_instruction(spl_token_interface::instruction::unwrap_lamports( + program_id, + &account, + &destination, + &authority.authority, + signer_refs.as_slice(), + amount_lamports, + )) + }, + }; +} + +fn build_batch( + program_id: &solana_pubkey::Pubkey, + operations: &[crate::ExSplClassicTokenSingleOperation], +) -> kb_core::Result { + if operations.len() > crate::EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_batch_instruction_limit_exceeded", + format!( + "SPL Token Batch child count {} exceeds {}", + operations.len(), + crate::EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS + ), + )); + } + let mut instructions = std::vec::Vec::with_capacity(operations.len()); + let mut account_count = 0_usize; + for operation in operations { + let instruction = match build_single(program_id, operation) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if instruction.data.len() > usize::from(u8::MAX) { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_batch_child_data_too_large", + format!( + "SPL Token Batch child {} has {} data bytes; maximum is {}", + instructions.len(), + instruction.data.len(), + u8::MAX + ), + )); + } + account_count = match account_count.checked_add(instruction.accounts.len()) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_batch_account_count_overflow", + "SPL Token Batch aggregate account count overflowed usize", + )); + }, + }; + if account_count > crate::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_batch_account_limit_exceeded", + format!( + "SPL Token Batch account occurrence count {account_count} exceeds {}", + crate::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS + ), + )); + } + instructions.push(instruction); + } + return official_instruction(spl_token_interface::instruction::batch( + program_id, + instructions.as_slice(), + )); +} + +fn official_instruction( + result: std::result::Result, +) -> kb_core::Result +where + BuilderError: std::fmt::Display, +{ + return match result { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_official_builder_failed", + error.to_string(), + )), + }; +} + +fn parse_pubkey(pubkey: &crate::MdPubkey, field: &str) -> kb_core::Result { + if pubkey.0.trim().is_empty() { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_pubkey_empty", + format!("SPL Token {field} public key must not be empty"), + )); + } + return match solana_pubkey::Pubkey::from_str(pubkey.0.as_str()) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_pubkey_invalid", + format!("invalid SPL Token {field} public key: {error}"), + )), + }; +} + +fn parse_amount(amount: &crate::ExSplClassicTokenAmount, field: &str) -> kb_core::Result { + if amount.0.is_empty() || !amount.0.bytes().all(|value| return value.is_ascii_digit()) { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_amount_invalid", + format!("SPL Token {field} must be an unsigned decimal u64 string"), + )); + } + return match amount.0.parse::() { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_amount_invalid", + format!("invalid SPL Token {field}: {error}"), + )), + }; +} + +fn parse_authority( + authority: &crate::ExSplClassicTokenAuthority, +) -> kb_core::Result { + if authority.multisig_signers.len() > crate::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_multisig_signer_limit_exceeded", + format!( + "SPL Token multisig signer occurrence count {} exceeds {}", + authority.multisig_signers.len(), + crate::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS + ), + )); + } + let parsed_authority = match parse_pubkey(&authority.authority, "authority") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut signers = std::vec::Vec::with_capacity(authority.multisig_signers.len()); + for signer in &authority.multisig_signers { + let signer = match parse_pubkey(signer, "multisig_signer") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + signers.push(signer); + } + return std::result::Result::Ok(ParsedAuthority { authority: parsed_authority, signers }); +} + +fn declared_spend_lamports(operation: &crate::ExSplClassicTokenOperation) -> kb_core::Result { + return match operation { + crate::ExSplClassicTokenOperation::Instruction { value } => single_spend_lamports(value), + crate::ExSplClassicTokenOperation::Batch { instructions } => { + let mut total = 0_u64; + for instruction in instructions { + let amount = match single_spend_lamports(instruction) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + total = match total.checked_add(amount) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_lamport_spend_overflow", + "SPL Token Batch declared lamport spend exceeds u64", + )); + }, + }; + } + std::result::Result::Ok(total) + }, + }; +} + +fn single_spend_lamports( + operation: &crate::ExSplClassicTokenSingleOperation, +) -> kb_core::Result { + return match operation { + crate::ExSplClassicTokenSingleOperation::UnwrapLamports { + amount_lamports: std::option::Option::Some(value), + .. + } => parse_amount(value, "amount_lamports"), + _ => std::result::Result::Ok(0), + }; +} + +fn has_unbounded_lamport_movement(operation: &crate::ExSplClassicTokenOperation) -> bool { + return match operation { + crate::ExSplClassicTokenOperation::Instruction { value } => { + single_has_unbounded_lamport_movement(value) + }, + crate::ExSplClassicTokenOperation::Batch { instructions } => { + instructions.iter().any(single_has_unbounded_lamport_movement) + }, + }; +} + +fn single_has_unbounded_lamport_movement( + operation: &crate::ExSplClassicTokenSingleOperation, +) -> bool { + return matches!( + operation, + crate::ExSplClassicTokenSingleOperation::WithdrawExcessLamports { .. } + | crate::ExSplClassicTokenSingleOperation::UnwrapLamports { + amount_lamports: std::option::Option::None, + .. + } + ); +} + +fn planned_instruction( + operation_code: &str, + instruction: &solana_instruction::Instruction, +) -> crate::ExApiPlannedInstruction { + return crate::ExApiPlannedInstruction { + program_id: crate::MdProgramId(instruction.program_id.to_string()), + operation_code: std::string::String::from(operation_code), + accounts: instruction + .accounts + .iter() + .map(|account| { + return crate::ExApiPlannedAccount { + pubkey: crate::MdPubkey(account.pubkey.to_string()), + is_signer: account.is_signer, + is_writable: account.is_writable, + }; + }) + .collect(), + data: instruction.data.clone(), + }; +} + +fn required_signers( + fee_payer: &crate::MdPubkey, + accounts: &[solana_instruction::AccountMeta], +) -> std::vec::Vec { + let mut required = vec![crate::ExApiRequiredSigner { + pubkey: fee_payer.clone(), + role: std::string::String::from("fee_payer"), + }]; + for account in accounts { + if !account.is_signer { + continue; + } + let pubkey = crate::MdPubkey(account.pubkey.to_string()); + if required.iter().any(|value| return value.pubkey == pubkey) { + continue; + } + required.push(crate::ExApiRequiredSigner { + pubkey, + role: std::string::String::from("token_authority_or_multisig_signer"), + }); + } + return required; +} + +#[cfg(test)] +pub(crate) mod tests { + fn pubkey(value: &str) -> crate::MdPubkey { + return crate::MdPubkey(std::string::String::from(value)); + } + + fn policy(signers: &[&str], materialization_required: bool) -> crate::ExApiExecutionPolicy { + return crate::ExApiExecutionPolicy { + cost_limit: crate::ExApiExecutionCostLimit { + max_spend_lamports: std::option::Option::Some(u64::MAX), + max_fee_lamports: std::option::Option::Some(10_000), + max_compute_unit_price_micro_lamports: std::option::Option::None, + }, + authorized_signers: signers.iter().map(|value| return pubkey(value)).collect(), + post_execution_validation: crate::ExApiPostExecutionValidationPolicy { + canonical_insert_required: true, + core_extraction_required: true, + decode_replay_required: true, + materialization_required, + }, + ..crate::ExApiExecutionPolicy::default() + }; + } + + pub(crate) fn intent( + operation: crate::ExSplClassicTokenOperation, + ) -> crate::ExSplClassicTokenExecutionIntent { + let fee_payer = kb_program_ids::SYSTEM_PROGRAM_ID; + return crate::ExSplClassicTokenExecutionIntent { + intent_id: std::string::String::from("token-intent-1"), + fee_payer: pubkey(fee_payer), + policy: policy( + &[fee_payer, kb_program_ids::VOTE_PROGRAM_ID, kb_program_ids::STAKE_PROGRAM_ID], + operation.requires_materialization(), + ), + operation, + }; + } + + fn authority(signers: &[&str]) -> crate::ExSplClassicTokenAuthority { + return crate::ExSplClassicTokenAuthority { + authority: pubkey(kb_program_ids::CONFIG_PROGRAM_ID), + multisig_signers: signers.iter().map(|value| return pubkey(value)).collect(), + }; + } + + fn transfer_checked() -> crate::ExSplClassicTokenSingleOperation { + return crate::ExSplClassicTokenSingleOperation::TransferChecked { + source: pubkey(kb_program_ids::SYSTEM_PROGRAM_ID), + mint: pubkey(kb_program_ids::CONFIG_PROGRAM_ID), + destination: pubkey(kb_program_ids::STAKE_PROGRAM_ID), + authority: authority(&[kb_program_ids::VOTE_PROGRAM_ID]), + amount: crate::ExSplClassicTokenAmount(std::string::String::from( + "18446744073709551615", + )), + decimals: 9, + }; + } + + fn all_single_operations() -> std::vec::Vec<(u8, crate::ExSplClassicTokenSingleOperation)> { + let first = pubkey(kb_program_ids::SYSTEM_PROGRAM_ID); + let second = pubkey(kb_program_ids::CONFIG_PROGRAM_ID); + let third = pubkey(kb_program_ids::STAKE_PROGRAM_ID); + let authority = authority(&[]); + let amount = crate::ExSplClassicTokenAmount(std::string::String::from("42")); + return vec![ + ( + 20, + crate::ExSplClassicTokenSingleOperation::InitializeMint { + mint: first.clone(), + mint_authority: second.clone(), + freeze_authority: std::option::Option::Some(third.clone()), + decimals: 9, + }, + ), + ( + 18, + crate::ExSplClassicTokenSingleOperation::InitializeAccount { + account: first.clone(), + mint: second.clone(), + owner: third.clone(), + }, + ), + ( + 19, + crate::ExSplClassicTokenSingleOperation::InitializeMultisig { + multisig: first.clone(), + members: vec![second.clone(), third.clone()], + threshold: 1, + }, + ), + ( + 3, + crate::ExSplClassicTokenSingleOperation::Transfer { + source: first.clone(), + destination: second.clone(), + authority: authority.clone(), + amount: amount.clone(), + }, + ), + ( + 4, + crate::ExSplClassicTokenSingleOperation::Approve { + source: first.clone(), + delegate: second.clone(), + authority: authority.clone(), + amount: amount.clone(), + }, + ), + ( + 5, + crate::ExSplClassicTokenSingleOperation::Revoke { + source: first.clone(), + authority: authority.clone(), + }, + ), + ( + 6, + crate::ExSplClassicTokenSingleOperation::SetAuthority { + owned: first.clone(), + authority_type: crate::ExSplClassicTokenAuthorityType::CloseAccount, + new_authority: std::option::Option::Some(second.clone()), + current_authority: authority.clone(), + }, + ), + ( + 7, + crate::ExSplClassicTokenSingleOperation::MintTo { + mint: first.clone(), + destination: second.clone(), + authority: authority.clone(), + amount: amount.clone(), + }, + ), + ( + 8, + crate::ExSplClassicTokenSingleOperation::Burn { + source: first.clone(), + mint: second.clone(), + authority: authority.clone(), + amount: amount.clone(), + }, + ), + ( + 9, + crate::ExSplClassicTokenSingleOperation::CloseAccount { + account: first.clone(), + destination: second.clone(), + authority: authority.clone(), + }, + ), + ( + 10, + crate::ExSplClassicTokenSingleOperation::FreezeAccount { + account: first.clone(), + mint: second.clone(), + authority: authority.clone(), + }, + ), + ( + 11, + crate::ExSplClassicTokenSingleOperation::ThawAccount { + account: first.clone(), + mint: second.clone(), + authority: authority.clone(), + }, + ), + (12, transfer_checked()), + ( + 13, + crate::ExSplClassicTokenSingleOperation::ApproveChecked { + source: first.clone(), + mint: second.clone(), + delegate: third.clone(), + authority: authority.clone(), + amount: amount.clone(), + decimals: 9, + }, + ), + ( + 14, + crate::ExSplClassicTokenSingleOperation::MintToChecked { + mint: first.clone(), + destination: second.clone(), + authority: authority.clone(), + amount: amount.clone(), + decimals: 9, + }, + ), + ( + 15, + crate::ExSplClassicTokenSingleOperation::BurnChecked { + source: first.clone(), + mint: second.clone(), + authority: authority.clone(), + amount: amount.clone(), + decimals: 9, + }, + ), + ( + 17, + crate::ExSplClassicTokenSingleOperation::SyncNative { + account: first.clone(), + include_rent_sysvar: true, + }, + ), + ( + 21, + crate::ExSplClassicTokenSingleOperation::GetAccountDataSize { mint: first.clone() }, + ), + ( + 22, + crate::ExSplClassicTokenSingleOperation::InitializeImmutableOwner { + account: first.clone(), + }, + ), + ( + 23, + crate::ExSplClassicTokenSingleOperation::AmountToUiAmount { + mint: first.clone(), + amount: amount.clone(), + }, + ), + ( + 24, + crate::ExSplClassicTokenSingleOperation::UiAmountToAmount { + mint: first.clone(), + ui_amount: std::string::String::from("0.000000042"), + }, + ), + ( + 38, + crate::ExSplClassicTokenSingleOperation::WithdrawExcessLamports { + account: first.clone(), + destination: second.clone(), + authority: authority.clone(), + }, + ), + ( + 45, + crate::ExSplClassicTokenSingleOperation::UnwrapLamports { + account: first, + destination: second, + authority, + amount_lamports: std::option::Option::Some(amount), + }, + ), + ]; + } + + #[test] + fn every_current_or_recent_single_operation_uses_its_published_tag() { + let operations = all_single_operations(); + assert_eq!(operations.len() + 1, crate::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES.len()); + for (tag, operation) in operations { + let intent = + intent(crate::ExSplClassicTokenOperation::Instruction { value: operation }); + let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan( + &crate::ExSplTokenExecutor, + &intent, + ) + .unwrap_or_else(|error| panic!("tag {tag} plan failed: {error}")); + assert_eq!(plan.instructions[0].data[0], tag); + } + } + + #[test] + fn checked_transfer_matches_official_builder_and_preserves_u64_max() { + let operation = transfer_checked(); + let intent = intent(crate::ExSplClassicTokenOperation::Instruction { value: operation }); + let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan( + &crate::ExSplTokenExecutor, + &intent, + ) + .unwrap_or_else(|error| panic!("checked transfer plan failed: {error}")); + assert_eq!(plan.instructions.len(), 1); + assert_eq!(plan.instructions[0].data[0], 12); + assert_eq!(&plan.instructions[0].data[1..9], &u64::MAX.to_le_bytes()); + assert_eq!(plan.instructions[0].data[9], 9); + assert_eq!(plan.required_signers.len(), 2); + } + + #[test] + fn multisig_meta_order_is_preserved_while_transaction_signers_are_unique() { + let first = kb_program_ids::VOTE_PROGRAM_ID; + let second = kb_program_ids::STAKE_PROGRAM_ID; + let operation = crate::ExSplClassicTokenSingleOperation::Transfer { + source: pubkey(kb_program_ids::SYSTEM_PROGRAM_ID), + destination: pubkey(kb_program_ids::CONFIG_PROGRAM_ID), + authority: authority(&[first, second, first]), + amount: crate::ExSplClassicTokenAmount(std::string::String::from("1")), + }; + let intent = intent(crate::ExSplClassicTokenOperation::Instruction { value: operation }); + let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan( + &crate::ExSplTokenExecutor, + &intent, + ) + .unwrap_or_else(|error| panic!("multisig transfer plan failed: {error}")); + let accounts = &plan.instructions[0].accounts; + assert_eq!(accounts[3].pubkey.0, first); + assert_eq!(accounts[4].pubkey.0, second); + assert_eq!(accounts[5].pubkey.0, first); + assert_eq!(plan.required_signers.len(), 3); + } + + #[test] + fn recent_unwrap_and_batch_match_published_wire() { + let unwrap = crate::ExSplClassicTokenSingleOperation::UnwrapLamports { + account: pubkey(kb_program_ids::SYSTEM_PROGRAM_ID), + destination: pubkey(kb_program_ids::STAKE_PROGRAM_ID), + authority: authority(&[]), + amount_lamports: std::option::Option::Some(crate::ExSplClassicTokenAmount( + std::string::String::from("42"), + )), + }; + let intent = intent(crate::ExSplClassicTokenOperation::Batch { + instructions: vec![unwrap, transfer_checked()], + }); + let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan( + &crate::ExSplTokenExecutor, + &intent, + ) + .unwrap_or_else(|error| panic!("recent Batch plan failed: {error}")); + assert_eq!(plan.instructions[0].data[0], 255); + assert_eq!(plan.instructions[0].data[2], 10); + assert_eq!(plan.instructions[0].data[3], 45); + assert_eq!(plan.requested_spend_lamports, 42); + } + + #[test] + fn invalid_amount_multisig_and_optional_simulation_fail_closed() { + let mut operation = transfer_checked(); + if let crate::ExSplClassicTokenSingleOperation::TransferChecked { amount, .. } = + &mut operation + { + amount.0 = std::string::String::from("-1"); + } + let pintent = intent(crate::ExSplClassicTokenOperation::Instruction { value: operation }); + let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan( + &crate::ExSplTokenExecutor, + &pintent, + ) + .expect_err("negative amount must fail"); + assert_eq!(error.code(), "execution_spl_token_amount_invalid"); + + let mut intent = + intent(crate::ExSplClassicTokenOperation::Instruction { value: transfer_checked() }); + intent.policy.simulation = crate::ExApiExecutionSimulationPolicy::Optional; + let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan( + &crate::ExSplTokenExecutor, + &intent, + ) + .expect_err("optional simulation must fail"); + assert_eq!(error.code(), "execution_spl_token_simulation_required"); + } + + #[test] + fn every_cluster_is_constructible_and_mainnet_remains_governed_by_common_safety() { + for cluster in [ + crate::ExApiExecutionCluster::Localnet, + crate::ExApiExecutionCluster::Devnet, + crate::ExApiExecutionCluster::Testnet, + crate::ExApiExecutionCluster::Mainnet, + ] { + let mut intent = intent(crate::ExSplClassicTokenOperation::Instruction { + value: transfer_checked(), + }); + intent.policy.cluster.expected_cluster = cluster; + let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan( + &crate::ExSplTokenExecutor, + &intent, + ) + .unwrap_or_else(|error| panic!("cluster plan failed: {error}")); + assert_eq!(plan.policy.cluster.expected_cluster, cluster); + let evaluation = crate::ExSafetyChecker + .evaluate_prepared_plan(&plan) + .unwrap_or_else(|error| panic!("safety evaluation failed: {error}")); + if cluster == crate::ExApiExecutionCluster::Mainnet { + assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny); + assert!(evaluation.violations.iter().any(|violation| { + return violation.code == "execution_mainnet_disabled"; + })); + let mut explicitly_enabled = plan.clone(); + explicitly_enabled.policy.cluster.allow_mainnet = true; + explicitly_enabled.policy.cluster.mainnet_confirmation = true; + let enabled_evaluation = crate::ExSafetyChecker + .evaluate_prepared_plan(&explicitly_enabled) + .unwrap_or_else(|error| { + panic!("enabled Mainnet safety evaluation failed: {error}") + }); + assert_eq!(enabled_evaluation.decision, crate::ExSafetyDecision::Allow); + } else { + assert_eq!(evaluation.decision, crate::ExSafetyDecision::Allow); + } + } + } +} diff --git a/kb-lib/src/executor/spl/token/constants.rs b/kb-lib/src/executor/spl/token/constants.rs new file mode 100644 index 0000000..a40b298 --- /dev/null +++ b/kb-lib/src/executor/spl/token/constants.rs @@ -0,0 +1,15 @@ +// file: kb-lib/src/executor/spl/token/constants.rs +// version: 3 + +//! Local constants for the SPL Token classic executor. Program identifiers live in `kb_program_ids`. + +/// Maximum number of children accepted by one Batch plan. +pub(crate) const EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS: usize = 64; +/// Maximum aggregate account-meta occurrences accepted by one Batch plan. +pub(crate) const EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS: usize = 512; +/// Maximum number of ordered multisig signer occurrences. +pub(crate) const EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS: usize = 11; +/// Maximum UI amount string length accepted before transaction assembly. +pub(crate) const EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES: usize = 255; +/// Canonical tracing target for this crate. +pub(crate) const TRACING_TARGET_EXECUTOR_SPL_TOKEN: &str = "kb-lib.executor.spl.token"; diff --git a/kb-lib/src/executor/spl/token/executor.rs b/kb-lib/src/executor/spl/token/executor.rs new file mode 100644 index 0000000..ead8031 --- /dev/null +++ b/kb-lib/src/executor/spl/token/executor.rs @@ -0,0 +1,244 @@ +// file: kb-lib/src/executor/spl/token/executor.rs +// version: 6 + +//! Exact classic SPL Token capability dispatch and typed plan construction. + +/// Classic SPL Token executor implementation. +#[derive(Clone, Debug, Default)] +pub struct ExSplTokenExecutor; + +impl crate::ExSplTokenExecutor { + fn exact_capability( + &self, + program_id: &crate::MdProgramId, + operation_code: &str, + ) -> crate::ExApiExecutionCapability { + if program_id.0 != kb_program_ids::SPL_TOKEN_PROGRAM_ID { + return crate::ExApiExecutionCapability::unsupported( + "execution_spl_token_program_not_owned", + format!("program {} is not owned by kb_executor_spl_token", program_id.0), + ); + } + if crate::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES.contains(&operation_code) { + return crate::ExApiExecutionCapability::supported(operation_code); + } + if matches!( + operation_code, + "spl_token.initialize_mint_with_rent" + | "spl_token.initialize_account_with_rent" + | "spl_token.initialize_multisig_with_rent" + | "spl_token.initialize_account2" + ) { + return crate::ExApiExecutionCapability::unsupported( + "execution_spl_token_historical_variant_decode_only", + format!( + "SPL Token operation {operation_code} is an obsolete historical initialization variant; use the current no-Rent operation" + ), + ); + } + return crate::ExApiExecutionCapability::unsupported( + "execution_spl_token_operation_unsupported", + format!("SPL Token operation {operation_code} is not implemented"), + ); + } +} + +impl crate::ExApiTypedInstructionExecutor for crate::ExSplTokenExecutor { + type Intent = crate::ExSplClassicTokenExecutionIntent; + + fn capability( + &self, + program_id: &crate::MdProgramId, + operation_code: &str, + ) -> crate::ExApiExecutionCapability { + return self.exact_capability(program_id, operation_code); + } + + fn build_prepared_plan( + &self, + intent: &Self::Intent, + ) -> kb_core::Result { + let program_id = + crate::MdProgramId(std::string::String::from(kb_program_ids::SPL_TOKEN_PROGRAM_ID)); + return match self.exact_capability(&program_id, intent.operation.operation_code()) { + crate::ExApiExecutionCapability::Supported { operation_code: _ } => { + crate::executor_spl_token_build_prepared_plan(intent) + }, + crate::ExApiExecutionCapability::Unsupported { reason_code, reason } => { + std::result::Result::Err(kb_core::Error::new(reason_code, reason)) + }, + }; + } +} + +impl crate::ExApiInstructionExecutor for crate::ExSplTokenExecutor { + fn executor_name(&self) -> &'static str { + return "kb_executor_spl_token"; + } + + fn executor_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn program_ids(&self) -> &'static [&'static str] { + return &[kb_program_ids::SPL_TOKEN_PROGRAM_ID]; + } + + fn supports_request( + &self, + request: &crate::ExApiExecutionRequest, + ) -> crate::ExApiExecutionSupport { + return match self.exact_capability(&request.program_id, &request.operation_code) { + crate::ExApiExecutionCapability::Supported { operation_code: _ } => { + crate::ExApiExecutionSupport::Yes + }, + crate::ExApiExecutionCapability::Unsupported { reason_code: _, reason: _ } => { + crate::ExApiExecutionSupport::No + }, + }; + } + + fn build_plan( + &self, + request: &crate::ExApiExecutionRequest, + ) -> kb_core::Result { + match self.exact_capability(&request.program_id, &request.operation_code) { + crate::ExApiExecutionCapability::Supported { operation_code: _ } => {}, + crate::ExApiExecutionCapability::Unsupported { reason_code, reason } => { + return std::result::Result::Err(kb_core::Error::new(reason_code, reason)); + }, + } + let intent = match serde_json::from_str::( + &request.payload_json, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_intent_deserialize_failed", + error.to_string(), + )); + }, + }; + if intent.operation.operation_code() != request.operation_code.as_str() { + return std::result::Result::Err(kb_core::Error::new( + "execution_operation_code_mismatch", + format!( + "request operation {} does not match typed intent operation {}", + request.operation_code, + intent.operation.operation_code() + ), + )); + } + let prepared = + match crate::ExApiTypedInstructionExecutor::build_prepared_plan(self, &intent) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let payload_value = match serde_json::to_value(&prepared) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token_plan_serialize_failed", + error.to_string(), + )); + }, + }; + let payload_json = match crate::executor_api_serialize_payload_json(&payload_value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::ExApiExecutionPlan { + executor_name: std::string::String::from("kb_executor_spl_token"), + instruction_count: prepared.instructions.len(), + payload_json, + }); + } +} + +#[cfg(test)] +mod tests { + fn request(program_id: &str, operation_code: &str) -> crate::ExApiExecutionRequest { + return crate::ExApiExecutionRequest { + program_id: crate::MdProgramId(program_id.to_string()), + operation_code: operation_code.to_string(), + payload_json: std::string::String::from("{}"), + }; + } + + #[test] + fn exact_capabilities_cover_current_and_recent_operations_only() { + let executor = crate::ExSplTokenExecutor; + for operation_code in crate::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES { + let request = request(kb_program_ids::SPL_TOKEN_PROGRAM_ID, operation_code); + assert_eq!( + crate::ExApiInstructionExecutor::supports_request(&executor, &request), + crate::ExApiExecutionSupport::Yes + ); + } + for operation_code in [ + "spl_token.initialize_mint_with_rent", + "spl_token.initialize_account_with_rent", + "spl_token.initialize_multisig_with_rent", + "spl_token.initialize_account2", + ] { + let capability = crate::ExApiTypedInstructionExecutor::capability( + &executor, + &crate::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), + operation_code, + ); + match capability { + crate::ExApiExecutionCapability::Unsupported { reason_code, reason: _ } => { + assert_eq!(reason_code, "execution_spl_token_historical_variant_decode_only"); + }, + crate::ExApiExecutionCapability::Supported { operation_code: _ } => { + panic!("historical variant must be decode-only"); + }, + } + } + } + + #[test] + fn program_dispatch_is_exact() { + let executor = crate::ExSplTokenExecutor; + let foreign = request( + kb_program_ids::SPL_TOKEN2022_PROGRAM_ID, + crate::EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION, + ); + assert_eq!( + crate::ExApiInstructionExecutor::supports_request(&executor, &foreign), + crate::ExApiExecutionSupport::No + ); + } + + #[test] + fn machine_readable_matrix_matches_executor_policy() { + let matrix = serde_json::from_str::(include_str!( + "../../../../../docs/SPL_TOKEN_MATRIX.json" + )) + .unwrap_or_else(|error| panic!("matrix parsing failed: {error}")); + let instructions = matrix["instructions"] + .as_array() + .unwrap_or_else(|| panic!("matrix instructions must be an array")); + let mut supported = 0_usize; + let mut operation_codes = std::vec::Vec::new(); + for instruction in instructions { + let status = instruction["executorSupport"]["status"] + .as_str() + .unwrap_or_else(|| panic!("executorSupport.status missing")); + if status == "supported" { + supported += 1; + operation_codes.push( + instruction["executorSupport"]["operationCode"] + .as_str() + .unwrap_or_else(|| panic!("supported operationCode missing")), + ); + } + } + assert_eq!(supported, crate::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES.len()); + assert_eq!(instructions.len() - supported, 4); + operation_codes.sort_unstable(); + let mut compiled = crate::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES.to_vec(); + compiled.sort_unstable(); + assert_eq!(operation_codes, compiled); + } +} diff --git a/kb-lib/src/executor/spl/token/intent.rs b/kb-lib/src/executor/spl/token/intent.rs new file mode 100644 index 0000000..ca2f436 --- /dev/null +++ b/kb-lib/src/executor/spl/token/intent.rs @@ -0,0 +1,463 @@ +// file: kb-lib/src/executor/spl/token/intent.rs +// version: 4 + +//! Typed classic SPL Token execution intents. + +use ts_rs::TS; // rust-rules: derive-import + +/// Stable operation code for mint initialization through `InitializeMint2`. +pub const EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION: &str = "spl_token.initialize_mint"; +/// Stable operation code for token-account initialization through `InitializeAccount3`. +pub const EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION: &str = "spl_token.initialize_account"; +/// Stable operation code for multisig initialization through `InitializeMultisig2`. +pub const EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION: &str = "spl_token.initialize_multisig"; +/// Stable operation code for an unchecked token transfer. +pub const EX_SPL_TOKEN_TRANSFER_OPERATION: &str = "spl_token.transfer"; +/// Stable operation code for an approval. +pub const EX_SPL_TOKEN_APPROVE_OPERATION: &str = "spl_token.approve"; +/// Stable operation code for a delegate revocation. +pub const EX_SPL_TOKEN_REVOKE_OPERATION: &str = "spl_token.revoke"; +/// Stable operation code for an authority change. +pub const EX_SPL_TOKEN_SET_AUTHORITY_OPERATION: &str = "spl_token.set_authority"; +/// Stable operation code for minting tokens. +pub const EX_SPL_TOKEN_MINT_TO_OPERATION: &str = "spl_token.mint_to"; +/// Stable operation code for burning tokens. +pub const EX_SPL_TOKEN_BURN_OPERATION: &str = "spl_token.burn"; +/// Stable operation code for closing a token account. +pub const EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION: &str = "spl_token.close_account"; +/// Stable operation code for freezing a token account. +pub const EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION: &str = "spl_token.freeze_account"; +/// Stable operation code for thawing a token account. +pub const EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION: &str = "spl_token.thaw_account"; +/// Stable operation code for a checked token transfer. +pub const EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION: &str = "spl_token.transfer_checked"; +/// Stable operation code for a checked approval. +pub const EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION: &str = "spl_token.approve_checked"; +/// Stable operation code for checked minting. +pub const EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION: &str = "spl_token.mint_to_checked"; +/// Stable operation code for checked burning. +pub const EX_SPL_TOKEN_BURN_CHECKED_OPERATION: &str = "spl_token.burn_checked"; +/// Stable operation code for synchronizing wrapped SOL. +pub const EX_SPL_TOKEN_SYNC_NATIVE_OPERATION: &str = "spl_token.sync_native"; +/// Stable operation code for querying account data size. +pub const EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION: &str = "spl_token.get_account_data_size"; +/// Stable operation code for the classic compatibility no-op. +pub const EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION: &str = + "spl_token.initialize_immutable_owner"; +/// Stable operation code for raw-to-UI amount conversion. +pub const EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION: &str = "spl_token.amount_to_ui_amount"; +/// Stable operation code for UI-to-raw amount conversion. +pub const EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION: &str = "spl_token.ui_amount_to_amount"; +/// Stable operation code for rescuing excess lamports. +pub const EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION: &str = + "spl_token.withdraw_excess_lamports"; +/// Stable operation code for the recent wrapped-SOL partial unwrap. +pub const EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION: &str = "spl_token.unwrap_lamports"; +/// Stable operation code for a recent bounded batch. +pub const EX_SPL_TOKEN_BATCH_OPERATION: &str = "spl_token.batch"; + +/// Operation codes constructible by this executor. +pub const EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES: &[&str] = &[ + EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION, + EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION, + EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION, + EX_SPL_TOKEN_TRANSFER_OPERATION, + EX_SPL_TOKEN_APPROVE_OPERATION, + EX_SPL_TOKEN_REVOKE_OPERATION, + EX_SPL_TOKEN_SET_AUTHORITY_OPERATION, + EX_SPL_TOKEN_MINT_TO_OPERATION, + EX_SPL_TOKEN_BURN_OPERATION, + EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION, + EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION, + EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION, + EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION, + EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION, + EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION, + EX_SPL_TOKEN_BURN_CHECKED_OPERATION, + EX_SPL_TOKEN_SYNC_NATIVE_OPERATION, + EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION, + EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION, + EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION, + EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION, + EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION, + EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION, + EX_SPL_TOKEN_BATCH_OPERATION, +]; + +/// Exact unsigned on-chain amount represented as a decimal JSON string. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib_executor_spl_token/intent/ExSplClassicTokenAmount.ts" +)] +pub struct ExSplClassicTokenAmount( + /// Canonical unsigned decimal representation. + pub std::string::String, +); + +/// Ordered simple or multisig authority supplied to an instruction builder. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib_executor_spl_token/intent/ExSplClassicTokenAuthority.ts" +)] +pub struct ExSplClassicTokenAuthority { + /// Authority account. It signs only when `multisig_signers` is empty. + pub authority: crate::MdPubkey, + /// Ordered multisig signer occurrences. Duplicate metas remain ordered. + pub multisig_signers: std::vec::Vec, +} + +/// Authority domain used by `SetAuthority`. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[serde(rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib_executor_spl_token/intent/ExSplClassicTokenAuthorityType.ts" +)] +pub enum ExSplClassicTokenAuthorityType { + /// Mint authority. + MintTokens, + /// Freeze authority. + FreezeAccount, + /// Token-account owner. + AccountOwner, + /// Token-account close authority. + CloseAccount, +} + +/// One officially constructible non-batch instruction. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[serde(tag = "instruction", rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib_executor_spl_token/intent/ExSplClassicTokenSingleOperation.ts" +)] +pub enum ExSplClassicTokenSingleOperation { + /// Initialize a mint with the current no-Rent builder. + InitializeMint { + /// Mint account already created for the classic Token program. + mint: crate::MdPubkey, + /// Mint authority. + mint_authority: crate::MdPubkey, + /// Optional freeze authority. + freeze_authority: std::option::Option, + /// Mint decimals. + decimals: u8, + }, + /// Initialize a token account with the current no-Rent builder. + InitializeAccount { + /// Token account already created for the classic Token program. + account: crate::MdPubkey, + /// Mint associated with the account. + mint: crate::MdPubkey, + /// Token-account owner. + owner: crate::MdPubkey, + }, + /// Initialize a multisig with the current no-Rent builder. + InitializeMultisig { + /// Multisig account already created for the classic Token program. + multisig: crate::MdPubkey, + /// Ordered member accounts. + members: std::vec::Vec, + /// Required member threshold. + threshold: u8, + }, + /// Transfer a raw token amount without carrying the mint or decimals in the wire. + Transfer { + /// Source token account. + source: crate::MdPubkey, + /// Destination token account. + destination: crate::MdPubkey, + /// Owner, delegate or multisig authority. + authority: crate::ExSplClassicTokenAuthority, + /// Exact raw token amount. + amount: crate::ExSplClassicTokenAmount, + }, + /// Approve a delegate for a raw token allowance. + Approve { + /// Source token account. + source: crate::MdPubkey, + /// Delegate account. + delegate: crate::MdPubkey, + /// Owner or multisig authority. + authority: crate::ExSplClassicTokenAuthority, + /// Exact raw allowance. + amount: crate::ExSplClassicTokenAmount, + }, + /// Revoke the current delegate. + Revoke { + /// Source token account. + source: crate::MdPubkey, + /// Owner or multisig authority. + authority: crate::ExSplClassicTokenAuthority, + }, + /// Change or revoke an authority. + SetAuthority { + /// Mint or token account whose authority changes. + owned: crate::MdPubkey, + /// Authority domain. + authority_type: crate::ExSplClassicTokenAuthorityType, + /// New authority, or `None` to revoke it. + new_authority: std::option::Option, + /// Current simple or multisig authority. + current_authority: crate::ExSplClassicTokenAuthority, + }, + /// Mint a raw token amount. + MintTo { + /// Mint account. + mint: crate::MdPubkey, + /// Destination token account. + destination: crate::MdPubkey, + /// Mint authority. + authority: crate::ExSplClassicTokenAuthority, + /// Exact raw amount. + amount: crate::ExSplClassicTokenAmount, + }, + /// Burn a raw token amount. + Burn { + /// Source token account. + source: crate::MdPubkey, + /// Mint account. + mint: crate::MdPubkey, + /// Owner, delegate or multisig authority. + authority: crate::ExSplClassicTokenAuthority, + /// Exact raw amount. + amount: crate::ExSplClassicTokenAmount, + }, + /// Close a token account. + CloseAccount { + /// Token account to close. + account: crate::MdPubkey, + /// Lamport destination. + destination: crate::MdPubkey, + /// Owner or close authority. + authority: crate::ExSplClassicTokenAuthority, + }, + /// Freeze a token account. + FreezeAccount { + /// Token account to freeze. + account: crate::MdPubkey, + /// Mint account. + mint: crate::MdPubkey, + /// Freeze authority. + authority: crate::ExSplClassicTokenAuthority, + }, + /// Thaw a token account. + ThawAccount { + /// Token account to thaw. + account: crate::MdPubkey, + /// Mint account. + mint: crate::MdPubkey, + /// Freeze authority. + authority: crate::ExSplClassicTokenAuthority, + }, + /// Transfer while checking an explicit mint and decimals value. + TransferChecked { + /// Source token account. + source: crate::MdPubkey, + /// Mint account. + mint: crate::MdPubkey, + /// Destination token account. + destination: crate::MdPubkey, + /// Owner, delegate or multisig authority. + authority: crate::ExSplClassicTokenAuthority, + /// Exact raw amount. + amount: crate::ExSplClassicTokenAmount, + /// Expected mint decimals. + decimals: u8, + }, + /// Approve a delegate while checking an explicit mint and decimals value. + ApproveChecked { + /// Source token account. + source: crate::MdPubkey, + /// Mint account. + mint: crate::MdPubkey, + /// Delegate account. + delegate: crate::MdPubkey, + /// Owner or multisig authority. + authority: crate::ExSplClassicTokenAuthority, + /// Exact raw allowance. + amount: crate::ExSplClassicTokenAmount, + /// Expected mint decimals. + decimals: u8, + }, + /// Mint while checking explicit decimals. + MintToChecked { + /// Mint account. + mint: crate::MdPubkey, + /// Destination token account. + destination: crate::MdPubkey, + /// Mint authority. + authority: crate::ExSplClassicTokenAuthority, + /// Exact raw amount. + amount: crate::ExSplClassicTokenAmount, + /// Expected mint decimals. + decimals: u8, + }, + /// Burn while checking explicit decimals. + BurnChecked { + /// Source token account. + source: crate::MdPubkey, + /// Mint account. + mint: crate::MdPubkey, + /// Owner, delegate or multisig authority. + authority: crate::ExSplClassicTokenAuthority, + /// Exact raw amount. + amount: crate::ExSplClassicTokenAmount, + /// Expected mint decimals. + decimals: u8, + }, + /// Synchronize a wrapped-SOL account. + SyncNative { + /// Wrapped-SOL token account. + account: crate::MdPubkey, + /// Include the optional Rent sysvar account published by interface 3.0.0. + include_rent_sysvar: bool, + }, + /// Query the token-account size for a mint. + GetAccountDataSize { + /// Mint account. + mint: crate::MdPubkey, + }, + /// Build the classic-program immutable-owner compatibility no-op. + InitializeImmutableOwner { + /// Token account being prepared. + account: crate::MdPubkey, + }, + /// Convert a raw amount using mint decimals through return data. + AmountToUiAmount { + /// Mint account. + mint: crate::MdPubkey, + /// Exact raw amount. + amount: crate::ExSplClassicTokenAmount, + }, + /// Convert a UI amount using mint decimals through return data. + UiAmountToAmount { + /// Mint account. + mint: crate::MdPubkey, + /// Exact UTF-8 UI amount. + ui_amount: std::string::String, + }, + /// Withdraw lamports above rent exemption. + WithdrawExcessLamports { + /// Program-owned source account. + account: crate::MdPubkey, + /// Lamport destination. + destination: crate::MdPubkey, + /// Owner or multisig authority. + authority: crate::ExSplClassicTokenAuthority, + }, + /// Unwrap some or all lamports from a wrapped-SOL account. + UnwrapLamports { + /// Wrapped-SOL token account. + account: crate::MdPubkey, + /// Lamport destination. + destination: crate::MdPubkey, + /// Owner or multisig authority. + authority: crate::ExSplClassicTokenAuthority, + /// Exact lamports to unwrap, or `None` for the complete balance. + amount_lamports: std::option::Option, + }, +} + +impl crate::ExSplClassicTokenSingleOperation { + /// Returns the stable operation code. + pub fn operation_code(&self) -> &'static str { + return match self { + Self::InitializeMint { .. } => crate::EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION, + Self::InitializeAccount { .. } => crate::EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION, + Self::InitializeMultisig { .. } => crate::EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION, + Self::Transfer { .. } => crate::EX_SPL_TOKEN_TRANSFER_OPERATION, + Self::Approve { .. } => crate::EX_SPL_TOKEN_APPROVE_OPERATION, + Self::Revoke { .. } => crate::EX_SPL_TOKEN_REVOKE_OPERATION, + Self::SetAuthority { .. } => crate::EX_SPL_TOKEN_SET_AUTHORITY_OPERATION, + Self::MintTo { .. } => crate::EX_SPL_TOKEN_MINT_TO_OPERATION, + Self::Burn { .. } => crate::EX_SPL_TOKEN_BURN_OPERATION, + Self::CloseAccount { .. } => crate::EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION, + Self::FreezeAccount { .. } => crate::EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION, + Self::ThawAccount { .. } => crate::EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION, + Self::TransferChecked { .. } => crate::EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION, + Self::ApproveChecked { .. } => crate::EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION, + Self::MintToChecked { .. } => crate::EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION, + Self::BurnChecked { .. } => crate::EX_SPL_TOKEN_BURN_CHECKED_OPERATION, + Self::SyncNative { .. } => crate::EX_SPL_TOKEN_SYNC_NATIVE_OPERATION, + Self::GetAccountDataSize { .. } => crate::EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION, + Self::InitializeImmutableOwner { .. } => { + crate::EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION + }, + Self::AmountToUiAmount { .. } => crate::EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION, + Self::UiAmountToAmount { .. } => crate::EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION, + Self::WithdrawExcessLamports { .. } => { + crate::EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION + }, + Self::UnwrapLamports { .. } => crate::EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION, + }; + } + + pub(crate) fn requires_materialization(&self) -> bool { + return !matches!( + self, + Self::GetAccountDataSize { .. } + | Self::InitializeImmutableOwner { .. } + | Self::AmountToUiAmount { .. } + | Self::UiAmountToAmount { .. } + ); + } +} + +/// Top-level SPL Token operation, including the recent bounded Batch builder. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[serde(tag = "operation", rename_all = "snake_case")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib_executor_spl_token/intent/ExSplClassicTokenOperation.ts" +)] +pub enum ExSplClassicTokenOperation { + /// Build one non-batch instruction. + Instruction { + /// Exact instruction arguments. + value: crate::ExSplClassicTokenSingleOperation, + }, + /// Build one Batch from ordered, non-batch child operations. + Batch { + /// Ordered child operations. Nested Batch is impossible in this contract. + instructions: std::vec::Vec, + }, +} + +impl crate::ExSplClassicTokenOperation { + /// Returns the stable top-level operation code. + pub fn operation_code(&self) -> &'static str { + return match self { + Self::Instruction { value } => value.operation_code(), + Self::Batch { .. } => crate::EX_SPL_TOKEN_BATCH_OPERATION, + }; + } + + pub(crate) fn requires_materialization(&self) -> bool { + return match self { + Self::Instruction { value } => value.requires_materialization(), + Self::Batch { instructions } => { + instructions.iter().any(|value| return value.requires_materialization()) + }, + }; + } +} + +/// Complete typed intent accepted by the classic SPL Token executor. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_lib_executor_spl_token/intent/ExSplClassicTokenExecutionIntent.ts" +)] +pub struct ExSplClassicTokenExecutionIntent { + /// Stable caller-provided identifier used for logs and replay correlation. + pub intent_id: std::string::String, + /// Transaction fee payer. + pub fee_payer: crate::MdPubkey, + /// Conservative execution policy. Its default is simulation-only. + pub policy: crate::ExApiExecutionPolicy, + /// Typed classic Token operation. + pub operation: crate::ExSplClassicTokenOperation, +} diff --git a/kb-lib/src/lib.rs b/kb-lib/src/lib.rs index aabc016..cec4890 100644 --- a/kb-lib/src/lib.rs +++ b/kb-lib/src/lib.rs @@ -581,6 +581,56 @@ pub use self::executor::EX_SPL_MEMO_ADD_MEMO_OPERATION; pub use self::executor::EX_SPL_MEMO_MAX_MESSAGE_BYTES; /// Exposes the SPL Memo signer bound. pub use self::executor::EX_SPL_MEMO_MAX_SIGNERS; +/// Exposes `EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_AMOUNT_TO_UI_AMOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_APPROVE_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_APPROVE_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_APPROVE_OPERATION; +/// Exposes `EX_SPL_TOKEN_BATCH_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_BATCH_OPERATION; +/// Exposes `EX_SPL_TOKEN_BURN_CHECKED_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_BURN_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_BURN_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_BURN_OPERATION; +/// Exposes `EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_CLOSE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_FREEZE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_GET_ACCOUNT_DATA_SIZE_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_INITIALIZE_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_INITIALIZE_IMMUTABLE_OWNER_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_INITIALIZE_MINT_OPERATION; +/// Exposes `EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_INITIALIZE_MULTISIG_OPERATION; +/// Exposes `EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_MINT_TO_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_MINT_TO_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_MINT_TO_OPERATION; +/// Exposes `EX_SPL_TOKEN_REVOKE_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_REVOKE_OPERATION; +/// Exposes `EX_SPL_TOKEN_SET_AUTHORITY_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_SET_AUTHORITY_OPERATION; +/// Exposes `EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES`. +pub use self::executor::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES; +/// Exposes `EX_SPL_TOKEN_SYNC_NATIVE_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_SYNC_NATIVE_OPERATION; +/// Exposes `EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_THAW_ACCOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION; +/// Exposes `EX_SPL_TOKEN_TRANSFER_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_TRANSFER_OPERATION; +/// Exposes `EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_UI_AMOUNT_TO_AMOUNT_OPERATION; +/// Exposes `EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_UNWRAP_LAMPORTS_OPERATION; +/// Exposes `EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION`. +pub use self::executor::EX_SPL_TOKEN_WITHDRAW_EXCESS_LAMPORTS_OPERATION; /// Exposes `EX_SPL_TOKEN2022_AMOUNT_TO_UI_AMOUNT_OPERATION`. pub use self::executor::EX_SPL_TOKEN2022_AMOUNT_TO_UI_AMOUNT_OPERATION; /// Exposes `EX_SPL_TOKEN2022_APPLY_PENDING_CONFIDENTIAL_BALANCE_OPERATION`. @@ -1027,6 +1077,18 @@ pub use self::executor::ExSplAssociatedTokenAccountExecutor; pub use self::executor::ExSplAssociatedTokenAccountOperation; /// Exposes the exact Token Program used by ATA derivation. pub use self::executor::ExSplAssociatedTokenProgram; +/// Exposes `ExSplClassicTokenAmount`. +pub use self::executor::ExSplClassicTokenAmount; +/// Exposes `ExSplClassicTokenAuthority`. +pub use self::executor::ExSplClassicTokenAuthority; +/// Exposes `ExSplClassicTokenAuthorityType`. +pub use self::executor::ExSplClassicTokenAuthorityType; +/// Exposes `ExSplClassicTokenExecutionIntent`. +pub use self::executor::ExSplClassicTokenExecutionIntent; +/// Exposes `ExSplClassicTokenOperation`. +pub use self::executor::ExSplClassicTokenOperation; +/// Exposes `ExSplClassicTokenSingleOperation`. +pub use self::executor::ExSplClassicTokenSingleOperation; /// Exposes the typed SPL ElGamal registry execution intent. pub use self::executor::ExSplElgamalRegistryExecutionIntent; /// Exposes the typed SPL ElGamal registry executor. @@ -1694,6 +1756,14 @@ pub(crate) use self::decoder::decoder_spl_token2022_decode; pub(crate) use self::decoder::decoder_spl_token2022_entry_for_tag; /// Returns the first byte of one retained Token-2022 payload. pub(crate) use self::decoder::decoder_spl_token2022_payload_tag; +/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS`. +pub(crate) use self::executor::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS`. +pub(crate) use self::executor::EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS`. +pub(crate) use self::executor::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS; +/// Crate-root access to `EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES`. +pub(crate) use self::executor::EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES; /// Maximum ordered multisig signer occurrences for SPL Token-2022. pub(crate) use self::executor::EX_SPL_TOKEN2022_MAX_MULTISIG_SIGNERS; /// Maximum transfer-fee source account count for SPL Token-2022. @@ -1708,6 +1778,8 @@ pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_ATA; pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_ELGAMAL_REGISTRY; /// Canonical SPL Memo tracing target. pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_MEMO; +/// Crate-root access to `TRACING_TARGET_EXECUTOR_SPL_TOKEN`. +pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_TOKEN; /// Canonical SPL Token-2022 tracing target. pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_TOKEN2022; /// Crate-root access to `build_prepared_plan` from `address_lookup_table`. @@ -1752,6 +1824,8 @@ pub(crate) use self::executor::executor_spl_ata_build_prepared_plan; pub(crate) use self::executor::executor_spl_elgamal_registry_build_prepared_plan; /// Internal SPL Memo plan builder. pub(crate) use self::executor::executor_spl_memo_build_prepared_plan; +/// Crate-root access to `executor_spl_token_build_prepared_plan`. +pub(crate) use self::executor::executor_spl_token_build_prepared_plan; /// Internal SPL Token-2022 prepared-plan builder. pub(crate) use self::executor::executor_spl_token2022_build_prepared_plan; /// Canonical processor name for the native and SPL administration materializer. diff --git a/kb-logging/src/tracing_runtime.rs b/kb-logging/src/tracing_runtime.rs index ed755b5..eaece0a 100644 --- a/kb-logging/src/tracing_runtime.rs +++ b/kb-logging/src/tracing_runtime.rs @@ -774,7 +774,8 @@ mod tests { .filter(|line| { let trimmed = line.trim(); return trimmed.starts_with("pub(crate) const ") - && trimmed.contains("TRACING_TARGET") + && trimmed.contains("TRACING_") + && trimmed.contains("TARGET") && trimmed.contains(": &str"); }) .count(); diff --git a/scripts/audit_khadhroony_workspace_rules.py b/scripts/audit_khadhroony_workspace_rules.py index 63de0e5..c943ac4 100755 --- a/scripts/audit_khadhroony_workspace_rules.py +++ b/scripts/audit_khadhroony_workspace_rules.py @@ -97,10 +97,10 @@ def audit_kb_lib_tracing(root: pathlib.Path) -> list[Violation]: re.MULTILINE, ) for match in re_export_pattern.finditer(root_text): - if match.group(1).endswith("_TRACING_TARGET"): + if match.group(1).startswith("TRACING_TARGET_"): re_exports.add(match.group(1)) target_pattern = re.compile( - r'^pub\(crate\) const ([A-Z][A-Z0-9_]+_TRACING_TARGET): &str = "([^"]+)";$', + r'^pub\(crate\) const (TRACING_TARGET_[A-Z0-9_]+): &str = "([^"]+)";$', re.MULTILINE, ) for constants in sorted((crate / "src").rglob("constants.rs")): @@ -121,7 +121,7 @@ def audit_kb_lib_tracing(root: pathlib.Path) -> list[Violation]: ) ) macro_pattern = re.compile( - r"tracing::(?:debug|error|info|trace|warn)!\(\s*target:\s*crate::([A-Z][A-Z0-9_]+_TRACING_TARGET)" + r"tracing::(?:debug|error|info|trace|warn)!\(\s*target:\s*crate::(TRACING_TARGET_[A-Z0-9_]+)" ) for path in sorted((crate / "src").rglob("*.rs")): relative = path.relative_to(root).as_posix() @@ -195,6 +195,8 @@ def audit_kb_lib_symbol_prefixes(root: pathlib.Path) -> list[Violation]: kind = match.group(1) name = match.group(2) expected = prefixes[0] if kind in {"const", "static"} else prefixes[2] if kind == "fn" else prefixes[1] + if kind in {"const", "static"} and name.startswith("TRACING_TARGET_"): + continue if not name.startswith(expected): violations.append( Violation( @@ -438,13 +440,13 @@ def audit_reserved_executor_scaffolds(root: pathlib.Path) -> list[Violation]: "reserved executor must build only an explicit zero-instruction plan", ) ) - if reserved_count != 99: + if reserved_count != 98: violations.append( Violation( "KH_EX007", "kb-lib/src/executor", 1, - f"expected 99 reserved executor boundaries, found {reserved_count}", + f"expected 98 reserved executor boundaries, found {reserved_count}", ) ) return violations