UNPKG

@dolaned/wallet-sdk-ts

Version:

Wallet SDK for the Nexa blockchain

170 lines (145 loc) 6.97 kB
import {Networkish, Networks, TransactionBuilder} from "libnexa-ts"; import WatchOnlyTransactionCreator from "./transactions/WatchOnlyTransactionCreator"; import {rostrumProvider} from "../network/RostrumProvider"; import {WatchOnlyAddress} from "../models/wallet.entities"; import {isArray, isBuffer, isNil, isObject, isString} from "lodash-es"; import ValidationUtils from "../utils/ValidationUtils"; import {isValidNexaAddress} from "../utils/WalletUtils"; import {SubscribeCallback} from "@vgrunner/electrum-cash"; import {isNullOrEmpty} from "../utils/CommonUtils"; /** * WatchOnlyWallet provides functionality for monitoring and creating transactions * for addresses without storing private keys. This allows users to track balances * and create unsigned transactions that can be signed elsewhere. */ export default class WatchOnlyWallet { /** The blockchain network this wallet operates on */ private readonly _network: Networkish /** Array of addresses to monitor without their private keys */ private readonly _addressesToWatch: WatchOnlyAddress[]; /** * Creates a new WatchOnlyWallet instance * @param addressesToWatch Array of addresses to monitor * @param network Optional network name (defaults to mainnet) * @throws Error if validation fails */ constructor(addressesToWatch: WatchOnlyAddress[], network?: string) { // Validate network parameter if (network !== undefined && !isString(network)) { throw new Error('Network must be a string'); } if (network !== undefined && isString(network)) { const n = Networks.get(network) if (n === undefined) { throw new Error(`Invalid network: ${network}`); } } this._network = Networks.get(network) ?? Networks.mainnet; // Validate addressesToWatch parameter this._addressesToWatch = this.validateAddressesToWatch(addressesToWatch); } /** * Validates the addresses to watch array * @param addressesToWatch Array of addresses to validate * @returns Validated array of WatchOnlyAddress objects * @throws Error if validation fails */ private validateAddressesToWatch(addressesToWatch: WatchOnlyAddress[]): WatchOnlyAddress[] { if (addressesToWatch === null || addressesToWatch === undefined) { throw new Error('addresesToWatch is required'); } if (!isArray(addressesToWatch)) { throw new Error('addressesToWatch must be an array'); } // Check if array is not empty if (addressesToWatch.length === 0) { throw new Error('addressesToWatch cannot be empty'); } // Validate each address object const validatedAddresses: WatchOnlyAddress[] = []; for (let i = 0; i < addressesToWatch.length; i++) { const addr = addressesToWatch[i]; // Check if address is an object if (!isObject(addr) || isArray(addr)) { throw new Error(`addressesToWatch[${i}] must be an object`); } // Check if address property exists and is a string if (!addr.hasOwnProperty('address') || !isString(addr.address)) { throw new Error(`addressesToWatch[${i}].address must be a string`); } // Check if address is not empty if (addr.address.trim() === '') { throw new Error(`addressesToWatch[${i}].address cannot be empty`); } // Validate address format if (!isValidNexaAddress(addr.address, this._network)) { throw new Error(`addressesToWatch[${i}].address is not a valid NEXA address: ${addr.address}`); } // Validate optional xPub property if (addr.xPub !== undefined && !isObject(addr.xPub)) { throw new Error(`addressesToWatch[${i}].xPub must be a PublicKey object`); } // Validate optional derivationPath property if (addr.derivationPath !== undefined && !isString(addr.derivationPath)) { throw new Error(`addressesToWatch[${i}].derivationPath must be a string`); } // Check for duplicate addresses const isDuplicate = validatedAddresses.some(existingAddr => existingAddr.address === addr.address ); if (isDuplicate) { throw new Error(`Duplicate address found: ${addr.address}`); } validatedAddresses.push({ address: addr.address.trim(), xPub: addr.xPub, derivationPath: addr.derivationPath }); } return validatedAddresses; } /** * Creates a new transaction creator for this watch-only wallet * @param x Optional transaction data - can be a TransactionBuilder, hex string, or Buffer * @returns WatchOnlyTransactionCreator configured with wallet's addresses and network */ public newTransaction(x?: TransactionBuilder | string | Buffer): WatchOnlyTransactionCreator { let tx: WatchOnlyTransactionCreator; // Handle different input types for creating transactions if (x instanceof TransactionBuilder) { // Use existing TransactionBuilder instance tx = new WatchOnlyTransactionCreator(x); } else if (isString(x)) { // Parse transaction from hex string tx = new WatchOnlyTransactionCreator().parseTxHex(x); } else if (isBuffer(x) && !isNil(x)) { // Parse transaction from buffer tx = new WatchOnlyTransactionCreator().parseTxBuffer(x); } else { // Create new empty transaction tx = new WatchOnlyTransactionCreator(); } // Configure transaction with wallet's addresses and network return tx.from(this._addressesToWatch).onNetwork(this._network); } /** * Broadcasts a signed transaction to the network * @param transaction Hex-encoded signed transaction * @returns Promise resolving to transaction ID * @throws Error if transaction is invalid or broadcast fails */ public async sendTransaction(transaction: string): Promise<string> { ValidationUtils.validateArgument(isString(transaction), 'transaction must be present and valid') return rostrumProvider.broadcast(transaction) } public async subscribeToAddressNotifications(callback: SubscribeCallback): Promise<void>{ await rostrumProvider.subscribeToAddresses(this._addressesToWatch.map(addr => addr.address), callback) } /** * Gets the list of addresses being watched * @returns Array of watched addresses (copy to prevent mutation) */ public getWatchedAddresses(): WatchOnlyAddress[] { return [...this._addressesToWatch]; } }