@asichain/asi-wallet-sdk
v2.0.0
Published
ASI Wallet SDK
Downloads
197
Keywords
Readme
ASI Chain: Wallet SDK
Part of the Artificial Superintelligence Alliance ecosystem
Uniting Fetch.ai, SingularityNET, and CUDOS
Table of Contents
- Overview
- Key Features
- Installation
- Quick Start
- Architecture
- Project Structure
- Documentation
- Security
- Development
- License
Overview
ASI Chain Wallet SDK is a modular TypeScript library designed to simplify wallet integration and key management for ASI Chain applications. It is organized around a high-level Client facade that manages multi-account HD wallets, secure encrypted storage, multi-network access, and reservation-aware transfers, while keeping secret material behind a strict signing boundary.
Key Features
- Client Facade - Single entry point for wallet lifecycle, networks, balances, and transfers via Client
- Multi-Account HD Wallets - Private-key and BIP-39/BIP-44 HD wallets with on-demand account derivation via Wallet and Account
- Separate Open and Unlocked States - Wallets are loaded into memory independently of the signing session that holds the decrypted secret, via Client and SigningSession
- Event Subscriptions - Typed, isolated listeners attachable at any time through
client.getEventBus()via ClientEventBus - Duplicate Protection - Wallets and accounts are matched on non-reversible key fingerprints, so re-importing the same secret is refused even while everything is locked, via KeyFingerprintService
- Encrypted Wallet Keyfiles - Password-protected export and import of a whole wallet, with a preview that reports which accounts are new before anything is written, via Client
- Versioned Storage with Migrations - Persisted data carries a schema version; incompatible or interrupted state is refused up front instead of being silently rewritten, via StorageMigrationRunner
- Secure Key Handling - PBKDF2 + AES-GCM encryption with key zeroization and a no-raw-export signing boundary via CryptoService and Signer
- Cross-Environment Storage - IndexedDB (browser) and node-persist (Node.js) behind a shared table abstraction via storage layer
- Pending-Transaction Reservations - Persistent, reservation-aware available balance with deploy-status polling via ReservationAdapter
- External Reservation Management - Deploys submitted outside the SDK can lock funds too: reservations can be added, updated, and removed directly, with validation and concurrency guards, via Client
- Detached Deploy Signing -
signDeploybuilds and signs a deploy and returns the signed envelope for callers that submit it themselves, via Client - Explicit Account Targeting - No active-account state: every call names its wallet and account, so concurrent operations on one wallet cannot sign for the wrong account, via Client
- Typed Failures - Decryption, storage, and node errors carry a machine-readable code and structured fields instead of an assembled message string, via CustomError
- Multi-Network Access - Runtime network switching over validator, read-only, and GraphQL indexer clients via ApiClientManager
- Per-Network Node Profiles - Legacy Scala and new Rust f1r3node request contracts behind one interface via NodeApiAdapter
- Transaction History - Indexed transfer history through a GraphQL anti-corruption layer via AccountDataService
Installation
npm install @asichain/asi-wallet-sdkQuick Start
Create the Client
The Client is the single entry point. Provide a per-network
configuration and (optionally) a default network.
import {
Client,
NodeApiProfile,
type TNetworksConfig,
} from "@asichain/asi-wallet-sdk";
const networksConfig: TNetworksConfig = {
DevNet: {
ValidatorURL: "http://validator-node:40403",
ReadOnlyURL: "http://observer-node:40403",
IndexerURL: "http://indexer-node:8080",
nodeApiProfile: NodeApiProfile.SCALA,
},
Dev: {
ValidatorURL: "",
ReadOnlyURL: "",
IndexerURL: "",
nodeApiProfile: NodeApiProfile.RUST,
},
MainNet: {
ValidatorURL: "",
ReadOnlyURL: "",
IndexerURL: "",
nodeApiProfile: NodeApiProfile.SCALA,
},
TestNet: {
ValidatorURL: "",
ReadOnlyURL: "",
IndexerURL: "",
nodeApiProfile: NodeApiProfile.SCALA,
},
};
const client = await Client.create({
networksConfig,
defaultNetwork: "DevNet",
});Client.create also brings storage up to date. Persisted data carries a schema
version, and startup refuses to touch anything it cannot safely handle: data
written by a newer SDK build, a malformed migration chain, or a migration that a
previous run left unfinished. Those rejections are StorageSchemaErrors carrying
an isStorageIntact flag, which tells you whether the data on disk is still
readable (update the SDK) or needs restoring from an export.
nodeApiProfile is required on every network. It selects which f1r3node
implementation the SDK talks to — SCALA for the legacy node, RUST for the new
one — and that choice drives the HTTP request shape, the endpoint a given call
targets, and which Rholang vault contract the built-in terms address. There is no
default: an omitted or unknown profile throws at Client.create, because guessing
it would silently send legacy-shaped requests to a new node. Custom networks added
at runtime through client.addNetwork must supply it too.
Create Wallets
import { MnemonicStrength } from "@asichain/asi-wallet-sdk";
// HD (mnemonic) wallet
const mnemonic = client.generateMnemonic(MnemonicStrength.TWELVE_WORDS);
const hdWallet = await client.createHDWallet(
{ mnemonic, accountName: "Account 1" },
"wallet-password",
);
// Private-key wallet
const privateKey = client.generatePrivateKey();
const pkWallet = await client.createPrivateKeyWallet(
{ privateKey, accountName: "Imported" },
"wallet-password",
);
// Derive another account on the HD wallet
const { account } = await client.deriveAccount(
hdWallet.getId(),
"Account 2",
"wallet-password",
);
console.log("New address:", account.getAddress());See Client, Wallet, and Account for the full API reference.
Creating a wallet whose secret is already stored throws DuplicateWalletError,
and one whose key already belongs to a stored account throws
DuplicateAccountError. Both carry the ids of the existing owner, and both work
while every wallet is closed, because the match runs on a key fingerprint rather
than on decrypted material.
Open, Unlock, and Close Wallets
A wallet being open (present in memory) and a wallet being unlocked (able to sign without a password) are two different states.
// Load a stored wallet into memory. With the default session policy it comes
// back already unlocked.
const wallet = await client.openWallet(signerId, "wallet-password");
client.isWalletOpen(wallet.getId()); // true
client.isWalletUnlocked(wallet.getId()); // true
// End the signing session, keep the wallet open
client.lockWallet(wallet.getId());
// Start a new session on the wallet that is already open
await client.unlockWallet(wallet.getId(), "wallet-password");
// Drop it from memory; storage is untouched
client.closeWallet(wallet.getId());
// Delete it from storage for good
await client.removeWallet(wallet.getId());Sessions are fixed-duration: they auto-lock autoLockMs after unlock (15 min by
default) and are not extended by activity. Signing after expiry without a
password throws WalletLockedError with an HTTP-style status 403, so a UI can
treat it like an expired token and re-prompt. See
Open vs unlocked.
Export and Import a Wallet Keyfile
A wallet keyfile is the password-protected backup of a whole wallet: its secret plus its account list, both encrypted.
// Export (returns an object; serialize it however you like)
const keyfile = await client.exportWalletKeyfile(wallet.getId(), "wallet-password");
// Import: look before you write
const preview = await client.previewWalletKeyfileImport(keyfile, "wallet-password");
if (preview.existingSignerId) {
// The secret is already stored - add only the accounts that are new
const newIndexes = preview.accounts
.filter((account) => account.status === "new")
.map((account) => account.index!);
await client.importKeyfileAccounts(keyfile, "wallet-password", {
accountIndexes: newIndexes,
});
} else {
// Unknown secret - create the wallet
await client.importWalletKeyfile(keyfile, "wallet-password");
}previewWalletKeyfileImport writes nothing. It decrypts the keyfile, derives
every account it declares, and reports each one as "new" or
"already-imported", so a UI can show what an import would actually do and let
the user choose. source accepts either the object or its JSON string.
Note that getExportedAccountData is a different thing: a public, passwordless
descriptor of one account (name, address, index). It carries no key material and
cannot restore anything. See keyfile export & import.
Subscribe to Events
client.getEventBus() returns a typed on / off source. Unlike the
constructor-time eventDispatcher, subscriptions can be added and removed at any
point in the client's life, and on hands back its own unsubscribe.
import { ClientEvent } from "@asichain/asi-wallet-sdk";
const unsubscribe = client
.getEventBus()
.on(ClientEvent.WALLETS_CHANGED, (wallets) => {
render(wallets);
});
// later
unsubscribe();Listeners are isolated: one that throws or rejects never breaks the emit loop or
the SDK operation behind it. Route those failures somewhere visible with the
onListenerError option on Client.create.
Check Balance and Transfer
// The SDK keeps no active account: every call names its target account
const [account] = hdWallet.getAccounts();
// Total and reservation-aware available balance
const balance = await client.getBalance(account.getAddress());
const available = await client.getAvailableBalance(
hdWallet.getId(),
account.getId(),
);
console.log("Balance:", client.toDisplayAmount(balance));
// Transfer tokens (amount in atomic units)
const reserved = await client.transfer(
{
walletId: hdWallet.getId(),
accountId: account.getId(),
to: recipientAddress,
amount: client.toAtomicAmount("10"), // 10 ASI
},
"wallet-password",
);
console.log("Deploy id:", reserved.deployId);
// Follow the deploy until the node confirms it
const unsubscribe = reserved.subscribe({
onStatus: (status) => console.log("Status:", status.status),
onConfirmed: () => console.log("Confirmed"),
});A balance read that cannot be trusted now throws BalanceUnavailableError
(status 502) carrying the address and a reason, instead of resolving to 0n.
A node that is unreachable, a vault that reports an error, and an account with no
funds are three different outcomes, so handle the error rather than reading a
falsy balance as "empty".
See Client for the full API reference. For amount conversions, see functions utilities.
Reserve Funds for a Deploy You Submit Yourself
transfer and deploy reserve funds on their own. When the deploy is submitted
outside the SDK — a hardware signer, a relayer, another app on the same vault —
the reservation can be created directly so the available balance and the pending
history stay correct.
const request = {
walletId: hdWallet.getId(),
accountId: account.getId(),
kind: "transfer" as const,
deployId, // the deploy you submitted yourself
to: recipientAddress,
amount: client.toAtomicAmount("10"),
gasCost: client.toAtomicAmount("0.1"),
pendingAmount: client.toAtomicAmount("10.1"), // must cover amount + gasCost
};
const reservation = await client.addTransactionReservation(
request,
"wallet-password",
);
// Correct it while it is still pending, keeping the same reservation id
await client.updateTransactionReservation(reservation.id, {
...request,
amount: client.toAtomicAmount("12"),
pendingAmount: client.toAtomicAmount("12.1"),
});
// Or release the funds early
await client.removeTransactionReservation(hdWallet.getId(), reservation.id);pendingAmount is the total to lock and must cover amount + gasCost; a
reservation that does not is rejected rather than under-locking the balance. The
whole available balance may be reserved. A reservation still expires on its own
after RESERVATION_EXPIRATION_TIME and is released when the deploy is confirmed,
so these calls are a correction channel, not a lifecycle to manage by hand.
Concurrent actions on the same account, deploy, or reservation are refused with
ReservationActionInProgressError (status 409) rather than interleaved. See
External reservations.
Sign a Deploy Without Submitting It
When the submission is yours but the signature is not, signDeploy stops after
signing and hands back the signed envelope. Nothing is sent to the node and no
reservation is created, so pair it with addTransactionReservation to lock the
funds locally.
const signed = await client.signDeploy(
{
walletId: hdWallet.getId(),
accountId: account.getId(),
term: rholangTerm,
phloLimit: 500_000, // optional, defaults from config
phloPrice: 1, // optional, defaults from config
shardId: "root", // optional, defaults to "root"
},
"wallet-password", // omit while a signing session is active
);
// { data, deployer, signature, sigAlgorithm } - submit it yourself
await submitToNode(signed);validAfterBlockNumber and timestamp are filled in by the SDK from the current
chain head, so the call still needs the network. The payload is validated before
anything is signed: a blank term, a non-positive or unsafe phloLimit /
phloPrice, or a blank shardId are rejected.
Architecture
SDK Components
┌──────────────────────────────────────────────────────────────────┐
│ Application │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ ASI Wallet SDK │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Client (facade) │ │
│ │ Wallet & account lifecycle • networks • balances • transfers │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Domains │ │
│ │ • Wallet / Account - Multi-account HD & PK wallets │ │
│ │ • Signer (HD / PK) - No-raw-export signing boundary │ │
│ │ • SigningSession - Fixed-window in-memory secret │ │
│ │ • LifecycleGuard - Invalidate + drain in-flight work │ │
│ │ • Asset - Token representation │ │
│ │ • ReservationAdapter - Pending-transaction reservations │ │
│ │ • ApiClientManager - Per-network transport clients │ │
│ │ • ApiServiceRegistry - Service composition root │ │
│ │ • Storage repositories- Signers / Accounts / Reservations │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Services / Managers │ │
│ │ • WalletManager / AccountManager - In-memory ownership │ │
│ │ • StorageManager - Persistence orchestration │ │
│ │ • ClientEventBus - Typed client event subscriptions │ │
│ │ • WalletOperationGuard- Duplicate & concurrency guards │ │
│ │ • ReservationOperationGuard - Per-network reservation locks │ │
│ │ • StorageBootstrap / StorageMigrationRunner (schema) │ │
│ │ • ExportKeyfileService / ImportKeyfileService │ │
│ │ • DeployService / BlockService / AccountDataService │ │
│ │ • AssetsService / TransactionService / DeployStatusPoller │ │
│ │ • CryptoService / KeysManager / KeyDerivation / Mnemonic │ │
│ │ • KeyFingerprintService - Non-reversible key identity │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Storage backends │ │
│ │ • BrowserStorage (IndexedDB) • NodeStorage (node-persist) │ │
│ │ selected automatically by environment │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Utils / Config │ │
│ │ • codec / constants / validators / functions / polyfills │ │
│ │ • decorators / guards / fabrics │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ ASI Chain Network │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Validator │ │ Observer │ │ Indexer │ │
│ │ Node │ │ Node │ │ (GraphQL) │ │
│ │ (Deploys) │ │ (Queries) │ │ (History) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────┘Cryptographic Flow
- Key Generation: secp256k1 elliptic curve keypairs via KeysManager
- Address Derivation: keccak256 hash → blake2b checksum → Base58 encoding with chain prefix
- Encryption: PBKDF2 (100,000 iterations) → AES-GCM via CryptoService
- Mnemonic: BIP-39 standard (12/24 words) via MnemonicService
- Derivation Path: BIP-44 via KeyDerivationService
Project Structure
asi-chain-wallet-sdk/
├── src/ # SDK source code
│ ├── config/ # Runtime defaults and constants
│ ├── domains/ # Domain models & transport (→ docs/DOMAINS.md)
│ ├── services/ # Managers & business logic (→ docs/SERVICES.md)
│ ├── fabrics/ # Factories: signer, storage, client, reservations (→ docs/UTILS.md)
│ ├── utils/ # Utilities & guards (→ docs/UTILS.md)
│ └── index.ts # Main export
│
├── playground/ # React demo app (→ docs/PLAYGROUND.md)
│ ├── src/
│ │ ├── sdk-react-kit/ # SDK ↔ React integration layer (hooks, context)
│ │ ├── components/ # UI components
│ │ ├── pages/ # WalletsPage, TxHistoryPage
│ │ └── router/ # Client-side routing
│ └── package.json
│
├── docs/ # API reference
│ ├── DOMAINS.md # Domain models & transport
│ ├── SERVICES.md # Managers & services
│ ├── UTILS.md # Utilities & config
│ └── PLAYGROUND.md # Playground components
│
├── package.json # SDK dependencies
├── tsconfig.build.json # TypeScript config
└── README.md # This fileDocumentation
SDK Reference
| Document | Description |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| docs/DOMAINS.md | Domain models & transport (Client, Wallet, Account, Signer, ReservationAdapter, ApiClientManager, NodeApiAdapter, storage repositories, and types) |
| docs/SERVICES.md | Managers & services (WalletManager, AccountManager, StorageManager, DeployService, AssetsService, TransactionService, CryptoService) |
| docs/UTILS.md | Utilities & config (codec, constants, validators, functions, guards, decorators, fabrics, polyfills) |
| docs/PLAYGROUND.md | Playground components, the sdk-react-kit integration layer, and usage examples |
Related Resources
| Resource | Link | | ----------------------- | ------------------------------------------------------------------------------------------------ | | ASI Chain Documentation | https://docs.asichain.io | | ASI Chain Node | github.com/asi-alliance/asi-chain | | ASI Chain Wallet | github.com/asi-alliance/asi-chain-wallet | | ASI Chain Explorer | github.com/asi-alliance/asi-chain-explorer | | ASI Chain Faucet | github.com/asi-alliance/asi-chain-faucet |
Security
| Document | Description | | ------------------------------------------------ | -------------------------------------------------------------------------- | | SECURITY.md | Vulnerability reporting policy, disclosure process, and supported versions | | THREAT_MODEL.md | Threat assumptions, trust boundaries, adversary model, and mitigations | | SECURITY_INVARIANTS.md | Non-negotiable key/storage/signing/documentation security guarantees | | CRYPTO_PROFILE.md | Versioned crypto parameters, key-handling profile, and migration notes |
Development
Prerequisites
- Node.js 18.x or higher
- npm 9.x or higher
Setup
# Install SDK dependencies
npm install
# Build the SDK
npm run build
# Watch mode for development
npm run dev
# Run the release gate locally: build, unit tests, security tests,
# secret-log scan, and a production-dependency audit
npm run gateThe gate was npm run security:gate and covered security checks only; it now
also runs npm run test:unit, so the same command CI runs is the one that has to
pass locally. The GitHub workflow is .github/workflows/gate.yml.
Playground
The playground provides a React-based demo application for testing SDK functionality:
cd playground
npm install
# Create .env file with per-network endpoints, e.g.:
# VITE_DEFAULT_NETWORK=DevNet
# VITE_DEVNET_VALIDATOR_URL=...
# VITE_DEVNET_READONLY_URL=...
# VITE_DEVNET_INDEXER_URL=...
npm run devPlayground available at http://localhost:5173. See docs/PLAYGROUND.md for component details.
Dependencies
SDK (package.json):
| Package | Version | Purpose | | ---------------------------------------------------------------- | ------- | ----------------------------------------- | | axios | 1.13.2 | HTTP client for node communication | | bip32 | 4.0.0 | BIP-32 hierarchical deterministic wallets | | bip39 | 3.1.0 | BIP-39 mnemonic generation | | blakejs | 1.2.1 | BLAKE2b hashing for addresses | | bs58 | 6.0.0 | Base58 encoding | | @noble/hashes | 1.6.0 | Cryptographic hash helpers | | @noble/secp256k1 | 1.7.0 | secp256k1 key generation and signing | | js-sha3 | 0.9.3 | keccak256 hashing | | node-persist | 4.0.4 | Node.js storage backend | | buffer | 6.0.3 | Browser Buffer compatibility |
Playground (playground/package.json):
| Package | Version | Purpose | | -------------------------- | ------- | ------------------------- | | react | 18.2.0 | UI framework | | vite | 7.2.6 | Build tool and dev server |
License
This project is licensed under the Apache 2.0 License. See LICENSE file for details.
ASI Alliance founding members: Fetch.ai, SingularityNET, and CUDOS
