Problem
spel exposes spel::tx::execute_instruction(...), but the function is still CLI-shaped:
- errors call
process::exit(1) instead of returning Result
- output is written directly to stdout/stderr
- wallet access is forced through
WalletCore::from_env()
- transaction resolution, public/private transaction build, submit, dry-run rendering, and confirmation are coupled
That makes it hard to reuse SPEL from Rust integration/e2e tests or from another application. Tests need the same IDL-driven account/argument/PDA behavior as the CLI, but they also need structured errors, controlled wallet construction, and no process termination.
Current entry points involved:
spel-cli/src/tx.rs::execute_instruction
spel-cli/src/lib.rs::run
Request
Expose a library-grade API for IDL-driven public and private transactions.
The API should be intentionally small, but public and private
transactions should stay explicit. Suggested public surface:
use wallet::AccountIdentity;
pub struct SpelInstructionRequest<'a> {
pub idl: &'a SpelIdl,
pub instruction: &'a str,
pub accounts: BTreeMap<String, Vec<AccountIdentity>>,
pub args: BTreeMap<String, String>,
pub extra_bins: BTreeMap<String, PathBuf>,
}
pub struct ResolvedPublicInstruction {
pub program_id: ProgramId,
pub accounts: Vec<AccountIdentity>,
pub instruction_data: InstructionData,
}
pub struct ResolvedPrivateInstruction {
pub program: ProgramWithDependencies,
pub accounts: Vec<AccountIdentity>,
pub instruction_data: InstructionData,
}
pub enum SpelTxError {
UnknownInstruction { instruction: String },
MissingInput { name: String },
UnexpectedInputCount { name: String, expected: &'static str, actual: usize },
ArgumentParse { name: String, source: Box<dyn std::error::Error + Send + Sync> },
AccountParse { name: String, source: Box<dyn std::error::Error + Send + Sync> },
PdaResolution { account: String, source: Box<dyn std::error::Error + Send + Sync> },
ProgramLoad { path: PathBuf, source: Box<dyn std::error::Error + Send + Sync> },
InstructionSerialization { source: Box<dyn std::error::Error + Send + Sync> },
Wallet { source: Box<dyn std::error::Error + Send + Sync> },
TransactionBuild { source: Box<dyn std::error::Error + Send + Sync> },
Sequencer { source: Box<dyn std::error::Error + Send + Sync> },
Confirmation { source: Box<dyn std::error::Error + Send + Sync> },
}
pub fn resolve_public_instruction(
request: SpelInstructionRequest<'_>,
program_id: ProgramId,
) -> Result<ResolvedPublicInstruction, SpelTxError>;
pub fn resolve_private_instruction(
request: SpelInstructionRequest<'_>,
program: ProgramWithDependencies,
) -> Result<ResolvedPrivateInstruction, SpelTxError>;
pub async fn build_public_transaction(
resolved: ResolvedPublicInstruction,
wallet: &WalletCore,
) -> Result<PublicTransaction, SpelTxError>;
pub async fn build_private_transaction(
resolved: ResolvedPrivateInstruction,
wallet: &WalletCore,
) -> Result<(PrivateTransaction, Vec<SharedSecretKey>), SpelTxError>;
pub async fn submit_public_transaction(
transaction: PublicTransaction,
wallet: &WalletCore,
) -> Result<HashType, SpelTxError>;
pub async fn submit_private_transaction(
transaction: PrivateTransaction,
wallet: &WalletCore,
) -> Result<HashType, SpelTxError>;
pub async fn confirm_transaction(
tx_hash: HashType,
wallet: &WalletCore,
) -> Result<LeeTransaction, SpelTxError>;
Exact names are not important. The important part is the boundary:
- no
process::exit
- no mandatory env-based wallet lookup
- no mandatory stdout/stderr output
- keep public and private transaction paths explicit
- split pure IDL/account/PDA/instruction resolution from wallet-dependent
signing/proving/submission in each transaction path
- reuse
wallet::AccountIdentity directly for account identity, privacy, and
signing intent instead of inventing a parallel SPEL account-reference enum
- map each caller-supplied, non-PDA IDL account name to
Vec<AccountIdentity>;
require exactly one identity for fixed accounts and allow zero or more
identities for rest accounts
- preserve current CLI behavior for IDL parsing,
Public/ and Private/
account intent, PDA derivation, instruction serialization, signer discovery,
--bin-* program-id fill-ins, dry-run rendering, and private transaction
dependency binaries
- preserve variadic/rest account behavior by allowing one IDL account name to
resolve to zero or more account inputs
- preserve PDA behavior by deriving PDA accounts from IDL seeds and resolved
account/argument inputs rather than requiring callers to pass derived account
IDs manually
- keep the private resolved form carrying the main program binary plus
dependencies, not only ProgramId; private transaction proving cannot be built
from a program ID alone
- allow callers to resolve only, build only, submit only, or submit and confirm
- return structured
SpelTxError values with enough context for tests and
applications to assert on the failing input, not only on a broad category
Type justification
Each public type must earn its place:
SpelInstructionRequest is justified because it is the library boundary for
raw caller input shared by both paths: IDL, instruction name, named account
identities, typed string arguments, and dependency binaries used to auto-fill
program-id arguments.
ResolvedPublicInstruction is justified because public transactions are
identified by ProgramId, must not depend on a program binary, and should
reject private account identities before wallet build.
ResolvedPrivateInstruction is justified because private transaction proving
requires ProgramWithDependencies, can include private account identities, and
returns shared secrets during build.
SpelTxError is justified because the current API exits the process. Variants
should carry names, paths, counts, and source errors so callers can assert on
the exact failure.
Avoid public types that only mirror transient implementation steps. In
particular, do not expose a combined ResolvedSpelInstruction or
BuiltSpelTransaction enum only to dispatch back into public/private branches.
That hides materially different transaction semantics behind one generic
function. Do not expose derived account IDs, signer account IDs, nonces, or
signing keys as part of the resolution result unless a caller-facing use case
requires them; wallet AccountManager already derives those from
AccountIdentity.
Wallet build-only API
SPEL should not copy wallet transaction construction. The wallet crate already
knows how to load account pre-states, derive nonces, discover public signing
keys, handle keycard signing, prepare private accounts, create proofs, and
return shared secrets. Add build-only wallet APIs, or expose equivalent wallet
internals, so SPEL can build without submitting:
pub async fn build_pub_tx(
&self,
accounts: Vec<AccountIdentity>,
instruction_data: InstructionData,
program_id: ProgramId,
) -> Result<PublicTransaction, ExecutionFailureKind>;
pub async fn build_private_tx(
&self,
accounts: Vec<AccountIdentity>,
instruction_data: InstructionData,
program: &ProgramWithDependencies,
) -> Result<(PrivateTransaction, Vec<SharedSecretKey>), ExecutionFailureKind>;
Existing submit APIs can then delegate to build-only APIs plus sequencer
submission. SPEL can call those wallet APIs rather than reimplementing
WalletCore/AccountManager signing, proof, nonce, pre-state, and
shared-secret logic.
Metadata and dry-run rendering
Dry-run output should not force a public SpelDryRunData or metadata type
unless there is a stable caller-facing contract for it. The CLI can preserve
current text/json dry-run behavior with a formatter over SpelInstructionRequest,
the matching IDL instruction, and either resolved public/private instruction
type. If a structured dry-run type becomes public later, justify it separately by
documenting its stable fields and consumers.
SPEL should use the wallet crate's AccountIdentity surface directly unless a
crate boundary makes that impossible. If a lower-level crate cannot depend on
wallet, keep the conversion at the SPEL transaction API boundary rather than
duplicating the model.
The reused wallet cases are:
AccountIdentity::Public
AccountIdentity::PublicNoSign
AccountIdentity::PublicKeycard
AccountIdentity::PrivateOwned
AccountIdentity::PrivateForeign
AccountIdentity::PrivatePdaOwned
AccountIdentity::PrivatePdaForeign
AccountIdentity::PrivateShared
AccountIdentity::PrivatePdaShared
Why
The wallet crate already provides reusable APIs for account creation, deployment submission, public transaction submission, private transaction submission, shared-secret return values, and transaction polling. The missing reusable layer is generic IDL-driven transaction resolution plus build-only access before submission. That may require splitting wallet public/private build from submit, or making the existing wallet build internals available to SPEL without copy-pasting them. Exposing that layer would let tests and applications exercise the same public and private transaction paths as spel without spawning a subprocess.
Problem
spelexposesspel::tx::execute_instruction(...), but the function is still CLI-shaped:process::exit(1)instead of returningResultWalletCore::from_env()That makes it hard to reuse SPEL from Rust integration/e2e tests or from another application. Tests need the same IDL-driven account/argument/PDA behavior as the CLI, but they also need structured errors, controlled wallet construction, and no process termination.
Current entry points involved:
spel-cli/src/tx.rs::execute_instructionspel-cli/src/lib.rs::runRequest
Expose a library-grade API for IDL-driven public and private transactions.
The API should be intentionally small, but public and private
transactions should stay explicit. Suggested public surface:
Exact names are not important. The important part is the boundary:
process::exitsigning/proving/submission in each transaction path
wallet::AccountIdentitydirectly for account identity, privacy, andsigning intent instead of inventing a parallel SPEL account-reference enum
Vec<AccountIdentity>;require exactly one identity for fixed accounts and allow zero or more
identities for rest accounts
Public/andPrivate/account intent, PDA derivation, instruction serialization, signer discovery,
--bin-*program-id fill-ins, dry-run rendering, and private transactiondependency binaries
resolve to zero or more account inputs
account/argument inputs rather than requiring callers to pass derived account
IDs manually
dependencies, not only
ProgramId; private transaction proving cannot be builtfrom a program ID alone
SpelTxErrorvalues with enough context for tests andapplications to assert on the failing input, not only on a broad category
Type justification
Each public type must earn its place:
SpelInstructionRequestis justified because it is the library boundary forraw caller input shared by both paths: IDL, instruction name, named account
identities, typed string arguments, and dependency binaries used to auto-fill
program-id arguments.
ResolvedPublicInstructionis justified because public transactions areidentified by
ProgramId, must not depend on a program binary, and shouldreject private account identities before wallet build.
ResolvedPrivateInstructionis justified because private transaction provingrequires
ProgramWithDependencies, can include private account identities, andreturns shared secrets during build.
SpelTxErroris justified because the current API exits the process. Variantsshould carry names, paths, counts, and source errors so callers can assert on
the exact failure.
Avoid public types that only mirror transient implementation steps. In
particular, do not expose a combined
ResolvedSpelInstructionorBuiltSpelTransactionenum only to dispatch back into public/private branches.That hides materially different transaction semantics behind one generic
function. Do not expose derived account IDs, signer account IDs, nonces, or
signing keys as part of the resolution result unless a caller-facing use case
requires them; wallet
AccountManageralready derives those fromAccountIdentity.Wallet build-only API
SPEL should not copy wallet transaction construction. The wallet crate already
knows how to load account pre-states, derive nonces, discover public signing
keys, handle keycard signing, prepare private accounts, create proofs, and
return shared secrets. Add build-only wallet APIs, or expose equivalent wallet
internals, so SPEL can build without submitting:
Existing submit APIs can then delegate to build-only APIs plus sequencer
submission. SPEL can call those wallet APIs rather than reimplementing
WalletCore/AccountManagersigning, proof, nonce, pre-state, andshared-secret logic.
Metadata and dry-run rendering
Dry-run output should not force a public
SpelDryRunDataor metadata typeunless there is a stable caller-facing contract for it. The CLI can preserve
current text/json dry-run behavior with a formatter over
SpelInstructionRequest,the matching IDL instruction, and either resolved public/private instruction
type. If a structured dry-run type becomes public later, justify it separately by
documenting its stable fields and consumers.
SPEL should use the wallet crate's
AccountIdentitysurface directly unless acrate boundary makes that impossible. If a lower-level crate cannot depend on
wallet, keep the conversion at the SPEL transaction API boundary rather thanduplicating the model.
The reused wallet cases are:
AccountIdentity::PublicAccountIdentity::PublicNoSignAccountIdentity::PublicKeycardAccountIdentity::PrivateOwnedAccountIdentity::PrivateForeignAccountIdentity::PrivatePdaOwnedAccountIdentity::PrivatePdaForeignAccountIdentity::PrivateSharedAccountIdentity::PrivatePdaSharedWhy
The wallet crate already provides reusable APIs for account creation, deployment submission, public transaction submission, private transaction submission, shared-secret return values, and transaction polling. The missing reusable layer is generic IDL-driven transaction resolution plus build-only access before submission. That may require splitting wallet public/private build from submit, or making the existing wallet build internals available to SPEL without copy-pasting them. Exposing that layer would let tests and applications exercise the same public and private transaction paths as
spelwithout spawning a subprocess.