dWallet Execution
How AURA uses Ika dWallet registrations and runtime sidecars 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 dWallets 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, the treasury must have an inline DWalletRecord registration for that chain. Asset-aware transfers also use the per-chain DWalletAccount runtime sidecar for limits, balances, and reservations.
import BN from "bn.js";
import { instructions } from "@aura-protocol/sdk-ts";
await instructions.dwallet.sendRegisterDwallet(client, owner, {
accounts: { owner: owner.publicKey, treasury },
args: {
chain: 1, // Ethereum
dwalletId: "dwallet-abc123", // from Ika dWallet provisioning
address: "0xYourEthAddress",
balanceUsd: new BN(5_000), // USD cents — balance hint
dwalletAccount: null,
authorizedUserPubkey: null,
messageMetadataDigest: null,
publicKeyHex: null,
timestamp: new BN(Math.floor(Date.now() / 1000)),
},
});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 denied by ViolationCode::BitcoinManualReview in the policy engine. This is not an approval-ladder requirement: approve_pending_execution can satisfy guardian/multisig/timelock levels on approved proposals, but it does not turn a denied BitcoinManualReview decision into an executable proposal.
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 inline DWalletRecord and optional DWalletAccount sidecar; 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 | Treasury sync emits audit/proposal events; optional analytics/activity sidecars capture finalization when supplied |
| Revocable | Owner can deregister a dWallet record at any time |
| Liveness checks | LivenessConfig.require_dwallet_freshness can require a fresh ExternalLiveness record |