Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,12 @@ export class ErrInvalidNetworkProviderKind extends Err {
super("Invalid network provider kind. Choose between `api` and `proxy`.");
}
}

/**
* Signals that the account condition was not reached
*/
export class ExpectedAccountConditionNotReachedError extends Err {
public constructor() {
super("The expected account condition was not reached.");
}
}
78 changes: 78 additions & 0 deletions src/networkProviders/accountAwaiter.dev.net.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { assert } from "chai";
import { Address } from "../address";
import { MarkCompleted, MockNetworkProvider, Wait } from "../testutils/mockNetworkProvider";
import { createAccountBalance } from "../testutils/utils";
import { loadTestWallet } from "../testutils/wallets";
import { Transaction } from "../transaction";
import { TransactionComputer } from "../transactionComputer";
import { AccountAwaiter } from "./accountAwaiter";
import { AccountOnNetwork } from "./accounts";
import { ApiNetworkProvider } from "./apiNetworkProvider";

describe("AccountAwaiter Tests", () => {
const provider = new MockNetworkProvider();

const watcher = new AccountAwaiter({
fetcher: provider,
pollingIntervalInMilliseconds: 42,
timeoutIntervalInMilliseconds: 42 * 42,
patienceTimeInMilliseconds: 0,
});

it("should await on balance increase", async () => {
const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th");
// alice account is created with 1000 EGLD
const initialBalance = (await provider.getAccount(alice)).balance;

// Mock balance timeline
provider.mockAccountBalanceTimelineByAddress(alice, [
new Wait(40),
new Wait(40),
new Wait(45),
new MarkCompleted(),
]);

const condition = (account: AccountOnNetwork) => {
return account.balance === initialBalance + createAccountBalance(7);
};

const account = await watcher.awaitOnCondition(alice, condition);

assert.equal(account.balance, createAccountBalance(1007));
});

it("should await for account balance increase on the network", async function () {
this.timeout(20000);
const alice = await loadTestWallet("alice");
const aliceAddress = alice.getAddress();
const frank = Address.newFromBech32("erd1kdl46yctawygtwg2k462307dmz2v55c605737dp3zkxh04sct7asqylhyv");

const api = new ApiNetworkProvider("https://devnet-api.multiversx.com");
const watcher = new AccountAwaiter({ fetcher: api });
const txComputer = new TransactionComputer();
const value = 100_000n;

// Create and sign the transaction
const transaction = new Transaction({
sender: aliceAddress,
receiver: frank,
gasLimit: 50000n,
chainID: "D",
value,
});
transaction.nonce = (await api.getAccount(aliceAddress)).nonce;
transaction.signature = await alice.signer.sign(txComputer.computeBytesForSigning(transaction));

const initialBalance = (await api.getAccount(frank)).balance;

const condition = (account: AccountOnNetwork) => {
return account.balance === initialBalance + value;
};

await api.sendTransaction(transaction);

const accountOnNetwork = await watcher.awaitOnCondition(frank, condition);

assert.equal(accountOnNetwork.balance, initialBalance + value);
});
});
104 changes: 104 additions & 0 deletions src/networkProviders/accountAwaiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Address } from "../address";
import { ExpectedAccountConditionNotReachedError } from "../errors";
import { AccountOnNetwork } from "./accounts";
import {
DEFAULT_ACCOUNT_AWAITING_PATIENCE_IN_MILLISECONDS,
DEFAULT_ACCOUNT_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS,
DEFAULT_ACCOUNT_AWAITING_TIMEOUT_IN_MILLISECONDS,
} from "./constants";

interface IAccountFetcher {
getAccount(address: Address): Promise<AccountOnNetwork>;
}

export class AccountAwaiter {
private readonly fetcher: IAccountFetcher;
private readonly pollingIntervalInMilliseconds: number;
private readonly timeoutIntervalInMilliseconds: number;
private readonly patienceTimeInMilliseconds: number;

/**
* AccountAwaiter allows one to await until a specific event occurs on a given address.
*
* @param fetcher - Used to fetch the account of the network.
* @param pollingIntervalInMilliseconds - The polling interval, in milliseconds.
* @param timeoutIntervalInMilliseconds - The timeout, in milliseconds.
* @param patienceTimeInMilliseconds - The patience, an extra time (in milliseconds) to wait, after the account has reached its desired condition.
*/
constructor(options: {
fetcher: IAccountFetcher;
pollingIntervalInMilliseconds?: number;
timeoutIntervalInMilliseconds?: number;
patienceTimeInMilliseconds?: number;
}) {
this.fetcher = options.fetcher;

this.pollingIntervalInMilliseconds =
options.pollingIntervalInMilliseconds ?? DEFAULT_ACCOUNT_AWAITING_POLLING_TIMEOUT_IN_MILLISECONDS;

this.timeoutIntervalInMilliseconds =
options.timeoutIntervalInMilliseconds ?? DEFAULT_ACCOUNT_AWAITING_TIMEOUT_IN_MILLISECONDS;

this.patienceTimeInMilliseconds =
options.patienceTimeInMilliseconds ?? DEFAULT_ACCOUNT_AWAITING_PATIENCE_IN_MILLISECONDS;
}

/**
* Waits until the condition is satisfied.
*
* @param address - The address to monitor.
* @param condition - A callable that evaluates the desired condition.
*/
async awaitOnCondition(
address: Address,
condition: (account: AccountOnNetwork) => boolean,
): Promise<AccountOnNetwork> {
const doFetch = async () => await this.fetcher.getAccount(address);

return this.awaitConditionally(condition, doFetch, new ExpectedAccountConditionNotReachedError());
}

private async awaitConditionally(
isSatisfied: (account: AccountOnNetwork) => boolean,
doFetch: () => Promise<AccountOnNetwork>,
error: Error,
): Promise<AccountOnNetwork> {
let isConditionSatisfied = false;
let fetchedData: AccountOnNetwork | null = null;

const maxNumberOfRetries = Math.floor(this.timeoutIntervalInMilliseconds / this.pollingIntervalInMilliseconds);

let numberOfRetries = 0;

while (numberOfRetries < maxNumberOfRetries) {
try {
fetchedData = await doFetch();
isConditionSatisfied = isSatisfied(fetchedData);

if (isConditionSatisfied) {
break;
}
} catch (ex) {
throw ex;
}
Comment on lines +81 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what we want?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is what we are doing on the transaction watcher also..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All right then for now. We should check if that's the best approach - for both of them (separately, another task / PR).


numberOfRetries += 1;
await this._sleep(this.pollingIntervalInMilliseconds);
}

if (!fetchedData || !isConditionSatisfied) {
throw error;
}

if (this.patienceTimeInMilliseconds) {
await this._sleep(this.patienceTimeInMilliseconds);
return doFetch();
}

return fetchedData;
}

private async _sleep(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
}
17 changes: 12 additions & 5 deletions src/networkProviders/apiNetworkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { prepareTransactionForBroadcasting, TransactionOnNetwork } from "../tran
import { TransactionStatus } from "../transactionStatus";
import { TransactionWatcher } from "../transactionWatcher";
import { getAxios } from "../utils";
import { AccountAwaiter } from "./accountAwaiter";
import { AccountOnNetwork } from "./accounts";
import { defaultAxiosConfig, defaultPagination } from "./config";
import { BaseUserAgent } from "./constants";
Expand Down Expand Up @@ -88,15 +89,21 @@ export class ApiNetworkProvider implements INetworkProvider {
return account;
}

awaitAccountOnCondition(
_address: Address,
_condition: (account: AccountOnNetwork) => boolean,
async awaitAccountOnCondition(
address: Address,
condition: (account: AccountOnNetwork) => boolean,
options?: AwaitingOptions,
): AccountOnNetwork {
): Promise<AccountOnNetwork> {
if (!options) {
options = new AwaitingOptions();
}
throw new Error("Method not implemented.");
const awaiter = new AccountAwaiter({
fetcher: this,
patienceTimeInMilliseconds: options.patienceInMilliseconds,
pollingIntervalInMilliseconds: options.pollingIntervalInMilliseconds,
timeoutIntervalInMilliseconds: options.timeoutInMilliseconds,
});
return await awaiter.awaitOnCondition(address, condition);
}

async sendTransaction(tx: Transaction): Promise<string> {
Expand Down
2 changes: 1 addition & 1 deletion src/networkProviders/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface INetworkProvider {
address: Address,
condition: (account: AccountOnNetwork) => boolean,
options?: AwaitingOptions,
): AccountOnNetwork;
): Promise<AccountOnNetwork>;

/**
* Broadcasts an already-signed transaction.
Expand Down
2 changes: 1 addition & 1 deletion src/networkProviders/proxyNetworkProvider.dev.net.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ describe("ProxyNetworkProvider Tests", function () {
});

it("should send and await for completed transaction", async function () {
this.timeout(40000);
this.timeout(50000);
const bob = await loadTestWallet("bob");
const transactionComputer = new TransactionComputer();
let transaction = new Transaction({
Expand Down
17 changes: 12 additions & 5 deletions src/networkProviders/proxyNetworkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { prepareTransactionForBroadcasting, TransactionOnNetwork } from "../tran
import { TransactionStatus } from "../transactionStatus";
import { TransactionWatcher } from "../transactionWatcher";
import { getAxios } from "../utils";
import { AccountAwaiter } from "./accountAwaiter";
import { AccountOnNetwork, GuardianData } from "./accounts";
import { defaultAxiosConfig } from "./config";
import { BaseUserAgent } from "./constants";
Expand Down Expand Up @@ -94,15 +95,21 @@ export class ProxyNetworkProvider implements INetworkProvider {
return account;
}

awaitAccountOnCondition(
_address: Address,
_condition: (account: AccountOnNetwork) => boolean,
async awaitAccountOnCondition(
address: Address,
condition: (account: AccountOnNetwork) => boolean,
options?: AwaitingOptions,
): AccountOnNetwork {
): Promise<AccountOnNetwork> {
if (!options) {
options = new AwaitingOptions();
}
throw new Error("Method not implemented.");
const awaiter = new AccountAwaiter({
fetcher: this,
patienceTimeInMilliseconds: options.patienceInMilliseconds,
pollingIntervalInMilliseconds: options.pollingIntervalInMilliseconds,
timeoutIntervalInMilliseconds: options.timeoutInMilliseconds,
});
return await awaiter.awaitOnCondition(address, condition);
}

async sendTransaction(tx: Transaction): Promise<string> {
Expand Down
27 changes: 26 additions & 1 deletion src/testutils/mockNetworkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class MockNetworkProvider implements INetworkProvider {
_address: Address,
_condition: (account: AccountOnNetwork) => boolean,
_options?: AwaitingOptions,
): AccountOnNetwork {
): Promise<AccountOnNetwork> {
throw new Error("Method not implemented.");
}
estimateTransactionCost(_tx: Transaction): Promise<TransactionCostResponse> {
Expand Down Expand Up @@ -159,6 +159,27 @@ export class MockNetworkProvider implements INetworkProvider {
this.getTransactionResponders.unshift(new GetTransactionResponder(predicate, response));
}

mockAccountBalanceTimelineByAddress(address: Address, timelinePoints: Array<MarkCompleted | Wait>): void {
const executeTimeline = async () => {
for (const point of timelinePoints) {
if (point instanceof MarkCompleted) {
// Mark account condition as reached
this.mockUpdateAccount(address, (account) => {
account.balance += createAccountBalance(7);
});
} else if (point instanceof Wait) {
// Wait for the specified time
await this.sleep(point.milliseconds);
}
}
};

// Start the timeline execution in a separate async "thread"
executeTimeline().catch((err) => {
console.error("Error executing timeline:", err);
});
}

async mockTransactionTimeline(transaction: Transaction, timelinePoints: any[]): Promise<void> {
return this.mockTransactionTimelineByHash(transaction.getHash(), timelinePoints);
}
Expand Down Expand Up @@ -187,6 +208,10 @@ export class MockNetworkProvider implements INetworkProvider {
}
}

private sleep(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

async getAccount(address: Address): Promise<AccountOnNetwork> {
let account = this.accounts.get(address.toBech32());
if (account) {
Expand Down
Loading