Skip to content

Commit

Permalink
Merge pull request #404 from multiversx/tx-watcher-for-old-transactions
Browse files Browse the repository at this point in the history
Refactor TransactionWatcher to accept either Transaction or hash
  • Loading branch information
popenta authored Mar 19, 2024
2 parents 52fdbf7 + dad51f8 commit ee7e2d4
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 54 deletions.
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export const DEFAULT_HRP = "erd";
export const ESDT_CONTRACT_ADDRESS = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u";
export const DEFAULT_MESSAGE_VERSION = 1;
export const MESSAGE_PREFIX = "\x17Elrond Signed Message:\n";
export const DEFAULT_HEX_HASH_LENGTH = 64;
27 changes: 25 additions & 2 deletions src/transactionWatcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { TransactionWatcher } from "./transactionWatcher";


describe("test transactionWatcher", () => {
it("should await status == executed", async () => {
let hash = new TransactionHash("abba");
it("should await status == executed using hash", async () => {
let hash = new TransactionHash("abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba");
let provider = new MockNetworkProvider();
let watcher = new TransactionWatcher(provider, {
pollingIntervalMilliseconds: 42,
Expand All @@ -28,4 +28,27 @@ describe("test transactionWatcher", () => {

assert.isTrue((await provider.getTransactionStatus(hash.hex())).isExecuted());
});

it("should await status == executed using transaction", async () => {
let hash = new TransactionHash("abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba");
let provider = new MockNetworkProvider();
let watcher = new TransactionWatcher(provider, {
pollingIntervalMilliseconds: 42,
timeoutMilliseconds: 42 * 42
});
let dummyTransaction = {
getHash: () => hash
}

provider.mockPutTransaction(hash, new TransactionOnNetwork({
status: new TransactionStatus("unknown")
}));

await Promise.all([
provider.mockTransactionTimelineByHash(hash, [new Wait(40), new TransactionStatus("pending"), new Wait(40), new TransactionStatus("executed"), new MarkCompleted()]),
watcher.awaitCompleted(dummyTransaction)
]);

assert.isTrue((await provider.getTransactionStatus(hash.hex())).isExecuted());
});
});
138 changes: 86 additions & 52 deletions src/transactionWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { AsyncTimer } from "./asyncTimer";
import { Err, ErrExpectedTransactionEventsNotFound, ErrExpectedTransactionStatusNotReached, ErrIsCompletedFieldIsMissingOnTransaction } from "./errors";
import { DEFAULT_HEX_HASH_LENGTH } from "./constants";
import {
Err,
ErrExpectedTransactionEventsNotFound,
ErrExpectedTransactionStatusNotReached,
ErrIsCompletedFieldIsMissingOnTransaction,
} from "./errors";
import { ITransactionFetcher } from "./interface";
import { ITransactionEvent, ITransactionOnNetwork, ITransactionStatus } from "./interfaceOfNetwork";
import { Logger } from "./logger";

export type PredicateIsAwaitedStatus = (status: ITransactionStatus) => boolean;

/**
* Internal interface: a transaction, as seen from the perspective of a {@link TransactionWatcher}.
*/
interface ITransaction {
getHash(): { hex(): string };
}

/**
* TransactionWatcher allows one to continuously watch (monitor), by means of polling, the status of a given transaction.
*/
Expand All @@ -14,7 +27,7 @@ export class TransactionWatcher {
static DefaultTimeout: number = TransactionWatcher.DefaultPollingInterval * 15;
static DefaultPatience: number = 0;

static NoopOnStatusReceived = (_: ITransactionStatus) => { };
static NoopOnStatusReceived = (_: ITransactionStatus) => {};

protected readonly fetcher: ITransactionFetcher;
protected readonly pollingIntervalMilliseconds: number;
Expand All @@ -23,7 +36,7 @@ export class TransactionWatcher {

/**
* A transaction watcher (awaiter).
*
*
* @param fetcher The transaction fetcher
* @param options The options
* @param options.pollingIntervalMilliseconds The polling interval, in milliseconds
Expand All @@ -33,12 +46,14 @@ export class TransactionWatcher {
constructor(
fetcher: ITransactionFetcher,
options: {
pollingIntervalMilliseconds?: number,
timeoutMilliseconds?: number,
patienceMilliseconds?: number
} = {}) {
pollingIntervalMilliseconds?: number;
timeoutMilliseconds?: number;
patienceMilliseconds?: number;
} = {},
) {
this.fetcher = new TransactionFetcherWithTracing(fetcher);
this.pollingIntervalMilliseconds = options.pollingIntervalMilliseconds || TransactionWatcher.DefaultPollingInterval;
this.pollingIntervalMilliseconds =
options.pollingIntervalMilliseconds || TransactionWatcher.DefaultPollingInterval;
this.timeoutMilliseconds = options.timeoutMilliseconds || TransactionWatcher.DefaultTimeout;
this.patienceMilliseconds = options.patienceMilliseconds || TransactionWatcher.DefaultPatience;
}
Expand All @@ -47,89 +62,108 @@ export class TransactionWatcher {
* Waits until the transaction reaches the "pending" status.
* @param txHash The hex-encoded transaction hash
*/
public async awaitPending(txHash: string): Promise<ITransactionOnNetwork> {
public async awaitPending(transactionOrTxHash: ITransaction | string): Promise<ITransactionOnNetwork> {
const isPending = (transaction: ITransactionOnNetwork) => transaction.status.isPending();
const doFetch = async () => await this.fetcher.getTransaction(txHash);
const doFetch = async () => {
const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash);
return await this.fetcher.getTransaction(hash);
};
const errorProvider = () => new ErrExpectedTransactionStatusNotReached();

return this.awaitConditionally<ITransactionOnNetwork>(
isPending,
doFetch,
errorProvider
);
return this.awaitConditionally<ITransactionOnNetwork>(isPending, doFetch, errorProvider);
}

/**
* Waits until the transaction is completely processed.
* @param txHash The hex-encoded transaction hash
*/
public async awaitCompleted(txHash: string): Promise<ITransactionOnNetwork> {
* Waits until the transaction is completely processed.
* @param txHash The hex-encoded transaction hash
*/
public async awaitCompleted(transactionOrTxHash: ITransaction | string): Promise<ITransactionOnNetwork> {
const isCompleted = (transactionOnNetwork: ITransactionOnNetwork) => {
if (transactionOnNetwork.isCompleted === undefined) {
throw new ErrIsCompletedFieldIsMissingOnTransaction();
}
return transactionOnNetwork.isCompleted
return transactionOnNetwork.isCompleted;
};

const doFetch = async () => await this.fetcher.getTransaction(txHash);
const doFetch = async () => {
const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash);
return await this.fetcher.getTransaction(hash);
};
const errorProvider = () => new ErrExpectedTransactionStatusNotReached();

return this.awaitConditionally<ITransactionOnNetwork>(
isCompleted,
doFetch,
errorProvider
);
return this.awaitConditionally<ITransactionOnNetwork>(isCompleted, doFetch, errorProvider);
}

public async awaitAllEvents(txHash: string, events: string[]): Promise<ITransactionOnNetwork> {
public async awaitAllEvents(
transactionOrTxHash: ITransaction | string,
events: string[],
): Promise<ITransactionOnNetwork> {
const foundAllEvents = (transactionOnNetwork: ITransactionOnNetwork) => {
const allEventIdentifiers = this.getAllTransactionEvents(transactionOnNetwork).map(event => event.identifier);
const allAreFound = events.every(event => allEventIdentifiers.includes(event));
const allEventIdentifiers = this.getAllTransactionEvents(transactionOnNetwork).map(
(event) => event.identifier,
);
const allAreFound = events.every((event) => allEventIdentifiers.includes(event));
return allAreFound;
};

const doFetch = async () => await this.fetcher.getTransaction(txHash);
const doFetch = async () => {
const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash);
return await this.fetcher.getTransaction(hash);
};
const errorProvider = () => new ErrExpectedTransactionEventsNotFound();

return this.awaitConditionally<ITransactionOnNetwork>(
foundAllEvents,
doFetch,
errorProvider
);
return this.awaitConditionally<ITransactionOnNetwork>(foundAllEvents, doFetch, errorProvider);
}

public async awaitAnyEvent(txHash: string, events: string[]): Promise<ITransactionOnNetwork> {
public async awaitAnyEvent(
transactionOrTxHash: ITransaction | string,
events: string[],
): Promise<ITransactionOnNetwork> {
const foundAnyEvent = (transactionOnNetwork: ITransactionOnNetwork) => {
const allEventIdentifiers = this.getAllTransactionEvents(transactionOnNetwork).map(event => event.identifier);
const anyIsFound = events.find(event => allEventIdentifiers.includes(event)) != undefined;
const allEventIdentifiers = this.getAllTransactionEvents(transactionOnNetwork).map(
(event) => event.identifier,
);
const anyIsFound = events.find((event) => allEventIdentifiers.includes(event)) != undefined;
return anyIsFound;
};

const doFetch = async () => await this.fetcher.getTransaction(txHash);
const doFetch = async () => {
const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash);
return await this.fetcher.getTransaction(hash);
};
const errorProvider = () => new ErrExpectedTransactionEventsNotFound();

return this.awaitConditionally<ITransactionOnNetwork>(
foundAnyEvent,
doFetch,
errorProvider
);
return this.awaitConditionally<ITransactionOnNetwork>(foundAnyEvent, doFetch, errorProvider);
}

public async awaitOnCondition(txHash: string, condition: (data: ITransactionOnNetwork) => boolean): Promise<ITransactionOnNetwork> {
const doFetch = async () => await this.fetcher.getTransaction(txHash);
public async awaitOnCondition(
transactionOrTxHash: ITransaction | string,
condition: (data: ITransactionOnNetwork) => boolean,
): Promise<ITransactionOnNetwork> {
const doFetch = async () => {
const hash = this.transactionOrTxHashToTxHash(transactionOrTxHash);
return await this.fetcher.getTransaction(hash);
};
const errorProvider = () => new ErrExpectedTransactionStatusNotReached();

return this.awaitConditionally<ITransactionOnNetwork>(
condition,
doFetch,
errorProvider
);
return this.awaitConditionally<ITransactionOnNetwork>(condition, doFetch, errorProvider);
}

private transactionOrTxHashToTxHash(transactionOrTxHash: ITransaction | string): string {
const hash =
typeof transactionOrTxHash === "string" ? transactionOrTxHash : transactionOrTxHash.getHash().hex();

if (hash.length !== DEFAULT_HEX_HASH_LENGTH) {
throw new Err("Invalid transaction hash length. The length of a hex encoded hash should be 64.");
}

return hash;
}

protected async awaitConditionally<TData>(
isSatisfied: (data: TData) => boolean,
doFetch: () => Promise<TData>,
createError: () => Err
createError: () => Err,
): Promise<TData> {
const periodicTimer = new AsyncTimer("watcher:periodic");
const patienceTimer = new AsyncTimer("watcher:patience");
Expand Down

0 comments on commit ee7e2d4

Please sign in to comment.