The managed Wallet is the highest-level surface in the SDKs. It wraps the Wallet SDK primitives with everything a real wallet application needs:
- Persistent, encrypted on-disk storage for keys and account metadata
- Multi-account management across BIP32 and keypair accounts
- Address derivation and discovery
- An event bus for chain notifications — balance, maturity, reorg
- Built-in send, transfer, and sweep flows
- Transaction history tracking
If you don’t want any of that — a custom signer, an indexer, a watch-only service, a hot-path process that keeps everything in memory — skip this and use the Wallet SDK directly.
Architecture
Four cooperating pieces:
| Component | Job |
|---|---|
| Wallet | Lifecycle, on-disk file storage, account list, event multiplexer. The object your code talks to. |
| RpcClient | The wRPC connection. Used internally for calls and as the source of node-pushed notifications. |
| UtxoProcessor | Subscribes to virtual-chain and UTXO notifications, tracks synced state, routes UTXO changes to the right context. |
| UtxoContext | One per activated account. Holds tracked addresses, per-state balance (mature, pending, outgoing), and the mature UTXO set the coin selector pulls from. |
The wallet does not poll the node for UTXO state. It is fed by the processor from notifications, which is why an account that hasn’t finished syncing reports an empty UTXO set rather than an error.
Lifecycle
A wallet moves through five states, and the transitions are ordered:
Constructed ──start()──▶ Started ──connect()──▶ Connected
│
wallet_create() / wallet_open()
▼
Active ◀──accounts_activate()── Open | Step | Effect |
|---|---|
| Construct | Builds the local file store and an internal wRPC client. No I/O. |
| Start | Boots the UTXO processor, the wRPC notifier, and the event-dispatch task. |
| Connect | Connects the wRPC client to a node, via a resolver or an explicit URL. |
| Open | Decrypts and loads a wallet file; secrets become available in memory. |
| Activate | Begins UTXO tracking and event emission for the chosen accounts. |
| Close | Releases the open wallet; activated accounts stop tracking. |
| Disconnect | Drops the wRPC connection; the wallet stays started. |
| Stop | Tears down the runtime and the event task. |
Create a wallet and derive an account
End to end: open a wallet file, add key material, derive a BIP32 account, activate it, and print the receive address.
use std::sync::Arc;
use kaspa_wallet_core::prelude::*;
use kaspa_wallet_core::storage::local::interface::LocalStore;
let store = Arc::new(LocalStore::try_new(false)?);
let wallet = Arc::new(Wallet::try_new(store, Some(Resolver::default()), Some(network_id))?);
wallet.start().await?;
wallet
.clone()
.connect_call(ConnectRequest { network_id, ..Default::default() })
.await?;
let wallet_secret = Secret::from("example-secret");
wallet
.clone()
.wallet_create(
wallet_secret.clone(),
WalletCreateArgs::new(
Some("demo".into()), // title
Some("demo".into()), // filename
EncryptionKind::XChaCha20Poly1305,
None, // user hint
false, // overwrite existing storage
),
)
.await?;
// Add key material, derive a BIP32 account, then activate it.
let descriptors = wallet.clone().accounts_enumerate().await?;
let account = &descriptors[0];
wallet.clone().accounts_activate(Some(vec![account.account_id])).await?;
println!("receive address: {:?}", account.receive_address); const { Wallet, Resolver, AccountKind, setDefaultStorageFolder } = kaspa;
setDefaultStorageFolder('./wallets');
const walletSecret = 'example-secret';
const filename = 'demo';
const wallet = new Wallet({
resident: false,
networkId: 'testnet-10',
resolver: new Resolver()
});
if (!(await wallet.exists(filename))) {
await wallet.walletCreate({ walletSecret, filename, title: 'demo' });
}
await wallet.walletOpen({ walletSecret, filename, accountDescriptors: false });
await wallet.accountsEnsureDefault({ walletSecret, type: new AccountKind('bip32') });
await wallet.connect();
await wallet.start();
const { accountDescriptors } = await wallet.accountsEnumerate({});
const account = accountDescriptors[0];
await wallet.accountsActivate({ accountIds: [account.accountId] });
console.log('receive address:', account.receiveAddress); import asyncio
from kaspa import Mnemonic, PrvKeyDataVariantKind, Resolver, Wallet
async def main():
wallet = Wallet(network_id="testnet-10", resolver=Resolver())
await wallet.start()
await wallet.connect(strategy="fallback", timeout_duration=5_000)
while not wallet.is_synced:
await asyncio.sleep(0.5)
await wallet.wallet_create(
wallet_secret="example-secret",
filename="demo",
title="demo",
)
prv_key_id = await wallet.prv_key_data_create(
wallet_secret="example-secret",
secret=Mnemonic.random().phrase,
kind=PrvKeyDataVariantKind.Mnemonic,
)
account = await wallet.accounts_create_bip32(
wallet_secret="example-secret",
prv_key_data_id=prv_key_id,
account_index=0,
)
await wallet.accounts_activate([account.account_id])
print("receive address:", account.receive_address)
await wallet.wallet_close()
await wallet.disconnect()
await wallet.stop()
asyncio.run(main())The three SDKs order create/open and connect/start slightly differently, and the JavaScript surface is request-object based where Python uses keyword arguments — but the state machine underneath is identical.
Re-running against an existing file raises “wallet already exists” on create. Switch to open, or pass the overwrite flag.
Sending
The managed wallet wraps the transaction generator, so a send is one call — it selects UTXOs from the account’s context, computes mass and fees, signs with the account’s keys, and submits:
let summary = wallet
.clone()
.accounts_send(AccountsSendRequest {
wallet_secret,
payment_secret: None,
account_id,
destination: PaymentOutputs::from((destination_address, kaspa_to_sompi(1.5))).into(),
fee_rate: None,
priority_fee_sompi: Fees::SenderPays(kaspa_to_sompi(0.001)),
payload: None,
})
.await?;
println!("{:?}", summary); const sendResult = await wallet.accountsSend({
walletSecret,
accountId: account.accountId,
priorityFeeSompi: kaspaToSompi('0.001'),
destination: [{ address: destinationAddress, amount: kaspaToSompi('1.5') }]
});
console.log(sendResult); from kaspa import Fees, FeeSource, PaymentOutput
result = await wallet.accounts_send(
wallet_secret="example-secret",
account_id=account.account_id,
priority_fee_sompi=Fees(100_000, FeeSource.SenderPays),
payment_secret=None,
fee_rate=None,
payload=None,
destination=[PaymentOutput(destination_address, 150_000_000)], # 1.5 KAS
)
print(result.final_transaction_id, result.fees)There are matching calls for transfer (move funds between accounts in the same wallet, without a chain transaction where possible) and sweep (consolidate an account’s UTXOs into a single output).
Events
The wallet is an event emitter, and this is how you observe everything asynchronous — balance changes, transaction maturity, sync progress, reorgs.
wallet.addEventListener(({ type, data }) => {
switch (type) {
case 'balance':
console.log(data.id, sompiToKaspaString(data.balance.mature));
break;
case 'maturity':
console.log('confirmed:', data.id);
break;
case 'reorg':
console.log('reorged out:', data.id);
break;
}
}); def on_event(event):
if event["type"] == "balance":
print(event["data"]["balance"]["mature"])
elif event["type"] == "maturity":
print("confirmed:", event["data"]["id"])
wallet.add_event_listener("all", on_event) # or a specific WalletEventTypeThe events that matter most:
balance— an account’s mature / pending / outgoing balance changed.maturity— a transaction is confirmed past the maturity threshold and its outputs are spendable. This is the correct confirmation gate, not a fixed sleep and not first appearance in a block.reorg— a previously accepted transaction was reorged out; its outputs are no longer mature.sync-state/server-status— the processor’s progress catching up to the node.daa-score-change— every DAA score tick. High volume; filter it.
In Rust, the same stream arrives on the wallet’s Events multiplexer channel rather than through a callback.
Sync state
Two different things are called “synced” and they are not the same:
- Node IBD — whether the node itself has caught up with the network. Read it from the server-info response.
- Processor readiness — whether the wallet’s UTXO processor has finished loading state for the activated accounts.
Balances read before the processor is ready are incomplete, and an account’s UTXO set legitimately returns empty. Wait for the synced flag or the sync-state event before trusting a balance.
Where to next
- Wallet SDK — the primitives underneath, for when you don’t want a managed wallet.
- Transactions — what
accounts_senddoes internally. - Networks — account addresses are network-specific.
- Python SDK wallet docs — the deepest per-method reference for the managed wallet today, covering wallet files, private keys, accounts, sweep, and transaction history.