dWallet Execution
How AURA uses Ika dWallet records to co-sign transactions across 6 chains without exposing a private key.
Why dWallet
Giving an AI agent a raw private key means the key can be extracted from memory, a compromised agent can drain the wallet instantly, and there is no on-chain record of what was authorized. Ika dWallet records solve all three: the agent never sees the key, every co-sign request is gated by the AURA program, and the audit trail lives on Solana.
Supported Chains
Chain IDs are u8 values used in ProposeTransactionArgs.target_chain and RegisterDwalletArgs.chain:
| ID | Chain | Notes |
|---|---|---|
| 0 | Bitcoin | bitcoin_manual_review_threshold_usd applies |
| 1 | Ethereum | EVM-compatible |
| 2 | Solana | Native; no bridge |
| 3 | Polygon | EVM-compatible |
| 4 | Arbitrum | EVM-compatible |
| 5 | Optimism | EVM-compatible |
These map to the Chain enum in aura-policy: Bitcoin, Ethereum, Solana, Polygon, Arbitrum, Optimism.
Registering a dWallet
Before any proposal can be approved for a chain, a DwalletRecord must be registered against the treasury. One record per chain.
// Register an Ethereum dWallet (owner signs)
await aura.dwallet.register({
treasury,
chain: 1, // Ethereum
dwalletId: "dwallet-abc123", // from Ika dWallet provisioning
address: "0xYourEthAddress",
balanceUsd: 5_000, // USD cents — balance hint
});RegisterDwalletArgs also accepts optional fields used in the full CPI path:
| Field | Type | Description |
|---|---|---|
chain | number | Chain ID 0–5 |
dwalletId | string | Unique dWallet identifier from Ika |
address | string | Native address on the target chain |
balanceUsd | BN | Current balance hint in USD cents |
dwalletAccount | PublicKey | null | Ika dWallet account (for CPI path) |
authorizedUserPubkey | Uint8Array | null | Authorized user public key bytes |
messageMetadataDigest | Uint8Array | null | 32-byte metadata digest |
publicKeyHex | string | null | Raw dWallet public key hex |
timestamp | BN | Registration timestamp |
Execution Flow
For legacy proposals with no chain binding, finalize_execution keeps the original behavior and closes the
proposal once the signature is verified. For chain-bound proposals, settlement is explicit: the signed digest
commits to replay-protection fields, mark_settlement_broadcast records the target-chain tx hash after relay,
and confirm_settlement is the point where wallet reservations, scheduled intent counters, and policy counters
are finally applied. Failed or reorged broadcasts can be resubmit_proposal with fresh replay fields or
abandon_proposal to release the reservation.
Chain Binding and native_message_hash
Chain-bound proposals require native_message_hash in ProposeTransactionArgs.
This is the 32-byte Keccak-256 digest of the exact bytes Ika will sign — not
a digest of the AURA audit string.
native_message_hash = keccak256(exact bytes passed to Ika requestSign)For Solana, this is keccak256(compiled_transaction_message_bytes). For EVM and
Bitcoin, it is the digest of the native transaction/sighash payload the caller
constructed. When native_message_hash is absent (legacy proposals), the
program falls back to keccak256(canonical_audit_message_string).
The canonical chain message string also includes a bind_native_message=<hex>
segment when the field is set, making the binding auditable on-chain.
Chain-bound proposal validation requires native_message_hash to be present.
Solana proposals additionally require solana_recent_blockhash. The separate
solana_message_hash field is a Solana-specific trace/audit field and is not
required for validation.
For the TypeScript SDK helpers that drive the full approval and broadcast flow, see the dWallet Execution guide.
execute_pending Accounts
| Account | Seed / Source | Purpose |
|---|---|---|
operator | signer | Drives execution lifecycle |
treasury | ["treasury", owner, agentId] | Treasury PDA |
message_approval | derived on dWallet program | Stores co-signed bytes |
dwallet | dWallet account from Ika | The co-signer |
caller_program | aura-core program ID | CPI caller identity |
cpi_authority | ["__ika_cpi_authority"] on aura-core | Signs the CPI |
dwallet_program | 87W54kGYFQ1rgWqMeu4XTPHWXWmXSQCcjm8vCTfiq1oY | Ika dWallet program |
dwallet_coordinator | dWallet coordinator account | Network coordinator |
external_liveness | ["external_liveness", treasury] | Optional freshness check |
system_program | 11111111111111111111111111111111 | Account creation |
MessageApproval digest selection
build_message_approval_request uses native_message_hash from the proposal
binding when present and falls back to keccak256(audit_message_string) for
legacy unbound proposals:
| Proposal type | Digest used for approve_message |
|---|---|
| Legacy (no binding) | keccak256(canonical_audit_string) |
| Chain-bound | native_message_hash from ChainExecutionBinding |
This means the MessageApproval PDA address changes depending on which digest
path is taken — the PDA is derived from whichever digest was passed to
approve_message.
MessageApproval PDA Derivation
The MessageApproval PDA is derived on the Ika dWallet program, not on aura-core. The SDK exports deriveMessageApprovalAddress which mirrors aura-core::find_message_approval_pda:
import { deriveMessageApprovalAddress } from "@aura-protocol/sdk-ts";
const [messageApproval] = deriveMessageApprovalAddress(
DWALLET_DEVNET_PROGRAM_ID,
curveCode, // 0=secp256k1, 1=secp256r1, 2=ed25519, 3=ristretto
publicKeyBytes, // raw dWallet public key bytes
signatureSchemeCode, // Ika signature scheme code
messageDigest, // 32-byte Keccak-256 digest
messageMetadataDigest, // optional 32-byte metadata digest
);Seeds (in order):
b"dwallet"u16_le(curveCode) ++ publicKey— chunked into 32-byte segmentsb"message_approval"u16_le(signatureSchemeCode)messageDigest(32 bytes)messageMetadataDigest(32 bytes, only if non-zero)
Bitcoin Manual Review
Bitcoin transactions above bitcoin_manual_review_threshold_usd (default: 5_000 USD cents = $50) are blocked by ViolationCode::BitcoinManualReview in evaluate_public_precheck. A guardian must approve via approve_pending_execution before execute_pending can proceed.
finalize_execution Accounts
| Account | Purpose |
|---|---|
operator | Signer driving finalization |
treasury | Treasury PDA |
message_approval | MessageApproval PDA with co-signed bytes |
swarm_pool | Optional — updated after finalization if swarm is configured |
budget_envelope | Optional — updated after finalization if envelope is configured |
exposure_group | Optional — updated after finalization if group is configured |
external_liveness | Optional — freshness check if liveness_config requires it |
confirm_settlement Accounts
| Account | Purpose |
|---|---|
operator | Owner or AI authority confirming the target-chain settlement |
treasury | Treasury PDA containing the signed pending proposal |
swarm_pool | Optional - updated when the proposal consumes shared swarm budget |
budget_envelope | Optional - updated when the proposal consumes a scoped budget |
exposure_group | Optional - updated when the proposal counts against a cross-treasury exposure group |
dwallet_state | Optional - required when the proposal reserved a chain-native asset transfer |
scheduled_intent | Optional - settled when the signed proposal was promoted from a scheduled intent |
ChainProfileAccount
Custom chain codes require a ChainProfileAccount at [b"chain_profile", chain_code]. The profile stores
address format, replay scheme, finality model, curve/scheme, native gas asset, optional EVM chain id, and
required confirmation depth. Built-in chains have defaults, but a profile may still be supplied to override
settlement or replay policy.
Security Properties
| Property | Guarantee |
|---|---|
| Key never exposed | Agent receives signed bytes, not the private key |
| Authorization gated | Only aura-core's cpi_authority PDA can request co-signs |
| Per-chain isolation | Each chain has its own DwalletRecord; chain-bound proposals also bind native replay fields |
| Settlement gate | Chain-bound proposals are not counted as settled until confirm_settlement reaches the required confirmations |
| Failure handling | Reorged or failed broadcasts can be re-signed with fresh replay data or abandoned with reservations released |
| Audit trail | Every execute_pending and finalize_execution emits on-chain events |
| Revocable | Owner can deregister a dWallet record at any time |
| Liveness checks | LivenessConfig.require_dwallet_freshness can require a fresh ExternalLiveness record |