nexa-wallet-sdk
v1.0.0
Published
Wallet SDK for the Nexa blockchain
Maintainers
Readme
Nexa Wallet SDK
Production-ready TypeScript SDK for building Nexa blockchain wallets and dApps.
Features
- Wallet Management: Create wallets from seed phrases or private keys
- Account Types: Support for multiple account types (Default, Vault, DApp)
- Transaction Building: Fluent API for building and signing transactions
- Token Operations: Create, mint, melt, and transfer tokens, NFTs, and SFTs
- Watch-Only Wallets: Monitor addresses without storing private keys
- Rostrum Provider: Direct access to Electrum RPC endpoints
- Network Support: Mainnet and testnet compatibility
- Multiple Formats: CommonJS, ES modules, and browser bundles
Installation
npm install nexa-wallet-sdkQuick Start
Basic Wallet Setup
import { Wallet, rostrumProvider } from 'nexa-wallet-sdk'
// Connect to the default mainnet node
await rostrumProvider.connect()
// Connect to a specific network (mainnet or testnet)
await rostrumProvider.connect('testnet') // Uses predefined testnet node
await rostrumProvider.connect('mainnet') // Uses predefined mainnet node
// Connect to a custom node
await rostrumProvider.connect({
host: 'your-custom-node.example.com',
port: 30004,
scheme: 'wss' // or 'ws' for unencrypted connection
})
// Create wallet from seed phrase
const wallet = new Wallet(
'your twelve word seed phrase goes here for wallet creation',
'testnet' // or 'mainnet'
)
// Initialize wallet — discovers accounts and loads balances
await wallet.initialize()
// Get the first default account (store key '0')
const account = wallet.accountStore.getAccount('0')Simple Transaction
const tx = await wallet.newTransaction(account)
.sendTo('nexatest:nqtsq5g5jsdmqqywaqd82lhnnk3a8wqunjz6gtxdtavnnekc', '10000')
.populate()
.sign()
.build()
// Pass the account to auto-refresh balances after broadcast
const txHash = await wallet.sendTransaction(tx, account)
console.log('Transaction hash:', txHash)Account Types
The SDK supports three account types, each designed for different use cases. All account types share the same base API for balances, transactions, and addresses.
Default Account (NEXA_ACCOUNT)
Standard BIP44 HD account with multiple receive and change addresses. Best for general wallet use.
- Derives from BIP44 path
m/44'/29223'/{account}' - Multiple receive keys and change keys (20 of each by default)
- Change outputs go to dedicated change addresses
- Store key: just the account index (e.g.,
'0','1')
import { AccountType } from 'nexa-wallet-sdk'
// Create a new default account
const account = await wallet.newAccount(AccountType.NEXA_ACCOUNT)
// Get a receive address (async — checks if current address is used before generating new)
const address = await account.getNewAddress()
// Get a change address (different from receive)
const changeAddress = await account.getNewChangeAddress()
// Check if the account supports change addresses
account.hasChangeAddresses() // true
// List all addresses in the account (receive + change)
const allAddresses = account.getAddresses()
for (const addrKey of allAddresses) {
console.log(addrKey.address, 'balance:', addrKey.balance)
}
// Access receive and change keys separately
const { receiveKeys, changeKeys } = account.accountKeys
console.log('Receive addresses:', receiveKeys.length) // 20 by default
console.log('Change addresses:', changeKeys.length) // 20 by defaultAddress generation modes:
- Discovery mode (default): Reuses unused addresses — only generates a new address when the current one has been used on-chain
- Sequential mode (
forceSequentialIndexing = true): Always generates new addresses, useful for multi-user apps where each user needs a unique address
Each default account manages its own pool of receive and change addresses internally. You don't need separate store keys to access individual addresses — use getNewAddress() to get the next available receive address, or accountKeys.receiveKeys / accountKeys.changeKeys to access the full list.
DApp Account (DAPP_ACCOUNT)
Single-key account for application-specific use. All DApp accounts derive from BIP44 account 2.
- Single address — no change keys
- Change outputs return to the same address
- Store key:
'2.{index}'(e.g.,'2.0','2.1')
const dappAccount = await wallet.newAccount(AccountType.DAPP_ACCOUNT)
// Single address for everything
const address = dappAccount.getNewAddress()
dappAccount.getNewChangeAddress() // same as getNewAddress()
dappAccount.hasChangeAddresses() // falseVault Account (VAULT_ACCOUNT)
Single-key account for secure storage. All Vault accounts derive from BIP44 account 1.
- Single address — no change keys
- Store key:
'1.{index}'(e.g.,'1.0','1.1')
const vaultAccount = await wallet.newAccount(AccountType.VAULT_ACCOUNT)
const vaultAddress = vaultAccount.getNewAddress()Account Store
The AccountStore manages all accounts in the wallet. After calling wallet.initialize(), discovered accounts are available via their store keys.
// List all accounts (returns a Map<string, BaseAccount>)
const allAccounts = wallet.accountStore.listAccounts()
// Get a specific account by store key
const defaultAccount = wallet.accountStore.getAccount('0') // first default account
const vaultAccount = wallet.accountStore.getAccount('1.0') // first vault account
const dappAccount = wallet.accountStore.getAccount('2.0') // first dapp account
// Get all accounts of a specific type
const allDappAccounts = wallet.accountStore.getAccountsByType(AccountType.DAPP_ACCOUNT)
const allVaultAccounts = wallet.accountStore.getAccountsByType(AccountType.VAULT_ACCOUNT)
const allDefaultAccounts = wallet.accountStore.getAccountsByType(AccountType.NEXA_ACCOUNT)
// Find the private key for any address across all accounts
const addressKey = wallet.accountStore.findKeyForAddress('nexa:nqtsq5g5...')
// Remove an account
wallet.accountStore.removeAccount('2.0')Sequential Indexing (Multi-User)
By default, account creation uses discovery-based indexing (scans the blockchain for existing activity). For multi-user scenarios where each user needs a unique account immediately, enable sequential indexing:
wallet.forceSequentialIndexing = true
// Accounts are created with sequential indexes regardless of blockchain activity
const alice = await wallet.newAccount(AccountType.DAPP_ACCOUNT) // 2.0
const bob = await wallet.newAccount(AccountType.DAPP_ACCOUNT) // 2.1
const charlie = await wallet.newAccount(AccountType.DAPP_ACCOUNT) // 2.2
wallet.forceSequentialIndexing = false // back to discovery-basedBalances
Balances are loaded during wallet.initialize() and when calling account.loadBalances(). They are not automatically refreshed after sending transactions — call loadBalances() to get the latest state.
NEXA Balance
// Loaded during initialize(), or refresh manually:
await account.loadBalances()
const balance = account.balance
console.log('Confirmed:', balance.confirmed)
console.log('Unconfirmed:', balance.unconfirmed)Token Balances
// Token balances are loaded alongside NEXA balance
await account.loadBalances()
const tokenBalances = account.tokenBalances
// Record<string, Balance> — keyed by token ID
for (const [tokenId, balance] of Object.entries(tokenBalances)) {
console.log(`${tokenId}: confirmed=${balance.confirmed}, unconfirmed=${balance.unconfirmed}`)
}Token Authorities
// Authorities are also loaded with loadBalances()
const authorities = account.tokenAuthorities
// Record<string, ITokenUtxo[]> — keyed by token ID
for (const [tokenId, authUtxos] of Object.entries(authorities)) {
console.log(`Token ${tokenId} has ${authUtxos.length} authority UTXO(s)`)
}Refreshing Balances After Transactions
Pass the source account to sendTransaction() and balances are refreshed automatically:
const tx = await wallet.newTransaction(account)
.sendTo(recipient, '50000')
.populate()
.sign()
.build()
// Account balances are refreshed automatically after broadcast
const txHash = await wallet.sendTransaction(tx, account)
// Balance is now up to date
console.log('Balance:', account.balance)
console.log('Tokens:', account.tokenBalances)If you broadcast without passing the account, you can still refresh manually:
await wallet.sendTransaction(tx)
await account.loadBalances() // manual refreshTransaction History
// Get all transactions for the account
const transactions = await account.getTransactions()
// Get transactions from a specific block height
const recentTxs = await account.getTransactions(800000)
// Get transactions for a specific address
const addressTxs = await account.getTransactions(undefined, 'nexa:nqtsq5g5...')
// Get the latest transaction for an address
const latestTx = await rostrumProvider.getLatestTransaction('nexa:nqtsq5g5...')
// Paginate transaction history
const page = await rostrumProvider.getTransactionHistory(address, { limit: 10, offset: 0 })
// Filter by block height range
const filtered = await rostrumProvider.getTransactionHistory(address, {
from_height: 800000,
to_height: 900000
})
// Token-only or exclude-token history
const tokenHistory = await rostrumProvider.getTransactionHistory(address, { tokens_only: true })
const nexaHistory = await rostrumProvider.getTransactionHistory(address, { exclude_tokens: true })Transaction Building
All transactions follow the pattern: create -> configure -> populate -> sign -> build
const tx = await wallet.newTransaction(account)
.sendTo(address, amount) // 1. Configure outputs
.addOpReturn(data) // 2. Add optional data
.populate() // 3. Find inputs and calculate fees
.sign() // 4. Sign the transaction
.build() // 5. Get final transaction hexBasic Send Transaction
const tx = await wallet.newTransaction(account)
.sendTo('nexatest:nqtsq5g5jsdmqqywaqd82lhnnk3a8wqunjz6gtxdtavnnekc', '50000')
.populate()
.sign()
.build()Multiple Outputs
const tx = await wallet.newTransaction(account)
.sendTo('nexatest:address1', '10000')
.sendTo('nexatest:address2', '20000')
.sendTo('nexatest:address3', '30000')
.addOpReturn('Multi-output transaction')
.populate()
.sign()
.build()Fee From Amount
// Deduct transaction fee from the send amount
const tx = await wallet.newTransaction(account)
.sendTo(recipient, '50000')
.feeFromAmount() // Fee will be subtracted from the 50000
.populate()
.sign()
.build()Consolidate UTXOs
// Consolidate all UTXOs to a single address
const tx = await wallet.newTransaction(account)
.consolidate('nexatest:nqtsq5g5jsdmqqywaqd82lhnnk3a8wqunjz6gtxdtavnnekc')
.populate()
.sign()
.build()Token Operations
Create a Fungible Token
const tx = await wallet.newTransaction(account)
.token(
'MyToken', // Token name
'MTK', // Ticker symbol
8, // Decimal places
'https://mytoken.com/info', // Documentation URL
'sha256hash' // Documentation hash
)
.populate()
.sign()
.build()Create an NFT Collection
const tx = await wallet.newTransaction(account)
.collection(
'My NFT Collection',
'MNC',
'https://mycollection.com/metadata',
'collectionhash'
)
.populate()
.sign()
.build()Mint an NFT
const parentCollectionId = 'nexatest:tq8r37lcjlqazz7vuvug84q2ev50573hesrnxkv9y6hvhhl5k5qqqnmyf79mx'
const tx = await wallet.newTransaction(account)
.nft(
parentCollectionId,
'https://mynft.com/content.zip',
'contenthash123'
)
.populate()
.sign()
.build()Mint an SFT (Semi-Fungible Token)
const parentCollectionId = 'nexatest:tq8r37lcjlqazz7vuvug84q2ev50573hesrnxkv9y6hvhhl5k5qqqnmyf79mx'
const tx = await wallet.newTransaction(account)
.sft(
parentCollectionId,
'https://mysft.com/content.zip', // Content URL
'contenthash456', // Content hash
100n // Quantity of SFTs to create
)
.populate()
.sign()
.build()Token Transfers
const tokenId = 'nexatest:tqtsq5g5jsdmqqywaqd82lhnnk3a8wqunjz6gtxdtavnnekc'
// Send tokens
const tx = await wallet.newTransaction(account)
.sendToToken('nexatest:recipient', '500', tokenId)
.populate()
.sign()
.build()
// Send NEXA and tokens in the same transaction
const tx2 = await wallet.newTransaction(account)
.sendTo('nexatest:recipient', '1000')
.sendToToken('nexatest:recipient', '500', tokenId)
.populate()
.sign()
.build()Mint Additional Tokens
const tx = await wallet.newTransaction(account)
.mint(tokenId, '1000000') // Mint 1,000,000 token units
.populate()
.sign()
.build()Burn (Melt) Tokens
const tx = await wallet.newTransaction(account)
.melt(tokenId, '500000') // Burn 500,000 token units
.populate()
.sign()
.build()Authority Management
Renew Token Authorities
const tx = await wallet.newTransaction(account)
.renewAuthority(
tokenId,
['mint', 'melt'], // Permissions to renew
'nexatest:nqtsq5g5...' // Optional: new authority address
)
.populate()
.sign()
.build()Send Authority to Another Address
const tx = await wallet.newTransaction(account)
.sendAuthority(
tokenId,
['mint', 'melt'], // Permissions to send
'nexatest:nqtsq5g5...' // Recipient address
)
.populate()
.sign()
.build()Delete Token Authority
const tx = await wallet.newTransaction(account)
.deleteAuthority(tokenId, 'abc123:0') // outpoint of authority to delete
.populate()
.sign()
.build()Watch-Only Wallets
Watch-only wallets allow you to monitor addresses and create unsigned transactions without storing private keys.
Create Watch-Only Wallet
import { WatchOnlyWallet } from 'nexa-wallet-sdk'
const watchOnlyWallet = new WatchOnlyWallet([
{ address: 'nexatest:nqtsq5g5dsgh6mwjchqypn8hvdrjue0xpmz293fl7rm926xv' }
], 'testnet')Create Unsigned Transaction
const unsignedTx = await watchOnlyWallet.newTransaction()
.sendTo('nexatest:nqtsq5g5jsdmqqywaqd82lhnnk3a8wqunjz6gtxdtavnnekc', '100000')
.addOpReturn("Watch-only transaction")
.populate()
.build()Sign Watch-Only Transaction
// Pass the unsigned transaction to a wallet with private keys
const signedTx = await wallet.newTransaction(account, unsignedTx)
.sign()
.build()Subscribe to Address Updates
const myCallback = (notification) => {
console.log('Address activity:', notification)
}
await watchOnlyWallet.subscribeToAddressNotifications(myCallback)
// Later, to prevent memory leaks:
await watchOnlyWallet.unsubscribeFromAddressNotifications(myCallback)Rostrum Provider (Electrum RPC)
The rostrumProvider is a singleton that provides direct access to all Electrum RPC endpoints. It is used internally by the SDK but is also exported for direct use.
Connection
import { rostrumProvider } from 'nexa-wallet-sdk'
// Connect to predefined nodes
await rostrumProvider.connect() // mainnet default
await rostrumProvider.connect('testnet') // testnet default
await rostrumProvider.connect('mainnet') // mainnet default
// Connect to a custom node
await rostrumProvider.connect({
host: 'my-node.example.com',
port: 20004,
scheme: 'wss' // 'ws' | 'wss' | 'tcp' | 'tcp_tls'
})
// Disconnect
await rostrumProvider.disconnect()Network Info
// Server version
const version = await rostrumProvider.getVersion()
// Current block tip
const tip = await rostrumProvider.getBlockTip()
console.log('Height:', tip.height)
// Connection latency (ms)
const latency = await rostrumProvider.getLatency()Address Queries
// NEXA balance (excludes tokens)
const balance = await rostrumProvider.getBalance(address)
// { confirmed: number, unconfirmed: number }
// Token balances
const tokenBalances = await rostrumProvider.getTokensBalance(address)
const specificToken = await rostrumProvider.getTokensBalance(address, tokenId)
// { confirmed: Record<string, bigint|number>, unconfirmed: Record<string, bigint|number> }
// NEXA UTXOs (excludes token UTXOs)
const nexaUtxos = await rostrumProvider.getNexaUtxos(address)
// IListUnspentRecord[] — { outpoint_hash, value, height, tx_hash, tx_pos, has_token }
// Token UTXOs for a specific token
const tokenUtxos = await rostrumProvider.getTokenUtxos(address, tokenId)
// ITokenUtxo[] — { outpoint_hash, value, group, token_amount, height, ... }
// First use of an address
const firstUse = await rostrumProvider.getFirstUse(address)
// { block_hash, block_height, tx_hash }
// Transaction history
const history = await rostrumProvider.getTransactionHistory(address)
// ITXHistory[] — { tx_hash, height, fee? }
// Transaction history with filters (pagination, height range, token filtering)
const page = await rostrumProvider.getTransactionHistory(address, {
limit: 10, // max results
offset: 0, // skip first n
from_height: 0, // include from height (inclusive)
to_height: -1, // include up to height (-1 = include mempool)
tokens_only: true, // only txs with tokens
exclude_tokens: true, // only txs without tokens
})
// Get latest transaction for an address (returns full ITransaction or null)
const latestTx = await rostrumProvider.getLatestTransaction(address)Transaction & UTXO Queries
// Get a full transaction by hash
const tx = await rostrumProvider.getTransaction(txHash)
// ITransaction — { txid, txidem, hex, vin, vout, confirmations, height, ... }
// Get a specific UTXO by outpoint
const utxo = await rostrumProvider.getUtxo(outpointHash)
// IUtxo — { addresses, amount, group, scriptpubkey, spent, ... }Token Queries
// Token genesis information
const genesis = await rostrumProvider.getTokenGenesis(tokenId)
// ITokenGenesis — { name, ticker, decimal_places, document_url, document_hash, ... }Subscriptions
The subscription callback receives a status hash (a hash of the address's current UTXO state), not a transaction hash. Use it as a signal that something changed, then fetch the actual transactions:
// Subscribe to address activity
await rostrumProvider.subscribeToAddresses(
['nexa:address1'],
async (statusHash) => {
// statusHash is a state change signal, NOT a tx hash
// Fetch transaction history to see what changed
const history = await rostrumProvider.getTransactionHistory('nexa:address1')
const latestTx = history[history.length - 1]
console.log('New transaction:', latestTx.tx_hash)
}
)
// Unsubscribe
await rostrumProvider.unsubscribeFromAddresses(addresses, callback)Broadcasting
// Broadcast a signed transaction, returns the transaction ID
const txId = await rostrumProvider.broadcast(signedTxHex)Advanced Features
Address Notifications
// Define your callback function
const myCallback = (notification) => {
console.log('Address notification:', notification)
}
// Subscribe to a single address
await wallet.subscribeToAddressNotifications(
'nexa:nqtsq5g5jsdmqqywaqd82lhnnk3a8wqunjz6gtxdtavnnekc',
myCallback
)
// Subscribe to multiple addresses
await wallet.subscribeToAddressNotifications(
['nexa:address1', 'nexa:address2'],
myCallback
)
// Subscribe to all wallet addresses
const accounts = wallet.accountStore.listAccounts()
const allAddresses = Array.from(accounts.values()).flatMap(account =>
account.getAddresses().map(addr => addr.address)
)
await wallet.subscribeToAddressNotifications(allAddresses, myCallback)
// Always unsubscribe when done to prevent memory leaks
await wallet.unsubscribeFromAddressNotifications(allAddresses, myCallback)Parse Existing Transactions
// From hex string
const tx = await wallet.newTransaction(account)
.parseTxHex('0100000001...')
.sign()
.build()
// From buffer
const txBuffer = Buffer.from('0100000001...', 'hex')
const tx2 = await wallet.newTransaction(account)
.parseTxBuffer(txBuffer)
.sign()
.build()Message Signing
// Sign a message
const address = account.getNewAddress()
const signature = wallet.signMessage('Hello Nexa', address)
// Verify a message
const isValid = wallet.verifyMessage('Hello Nexa', signature, address)Export Wallet Data
const walletData = wallet.export()
// { phrase, masterKey, accounts }Available Exports
import {
// Core classes
Wallet,
WatchOnlyWallet,
// Account types
BaseAccount,
DefaultAccount,
DappAccount,
VaultAccount,
AccountStore,
// Transaction creators
WalletTransactionCreator,
WatchOnlyTransactionCreator,
// Network provider
rostrumProvider,
// Rostrum types & constants
RostrumScheme, // { WS, WSS, TCP, TCP_TLS }
// type: RostrumParams, RostrumTransportScheme, BlockTip, IFirstUse,
// ITokenGenesis, ITokensBalance, ITokenListUnspent, ITokenUtxo,
// IListUnspentRecord, IUtxo, ISpent, ITransaction, ITXInput,
// ITXOutput, IScriptSig, IScriptPubKey, IHistoryFilter, ITXHistory
// Signing
SighashType,
// Utility functions & classes
ValidationUtils,
isValidNexaAddress,
AccountKeysUtils,
// Enums
AccountType,
TxTokenType,
// Types
// AccountKeys, AccountIndexes, AddressKey, Balance,
// WatchOnlyAddress, TxStatus, HodlStatus, TxEntityState,
// TxTemplateData, TxOptions, TokenAction, PermissionLabel,
// TransactionEntity
} from 'nexa-wallet-sdk'Error Handling
try {
const tx = await wallet.newTransaction(account)
.sendTo('invalid-address', '1000')
.populate()
.sign()
.build()
} catch (error) {
if (error.message.includes('Invalid Address')) {
console.error('Invalid Nexa address provided')
} else if (error.message.includes('Not enough Nexa balance')) {
console.error('Insufficient NEXA balance')
} else if (error.message.includes('Not enough token balance')) {
console.error('Insufficient token balance')
} else if (error.message.includes('authority not found')) {
console.error('Required token authority not found')
} else {
console.error('Transaction failed:', error.message)
}
}Network Configuration
// Testnet (for development)
const testnetWallet = new Wallet(seedPhrase, 'testnet')
// Mainnet (for production)
const mainnetWallet = new Wallet(seedPhrase, 'mainnet')
// The wallet remembers its network — transactions inherit it automatically
const tx = await wallet.newTransaction(account)
.sendTo(address, amount)
.populate()
.sign()
.build()Best Practices
- Always Connect First: Call
await rostrumProvider.connect()before wallet operations - Network Consistency: Ensure wallet and transaction networks match
- Refresh Balances: Call
account.loadBalances()after sending transactions to get updated state - Amount Precision: Use strings for amounts to avoid floating-point precision issues
- Error Handling: Wrap wallet operations in try-catch blocks
- Private Key Security: Never log or expose private keys or seed phrases
- Address Validation: Validate addresses before sending transactions
- Memory Management: Always unsubscribe from address notifications when done to prevent memory leaks
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
For issues and questions:
- GitHub Issues: wallet-sdk-ts issues
- Documentation: Nexa Documentation
