0.1.0
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# file: kb_executor_spl_token/Cargo.toml
|
||||
# version: 3
|
||||
|
||||
[package]
|
||||
name = "kb_executor_spl_token"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
kb_core = { path = "../kb_core" }
|
||||
kb_execution_api = { path = "../kb_execution_api" }
|
||||
kb_model = { path = "../kb_model" }
|
||||
kb_program_ids = { path = "../kb_program_ids" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
solana-instruction.workspace = true
|
||||
solana-pubkey.workspace = true
|
||||
spl-token-interface.workspace = true
|
||||
tracing.workspace = true
|
||||
ts-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
kb_execution_safety = { path = "../kb_execution_safety" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,98 @@
|
||||
<!-- file: kb_executor_spl_token/README.md -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# kb_executor_spl_token
|
||||
|
||||
Ce crate construit des plans typés pour le programme SPL Token classique exact :
|
||||
|
||||
```text
|
||||
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
|
||||
```
|
||||
|
||||
Il ne dépend ni du RPC, ni du wallet, ni de Tauri, ni du décodeur. La lecture d’état, la simulation,
|
||||
la signature, l’envoi et la validation post-exécution appartiennent à l’orchestration commune.
|
||||
|
||||
## Politique de surface
|
||||
|
||||
Le décodeur conserve les 28 tags publiés, y compris les formes historiques. L’exécuteur expose 24
|
||||
opérations encore courantes ou récentes et refuse quatre initialisations obsolètes dépendantes de
|
||||
Rent : `InitializeMint`, `InitializeAccount`, `InitializeMultisig` et `InitializeAccount2`. Leurs
|
||||
équivalents actuels utilisent respectivement `InitializeMint2`, `InitializeAccount3` et
|
||||
`InitializeMultisig2`.
|
||||
|
||||
Les opérations non checked `Transfer`, `Approve`, `MintTo` et `Burn` restent constructibles : leur
|
||||
wire est ancien, mais les builders officiels 3.0.0 les publient toujours et elles ne sont pas
|
||||
obsolètes. Les variantes checked doivent être préférées lorsqu’un mint et ses decimals sont connus.
|
||||
|
||||
## Contrat typé
|
||||
|
||||
`SplTokenExecutionIntent` contient :
|
||||
|
||||
- un identifiant d’intent et un fee payer ;
|
||||
- une `ExecutionPolicy` commune, simulation obligatoire et dry-run par défaut ;
|
||||
- une instruction typée ou un `Batch` de sous-instructions non batch ;
|
||||
- les comptes métier explicites ;
|
||||
- une autorité simple ou un multisig avec signataires ordonnés ;
|
||||
- les montants bruts sous forme de chaînes décimales JSON, converties en `u64` seulement dans le
|
||||
builder ;
|
||||
- les decimals séparés lorsqu’ils appartiennent réellement au wire.
|
||||
|
||||
Les doublons des metas multisig sont conservés dans l’ordre officiel. La liste des signataires
|
||||
transactionnels est dédupliquée séparément avec le fee payer.
|
||||
|
||||
## Builders
|
||||
|
||||
Chaque opération appelle directement le builder `spl-token-interface 3.0.0`. Cela couvre les
|
||||
initialisations actuelles, transferts, délégations, changements d’autorité, mint/burn, close,
|
||||
freeze/thaw, variantes checked, `SyncNative`, les trois instructions de return data,
|
||||
`InitializeImmutableOwner`, `WithdrawExcessLamports`, `UnwrapLamports` et `Batch`.
|
||||
|
||||
`InitializeImmutableOwner` est explicitement un no-op de compatibilité sur le programme classique ;
|
||||
il n’est jamais présenté comme une extension Token-2022.
|
||||
|
||||
`Batch` est borné à 64 enfants, 512 occurrences de comptes et 255 octets de data par enfant. Son
|
||||
type enfant exclut structurellement un autre `Batch`. Les slices de comptes et l’ordre produits par
|
||||
le builder officiel sont conservés.
|
||||
|
||||
## Constructibilité et clusters
|
||||
|
||||
La constructibilité de bibliothèque ne dépend pas du cluster. `UnwrapLamports` et `Batch` sont
|
||||
publiés avec un builder exact dans l’interface 3.0.0 et sont donc constructibles. Deux simulations
|
||||
réussies le 16 juillet 2026 prouvent leur présence sur le programme classique Devnet. Cette preuve
|
||||
n'est pas extrapolée à Localnet, Testnet ou Mainnet. Mainnet demeure gouverné par
|
||||
`kb_execution_safety` et la confirmation opérateur, pas par une interdiction interne à ce crate.
|
||||
|
||||
Les opérations d’initialisation supposent que les comptes ont déjà été créés avec la bonne taille,
|
||||
le bon owner et le bon financement. La création System et le rent ne sont pas masqués dans une
|
||||
instruction Token ; le préflight stateful de la tranche suivante les vérifiera sans utiliser ATA.
|
||||
|
||||
## Coûts et validation
|
||||
|
||||
Les montants token ne sont pas des frais réseau et ne sont pas reportés comme lamports dépensés.
|
||||
Un montant explicite `UnwrapLamports` est déclaré dans `requested_spend_lamports`; un unwrap total
|
||||
et `WithdrawExcessLamports` restent simulation-only tant qu’une lecture d’état n’a pas borné les
|
||||
lamports déplaçables. Le plafond de frais reste séparé dans `ExecutionCostLimit`.
|
||||
|
||||
Toute construction impose une simulation, un plafond de frais positif, une insertion canonique,
|
||||
l’extraction core et le replay décodé. Les mutations exigent aussi la validation de matérialisation ;
|
||||
les conversions, requêtes de return data et le no-op de compatibilité n’inventent aucune projection.
|
||||
|
||||
## Limites actuelles
|
||||
|
||||
- aucun préflight Mint/Account/Multisig dans ce crate : ces lectures sont implémentées dans
|
||||
`kb_pipeline` afin de conserver l'exécuteur pur ;
|
||||
- aucun envoi ni simulation RPC dans ce crate ; l'orchestrateur commun a validé ces étapes sur
|
||||
Devnet pour `TransferChecked` et le lifecycle contrôlé ;
|
||||
- aucun compte ATA créé implicitement ;
|
||||
- aucun support Token-2022 ;
|
||||
- aucun déploiement de `UnwrapLamports` ou `Batch` revendiqué hors Devnet sans simulation cluster
|
||||
dédiée.
|
||||
|
||||
Le parcours Devnet réel a prouvé simulation, soumission, confirmation, hydratation canonique,
|
||||
extraction, replay, matérialisation et idempotence pour les opérations du lifecycle documentées dans
|
||||
`docs/SPL_TOKEN_MATRIX.json`. La démo Tauri refuse volontairement un envoi si le wallet du profil ne
|
||||
résout pas aussi l'autorité Token requise ; elle ne prend jamais en charge une clé externe implicite.
|
||||
|
||||
Le probe Devnet simulation-only du 16 juillet 2026 a validé le wire déployé de `Batch` avec un
|
||||
enfant `TransferChecked` de montant nul en 270 compute units, puis `UnwrapLamports` d'un lamport en
|
||||
140 compute units. Aucune transaction n'a été signée ou envoyée par ce probe.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
// file: kb_executor_spl_token/src/constants.rs
|
||||
// version: 3
|
||||
|
||||
//! Local constants for the `kb_executor_spl_token` crate. Program identifiers live in `kb_program_ids`.
|
||||
|
||||
/// Maximum number of children accepted by one Batch plan.
|
||||
pub(crate) const MAX_BATCH_INSTRUCTIONS: usize = 64;
|
||||
/// Maximum aggregate account-meta occurrences accepted by one Batch plan.
|
||||
pub(crate) const MAX_BATCH_ACCOUNTS: usize = 512;
|
||||
/// Maximum number of ordered multisig signer occurrences.
|
||||
pub(crate) const MAX_MULTISIG_SIGNERS: usize = 11;
|
||||
/// Maximum UI amount string length accepted before transaction assembly.
|
||||
pub(crate) const MAX_UI_AMOUNT_BYTES: usize = 255;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb_executor_spl_token";
|
||||
@@ -0,0 +1,241 @@
|
||||
// file: kb_executor_spl_token/src/executor.rs
|
||||
// version: 4
|
||||
|
||||
//! Exact classic SPL Token capability dispatch and typed plan construction.
|
||||
|
||||
/// Classic SPL Token executor implementation.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SplTokenExecutor;
|
||||
|
||||
impl crate::SplTokenExecutor {
|
||||
fn exact_capability(
|
||||
&self,
|
||||
program_id: &kb_model::ProgramId,
|
||||
operation_code: &str,
|
||||
) -> kb_execution_api::ExecutionCapability {
|
||||
if program_id.0 != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
||||
return kb_execution_api::ExecutionCapability::unsupported(
|
||||
"execution_spl_token_program_not_owned",
|
||||
format!("program {} is not owned by kb_executor_spl_token", program_id.0),
|
||||
);
|
||||
}
|
||||
if crate::SUPPORTED_OPERATION_CODES.contains(&operation_code) {
|
||||
return kb_execution_api::ExecutionCapability::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 kb_execution_api::ExecutionCapability::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 kb_execution_api::ExecutionCapability::unsupported(
|
||||
"execution_spl_token_operation_unsupported",
|
||||
format!("SPL Token operation {operation_code} is not implemented"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_execution_api::TypedInstructionExecutor for crate::SplTokenExecutor {
|
||||
type Intent = crate::SplTokenExecutionIntent;
|
||||
|
||||
fn capability(
|
||||
&self,
|
||||
program_id: &kb_model::ProgramId,
|
||||
operation_code: &str,
|
||||
) -> kb_execution_api::ExecutionCapability {
|
||||
return self.exact_capability(program_id, operation_code);
|
||||
}
|
||||
|
||||
fn build_prepared_plan(
|
||||
&self,
|
||||
intent: &Self::Intent,
|
||||
) -> kb_core::Result<kb_execution_api::PreparedExecutionPlan> {
|
||||
let program_id =
|
||||
kb_model::ProgramId(std::string::String::from(kb_program_ids::SPL_TOKEN_PROGRAM_ID));
|
||||
return match self.exact_capability(&program_id, intent.operation.operation_code()) {
|
||||
kb_execution_api::ExecutionCapability::Supported { operation_code: _ } => {
|
||||
crate::build_prepared_plan(intent)
|
||||
},
|
||||
kb_execution_api::ExecutionCapability::Unsupported { reason_code, reason } => {
|
||||
std::result::Result::Err(kb_core::Error::new(reason_code, reason))
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_execution_api::InstructionExecutor for crate::SplTokenExecutor {
|
||||
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: &kb_execution_api::ExecutionRequest,
|
||||
) -> kb_execution_api::ExecutionSupport {
|
||||
return match self.exact_capability(&request.program_id, &request.operation_code) {
|
||||
kb_execution_api::ExecutionCapability::Supported { operation_code: _ } => {
|
||||
kb_execution_api::ExecutionSupport::Yes
|
||||
},
|
||||
kb_execution_api::ExecutionCapability::Unsupported { reason_code: _, reason: _ } => {
|
||||
kb_execution_api::ExecutionSupport::No
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn build_plan(
|
||||
&self,
|
||||
request: &kb_execution_api::ExecutionRequest,
|
||||
) -> kb_core::Result<kb_execution_api::ExecutionPlan> {
|
||||
match self.exact_capability(&request.program_id, &request.operation_code) {
|
||||
kb_execution_api::ExecutionCapability::Supported { operation_code: _ } => {},
|
||||
kb_execution_api::ExecutionCapability::Unsupported { reason_code, reason } => {
|
||||
return std::result::Result::Err(kb_core::Error::new(reason_code, reason));
|
||||
},
|
||||
}
|
||||
let intent =
|
||||
match serde_json::from_str::<crate::SplTokenExecutionIntent>(&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 kb_execution_api::TypedInstructionExecutor::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 kb_execution_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(kb_execution_api::ExecutionPlan {
|
||||
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) -> kb_execution_api::ExecutionRequest {
|
||||
return kb_execution_api::ExecutionRequest {
|
||||
program_id: kb_model::ProgramId(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::SplTokenExecutor;
|
||||
for operation_code in crate::SUPPORTED_OPERATION_CODES {
|
||||
let request = request(kb_program_ids::SPL_TOKEN_PROGRAM_ID, operation_code);
|
||||
assert_eq!(
|
||||
kb_execution_api::InstructionExecutor::supports_request(&executor, &request),
|
||||
kb_execution_api::ExecutionSupport::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 = kb_execution_api::TypedInstructionExecutor::capability(
|
||||
&executor,
|
||||
&kb_model::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
||||
operation_code,
|
||||
);
|
||||
match capability {
|
||||
kb_execution_api::ExecutionCapability::Unsupported { reason_code, reason: _ } => {
|
||||
assert_eq!(reason_code, "execution_spl_token_historical_variant_decode_only");
|
||||
},
|
||||
kb_execution_api::ExecutionCapability::Supported { operation_code: _ } => {
|
||||
panic!("historical variant must be decode-only");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_dispatch_is_exact() {
|
||||
let executor = crate::SplTokenExecutor;
|
||||
let foreign =
|
||||
request(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, crate::TRANSFER_CHECKED_OPERATION);
|
||||
assert_eq!(
|
||||
kb_execution_api::InstructionExecutor::supports_request(&executor, &foreign),
|
||||
kb_execution_api::ExecutionSupport::No
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn machine_readable_matrix_matches_executor_policy() {
|
||||
let matrix = serde_json::from_str::<serde_json::Value>(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::SUPPORTED_OPERATION_CODES.len());
|
||||
assert_eq!(instructions.len() - supported, 4);
|
||||
operation_codes.sort_unstable();
|
||||
let mut compiled = crate::SUPPORTED_OPERATION_CODES.to_vec();
|
||||
compiled.sort_unstable();
|
||||
assert_eq!(operation_codes, compiled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
// file: kb_executor_spl_token/src/intent.rs
|
||||
// version: 3
|
||||
|
||||
//! Typed classic SPL Token execution intents.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Stable operation code for mint initialization through `InitializeMint2`.
|
||||
pub const INITIALIZE_MINT_OPERATION: &str = "spl_token.initialize_mint";
|
||||
/// Stable operation code for token-account initialization through `InitializeAccount3`.
|
||||
pub const INITIALIZE_ACCOUNT_OPERATION: &str = "spl_token.initialize_account";
|
||||
/// Stable operation code for multisig initialization through `InitializeMultisig2`.
|
||||
pub const INITIALIZE_MULTISIG_OPERATION: &str = "spl_token.initialize_multisig";
|
||||
/// Stable operation code for an unchecked token transfer.
|
||||
pub const TRANSFER_OPERATION: &str = "spl_token.transfer";
|
||||
/// Stable operation code for an approval.
|
||||
pub const APPROVE_OPERATION: &str = "spl_token.approve";
|
||||
/// Stable operation code for a delegate revocation.
|
||||
pub const REVOKE_OPERATION: &str = "spl_token.revoke";
|
||||
/// Stable operation code for an authority change.
|
||||
pub const SET_AUTHORITY_OPERATION: &str = "spl_token.set_authority";
|
||||
/// Stable operation code for minting tokens.
|
||||
pub const MINT_TO_OPERATION: &str = "spl_token.mint_to";
|
||||
/// Stable operation code for burning tokens.
|
||||
pub const BURN_OPERATION: &str = "spl_token.burn";
|
||||
/// Stable operation code for closing a token account.
|
||||
pub const CLOSE_ACCOUNT_OPERATION: &str = "spl_token.close_account";
|
||||
/// Stable operation code for freezing a token account.
|
||||
pub const FREEZE_ACCOUNT_OPERATION: &str = "spl_token.freeze_account";
|
||||
/// Stable operation code for thawing a token account.
|
||||
pub const THAW_ACCOUNT_OPERATION: &str = "spl_token.thaw_account";
|
||||
/// Stable operation code for a checked token transfer.
|
||||
pub const TRANSFER_CHECKED_OPERATION: &str = "spl_token.transfer_checked";
|
||||
/// Stable operation code for a checked approval.
|
||||
pub const APPROVE_CHECKED_OPERATION: &str = "spl_token.approve_checked";
|
||||
/// Stable operation code for checked minting.
|
||||
pub const MINT_TO_CHECKED_OPERATION: &str = "spl_token.mint_to_checked";
|
||||
/// Stable operation code for checked burning.
|
||||
pub const BURN_CHECKED_OPERATION: &str = "spl_token.burn_checked";
|
||||
/// Stable operation code for synchronizing wrapped SOL.
|
||||
pub const SYNC_NATIVE_OPERATION: &str = "spl_token.sync_native";
|
||||
/// Stable operation code for querying account data size.
|
||||
pub const GET_ACCOUNT_DATA_SIZE_OPERATION: &str = "spl_token.get_account_data_size";
|
||||
/// Stable operation code for the classic compatibility no-op.
|
||||
pub const INITIALIZE_IMMUTABLE_OWNER_OPERATION: &str = "spl_token.initialize_immutable_owner";
|
||||
/// Stable operation code for raw-to-UI amount conversion.
|
||||
pub const AMOUNT_TO_UI_AMOUNT_OPERATION: &str = "spl_token.amount_to_ui_amount";
|
||||
/// Stable operation code for UI-to-raw amount conversion.
|
||||
pub const UI_AMOUNT_TO_AMOUNT_OPERATION: &str = "spl_token.ui_amount_to_amount";
|
||||
/// Stable operation code for rescuing excess lamports.
|
||||
pub const WITHDRAW_EXCESS_LAMPORTS_OPERATION: &str = "spl_token.withdraw_excess_lamports";
|
||||
/// Stable operation code for the recent wrapped-SOL partial unwrap.
|
||||
pub const UNWRAP_LAMPORTS_OPERATION: &str = "spl_token.unwrap_lamports";
|
||||
/// Stable operation code for a recent bounded batch.
|
||||
pub const BATCH_OPERATION: &str = "spl_token.batch";
|
||||
|
||||
/// Operation codes constructible by this executor.
|
||||
pub const SUPPORTED_OPERATION_CODES: &[&str] = &[
|
||||
INITIALIZE_MINT_OPERATION,
|
||||
INITIALIZE_ACCOUNT_OPERATION,
|
||||
INITIALIZE_MULTISIG_OPERATION,
|
||||
TRANSFER_OPERATION,
|
||||
APPROVE_OPERATION,
|
||||
REVOKE_OPERATION,
|
||||
SET_AUTHORITY_OPERATION,
|
||||
MINT_TO_OPERATION,
|
||||
BURN_OPERATION,
|
||||
CLOSE_ACCOUNT_OPERATION,
|
||||
FREEZE_ACCOUNT_OPERATION,
|
||||
THAW_ACCOUNT_OPERATION,
|
||||
TRANSFER_CHECKED_OPERATION,
|
||||
APPROVE_CHECKED_OPERATION,
|
||||
MINT_TO_CHECKED_OPERATION,
|
||||
BURN_CHECKED_OPERATION,
|
||||
SYNC_NATIVE_OPERATION,
|
||||
GET_ACCOUNT_DATA_SIZE_OPERATION,
|
||||
INITIALIZE_IMMUTABLE_OWNER_OPERATION,
|
||||
AMOUNT_TO_UI_AMOUNT_OPERATION,
|
||||
UI_AMOUNT_TO_AMOUNT_OPERATION,
|
||||
WITHDRAW_EXCESS_LAMPORTS_OPERATION,
|
||||
UNWRAP_LAMPORTS_OPERATION,
|
||||
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_executor_spl_token/intent/SplTokenAmount.ts"
|
||||
)]
|
||||
pub struct SplTokenAmount(
|
||||
/// 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_executor_spl_token/intent/SplTokenAuthority.ts"
|
||||
)]
|
||||
pub struct SplTokenAuthority {
|
||||
/// Authority account. It signs only when `multisig_signers` is empty.
|
||||
pub authority: kb_model::Pubkey,
|
||||
/// Ordered multisig signer occurrences. Duplicate metas remain ordered.
|
||||
pub multisig_signers: std::vec::Vec<kb_model::Pubkey>,
|
||||
}
|
||||
|
||||
/// 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_executor_spl_token/intent/SplTokenAuthorityType.ts"
|
||||
)]
|
||||
pub enum SplTokenAuthorityType {
|
||||
/// 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_executor_spl_token/intent/SplTokenSingleOperation.ts"
|
||||
)]
|
||||
pub enum SplTokenSingleOperation {
|
||||
/// Initialize a mint with the current no-Rent builder.
|
||||
InitializeMint {
|
||||
/// Mint account already created for the classic Token program.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Mint authority.
|
||||
mint_authority: kb_model::Pubkey,
|
||||
/// Optional freeze authority.
|
||||
freeze_authority: std::option::Option<kb_model::Pubkey>,
|
||||
/// 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: kb_model::Pubkey,
|
||||
/// Mint associated with the account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Token-account owner.
|
||||
owner: kb_model::Pubkey,
|
||||
},
|
||||
/// Initialize a multisig with the current no-Rent builder.
|
||||
InitializeMultisig {
|
||||
/// Multisig account already created for the classic Token program.
|
||||
multisig: kb_model::Pubkey,
|
||||
/// Ordered member accounts.
|
||||
members: std::vec::Vec<kb_model::Pubkey>,
|
||||
/// Required member threshold.
|
||||
threshold: u8,
|
||||
},
|
||||
/// Transfer a raw token amount without carrying the mint or decimals in the wire.
|
||||
Transfer {
|
||||
/// Source token account.
|
||||
source: kb_model::Pubkey,
|
||||
/// Destination token account.
|
||||
destination: kb_model::Pubkey,
|
||||
/// Owner, delegate or multisig authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
/// Exact raw token amount.
|
||||
amount: crate::SplTokenAmount,
|
||||
},
|
||||
/// Approve a delegate for a raw token allowance.
|
||||
Approve {
|
||||
/// Source token account.
|
||||
source: kb_model::Pubkey,
|
||||
/// Delegate account.
|
||||
delegate: kb_model::Pubkey,
|
||||
/// Owner or multisig authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
/// Exact raw allowance.
|
||||
amount: crate::SplTokenAmount,
|
||||
},
|
||||
/// Revoke the current delegate.
|
||||
Revoke {
|
||||
/// Source token account.
|
||||
source: kb_model::Pubkey,
|
||||
/// Owner or multisig authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
},
|
||||
/// Change or revoke an authority.
|
||||
SetAuthority {
|
||||
/// Mint or token account whose authority changes.
|
||||
owned: kb_model::Pubkey,
|
||||
/// Authority domain.
|
||||
authority_type: crate::SplTokenAuthorityType,
|
||||
/// New authority, or `None` to revoke it.
|
||||
new_authority: std::option::Option<kb_model::Pubkey>,
|
||||
/// Current simple or multisig authority.
|
||||
current_authority: crate::SplTokenAuthority,
|
||||
},
|
||||
/// Mint a raw token amount.
|
||||
MintTo {
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Destination token account.
|
||||
destination: kb_model::Pubkey,
|
||||
/// Mint authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
/// Exact raw amount.
|
||||
amount: crate::SplTokenAmount,
|
||||
},
|
||||
/// Burn a raw token amount.
|
||||
Burn {
|
||||
/// Source token account.
|
||||
source: kb_model::Pubkey,
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Owner, delegate or multisig authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
/// Exact raw amount.
|
||||
amount: crate::SplTokenAmount,
|
||||
},
|
||||
/// Close a token account.
|
||||
CloseAccount {
|
||||
/// Token account to close.
|
||||
account: kb_model::Pubkey,
|
||||
/// Lamport destination.
|
||||
destination: kb_model::Pubkey,
|
||||
/// Owner or close authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
},
|
||||
/// Freeze a token account.
|
||||
FreezeAccount {
|
||||
/// Token account to freeze.
|
||||
account: kb_model::Pubkey,
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Freeze authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
},
|
||||
/// Thaw a token account.
|
||||
ThawAccount {
|
||||
/// Token account to thaw.
|
||||
account: kb_model::Pubkey,
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Freeze authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
},
|
||||
/// Transfer while checking an explicit mint and decimals value.
|
||||
TransferChecked {
|
||||
/// Source token account.
|
||||
source: kb_model::Pubkey,
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Destination token account.
|
||||
destination: kb_model::Pubkey,
|
||||
/// Owner, delegate or multisig authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
/// Exact raw amount.
|
||||
amount: crate::SplTokenAmount,
|
||||
/// Expected mint decimals.
|
||||
decimals: u8,
|
||||
},
|
||||
/// Approve a delegate while checking an explicit mint and decimals value.
|
||||
ApproveChecked {
|
||||
/// Source token account.
|
||||
source: kb_model::Pubkey,
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Delegate account.
|
||||
delegate: kb_model::Pubkey,
|
||||
/// Owner or multisig authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
/// Exact raw allowance.
|
||||
amount: crate::SplTokenAmount,
|
||||
/// Expected mint decimals.
|
||||
decimals: u8,
|
||||
},
|
||||
/// Mint while checking explicit decimals.
|
||||
MintToChecked {
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Destination token account.
|
||||
destination: kb_model::Pubkey,
|
||||
/// Mint authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
/// Exact raw amount.
|
||||
amount: crate::SplTokenAmount,
|
||||
/// Expected mint decimals.
|
||||
decimals: u8,
|
||||
},
|
||||
/// Burn while checking explicit decimals.
|
||||
BurnChecked {
|
||||
/// Source token account.
|
||||
source: kb_model::Pubkey,
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Owner, delegate or multisig authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
/// Exact raw amount.
|
||||
amount: crate::SplTokenAmount,
|
||||
/// Expected mint decimals.
|
||||
decimals: u8,
|
||||
},
|
||||
/// Synchronize a wrapped-SOL account.
|
||||
SyncNative {
|
||||
/// Wrapped-SOL token account.
|
||||
account: kb_model::Pubkey,
|
||||
/// 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: kb_model::Pubkey,
|
||||
},
|
||||
/// Build the classic-program immutable-owner compatibility no-op.
|
||||
InitializeImmutableOwner {
|
||||
/// Token account being prepared.
|
||||
account: kb_model::Pubkey,
|
||||
},
|
||||
/// Convert a raw amount using mint decimals through return data.
|
||||
AmountToUiAmount {
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Exact raw amount.
|
||||
amount: crate::SplTokenAmount,
|
||||
},
|
||||
/// Convert a UI amount using mint decimals through return data.
|
||||
UiAmountToAmount {
|
||||
/// Mint account.
|
||||
mint: kb_model::Pubkey,
|
||||
/// Exact UTF-8 UI amount.
|
||||
ui_amount: std::string::String,
|
||||
},
|
||||
/// Withdraw lamports above rent exemption.
|
||||
WithdrawExcessLamports {
|
||||
/// Program-owned source account.
|
||||
account: kb_model::Pubkey,
|
||||
/// Lamport destination.
|
||||
destination: kb_model::Pubkey,
|
||||
/// Owner or multisig authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
},
|
||||
/// Unwrap some or all lamports from a wrapped-SOL account.
|
||||
UnwrapLamports {
|
||||
/// Wrapped-SOL token account.
|
||||
account: kb_model::Pubkey,
|
||||
/// Lamport destination.
|
||||
destination: kb_model::Pubkey,
|
||||
/// Owner or multisig authority.
|
||||
authority: crate::SplTokenAuthority,
|
||||
/// Exact lamports to unwrap, or `None` for the complete balance.
|
||||
amount_lamports: std::option::Option<crate::SplTokenAmount>,
|
||||
},
|
||||
}
|
||||
|
||||
impl crate::SplTokenSingleOperation {
|
||||
/// Returns the stable operation code.
|
||||
pub fn operation_code(&self) -> &'static str {
|
||||
return match self {
|
||||
Self::InitializeMint { .. } => crate::INITIALIZE_MINT_OPERATION,
|
||||
Self::InitializeAccount { .. } => crate::INITIALIZE_ACCOUNT_OPERATION,
|
||||
Self::InitializeMultisig { .. } => crate::INITIALIZE_MULTISIG_OPERATION,
|
||||
Self::Transfer { .. } => crate::TRANSFER_OPERATION,
|
||||
Self::Approve { .. } => crate::APPROVE_OPERATION,
|
||||
Self::Revoke { .. } => crate::REVOKE_OPERATION,
|
||||
Self::SetAuthority { .. } => crate::SET_AUTHORITY_OPERATION,
|
||||
Self::MintTo { .. } => crate::MINT_TO_OPERATION,
|
||||
Self::Burn { .. } => crate::BURN_OPERATION,
|
||||
Self::CloseAccount { .. } => crate::CLOSE_ACCOUNT_OPERATION,
|
||||
Self::FreezeAccount { .. } => crate::FREEZE_ACCOUNT_OPERATION,
|
||||
Self::ThawAccount { .. } => crate::THAW_ACCOUNT_OPERATION,
|
||||
Self::TransferChecked { .. } => crate::TRANSFER_CHECKED_OPERATION,
|
||||
Self::ApproveChecked { .. } => crate::APPROVE_CHECKED_OPERATION,
|
||||
Self::MintToChecked { .. } => crate::MINT_TO_CHECKED_OPERATION,
|
||||
Self::BurnChecked { .. } => crate::BURN_CHECKED_OPERATION,
|
||||
Self::SyncNative { .. } => crate::SYNC_NATIVE_OPERATION,
|
||||
Self::GetAccountDataSize { .. } => crate::GET_ACCOUNT_DATA_SIZE_OPERATION,
|
||||
Self::InitializeImmutableOwner { .. } => crate::INITIALIZE_IMMUTABLE_OWNER_OPERATION,
|
||||
Self::AmountToUiAmount { .. } => crate::AMOUNT_TO_UI_AMOUNT_OPERATION,
|
||||
Self::UiAmountToAmount { .. } => crate::UI_AMOUNT_TO_AMOUNT_OPERATION,
|
||||
Self::WithdrawExcessLamports { .. } => crate::WITHDRAW_EXCESS_LAMPORTS_OPERATION,
|
||||
Self::UnwrapLamports { .. } => crate::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_executor_spl_token/intent/SplTokenOperation.ts"
|
||||
)]
|
||||
pub enum SplTokenOperation {
|
||||
/// Build one non-batch instruction.
|
||||
Instruction {
|
||||
/// Exact instruction arguments.
|
||||
value: crate::SplTokenSingleOperation,
|
||||
},
|
||||
/// Build one Batch from ordered, non-batch child operations.
|
||||
Batch {
|
||||
/// Ordered child operations. Nested Batch is impossible in this contract.
|
||||
instructions: std::vec::Vec<crate::SplTokenSingleOperation>,
|
||||
},
|
||||
}
|
||||
|
||||
impl crate::SplTokenOperation {
|
||||
/// 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::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_executor_spl_token/intent/SplTokenExecutionIntent.ts"
|
||||
)]
|
||||
pub struct SplTokenExecutionIntent {
|
||||
/// Stable caller-provided identifier used for logs and replay correlation.
|
||||
pub intent_id: std::string::String,
|
||||
/// Transaction fee payer.
|
||||
pub fee_payer: kb_model::Pubkey,
|
||||
/// Conservative execution policy. Its default is simulation-only.
|
||||
pub policy: kb_execution_api::ExecutionPolicy,
|
||||
/// Typed classic Token operation.
|
||||
pub operation: crate::SplTokenOperation,
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// file: kb_executor_spl_token/src/lib.rs
|
||||
// version: 8
|
||||
|
||||
//! Executor crate for `spl_token`.
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod builder;
|
||||
mod constants;
|
||||
mod executor;
|
||||
mod intent;
|
||||
|
||||
/// Crate-root access to `build_prepared_plan` from `builder`.
|
||||
pub(crate) use crate::builder::build_prepared_plan;
|
||||
/// Maximum aggregate account-meta occurrences accepted by one Batch plan.
|
||||
pub(crate) use crate::constants::MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum number of children accepted by one Batch plan.
|
||||
pub(crate) use crate::constants::MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum number of ordered multisig signer occurrences.
|
||||
pub(crate) use crate::constants::MAX_MULTISIG_SIGNERS;
|
||||
/// Maximum UI amount string length accepted before transaction assembly.
|
||||
pub(crate) use crate::constants::MAX_UI_AMOUNT_BYTES;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) use crate::constants::TRACING_TARGET;
|
||||
|
||||
/// Exposes the typed SPL Token executor.
|
||||
pub use crate::executor::SplTokenExecutor;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::AMOUNT_TO_UI_AMOUNT_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::APPROVE_CHECKED_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::APPROVE_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::BATCH_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::BURN_CHECKED_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::BURN_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::CLOSE_ACCOUNT_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::FREEZE_ACCOUNT_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::GET_ACCOUNT_DATA_SIZE_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::INITIALIZE_ACCOUNT_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::INITIALIZE_IMMUTABLE_OWNER_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::INITIALIZE_MINT_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::INITIALIZE_MULTISIG_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::MINT_TO_CHECKED_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::MINT_TO_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::REVOKE_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::SET_AUTHORITY_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::SUPPORTED_OPERATION_CODES;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::SYNC_NATIVE_OPERATION;
|
||||
/// Exposes the exact raw amount string contract.
|
||||
pub use crate::intent::SplTokenAmount;
|
||||
/// Exposes the typed authority contract.
|
||||
pub use crate::intent::SplTokenAuthority;
|
||||
/// Exposes the typed authority-kind contract.
|
||||
pub use crate::intent::SplTokenAuthorityType;
|
||||
/// Exposes the complete typed execution intent.
|
||||
pub use crate::intent::SplTokenExecutionIntent;
|
||||
/// Exposes the top-level operation contract.
|
||||
pub use crate::intent::SplTokenOperation;
|
||||
/// Exposes the non-batch operation contract.
|
||||
pub use crate::intent::SplTokenSingleOperation;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::THAW_ACCOUNT_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::TRANSFER_CHECKED_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::TRANSFER_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::UI_AMOUNT_TO_AMOUNT_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::UNWRAP_LAMPORTS_OPERATION;
|
||||
/// Exposes stable operation codes for generic executor dispatch.
|
||||
pub use crate::intent::WITHDRAW_EXCESS_LAMPORTS_OPERATION;
|
||||
Reference in New Issue
Block a user