diff --git a/src/providers/abstract-provider.ts b/src/providers/abstract-provider.ts index 71b21630..45683b00 100644 --- a/src/providers/abstract-provider.ts +++ b/src/providers/abstract-provider.ts @@ -77,6 +77,7 @@ import { PollingEventSubscriber, PollingOrphanSubscriber, PollingTransactionSubscriber, + QiPollingTransactionSubscriber, } from './subscriber-polling.js'; import { getNodeLocationFromZone, getZoneFromNodeLocation } from '../utils/shards.js'; import { fromShard } from '../constants/shards.js'; @@ -159,6 +160,12 @@ export type Subscription = hash: string; zone: Zone; } + | { + type: 'qiTransaction'; + tag: string; + hash: string; + zone: Zone; + } | { type: 'event'; tag: string; @@ -280,7 +287,7 @@ function concisify(items: Array): Array { // todo `provider` is not used, remove or re-write // eslint-disable-next-line @typescript-eslint/no-unused-vars -async function getSubscription(_event: ProviderEvent, zone?: Zone): Promise { +async function getSubscription(_event: ProviderEvent, zone?: Zone, hash?: string): Promise { if (_event == null) { throw new Error('invalid event'); } @@ -307,12 +314,17 @@ async function getSubscription(_event: ProviderEvent, zone?: Zone): Promise implements Provider { // For QiTransaction, use fromProto() directly return new QiTransactionResponse(tx, this); } else { - throw new Error('Unknown transaction type'); + throw new Error(`Unknown transaction type: ${tx.type}`); } } catch (error) { console.error('Error in _wrapTransactionResponse:', error); @@ -1674,8 +1686,8 @@ export class AbstractProvider implements Provider { return this._wrapBlock(params, network); } - async getTransaction(hash: string): Promise { - const zone = toZone(this.shardFromHash(hash)); + async getTransaction(hash: string, _zone?: Zone): Promise { + const zone = _zone ?? toZone(this.shardFromHash(hash)); const { network, params } = await resolveProperties({ network: this.getNetwork(), params: this.#perform({ method: 'getTransaction', hash, zone: zone }), @@ -1893,6 +1905,8 @@ export class AbstractProvider implements Provider { return new PollingEventSubscriber(this as AbstractProvider, sub.filter); case 'transaction': return new PollingTransactionSubscriber(this as AbstractProvider, sub.hash, sub.zone); + case 'qiTransaction': + return new QiPollingTransactionSubscriber(this as AbstractProvider, sub.hash, sub.zone); case 'orphan': return new PollingOrphanSubscriber(this as AbstractProvider, sub.filter, sub.zone); } @@ -1928,8 +1942,8 @@ export class AbstractProvider implements Provider { } } - async #hasSub(event: ProviderEvent, emitArgs?: Array, zone?: Zone): Promise { - let sub = await getSubscription(event, zone); + async #hasSub(event: ProviderEvent, emitArgs?: Array, zone?: Zone, hash?: string): Promise { + let sub = await getSubscription(event, zone, hash); // This is a log that is removing an existing log; we actually want // to emit an orphan event for the removed log if (sub.type === 'event' && emitArgs && emitArgs.length > 0 && emitArgs[0].removed === true) { @@ -1938,8 +1952,8 @@ export class AbstractProvider implements Provider { return this.#subs.get(sub.tag) || null; } - async #getSub(event: ProviderEvent, zone?: Zone): Promise { - const subscription = await getSubscription(event, zone); + async #getSub(event: ProviderEvent, zone?: Zone, hash?: string): Promise { + const subscription = await getSubscription(event, zone, hash); // Prevent tampering with our tag in any subclass' _getSubscriber const tag = subscription.tag; @@ -1957,8 +1971,8 @@ export class AbstractProvider implements Provider { return sub; } - async on(event: ProviderEvent, listener: Listener, zone?: Zone): Promise { - const sub = await this.#getSub(event, zone); + async on(event: ProviderEvent, listener: Listener, zone?: Zone, hash?: string): Promise { + const sub = await this.#getSub(event, zone, hash); sub.listeners.push({ listener, once: false }); if (!sub.started) { sub.subscriber.start(); @@ -2042,8 +2056,8 @@ export class AbstractProvider implements Provider { return result; } - async off(event: ProviderEvent, listener?: Listener, zone?: Zone): Promise { - const sub = await this.#hasSub(event, [], zone); + async off(event: ProviderEvent, listener?: Listener, zone?: Zone, hash?: string): Promise { + const sub = await this.#hasSub(event, [], zone, hash); if (!sub) { return this; } diff --git a/src/providers/provider.ts b/src/providers/provider.ts index 851eb332..39db9e1c 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -2412,6 +2412,105 @@ export class QiTransactionResponse implements QiTransactionLike, QiTransactionRe return blockNumber - this.blockNumber + 1; } + async wait(_confirms?: number, _timeout?: number): Promise { + const confirms = _confirms == null ? 1 : _confirms; + const timeout = _timeout == null ? 0 : _timeout; + + const startBlock = this.startBlock; + let stopScanning = startBlock === -1 ? true : false; + + const zoneFromInput = async (txInputs: Array | undefined): Promise => { + if (!txInputs || txInputs.length === 0) { + throw new Error('No transaction inputs provided'); + } + + const firstInput = txInputs[0]; + if (!firstInput.pubkey) { + throw new Error('Public key not found in the first transaction input'); + } + const address = computeAddress(firstInput.pubkey); + const zone = getZoneForAddress(address); + if (!zone) { + throw new Error(`Invalid zone for address: ${address}`); + } + return zone; + }; + + const zone = await zoneFromInput(this.txInputs); + + const response = await this.provider.getTransaction(this.hash, zone); + if (response && response.isMined() && confirms === 0) { + return response as QiTransactionResponse; + } + + if (response) { + if ((await response.confirmations()) >= confirms) { + return response as QiTransactionResponse; + } + } else { + // Allow null only when the confirms is 0 + if (confirms === 0) { + return null; + } + } + + const waiter = new Promise((resolve, reject) => { + const cancellers: Array<() => void> = []; + const cancel = () => { + cancellers.forEach((c) => c()); + }; + + cancellers.push(() => { + stopScanning = true; + }); + + if (timeout > 0) { + const timer = setTimeout(() => { + cancel(); + reject(makeError('wait for transaction timeout', 'TIMEOUT')); + }, timeout); + cancellers.push(() => { + clearTimeout(timer); + }); + } + + const txListener = async (response: QiTransactionResponse) => { + if ((await response.confirmations()) >= confirms) { + cancel(); + resolve(response); + } + }; + + cancellers.push(() => { + this.provider.off('qiTransaction', txListener, zone, this.hash); + }); + this.provider.on('qiTransaction', txListener, zone, this.hash); + + if (startBlock >= 0) { + const blockListener = async () => { + const currentBlock = await this.provider.getBlockNumber(toShard(zone)); + if (currentBlock - startBlock >= confirms) { + const response = await this.provider.getTransaction(this.hash, zone); + if (response && response.isMined()) { + cancel(); + resolve(response); + } + } + + if (!stopScanning) { + this.provider.once('block', blockListener, zone); + } + }; + cancellers.push(() => { + this.provider.off('block', blockListener, zone); + }); + this.provider.once('block', blockListener, zone); + } + }); + + return await (>waiter); + } + /** * Returns `true` if this transaction has been included. * @@ -2860,7 +2959,7 @@ export interface Provider extends ContractRunner, EventEmitterable} A promise resolving to the transaction or null if not found. */ - getTransaction(hash: string): Promise; + getTransaction(hash: string, zone?: Zone): Promise; /** * Resolves to the transaction receipt for `hash`, if mined. diff --git a/src/providers/subscriber-polling.ts b/src/providers/subscriber-polling.ts index f109179c..109b3c20 100644 --- a/src/providers/subscriber-polling.ts +++ b/src/providers/subscriber-polling.ts @@ -25,7 +25,12 @@ function copy(obj: any): any { * @returns {Subscriber} The polling subscriber. * @throws {Error} If the event is unsupported. */ -export function getPollingSubscriber(provider: AbstractProvider, event: ProviderEvent, zone: Zone): Subscriber { +export function getPollingSubscriber( + provider: AbstractProvider, + event: ProviderEvent, + zone: Zone, + hash?: string, +): Subscriber { if (event === 'block') { return new PollingBlockSubscriber(provider, zone); } @@ -34,6 +39,11 @@ export function getPollingSubscriber(provider: AbstractProvider, event: Provider return new PollingTransactionSubscriber(provider, event, zone); } + if (event === 'qiTransaction') { + assert(hash != null, "hash is required for 'qiTransaction' event", 'MISSING_ARGUMENT'); + return new QiPollingTransactionSubscriber(provider, hash, zone); + } + assert(false, 'unsupported polling event', 'UNSUPPORTED_OPERATION', { operation: 'getPollingSubscriber', info: { event }, @@ -321,6 +331,24 @@ export class PollingTransactionSubscriber extends OnBlockSubscriber { } } +export class QiPollingTransactionSubscriber extends OnBlockSubscriber { + #hash: string; + #zone: Zone; + + constructor(provider: AbstractProvider, hash: string, zone: Zone) { + super(provider, zone); + this.#hash = hash; + this.#zone = zone; + } + + async _poll(blockNumber: number, provider: AbstractProvider): Promise { + const tx = await provider.getTransaction(this.#hash, this.#zone); + if (tx) { + provider.emit(this.#hash, this.#zone, tx); + } + } +} + /** * A **PollingEventSubscriber** will poll for a given filter for its logs. * diff --git a/src/utils/events.ts b/src/utils/events.ts index 07430f53..95a2ba39 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -23,7 +23,7 @@ export interface EventEmitterable { /** * Registers a `listener` that is called whenever the `event` occurs until unregistered. */ - on(event: T, listener: Listener, zone?: Zone): Promise; + on(event: T, listener: Listener, zone?: Zone, hash?: string): Promise; /** * Registers a `listener` that is called the next time `event` occurs. @@ -48,7 +48,7 @@ export interface EventEmitterable { /** * Unregister the `listener` for `event`. If `listener` is unspecified, all listeners are unregistered. */ - off(event: T, listener?: Listener, zone?: Zone): Promise; + off(event: T, listener?: Listener, zone?: Zone, hash?: string): Promise; /** * Unregister all listeners for `event`.