phase 2.1-2.4: send path — submit + status, txbuilder, wallet.send, wallet.tx_status

chain backend grew submit_tx (POST /submittx, raw cbor body) and
tx_status (POST /tx_info → Confirmed{block,epoch}|NotFound). serde
tag-based status enum so the mcp tool returns clean json.

new core::tx module: ProtocolParams + InputUtxo + build_signed_payment.
two-pass fee refinement — build unsigned, measure size, add witness
overhead constant (128 bytes for vkey+sig+cbor framing), recompute
real fee, build with final fee, sign once (PrivateKey doesn't impl
Clone in pallas-wallet, so we don't double-sign). change below
min-utxo merges into fee instead of emitting dust.

added pallas-txbuilder + pallas-wallet 0.32 deps. PaymentKey gains
crate-private xprv() accessor; payment_key_to_private converts
ed25519-bip32 XPrv → pallas-wallet PrivateKey::Extended via the
64-byte extended secret bytes.

mcp tools.rs: 4 → 6 tools.
- wallet.send (to_address, lovelace, force) with hard-cap guard
- wallet.tx_status (tx_hash) → status json
SendArgs/TxStatusArgs use schemars derive so rmcp generates proper
input schemas. config.rs adds max_send_lovelace (default 100 ADA,
ALDABRA_MAX_SEND_LOVELACE env override).

37 unit tests. mcp tools/list smoke confirms all 6 tools register
with correct schemas (force defaults false, lovelace required uint64,
to_address required string).

phase 2.5 (native-asset send), 2.6 (cold-sign offline mode), and
2.7 (real preprod smoke against a funded wallet) still open.
This commit is contained in:
Sulkta 2026-05-04 11:18:33 -07:00
parent 2b4fdff0c0
commit 44bae07bc9
11 changed files with 821 additions and 29 deletions

View file

@ -55,11 +55,39 @@ pub struct Balance {
pub assets: std::collections::BTreeMap<String, u64>,
}
/// Confirmation status of a submitted transaction.
///
/// `NotFound` covers two cases the chain backend can't easily
/// distinguish: the tx is in some mempool but not yet indexed by
/// Koios, or it was never submitted / was rejected. Treat
/// `NotFound` as "keep polling" up to a reasonable timeout, after
/// which give up.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum TxStatus {
/// Confirmed on-chain. `block_height` and `epoch` are populated
/// when Koios returns them.
Confirmed {
block_height: Option<u64>,
epoch: Option<u64>,
},
/// Not (yet) seen by the chain backend.
NotFound,
}
#[async_trait::async_trait]
pub trait ChainBackend: Send + Sync {
/// All UTXOs at the given address.
async fn get_utxos(&self, address: &str) -> Result<Vec<Utxo>, ChainError>;
/// Aggregated ADA + native-asset balance at the address.
async fn get_balance(&self, address: &str) -> Result<Balance, ChainError>;
// Phase 2:
// async fn submit_tx(&self, raw_tx_cbor: &[u8]) -> Result<String, ChainError>;
// async fn tx_status(&self, tx_hash: &str) -> Result<TxStatus, ChainError>;
/// Submit a signed transaction. `raw_tx_cbor` is the binary CBOR
/// payload of the signed tx; on success returns the tx hash as
/// a hex string.
async fn submit_tx(&self, raw_tx_cbor: &[u8]) -> Result<String, ChainError>;
/// Poll the chain backend for a tx's current confirmation status.
async fn tx_status(&self, tx_hash: &str) -> Result<TxStatus, ChainError>;
}