From e7ac86c0a463fc979b6b1264d5da3e36227dbfa4 Mon Sep 17 00:00:00 2001 From: Jim Hamill Date: Tue, 22 Apr 2025 00:12:00 +0000 Subject: [PATCH] Make waitForTransaction part of BaseBlockchain --- .gitlab-ci.yml | 5 -- packages/primitives/src/address.ts | 16 +----- packages/primitives/src/script.ts | 21 +++++++- .../stack/src/blockchain/base-blockchain.ts | 53 ++++++++++++++++++- .../src/blockchain/blockchain-electrum.ts | 34 ------------ 5 files changed, 73 insertions(+), 56 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9fcd20d..197c742 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,11 +35,6 @@ publish:development: GITLAB_USER_EMAIL: 'ci@stack.xo.cash' GITLAB_USER_NAME: 'GitLab CI' script: - # Add a small delay to allow GitLab to fully process the merge - # NOTE: Temporary hack to check if publish is not working due to race condition. - - echo "Waiting 30 seconds for GitLab to process merge..." - - sleep 30 - # Setup git config - git config --global user.email "${GITLAB_USER_EMAIL}" - git config --global user.name "${GITLAB_USER_NAME}" diff --git a/packages/primitives/src/address.ts b/packages/primitives/src/address.ts index 3b969ae..0a22bd3 100644 --- a/packages/primitives/src/address.ts +++ b/packages/primitives/src/address.ts @@ -184,24 +184,10 @@ export class Address { let payload = sha256.hash(bytes); if (type === 'P2SH20') { - // Make sure length is 20 bytes. - if (bytes.length !== 20) { - throw new Error( - `Invalid payload length. Received '${bytes.length}', expected 20 for P2SH20`, - ); - } - // RIPEMD160 the SHA256 hash. payload = ripemd160.hash(payload); } else if (type === 'P2SH32') { - // Make sure length is 32 bytes. - if (bytes.length !== 32) { - throw new Error( - `Invalid payload length. Received '${bytes.length}', expected 32 for P2SH20`, - ); - } - - // SHA256 Hash the SHA256 hash a seconr time. + // SHA256 Hash the SHA256 hash a second time. payload = sha256.hash(payload); } diff --git a/packages/primitives/src/script.ts b/packages/primitives/src/script.ts index c6d1083..af85a0f 100644 --- a/packages/primitives/src/script.ts +++ b/packages/primitives/src/script.ts @@ -1,6 +1,10 @@ import { Bytes } from './bytes.js'; -import { assembleBytecodeBCH, disassembleBytecodeBCH } from '@bitauth/libauth'; +import { + assembleBytecodeBCH, + disassembleBytecodeBCH, + compileCashAssembly, +} from '@bitauth/libauth'; /** * Script Entity. @@ -109,6 +113,21 @@ export class Script { return new this(assembleResult.bytecode); } + // NOTE: In its current form, this is pretty useless because it does not allow OP_CODES. + // Disabling because it will probably just cause confusion. + /* + public static fromCashASM(cashASM: string) { + const result = compileCashAssembly(cashASM); + + if (typeof result === 'string') { + // TODO: Make error more detailed. + throw new Error(`Failed to parse CashASM: ${result}`); + } + + return new this(result); + } + */ + /** * Create a Script Entity from the given bytes (as hexadecimal string). * diff --git a/packages/stack/src/blockchain/base-blockchain.ts b/packages/stack/src/blockchain/base-blockchain.ts index 0731388..228bae9 100644 --- a/packages/stack/src/blockchain/base-blockchain.ts +++ b/packages/stack/src/blockchain/base-blockchain.ts @@ -6,7 +6,7 @@ import { type BlockchainTransactions, } from './types.js'; -import { type Transaction } from '@xocash/primitives'; +import { Transaction } from '@xocash/primitives'; export type BlockchainEvents = { isConnectedUpdated: boolean; @@ -177,4 +177,55 @@ export abstract class BaseBlockchain< transactionHash: string, callback: BlockchainCallback, ): Promise; + + /** + * Waits for a transaction to be seen by the node. + * + * @param tx - The transaction to wait for. Can be a Uint8Array, Transaction instance, or a string. + * @param timeoutMs - The maximum time to wait in milliseconds before timing out. Defaults to 10,000ms (10 seconds). + * @returns A Promise that resolves when the transaction is seen by the node or rejects if the timeout is reached. + * @throws Error if the transaction is not seen by the node within the specified timeout. + */ + async waitForTransaction( + tx: Uint8Array | Transaction | string, + timeoutMs = 10_000, + ) { + // Cast the transaction. + if (!(tx instanceof Transaction)) { + tx = Transaction.from(tx); + } + + const txHash = tx.toHashHex(); + + // First, subscribe to the transaction + let unsubscribeCallback: () => void; + const subscriptionPromise = this.subscribeTransaction(txHash, () => { + if (unsubscribeCallback) { + unsubscribeCallback(); + } + }); + + // Then create the wait promise without an async executor + const waitForTxPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (unsubscribeCallback) { + unsubscribeCallback(); + } + reject(new Error(`Failed to broadcast ${txHash}`)); + }, timeoutMs); + + // Create a subscribe function so that we get notified when transaction is seen by network + unsubscribeCallback = () => { + clearTimeout(timeout); + this.unsubscribeTransaction(txHash, unsubscribeCallback); + resolve(); + }; + }); + + // Wait for subscription to be set up first + await subscriptionPromise; + + // Then wait for either resolution or timeout + return waitForTxPromise; + } } diff --git a/packages/stack/src/blockchain/blockchain-electrum.ts b/packages/stack/src/blockchain/blockchain-electrum.ts index a089a20..bffaded 100644 --- a/packages/stack/src/blockchain/blockchain-electrum.ts +++ b/packages/stack/src/blockchain/blockchain-electrum.ts @@ -317,40 +317,6 @@ export class BlockchainElectrum extends BaseBlockchain< return transactionsIndexed; } - async waitForTransaction( - tx: Uint8Array | Transaction | string, - timeoutMs = 10_000, - ) { - // Cast the transaction. - if (!(tx instanceof Transaction)) { - tx = Transaction.from(tx); - } - - // Attempt to broadcast the transaction. - const waitForTxPromise = new Promise(async (resolve, reject) => { - // Set a timeout. - const timeout = setTimeout(() => { - reject(new Error(`Failed to broadcast ${tx.toHashHex()}`)); - }, timeoutMs); - - // Create a subscribe function so that we get notified when transaction is seen by network - const subscribeCallback = () => { - clearTimeout(timeout); - this.unsubscribeTransaction(tx.toHashHex(), subscribeCallback); - resolve(); - }; - - // Subscribe to the transaction. - await this.subscribeTransaction(tx.toHashHex(), subscribeCallback); - }); - - return waitForTxPromise; - } - - // TODO: Split out the "waitUntilSeen" into own function. - // This'll be useful for protocols like BIP70/JPP/CashConnect, etc. - // Also consider a fail callback: - // If a "waitFor" fails, we probably want to invalidate the tx by sending those inputs elsewhere. async broadcastTransaction( tx: Transaction | Uint8Array | string, opts: BroadcastOpts = { timeoutMs: 10_000, waitUntilSeen: true },