@dolaned/wallet-sdk-ts
Version:
Wallet SDK for the Nexa blockchain
398 lines (375 loc) • 15 kB
text/typescript
import {HDPrivateKey, Message, Networkish, Networks, TransactionBuilder,} from "libnexa-ts";
import * as Bip39 from 'bip39'
import {rostrumProvider} from "../network/RostrumProvider";
import {AccountType, discoverWallet,} from "../utils/WalletUtils";
import {isBuffer, isNil, isString} from "lodash-es";
import WalletTransactionCreator from "./transactions/WalletTransactionCreator";
import AccountStore from "./accounts/AccountStore";
import {BaseAccount} from "./accounts/interfaces/BaseAccountInterface";
import ValidationUtils from "../utils/ValidationUtils";
import {AddressKey} from "../models/wallet.entities";
/**
* Main Wallet class for managing Nexa blockchain wallet operations
*
* This class provides comprehensive wallet functionality including:
* - Creating wallets from seed phrases or private keys
* - Account discovery and management
* - Transaction creation and signing
* - Message signing and verification
* - Multi-account support with different account types
*
* @example
* ```typescript
* // Create a new wallet with random seed phrase
* const wallet = Wallet.create();
*
* // Restore wallet from existing seed phrase
* const wallet = Wallet.fromSeedPhrase('your twelve word seed phrase here');
*
* // Initialize wallet (discovers accounts and balances)
* await wallet.initialize();
*
* // Create a new account
* const account = await wallet.newAccount('DefaultAccount');
*
* // Create and send a transaction
* const tx = wallet.newTransaction(account)
* .to('nexa:address', 1000000) // 1 NEXA in satoshis
* .sign();
*
* const txId = await wallet.sendTransaction(tx.toHex());
* ```
*/
export default class Wallet {
/** The master HD private key derived from the seed phrase */
private readonly masterKey!: HDPrivateKey;
/** Store for managing wallet accounts */
private _accountStore: AccountStore;
/** The blockchain network this wallet operates on */
private readonly _network: Networkish
/** The BIP39 seed phrase used to generate this wallet (if created from phrase) */
private readonly phrase?: string;
/**
* Creates a new Wallet instance
*
* @param data - Optional wallet data:
* - undefined: Generate new random seed phrase
* - string: Use as BIP39 seed phrase
* - HDPrivateKey: Use as master key directly
* @param network - Network name ('mainnet', 'testnet', 'regtest'). Defaults to 'mainnet'
*
* @example
* ```typescript
* // Create new wallet with random seed
* const wallet = new Wallet();
*
* // Create from seed phrase
* const wallet = new Wallet('abandon abandon abandon...');
*
* // Create from master key
* const masterKey = HDPrivateKey.fromString('xprv...');
* const wallet = new Wallet(masterKey);
*
* // Create on testnet
* const wallet = new Wallet(undefined, 'testnet');
* ```
*/
constructor(data?: string | HDPrivateKey | undefined, network?: string) {
this._network = Networks.get(network) ?? Networks.mainnet
this._accountStore = new AccountStore()
if(isNil(data)) {
this.phrase = Bip39.generateMnemonic(128, undefined, Bip39.wordlists.english)
const seed = Bip39.mnemonicToSeedSync(this.phrase, '')
const masterKey = HDPrivateKey.fromSeed(seed, this._network ?? Networks.mainnet)
this.masterKey = masterKey.deriveChild(44, true).deriveChild(29223, true)
} else if(data instanceof HDPrivateKey) {
this.masterKey = data
} else if (isString(data)) {
this.phrase = data
const seed = Bip39.mnemonicToSeedSync(this.phrase, '')
const masterKey = HDPrivateKey.fromSeed(seed, this._network ?? Networks.mainnet)
this.masterKey = masterKey.deriveChild(44, true).deriveChild(29223, true)
}
}
/**
* Create a new wallet with a randomly generated seed phrase
*
* This is the recommended way to create a new wallet for first-time users.
* The generated seed phrase should be securely stored by the user.
*
* @returns A new Wallet instance with a random 12-word seed phrase
*
* @example
* ```typescript
* const wallet = Wallet.create();
* console.log(wallet.export().phrase); // Store this securely!
* ```
*/
public static create(): Wallet {
return new Wallet()
}
/**
* Create a wallet from an existing BIP39 seed phrase
*
* Use this method to restore a wallet from a previously generated seed phrase.
* The seed phrase should be a valid BIP39 mnemonic.
*
* @param phrase - The BIP39 seed phrase (12 or 24 words)
* @param network - Optional network name ('mainnet', 'testnet', 'regtest')
* @returns A new Wallet instance restored from the seed phrase
* @throws {Error} If the seed phrase is invalid or not provided
*
* @example
* ```typescript
* const wallet = Wallet.fromSeedPhrase(
* 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
* 'testnet'
* );
* ```
*/
public static fromSeedPhrase(phrase: string, network?: string): Wallet {
ValidationUtils.validateArgument(isString(phrase), 'seedphrase must be provided')
return new Wallet(phrase, network)
}
/**
* Create a wallet from an extended private key (xpriv)
*
* Use this method to create a wallet from a master private key in extended format.
* This is useful for advanced users who want to use a specific key derivation.
*
* @param xpriv - The extended private key string (starts with 'xprv')
* @param network - Optional network name ('mainnet', 'testnet', 'regtest')
* @returns A new Wallet instance using the provided master key
* @throws {Error} If the private key is invalid or not provided
*
* @example
* ```typescript
* const wallet = Wallet.fromXpriv(
* 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'
* );
* ```
*/
public static fromXpriv(xpriv: string, network?: string): Wallet {
ValidationUtils.validateArgument(isString(xpriv), 'private key must be provided')
const masterKey = HDPrivateKey.fromString(xpriv)
return new Wallet(masterKey, network)
}
/**
* Initialize the wallet by discovering accounts and loading balances
*
* This method performs account discovery using the BIP44 derivation path
* and scans for existing accounts with transaction history or balances.
* Must be called before using the wallet's accounts.
*
* @returns Promise that resolves when initialization is complete
*
* @example
* ```typescript
* const wallet = Wallet.fromSeedPhrase('your seed phrase');
* await wallet.initialize();
*
* // Now you can access discovered accounts
* const accounts = wallet.accountStore.listAccounts();
* ```
*/
public async initialize(): Promise<void> {
const walletAccounts: BaseAccount[] = await discoverWallet(this.masterKey)
for(const account of walletAccounts){
this._accountStore.importAccount(account)
}
}
/**
* Create a new transaction builder for this wallet
*
* @param fromAccount - The account to send the transaction from
* @param x - Optional existing transaction data:
* - TransactionBuilder: Use existing transaction builder
* - string: Parse from hex string
* - Buffer: Parse from binary buffer
* - undefined: Create new empty transaction
* @returns A new WalletTransactionCreator instance
*
* @example
* ```typescript
* const account = wallet.accountStore.getAccount(0);
* const tx = wallet.newTransaction(account)
* .to('nexa:address', 1000000) // 1 NEXA
* .sign();
*
* // Or from existing transaction hex
* const tx = wallet.newTransaction(account, 'raw_tx_hex')
* .sign();
* ```
*/
public newTransaction(fromAccount: BaseAccount, x?: TransactionBuilder | string | Buffer): WalletTransactionCreator {
let tx: WalletTransactionCreator;
if (x instanceof TransactionBuilder) {
tx = new WalletTransactionCreator(fromAccount, x);
} else if (isString(x)) {
tx = new WalletTransactionCreator(fromAccount).parseTxHex(x);
} else if (isBuffer(x) && !isNil(x)) {
tx = new WalletTransactionCreator(fromAccount).parseTxBuffer(x);
} else {
tx = new WalletTransactionCreator(fromAccount);
}
return tx.onNetwork(this._network);
}
/**
* Create a new account for this wallet
*
* @param accountType - The type of account to create:
* - 'DefaultAccount': Standard account for general use
* - 'VaultAccount': Secured account with additional protection
* - 'DappAccount': Account optimized for dApp interactions
* @returns Promise that resolves to the newly created account
*
* @example
* ```typescript
* const defaultAccount = await wallet.newAccount('DefaultAccount');
* const vaultAccount = await wallet.newAccount('VaultAccount');
* const dappAccount = await wallet.newAccount('DappAccount');
* ```
*/
public async newAccount(accountType: AccountType): Promise<BaseAccount>{
return await this.accountStore.createAccount(accountType, this.masterKey)
}
/**
* Broadcast a signed transaction to the Nexa network
*
* @param transaction - The signed transaction in hex format
* @returns Promise that resolves to the transaction ID (txid)
* @throws {Error} If the transaction is invalid or broadcast fails
*
* @example
* ```typescript
* const tx = wallet.newTransaction(account)
* .to('nexa:address', 1000000)
* .sign();
*
* const txId = await wallet.sendTransaction(tx.toHex());
* console.log('Transaction sent:', txId);
* ```
*/
public async sendTransaction(transaction: string): Promise<string> {
ValidationUtils.validateArgument(isString(transaction), 'transaction must be present and valid')
return rostrumProvider.broadcast(transaction)
}
/**
* Sign a message using a specific address from this wallet
*
* The message is signed using the private key associated with the given address.
* This can be used for authentication or to prove ownership of an address.
*
* @param message - The message to sign
* @param addressToUse - The address whose private key should sign the message
* @returns The signature as a base64-encoded string
* @throws {Error} If the address is not owned by this wallet
*
* @example
* ```typescript
* const account = wallet.accountStore.getAccount(0);
* const address = account.getReceiveAddress();
* const signature = wallet.signMessage('Hello World', address);
* ```
*/
public signMessage(message: string, addressToUse: string): string {
let msg = new Message(message);
const addressKey = this.accountStore.findKeyForAddress(addressToUse)
ValidationUtils.validateArgument(isNil(addressKey), "You dont own this private key")
return msg.sign(addressKey?.key.privateKey!)
}
/**
* Verify a message signature against an address
*
* This method can verify signatures created by any address, not just addresses
* owned by this wallet. It's useful for verifying messages from other parties.
*
* @param message - The original message that was signed
* @param signature - The signature to verify (base64-encoded)
* @param address - The address that supposedly signed the message
* @returns true if the signature is valid, false otherwise
* @throws {Error} If any parameters are missing or invalid
*
* @example
* ```typescript
* const isValid = wallet.verifyMessage(
* 'Hello World',
* 'signature_string',
* 'nexa:address'
* );
* console.log('Signature valid:', isValid);
* ```
*/
public verifyMessage(message: string, signature: string, address: string): boolean {
ValidationUtils.validateArgument(!isNil(message), 'message is required')
ValidationUtils.validateArgument(!isNil(signature), 'signature is required')
ValidationUtils.validateArgument(!isNil(address), 'address is required ')
let msg = new Message(message);
const addressKey = this.accountStore.findKeyForAddress(address)
ValidationUtils.validateArgument(isNil(addressKey), "You dont own this private key")
return msg.verify(address, signature)
}
/**
* Export the wallet data for backup or storage
*
* Returns an object containing the wallet's seed phrase, master key, and accounts.
* This data can be used to restore the wallet later. The seed phrase should be
* stored securely as it provides full access to the wallet.
*
* @returns Object containing wallet data
* @property {string} phrase - The BIP39 seed phrase (if available)
* @property {HDPrivateKey} masterKey - The master private key
* @property {BaseAccount[]} accounts - Array of discovered accounts
*
* @example
* ```typescript
* const walletData = wallet.export();
*
* // Store the seed phrase securely
* const seedPhrase = walletData.phrase;
*
* // Later, restore the wallet
* const restoredWallet = Wallet.fromSeedPhrase(seedPhrase);
* ```
*/
public export(): any {
return {
phrase: this.phrase,
masterKey: this.masterKey,
accounts: this._accountStore.listAccounts(),
accountIndexes: this.accountStore.listAccounts().keys()
}
}
/**
* Get the account store for managing wallet accounts
*
* The account store provides methods to create, import, and manage accounts
* within this wallet. Each account has its own set of addresses and keys.
*
* @returns The wallet's account store
*
* @example
* ```typescript
* const accountStore = wallet.accountStore;
* const accounts = accountStore.listAccounts();
* const firstAccount = accountStore.getAccount(0);
* ```
*/
get accountStore(): AccountStore {
return this._accountStore;
}
/**
* Get the network this wallet is operating on
*
* @returns The network object (mainnet, testnet, or regtest)
*
* @example
* ```typescript
* const network = wallet.network;
* console.log('Network:', network.name);
* ```
*/
get network(): Networkish {
return this._network;
}
}