diff --git a/src/AeSdkMethods.ts b/src/AeSdkMethods.ts index 64c5fdce64..80983fbc41 100644 --- a/src/AeSdkMethods.ts +++ b/src/AeSdkMethods.ts @@ -1,4 +1,5 @@ import * as chainMethods from './chain'; +import { sendTransaction } from './send-transaction'; import * as aensMethods from './aens'; import * as spendMethods from './spend'; import * as oracleMethods from './oracle'; @@ -15,10 +16,9 @@ import CompilerBase from './contract/compiler/Base'; export type OnAccount = Encoded.AccountAddress | AccountBase | undefined; -const { InvalidTxError: _2, ...chainMethodsOther } = chainMethods; - const methods = { - ...chainMethodsOther, + ...chainMethods, + sendTransaction, ...aensMethods, ...spendMethods, ...oracleMethods, diff --git a/src/aens.ts b/src/aens.ts index b1a2495f89..4562629720 100644 --- a/src/aens.ts +++ b/src/aens.ts @@ -12,7 +12,8 @@ import { commitmentHash, isAuctionName } from './tx/builder/helpers'; import { Tag, AensName, ConsensusProtocolVersion } from './tx/builder/constants'; import { Encoded, Encoding } from './utils/encoder'; import { UnsupportedProtocolError } from './utils/errors'; -import { sendTransaction, SendTransactionOptions, getName } from './chain'; +import { getName } from './chain'; +import { sendTransaction, SendTransactionOptions } from './send-transaction'; import { buildTxAsync, BuildTxOptions } from './tx/builder'; import { TransformNodeType } from './Node'; import { NameEntry, NamePointer } from './apis/node'; diff --git a/src/chain.ts b/src/chain.ts index f9bedc5d3a..159783a191 100644 --- a/src/chain.ts +++ b/src/chain.ts @@ -1,13 +1,9 @@ import { AE_AMOUNT_FORMATS, formatAmount } from './utils/amount-formatter'; -import verifyTransaction, { ValidatorResult } from './tx/validator'; -import { - ensureError, isAccountNotFoundError, pause, unwrapProxy, -} from './utils/other'; +import { isAccountNotFoundError, pause, unwrapProxy } from './utils/other'; import { isNameValid, produceNameId } from './tx/builder/helpers'; -import { DRY_RUN_ACCOUNT } from './tx/builder/schema'; -import { AensName } from './tx/builder/constants'; +import { AensName, DRY_RUN_ACCOUNT } from './tx/builder/constants'; import { - AensPointerContextError, DryRunError, InvalidAensNameError, TransactionError, + AensPointerContextError, DryRunError, InvalidAensNameError, TxTimedOutError, TxNotInChainError, InternalError, } from './utils/errors'; import Node, { TransformNodeType } from './Node'; @@ -18,8 +14,6 @@ import { import { decode, encode, Encoded, Encoding, } from './utils/encoder'; -import AccountBase from './account/Base'; -import { buildTxHash } from './tx/builder'; /** * @category chain @@ -38,26 +32,6 @@ export function _getPollInterval( return Math.floor(base / 3); } -/** - * @category exception - */ -export class InvalidTxError extends TransactionError { - validation: ValidatorResult[]; - - transaction: Encoded.Transaction; - - constructor( - message: string, - validation: ValidatorResult[], - transaction: Encoded.Transaction, - ) { - super(message); - this.name = 'InvalidTxError'; - this.validation = validation; - this.transaction = transaction; - } -} - const heightCache: WeakMap = new WeakMap(); /** @@ -164,107 +138,6 @@ export async function waitForTxConfirm( } } -/** - * Signs and submits transaction for mining - * @category chain - * @param txUnsigned - Transaction to sign and submit - * @param options - Options - * @returns Transaction details - */ -export async function sendTransaction( - txUnsigned: Encoded.Transaction, - { - onNode, onAccount, verify = true, waitMined = true, confirm, innerTx, ...options - }: - SendTransactionOptions, -): Promise { - const tx = await onAccount.signTransaction(txUnsigned, { - ...options, - onNode, - innerTx, - networkId: await onNode.getNetworkId(), - }); - - if (innerTx === true) return { hash: buildTxHash(tx), rawTx: tx }; - - if (verify) { - const validation = await verifyTransaction(tx, onNode); - if (validation.length > 0) { - const message = `Transaction verification errors: ${ - validation.map((v: { message: string }) => v.message).join(', ')}`; - throw new InvalidTxError(message, validation, tx); - } - } - - try { - let __queue; - try { - __queue = onAccount != null ? `tx-${onAccount.address}` : null; - } catch (error) { - __queue = null; - } - const { txHash } = await onNode.postTransaction( - { tx }, - __queue != null ? { requestOptions: { customHeaders: { __queue } } } : {}, - ); - - if (waitMined) { - const pollResult = await poll(txHash, { onNode, ...options }); - const txData = { - ...pollResult, - hash: pollResult.hash as Encoded.TxHash, - rawTx: tx, - }; - // wait for transaction confirmation - if (confirm != null && +confirm > 0) { - const c = typeof confirm === 'boolean' ? undefined : confirm; - return { - ...txData, - confirmationHeight: await waitForTxConfirm(txHash, { onNode, confirm: c, ...options }), - }; - } - return txData; - } - return { hash: txHash, rawTx: tx }; - } catch (error) { - ensureError(error); - throw Object.assign(error, { - rawTx: tx, - verifyTx: async () => verifyTransaction(tx, onNode), - }); - } -} - -type SendTransactionOptionsType = { - /** - * Node to use - */ - onNode: Node; - /** - * Account to use - */ - onAccount: AccountBase; - /** - * Verify transaction before broadcast, throw error if not - */ - verify?: boolean; - /** - * Ensure that transaction get into block - */ - waitMined?: boolean; - /** - * Number of micro blocks that should be mined after tx get included - */ - confirm?: boolean | number; -} & Parameters[1] & Omit[1], 'confirm'> -& Parameters[1]; -export interface SendTransactionOptions extends SendTransactionOptionsType {} -interface SendTransactionReturnType extends Partial> { - hash: Encoded.TxHash; - rawTx: Encoded.Transaction; - confirmationHeight?: number; -} - /** * Get account by account public key * @category chain diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index 37c313f0b1..ed855ec46d 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -6,8 +6,9 @@ */ import { Encoder as Calldata } from '@aeternity/aepp-calldata'; -import { DRY_RUN_ACCOUNT } from '../tx/builder/schema'; -import { Tag, AensName, ConsensusProtocolVersion } from '../tx/builder/constants'; +import { + Tag, AensName, ConsensusProtocolVersion, DRY_RUN_ACCOUNT, +} from '../tx/builder/constants'; import { buildContractIdByContractTx, unpackTx, buildTxAsync, BuildTxOptions, buildTxHash, } from '../tx/builder'; @@ -39,9 +40,9 @@ import { import CompilerBase, { Aci } from './compiler/Base'; import Node, { TransformNodeType } from '../Node'; import { - getAccount, getContract, getContractByteCode, resolveName, txDryRun, sendTransaction, - SendTransactionOptions, + getAccount, getContract, getContractByteCode, resolveName, txDryRun, } from '../chain'; +import { sendTransaction, SendTransactionOptions } from '../send-transaction'; import AccountBase from '../account/Base'; import { TxUnpacked } from '../tx/builder/schema.generated'; import { isAccountNotFoundError } from '../utils/other'; @@ -259,7 +260,7 @@ class Contract { 'init', { ...opt, onAccount: opt.onAccount }, ); - this.$options.address = buildContractIdByContractTx(tx); + this.$options.address = buildContractIdByContractTx(other.rawTx); return { ...other, ...other.result?.log != null && { diff --git a/src/contract/ga.ts b/src/contract/ga.ts index ab62070b93..bf4ac4e514 100644 --- a/src/contract/ga.ts +++ b/src/contract/ga.ts @@ -15,7 +15,8 @@ import { concatBuffers } from '../utils/other'; import AccountBase from '../account/Base'; import Contract from './Contract'; import Node from '../Node'; -import { sendTransaction, SendTransactionOptions, getAccount } from '../chain'; +import { getAccount } from '../chain'; +import { sendTransaction, SendTransactionOptions } from '../send-transaction'; import CompilerBase from './compiler/Base'; /** @@ -57,10 +58,10 @@ export async function createGeneralizedAccount( callData: contract._calldata.encode(contract._name, 'init', args), authFun: hash(authFnName), }); - const contractId = buildContractIdByContractTx(tx); const { hash: transaction, rawTx } = await sendTransaction(tx, { onNode, onAccount, onCompiler, ...options, }); + const contractId = buildContractIdByContractTx(rawTx); return Object.freeze({ owner: ownerId, diff --git a/src/index-browser.ts b/src/index-browser.ts index 2c4cbc30cf..edbae28ddf 100644 --- a/src/index-browser.ts +++ b/src/index-browser.ts @@ -1,9 +1,10 @@ export { - _getPollInterval, InvalidTxError, getHeight, poll, awaitHeight, waitForTxConfirm, sendTransaction, + _getPollInterval, getHeight, poll, awaitHeight, waitForTxConfirm, getAccount, getBalance, getCurrentGeneration, getGeneration, getMicroBlockTransactions, getKeyBlock, getMicroBlockHeader, txDryRun, getContractByteCode, getContract, getName, resolveName, } from './chain'; +export { InvalidTxError, sendTransaction } from './send-transaction'; export { getAddressFromPriv, isAddressValid, genSalt, encodeUnsigned, hash, encodeContractAddress, generateKeyPairFromSecret, generateKeyPair, sign, verify, messageToHash, signMessage, @@ -22,11 +23,11 @@ export { export { MAX_AUTH_FUN_GAS, MIN_GAS_PRICE, NAME_FEE_MULTIPLIER, NAME_FEE_BID_INCREMENT, NAME_BID_TIMEOUT_BLOCKS, NAME_MAX_LENGTH_FEE, - NAME_BID_RANGES, ConsensusProtocolVersion, VmVersion, AbiVersion, Tag, + NAME_BID_RANGES, ConsensusProtocolVersion, VmVersion, AbiVersion, Tag, DRY_RUN_ACCOUNT, } from './tx/builder/constants'; export type { Int, AensName } from './tx/builder/constants'; // TODO: move to constants -export { ORACLE_TTL_TYPES, DRY_RUN_ACCOUNT, CallReturnType } from './tx/builder/schema'; +export { ORACLE_TTL_TYPES, CallReturnType } from './tx/builder/schema'; export { DelegationTag } from './tx/builder/delegation/schema'; export { packDelegation, unpackDelegation } from './tx/builder/delegation'; export { diff --git a/src/oracle.ts b/src/oracle.ts index ce94903544..f4934f0a07 100644 --- a/src/oracle.ts +++ b/src/oracle.ts @@ -14,9 +14,8 @@ import { RequestTimedOutError } from './utils/errors'; import { decode, encode, Encoded, Encoding, } from './utils/encoder'; -import { - _getPollInterval, getHeight, sendTransaction, SendTransactionOptions, -} from './chain'; +import { _getPollInterval, getHeight } from './chain'; +import { sendTransaction, SendTransactionOptions } from './send-transaction'; import Node from './Node'; import AccountBase from './account/Base'; diff --git a/src/send-transaction.ts b/src/send-transaction.ts new file mode 100644 index 0000000000..b02af4f281 --- /dev/null +++ b/src/send-transaction.ts @@ -0,0 +1,135 @@ +import verifyTransaction, { ValidatorResult } from './tx/validator'; +import { ensureError } from './utils/other'; +import { TransactionError } from './utils/errors'; +import Node, { TransformNodeType } from './Node'; +import { SignedTx } from './apis/node'; +import { Encoded } from './utils/encoder'; +import AccountBase from './account/Base'; +import { buildTxHash } from './tx/builder'; +import { poll, waitForTxConfirm } from './chain'; + +/** + * @category exception + */ +export class InvalidTxError extends TransactionError { + validation: ValidatorResult[]; + + transaction: Encoded.Transaction; + + constructor( + message: string, + validation: ValidatorResult[], + transaction: Encoded.Transaction, + ) { + super(message); + this.name = 'InvalidTxError'; + this.validation = validation; + this.transaction = transaction; + } +} + +/** + * Signs and submits transaction for mining + * @category chain + * @param txUnsigned - Transaction to sign and submit + * @param options - Options + * @returns Transaction details + */ +export async function sendTransaction( + txUnsigned: Encoded.Transaction, + { + onNode, onAccount, verify = true, waitMined = true, confirm, innerTx, ...options + }: + SendTransactionOptions, +): Promise { + const tx = await onAccount.signTransaction(txUnsigned, { + ...options, + onNode, + innerTx, + networkId: await onNode.getNetworkId(), + }); + + if (innerTx === true) return { hash: buildTxHash(tx), rawTx: tx }; + + if (verify) { + const validation = await verifyTransaction(tx, onNode); + if (validation.length > 0) { + const message = `Transaction verification errors: ${ + validation.map((v: { message: string }) => v.message).join(', ')}`; + throw new InvalidTxError(message, validation, tx); + } + } + + try { + let __queue; + try { + __queue = onAccount != null ? `tx-${onAccount.address}` : null; + } catch (error) { + __queue = null; + } + const { txHash } = await onNode.postTransaction({ tx }, { + requestOptions: { + customHeaders: { + // TODO: remove __retry-code after fixing https://github.com/aeternity/aeternity/issues/3803 + '__retry-code': '400', + ...__queue != null ? { __queue } : {}, + }, + }, + }); + + if (waitMined) { + const pollResult = await poll(txHash, { onNode, ...options }); + const txData = { + ...pollResult, + hash: pollResult.hash as Encoded.TxHash, + rawTx: tx, + }; + // wait for transaction confirmation + if (confirm != null && +confirm > 0) { + const c = typeof confirm === 'boolean' ? undefined : confirm; + return { + ...txData, + confirmationHeight: await waitForTxConfirm(txHash, { onNode, confirm: c, ...options }), + }; + } + return txData; + } + return { hash: txHash, rawTx: tx }; + } catch (error) { + ensureError(error); + throw Object.assign(error, { + rawTx: tx, + verifyTx: async () => verifyTransaction(tx, onNode), + }); + } +} + +type SendTransactionOptionsType = { + /** + * Node to use + */ + onNode: Node; + /** + * Account to use + */ + onAccount: AccountBase; + /** + * Verify transaction before broadcast, throw error if not + */ + verify?: boolean; + /** + * Ensure that transaction get into block + */ + waitMined?: boolean; + /** + * Number of micro blocks that should be mined after tx get included + */ + confirm?: boolean | number; +} & Parameters[1] & Omit[1], 'confirm'> +& Parameters[1]; +export interface SendTransactionOptions extends SendTransactionOptionsType {} +interface SendTransactionReturnType extends Partial> { + hash: Encoded.TxHash; + rawTx: Encoded.Transaction; + confirmationHeight?: number; +} diff --git a/src/spend.ts b/src/spend.ts index 8fa52b0dcb..90d32fc977 100644 --- a/src/spend.ts +++ b/src/spend.ts @@ -1,7 +1,6 @@ import BigNumber from 'bignumber.js'; -import { - sendTransaction, getBalance, resolveName, SendTransactionOptions, -} from './chain'; +import { getBalance, resolveName } from './chain'; +import { sendTransaction, SendTransactionOptions } from './send-transaction'; import { buildTxAsync, BuildTxOptions, unpackTx } from './tx/builder'; import { ArgumentError } from './utils/errors'; import { Encoded, Encoding } from './utils/encoder'; diff --git a/src/tx/builder/constants.ts b/src/tx/builder/constants.ts index efefa25552..bad55c1bd7 100644 --- a/src/tx/builder/constants.ts +++ b/src/tx/builder/constants.ts @@ -1,6 +1,11 @@ import BigNumber from 'bignumber.js'; import { mapObject } from '../../utils/other'; +export const DRY_RUN_ACCOUNT = { + pub: 'ak_11111111111111111111111111111111273Yts', + amount: 100000000000000000000000000000000000n, +} as const; + export const MAX_AUTH_FUN_GAS = 50000; export type Int = number | string | BigNumber; export type AensName = `${string}.chain`; diff --git a/src/tx/builder/field-types/coin-amount.ts b/src/tx/builder/field-types/coin-amount.ts index 0f53bea7a4..03b2eb478e 100644 --- a/src/tx/builder/field-types/coin-amount.ts +++ b/src/tx/builder/field-types/coin-amount.ts @@ -6,19 +6,20 @@ export default { ...uInt, // eslint-disable-next-line @typescript-eslint/no-unused-vars - serializeAettos(value: string | undefined, params: {}): string { + serializeAettos(value: string | undefined, params: {}, options: {}): string { return value ?? '0'; }, serialize( value: Int | undefined, params: {}, - { denomination = AE_AMOUNT_FORMATS.AETTOS }: { denomination?: AE_AMOUNT_FORMATS }, + { denomination = AE_AMOUNT_FORMATS.AETTOS, ...options }: { denomination?: AE_AMOUNT_FORMATS }, ): Buffer { return uInt.serialize( this.serializeAettos( value != null ? formatAmount(value, { denomination }) : value, params, + options, ), ); }, diff --git a/src/tx/builder/field-types/fee.ts b/src/tx/builder/field-types/fee.ts index 623c62aef4..8a56f2129f 100644 --- a/src/tx/builder/field-types/fee.ts +++ b/src/tx/builder/field-types/fee.ts @@ -138,14 +138,14 @@ export default { serializeAettos( _value: string | undefined, { - rebuildTx, unpackTx, buildTx, _computingMinFee, _pickBiggerFee, + rebuildTx, unpackTx, buildTx, _computingMinFee, }: { rebuildTx: (params: any) => Encoded.Transaction; unpackTx: typeof unpackTxType; buildTx: typeof buildTxType; _computingMinFee?: BigNumber; - _pickBiggerFee?: boolean; }, + { _canIncreaseFee }: { _canIncreaseFee?: boolean }, ): string { if (_computingMinFee != null) return _computingMinFee.toFixed(); const minFee = calculateMinFee( @@ -155,9 +155,17 @@ export default { ); const value = new BigNumber(_value ?? minFee); if (minFee.gt(value)) { - if (_pickBiggerFee === true) return minFee.toFixed(); + if (_canIncreaseFee === true) return minFee.toFixed(); throw new IllegalArgumentError(`Fee ${value.toString()} must be bigger than ${minFee}`); } return value.toFixed(); }, + + serialize( + value: Parameters[0], + params: Parameters[1], + options: { _canIncreaseFee?: boolean } & Parameters[2], + ): Buffer { + return coinAmount.serialize.call(this, value, params, options); + }, }; diff --git a/src/tx/builder/field-types/gas-limit.ts b/src/tx/builder/field-types/gas-limit.ts index e761723301..d5f28dc2ff 100644 --- a/src/tx/builder/field-types/gas-limit.ts +++ b/src/tx/builder/field-types/gas-limit.ts @@ -33,7 +33,7 @@ export default { const gasLimitMax = tag === Tag.GaMetaTx ? MAX_AUTH_FUN_GAS : calculateGasLimitMax( gasMax, - (gasLimit) => rebuildTx({ _computingGasLimit: gasLimit, _pickBiggerFee: true }), + (gasLimit) => rebuildTx({ _computingGasLimit: gasLimit, _canIncreaseFee: true }), unpackTx, buildTx, ); diff --git a/src/tx/builder/field-types/ttl.ts b/src/tx/builder/field-types/ttl.ts index 9bb4665c9e..6a9a3f8da4 100644 --- a/src/tx/builder/field-types/ttl.ts +++ b/src/tx/builder/field-types/ttl.ts @@ -1,6 +1,7 @@ import shortUInt from './short-u-int'; import Node from '../../../Node'; import { ArgumentError } from '../../../utils/errors'; +import { _getPollInterval, getHeight } from '../../../chain'; /** * Time to leave @@ -16,11 +17,14 @@ export default { value: number | undefined, params: {}, // TODO: { absoluteTtl: true } | { absoluteTtl: false, onNode: Node } - { onNode, absoluteTtl }: { onNode?: Node; absoluteTtl?: boolean }, + { onNode, absoluteTtl, ...options }: { + onNode?: Node; + absoluteTtl?: boolean; + } & Parameters[1], ) { if (absoluteTtl !== true && value !== 0 && value != null) { if (onNode == null) throw new ArgumentError('onNode', 'provided', onNode); - value += (await onNode.getCurrentKeyBlock()).height; + value += await getHeight({ ...options, onNode, cached: true }); } return value; }, diff --git a/src/tx/builder/index.ts b/src/tx/builder/index.ts index d1730938fd..b6214ee9d3 100644 --- a/src/tx/builder/index.ts +++ b/src/tx/builder/index.ts @@ -105,7 +105,7 @@ export function buildTxHash(rawTx: Encoded.Transaction | Uint8Array): Encoded.Tx } /** - * Build a contract public key by contractCreateTx or gaAttach + * Build a contract public key by contractCreateTx, gaAttach or signedTx * @category contract * @param contractTx - Transaction * @returns Contract public key @@ -113,7 +113,8 @@ export function buildTxHash(rawTx: Encoded.Transaction | Uint8Array): Encoded.Tx export function buildContractIdByContractTx( contractTx: Encoded.Transaction, ): Encoded.ContractAddress { - const params = unpackTx(contractTx); + let params = unpackTx(contractTx); + if (Tag.SignedTx === params.tag) params = params.encodedTx; if (Tag.ContractCreateTx !== params.tag && Tag.GaAttachTx !== params.tag) { throw new ArgumentError('contractTx', 'a contractCreateTx or gaAttach', params.tag); } diff --git a/src/tx/builder/schema.ts b/src/tx/builder/schema.ts index 7933622d92..8666e8be12 100644 --- a/src/tx/builder/schema.ts +++ b/src/tx/builder/schema.ts @@ -20,12 +20,6 @@ export enum ORACLE_TTL_TYPES { block = 1, } -// # CONTRACT -export const DRY_RUN_ACCOUNT = { - pub: 'ak_11111111111111111111111111111111273Yts', - amount: 100000000000000000000000000000000000n, -} as const; - export enum CallReturnType { Ok = 0, Error = 1, diff --git a/src/tx/validator.ts b/src/tx/validator.ts index 60170f0b0d..b99ce2dd3d 100644 --- a/src/tx/validator.ts +++ b/src/tx/validator.ts @@ -115,7 +115,7 @@ validators.push( }, (tx, { height }) => { if (!('ttl' in tx)) return []; - if (tx.ttl === 0 || tx.ttl >= height) return []; + if (tx.ttl === 0 || tx.ttl > height) return []; return [{ message: `TTL ${tx.ttl} is already expired, current height is ${height}`, key: 'ExpiredTTL', diff --git a/src/utils/autorest.ts b/src/utils/autorest.ts index 19a8853560..8c498544a3 100644 --- a/src/utils/autorest.ts +++ b/src/utils/autorest.ts @@ -16,9 +16,7 @@ export const genRequestQueuesPolicy = (): AdditionalPolicyConfig => { const getResponse = async (): Promise => next(request); if (key == null) return getResponse(); const req = (requestQueues.get(key) ?? Promise.resolve()).then(getResponse); - // TODO: remove pause after fixing https://github.com/aeternity/aeternity/issues/3803 - // gap to ensure that node won't reject the nonce - requestQueues.set(key, req.then(async () => pause(750), () => {})); + requestQueues.set(key, req.catch(() => {})); return req; }, }, @@ -124,7 +122,9 @@ export const genRetryOnFailurePolicy = ( policy: { name: 'retry-on-failure', async sendRequest(request, next) { - const statusesToNotRetry = [200, 400, 403, 410, 500]; + const retryCode = request.headers.get('__retry-code') ?? NaN; + request.headers.delete('__retry-code'); + const statusesToNotRetry = [200, 400, 403, 410, 500].filter((c) => c !== +retryCode); const intervals = new Array(retryCount).fill(0) .map((_, idx) => ((idx + 1) / retryCount) ** 2); diff --git a/test/integration/chain.ts b/test/integration/chain.ts index ae0de7580d..789c0c605b 100644 --- a/test/integration/chain.ts +++ b/test/integration/chain.ts @@ -137,6 +137,7 @@ describe('Node Chain', () => { const accounts = new Array(10).fill(undefined).map(() => MemoryAccount.generate()); const transactions: Encoded.TxHash[] = []; + const txPostRetry = '/v3/transactions?int-as-string=true&__sdk-retry='; it('multiple spends from one account', async () => { const { nextNonce } = await aeSdk.api.getAccountNextNonce(aeSdk.address); const getCount = bindRequestCounter(aeSdk.api); @@ -147,7 +148,7 @@ describe('Node Chain', () => { ))); transactions.push(...spends.map(({ hash }) => hash)); const txPostCount = accounts.length; - expect(getCount()).to.be.equal(txPostCount); + expect(getCount({ exclude: [txPostRetry] })).to.be.equal(txPostCount); }); it('multiple spends from different accounts', async () => { diff --git a/test/integration/index.ts b/test/integration/index.ts index b6b9ff9d08..14fe18373b 100644 --- a/test/integration/index.ts +++ b/test/integration/index.ts @@ -1,4 +1,4 @@ -import { after } from 'mocha'; +import { after, afterEach } from 'mocha'; import { AeSdk, CompilerHttpNode, MemoryAccount, Node, Encoded, ConsensusProtocolVersion, } from '../../src'; @@ -25,6 +25,7 @@ const configuration = { testnet: { networkId: 'ae_uat', url: 'https://testnet.aeternity.io', + debugUrl: 'https://testnet.aeternity.io', channelUrl: 'wss://testnet.aeternity.io/channel', // TODO: deploy v8 compiler and v7.4.1 compilerUrl: 'http://localhost:3080', @@ -45,6 +46,7 @@ const configuration = { '': { networkId: 'ae_devnet', url: 'http://localhost:3013', + debugUrl: 'http://localhost:3113', channelUrl: 'ws://localhost:3014/channel', compilerUrl: 'http://localhost:3080', compilerUrl7: 'http://localhost:3081', @@ -116,3 +118,16 @@ export async function getSdk(accountCount = 1): Promise { return sdk; } + +afterEach(async function describeTxError() { + const { err } = this.currentTest ?? {}; + if (configuration.debugUrl == null || err?.message == null) return; + const match = err.message.match(/Giving up after \d+ blocks mined, transaction hash: (th_.+)/); + if (match == null) return; + const hash = match[1]; + const u = `${configuration.debugUrl}/v3/debug/check-tx/pool/${hash}`; + const response = await fetch(u); + if (response.status !== 200) throw new Error(`Invalid ${u} response: ${response.status}`); + const { status } = await response.json(); + err.message += ` (node-provided transaction status: ${status})`; +}); diff --git a/test/utils.ts b/test/utils.ts index a2260d262f..aedb5424c0 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -32,18 +32,28 @@ export type InputNumber = number | bigint | string | BigNumber; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function checkOnlyTypes(cb: Function): void {} -export function bindRequestCounter(node: Node): () => number { - const name = `counter-${randomString(6)}`; - let counter = 0; +export function bindRequestTracker(node: Node): () => string[] { + const name = `tracker-${randomString(6)}`; + const requestUrls: string[] = []; node.pipeline.addPolicy({ name, async sendRequest(request, next) { - counter += 1; + requestUrls.push(request.url); return next(request); }, }, { phase: 'Deserialize' }); return () => { node.pipeline.removePolicy({ name }); - return counter; + return requestUrls; }; } + +export function bindRequestCounter( + node: Node, +): (params?: { filter?: string[]; exclude?: string[] }) => number { + const getRequestUrls = bindRequestTracker(node); + return ({ filter = [], exclude = [] } = {}) => getRequestUrls() + .filter((url) => !exclude.some((p) => url.includes(p))) + .filter((url) => (filter.length === 0) || filter.some((p) => url.includes(p))) + .length; +}