@dolaned/wallet-sdk-ts
Version:
Wallet SDK for the Nexa blockchain
170 lines (145 loc) • 6.97 kB
text/typescript
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];
}
}