Kaspalytics Learn is a work in progress.
Updated: Aug 2, 2026

Wallet

The managed Kaspa wallet - encrypted on-disk storage, accounts, the lifecycle state machine, sending, and the event bus.

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:

ComponentJob
WalletLifecycle, on-disk file storage, account list, event multiplexer. The object your code talks to.
RpcClientThe wRPC connection. Used internally for calls and as the source of node-pushed notifications.
UtxoProcessorSubscribes to virtual-chain and UTXO notifications, tracks synced state, routes UTXO changes to the right context.
UtxoContextOne 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
StepEffect
ConstructBuilds the local file store and an internal wRPC client. No I/O.
StartBoots the UTXO processor, the wRPC notifier, and the event-dispatch task.
ConnectConnects the wRPC client to a node, via a resolver or an explicit URL.
OpenDecrypts and loads a wallet file; secrets become available in memory.
ActivateBegins UTXO tracking and event emission for the chosen accounts.
CloseReleases the open wallet; activated accounts stop tracking.
DisconnectDrops the wRPC connection; the wallet stays started.
StopTears 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 WalletEventType

The 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_send does 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.
Kaspalytics strives to provide accurate data - our highest level of effort is given to data validation and maintenance. However, we cannot guarantee 100% accuracy. Data is subject to inaccuracies and change.

Contact | © 2026 Kaspalytics