From f8a516c94c9faa23bfcf39328a7db0e8e5de7727 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Mon, 2 Oct 2023 23:18:44 +0200 Subject: [PATCH] chore: extra test cases; adding timeout handler --- package-lock.json | 11 +++-- src/clients/SubtopiaClient.ts | 56 ++++++++++++++++++++---- src/clients/SubtopiaRegistryClient.ts | 38 +++++++++++++--- src/constants/index.ts | 3 ++ src/index.ts | 1 + src/utils/index.ts | 63 +++++++++++++++++++++++++-- tests/utils.test.ts | 34 +++++++++++++++ 7 files changed, 183 insertions(+), 23 deletions(-) create mode 100644 tests/utils.test.ts diff --git a/package-lock.json b/package-lock.json index 02a3093..c4528e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "subtopia-js", - "version": "3.0.0-beta.16", + "version": "3.0.0-beta.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "subtopia-js", - "version": "3.0.0-beta.16", + "version": "3.0.0-beta.19", "license": "SEE LICENSE IN LICENSE.md", "devDependencies": { "@feki.de/semantic-release-yarn": "1.0.1", @@ -5056,11 +5056,10 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } diff --git a/src/clients/SubtopiaClient.ts b/src/clients/SubtopiaClient.ts index 7d466b4..ed54d67 100644 --- a/src/clients/SubtopiaClient.ts +++ b/src/clients/SubtopiaClient.ts @@ -27,6 +27,7 @@ import { calculateProductDiscountBoxCreateMbr, calculateProductSubscriptionBoxCreateMbr, optInAsset, + asyncWithTimeout, } from "../utils"; import { getAssetByID } from "../utils"; import { @@ -34,6 +35,7 @@ import { TESTNET_SUBTOPIA_REGISTRY_ID, MIN_APP_CREATE_MBR, MIN_ASA_CREATE_MBR, + DEFAULT_TXN_SIGN_TIMEOUT_SECONDS, } from "../constants"; import { PriceNormalizationType, @@ -69,6 +71,7 @@ export class SubtopiaClient { appID: number; appAddress: string; appSpec: ApplicationSpec; + timeout: number; protected constructor({ algodClient, @@ -82,6 +85,7 @@ export class SubtopiaClient { price, coin, version, + timeout, }: { algodClient: AlgodClient; productName: string; @@ -94,6 +98,7 @@ export class SubtopiaClient { price: number; coin: AssetMetadata; version: string; + timeout: number; }) { this.algodClient = algodClient; this.productName = productName; @@ -106,12 +111,14 @@ export class SubtopiaClient { this.price = price; this.coin = coin; this.version = version; + this.timeout = timeout; } public static async init( algodClient: AlgodClient, productID: number, - creator: TransactionSignerAccount + creator: TransactionSignerAccount, + timeout: number = DEFAULT_TXN_SIGN_TIMEOUT_SECONDS ): Promise { const productGlobalState = await getAppGlobalState( productID, @@ -194,6 +201,7 @@ export class SubtopiaClient { price: productPrice, coin, version, + timeout, }); } @@ -224,7 +232,12 @@ export class SubtopiaClient { suggestedParams: await getParamsWithFeeCount(this.algodClient, 1), }); - const response = await updateLifecycleAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + updateLifecycleAtc.execute.bind(updateLifecycleAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, @@ -509,7 +522,12 @@ export class SubtopiaClient { suggestedParams: await getParamsWithFeeCount(this.algodClient, 2), }); - const response = await createDiscountAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + createDiscountAtc.execute.bind(createDiscountAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, @@ -545,7 +563,12 @@ export class SubtopiaClient { suggestedParams: await getParamsWithFeeCount(this.algodClient, 2), }); - const response = await deleteDiscountAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + deleteDiscountAtc.execute.bind(deleteDiscountAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, @@ -706,7 +729,12 @@ export class SubtopiaClient { ), }); - const response = await createSubscriptionAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + createSubscriptionAtc.execute.bind(createSubscriptionAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, @@ -760,7 +788,9 @@ export class SubtopiaClient { suggestedParams: await getParamsWithFeeCount(this.algodClient, 2), }); - const response = await transferSubscriptionAtc.execute( + const response = await asyncWithTimeout( + transferSubscriptionAtc.execute.bind(transferSubscriptionAtc), + this.timeout, this.algodClient, 10 ); @@ -822,7 +852,12 @@ export class SubtopiaClient { suggestedParams: await getParamsWithFeeCount(this.algodClient, 2), }); - const response = await claimSubscriptionAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + claimSubscriptionAtc.execute.bind(claimSubscriptionAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, @@ -878,7 +913,12 @@ export class SubtopiaClient { ), }); - const response = await deleteSubscriptionAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + deleteSubscriptionAtc.execute.bind(deleteSubscriptionAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, diff --git a/src/clients/SubtopiaRegistryClient.ts b/src/clients/SubtopiaRegistryClient.ts index a53ca5b..29a3658 100644 --- a/src/clients/SubtopiaRegistryClient.ts +++ b/src/clients/SubtopiaRegistryClient.ts @@ -25,6 +25,7 @@ import { calculateLockerCreationMbr, calculateRegistryLockerBoxCreateMbr, getLockerBoxPrefix, + asyncWithTimeout, } from "../utils"; import { getAssetByID } from "../utils"; import { @@ -37,6 +38,7 @@ import { LOCKER_CLEAR_KEY, SUBTOPIA_REGISTRY_ID, MIN_APP_CREATE_MBR, + DEFAULT_TXN_SIGN_TIMEOUT_SECONDS, } from "../constants"; import { SubscriptionType, @@ -65,6 +67,7 @@ export class SubtopiaRegistryClient { appAddress: string; appSpec: ApplicationSpec; oracleID: number; + timeout: number; protected constructor({ algodClient, @@ -74,6 +77,7 @@ export class SubtopiaRegistryClient { appSpec, oracleID, version, + timeout, }: { algodClient: AlgodClient; creator: TransactionSignerAccount; @@ -82,6 +86,7 @@ export class SubtopiaRegistryClient { appSpec: ApplicationSpec; oracleID: number; version: string; + timeout: number; }) { this.algodClient = algodClient; this.creator = creator; @@ -90,12 +95,14 @@ export class SubtopiaRegistryClient { this.appSpec = appSpec; this.oracleID = oracleID; this.version = version; + this.timeout = timeout; } public static async init( algodClient: AlgodClient, creator: TransactionSignerAccount, - chainType: ChainType + chainType: ChainType, + timeout: number = DEFAULT_TXN_SIGN_TIMEOUT_SECONDS ): Promise { const registryID = SUBTOPIA_REGISTRY_ID(chainType); const registryAddress = getApplicationAddress(registryID); @@ -157,6 +164,7 @@ export class SubtopiaRegistryClient { }, oracleID: oracleID, version: version, + timeout: timeout, }); } @@ -301,7 +309,12 @@ export class SubtopiaRegistryClient { suggestedParams: await getParamsWithFeeCount(this.algodClient, 4), }); - const response = await createLockerAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + createLockerAtc.execute.bind(createLockerAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, @@ -449,7 +462,12 @@ export class SubtopiaRegistryClient { ), }); - const response = await transferInfraAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + transferInfraAtc.execute.bind(transferInfraAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, @@ -642,7 +660,12 @@ export class SubtopiaRegistryClient { ), }); - const response = await createInfraAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + createInfraAtc.execute.bind(createInfraAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, @@ -693,7 +716,12 @@ export class SubtopiaRegistryClient { suggestedParams: await getParamsWithFeeCount(this.algodClient, 4), }); - const response = await deleteInfraAtc.execute(this.algodClient, 10); + const response = await asyncWithTimeout( + deleteInfraAtc.execute.bind(deleteInfraAtc), + this.timeout, + this.algodClient, + 10 + ); return { txID: response.txIDs.pop() as string, diff --git a/src/constants/index.ts b/src/constants/index.ts index 3a5c7c2..c413bdf 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -52,3 +52,6 @@ export const LOCKER_GLOBAL_NUM_UINTS = 1; export const LOCKER_GLOBAL_NUM_BYTE_SLICES = 1; export const LOCKER_LOCAL_NUM_UINTS = 0; export const LOCKER_LOCAL_NUM_BYTE_SLICES = 0; + +// Misc +export const DEFAULT_TXN_SIGN_TIMEOUT_SECONDS = 60; diff --git a/src/index.ts b/src/index.ts index be640b5..294c5c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,4 +22,5 @@ export { ORACLE_VERSION, PRODUCT_VERSION, SUBTOPIA_REGISTRY_ID, + DEFAULT_TXN_SIGN_TIMEOUT_SECONDS, } from "./constants"; diff --git a/src/utils/index.ts b/src/utils/index.ts index 7fd9fb8..9cf9f22 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -23,6 +23,7 @@ import { LOCKER_EXTRA_PAGES, LOCKER_GLOBAL_NUM_UINTS, LOCKER_GLOBAL_NUM_BYTE_SLICES, + DEFAULT_TXN_SIGN_TIMEOUT_SECONDS, } from "../constants"; import { LockerType, PriceNormalizationType } from "../enums"; import { APP_PAGE_MAX_SIZE } from "@algorandfoundation/algokit-utils/types/app"; @@ -36,11 +37,13 @@ export async function transferAsset( assetID: number; amount: number; }, - client: Algodv2 + client: Algodv2, + timeout = DEFAULT_TXN_SIGN_TIMEOUT_SECONDS ): Promise<{ confirmedRound: number; txIDs: string[]; methodResults: algosdk.ABIResult[]; + timeout?: number; }> { const { sender, recipient, assetID, amount } = transfer; const transferAtc = new AtomicTransactionComposer(); @@ -58,7 +61,9 @@ export async function transferAsset( signer: sender.signer, }); - const transferResult = await transferAtc.execute( + const transferResult = await asyncWithTimeout( + transferAtc.execute.bind(transferAtc), + timeout, client, DEFAULT_AWAIT_ROUNDS ); @@ -70,10 +75,12 @@ export async function optInAsset({ client, account, assetID, + timeout = DEFAULT_TXN_SIGN_TIMEOUT_SECONDS, }: { client: AlgodClient | Algodv2; account: TransactionSignerAccount; assetID: number; + timeout?: number; }): Promise<{ confirmedRound: number; txIDs: string[]; @@ -90,7 +97,12 @@ export async function optInAsset({ }), signer: account.signer, }); - const optInResult = await optInAtc.execute(client, DEFAULT_AWAIT_ROUNDS); + const optInResult = await asyncWithTimeout( + optInAtc.execute.bind(optInAtc), + timeout, + client, + DEFAULT_AWAIT_ROUNDS + ); return optInResult; } @@ -99,10 +111,12 @@ export async function optOutAsset({ client, account, assetID, + timeout = DEFAULT_TXN_SIGN_TIMEOUT_SECONDS, }: { client: Algodv2; account: TransactionSignerAccount; assetID: number; + timeout?: number; }): Promise<{ confirmedRound: number; txIDs: string[]; @@ -120,7 +134,12 @@ export async function optOutAsset({ }), signer: account.signer, }); - const optInResult = await optInAtc.execute(client, DEFAULT_AWAIT_ROUNDS); + const optInResult = await asyncWithTimeout( + optInAtc.execute.bind(optInAtc), + timeout, + client, + DEFAULT_AWAIT_ROUNDS + ); return optInResult; } @@ -291,3 +310,39 @@ export function getLockerBoxPrefix(lockerType: LockerType): Buffer { throw new Error(`Unknown locker type: ${lockerType}`); } } + +export function wait(ms: number) { + const resp = new Promise((_, reject) => + setTimeout(() => reject(new Error("TRANSACTION_SIGNING_TIMED_OUT")), ms) + ); + + return resp; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function asyncWithTimeout( + fn: (...args: A) => Promise, + timeout: number, + ...args: A +): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error(`Timeout error: exceeded ${timeout} seconds`)); + }, timeout * 1000); + + const resultPromise = fn(...args); + if (!resultPromise || typeof resultPromise.then !== "function") { + reject(new Error("Function did not return a promise")); + return; + } + resultPromise + .then((result) => { + clearTimeout(timeoutId); + resolve(result); + }) + .catch((err) => { + clearTimeout(timeoutId); + reject(err); + }); + }); +} diff --git a/tests/utils.test.ts b/tests/utils.test.ts new file mode 100644 index 0000000..1420f38 --- /dev/null +++ b/tests/utils.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { asyncWithTimeout } from "../src/utils"; + +describe("asyncWithTimeout", () => { + // Function returns a promise that resolves with the expected result + it("should resolve with the expected result when the function completes within the timeout", async () => { + const expectedResult = "result"; + const myAsyncFunction = async () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(expectedResult); + }, 500); + }); + }; + + const result = await asyncWithTimeout(myAsyncFunction, 1); + expect(result).toEqual(expectedResult); + }); + + // Function returns a promise that rejects with an error when the function takes longer than the timeout to complete + it("should reject with an error when the function takes longer than the timeout to complete", async () => { + const myAsyncFunction = async () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve("result"); + }, 2000); + }); + }; + + await expect(asyncWithTimeout(myAsyncFunction, 1)).rejects.toEqual( + new Error("Timeout error: exceeded 1 seconds") + ); + }, 100000); +});