From 4cf729f244035239d959b934022f7a5527c2ef6a Mon Sep 17 00:00:00 2001 From: Jim Hamill Date: Wed, 18 Jun 2025 06:53:30 +0000 Subject: [PATCH] signTransaction returns TransactionBuilder --- packages/primitives/src/address.ts | 35 +++++++++++++++++-- packages/stack/src/wallet/base-wallet.ts | 3 +- .../stack/src/wallet/transaction-builder.ts | 10 ++++++ packages/stack/src/wallet/wallet-hd.ts | 16 ++++----- packages/stack/src/wallet/wallet-p2pkh.ts | 12 +++---- 5 files changed, 57 insertions(+), 19 deletions(-) diff --git a/packages/primitives/src/address.ts b/packages/primitives/src/address.ts index d5fce8a..f7d394d 100644 --- a/packages/primitives/src/address.ts +++ b/packages/primitives/src/address.ts @@ -68,7 +68,7 @@ export class Address { ): Address { // Attempt to decode as a Cash Address. try { - return this.fromCashAddr(address, tryPrefixes); + return this.fromCashAddr(address, false, tryPrefixes); } catch (_error) { // And if that fails, attempt to decode as a Base58 Address. return this.fromLegacy(address); @@ -89,6 +89,7 @@ export class Address { */ public static fromCashAddr( cashAddr: string, + requireTokenSupport = false, tryPrefixes: Array = DEFAULT_ADDRESS_PREFIXES, ): Address { // Try all prefixes (including empty string) until we find a valid decode @@ -105,6 +106,16 @@ export class Address { // Define the encode type in advance. let encodeType: AddressType; + // Determine whether this is a CashToken address. + const isTokenAddress = + decodeResult.type === 'p2pkhWithTokens' || + decodeResult.type === 'p2shWithTokens'; + + // If this is not a CashToken Address, but we require a CashToken address, throw an error. + if (!isTokenAddress && requireTokenSupport) { + throw new Error(`Address '${cashAddr}' does not support tokens`); + } + // Determine the encoding to use based on the Decoding Result's Type. switch (decodeResult.type) { case 'p2pkh': @@ -365,7 +376,27 @@ export class Address { tryPrefixes = DEFAULT_ADDRESS_PREFIXES, ): boolean { try { - Address.fromCashAddr(address, tryPrefixes); + Address.fromCashAddr(address, false, tryPrefixes); + } catch (_error) { + return false; + } + + return true; + } + + /** + * Checks whether the given address is a valid CashAddr with Token Support. + * + * @param address The address. + * + * @returns boolean indicating whether the address is a valid CashAddr with Token Support. + */ + public static isValidTokenAddress( + address: string, + tryPrefixes = DEFAULT_ADDRESS_PREFIXES, + ): boolean { + try { + Address.fromCashAddr(address, true, tryPrefixes); } catch (_error) { return false; } diff --git a/packages/stack/src/wallet/base-wallet.ts b/packages/stack/src/wallet/base-wallet.ts index 3146dea..db5637b 100644 --- a/packages/stack/src/wallet/base-wallet.ts +++ b/packages/stack/src/wallet/base-wallet.ts @@ -1,3 +1,4 @@ +import { type TransactionBuilder } from './transaction-builder.js'; import { WalletUTXOs, type InputTemplateBCH, @@ -207,7 +208,7 @@ export abstract class BaseWallet< */ abstract signTransaction( template: Partial, - ): Promise; + ): Promise; /** * Select UTXOs from this wallet to meet the amounts required and broadcast it to the Blockchain. diff --git a/packages/stack/src/wallet/transaction-builder.ts b/packages/stack/src/wallet/transaction-builder.ts index 9f89ca3..9c697de 100644 --- a/packages/stack/src/wallet/transaction-builder.ts +++ b/packages/stack/src/wallet/transaction-builder.ts @@ -13,6 +13,7 @@ import { encodeTransactionOutput, generateTransaction, getDustThreshold, + isDustOutput, } from '@bitauth/libauth'; // TODO: This could probably still use a refactor, but isn't terrible anymore. @@ -303,6 +304,15 @@ export class TransactionBuilder { throw new Error(`Failed to generate transaction: ${errors}`); } + // Check for dust outputs. + const hasDustOutputs = generatedTransaction.transaction.outputs.some( + (output) => isDustOutput(output), + ); + + if (hasDustOutputs) { + throw new Error('Output amounts must be above the dust threshold'); + } + return generatedTransaction.transaction; } diff --git a/packages/stack/src/wallet/wallet-hd.ts b/packages/stack/src/wallet/wallet-hd.ts index f8e6ed9..a7e1621 100644 --- a/packages/stack/src/wallet/wallet-hd.ts +++ b/packages/stack/src/wallet/wallet-hd.ts @@ -293,7 +293,7 @@ export class WalletHD extends BaseWallet { async signTransaction( txTemplate: Partial, - ): Promise { + ): Promise { // Populate the final transaction template by merging with defaults. // NOTE: This is just a convenience to make "send" easier to use. const txTemplateFinal: TransactionTemplate = { @@ -317,21 +317,19 @@ export class WalletHD extends BaseWallet { const unspents = await this.getUnspentDirectives(); // Build the transaction. - const tx = TransactionBuilder.build( + return TransactionBuilder.build( txTemplateFinal, unspents.toArray(), changeLockingBytecode, ); - - // Generate the transaction. - const generatedTxBytes = tx.toBytes(); - - return generatedTxBytes; } async sendTransaction(txTemplate: Partial) { - // Sign the transaction template. - const generatedTxBytes = await this.signTransaction(txTemplate); + // Generate the transaction. + const generatedTx = await this.signTransaction(txTemplate); + + // Convert the tx to bytes. + const generatedTxBytes = generatedTx.toBytes(); // Broadcast the transaction to the blockchain. await this.blockchain.broadcastTransaction(generatedTxBytes); diff --git a/packages/stack/src/wallet/wallet-p2pkh.ts b/packages/stack/src/wallet/wallet-p2pkh.ts index 4cf64c4..9cc4085 100644 --- a/packages/stack/src/wallet/wallet-p2pkh.ts +++ b/packages/stack/src/wallet/wallet-p2pkh.ts @@ -360,21 +360,19 @@ export class WalletP2PKH extends BaseWallet { const unspents = await this.getUnspentDirectives(); // Build the transaction. - const tx = TransactionBuilder.build( + return TransactionBuilder.build( txTemplateFinal, unspents.toArray(), changeLockingBytecode, ); - - // Generate the transaction. - const generatedTxBytes = tx.toBytes(); - - return generatedTxBytes; } async sendTransaction(txTemplate: Partial) { // Generate the transaction. - const generatedTxBytes = await this.signTransaction(txTemplate); + const generatedTx = await this.signTransaction(txTemplate); + + // Convert the tx to bytes. + const generatedTxBytes = generatedTx.toBytes(); // Broadcast the transaction to the blockchain. await this.blockchain.broadcastTransaction(generatedTxBytes);