-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(node): add NodeGateway retrying nonce requests if resp outdated
- Loading branch information
Showing
8 changed files
with
200 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import NodeDirect from './Direct'; | ||
import { getIntervals } from '../utils/autorest'; | ||
import { pause } from '../utils/other'; | ||
import { buildTx, unpackTx } from '../tx/builder'; | ||
import { Tag } from '../tx/builder/constants'; | ||
import getTransactionSignerAddress from '../tx/transaction-signer'; | ||
import { Encoded } from '../utils/encoder'; | ||
import { IllegalArgumentError } from '../utils/errors'; | ||
|
||
/** | ||
* Implements request retry strategies to improve reliability of connection to multiple nodes behind | ||
* load balancer. | ||
*/ | ||
export default class NodeGateway extends NodeDirect { | ||
#nonces: Record<string, number> = {}; | ||
|
||
readonly #retryIntervals: number[]; | ||
|
||
/** | ||
* @param url - Url for node API | ||
* @param options - Options | ||
*/ | ||
constructor( | ||
url: string, | ||
{ | ||
retryCount = 8, retryOverallDelay = 3000, ...options | ||
}: ConstructorParameters<typeof NodeDirect>[1] = {}, | ||
) { | ||
super(url, { | ||
...options, retryCount, retryOverallDelay, _disableGatewayWarning: true, | ||
}); | ||
this.#retryIntervals = getIntervals(retryCount, retryOverallDelay); | ||
} | ||
|
||
#saveNonce(tx: Encoded.Transaction): void { | ||
const { encodedTx } = unpackTx(tx, Tag.SignedTx); | ||
if (encodedTx.tag === Tag.GaMetaTx) return; | ||
if (!('nonce' in encodedTx)) { | ||
throw new IllegalArgumentError('Transaction doesn\'t have nonce field'); | ||
} | ||
const address = getTransactionSignerAddress(tx); | ||
this.#nonces[address] = encodedTx.nonce; | ||
if (encodedTx.tag === Tag.PayingForTx) { | ||
this.#saveNonce(buildTx(encodedTx.tx)); | ||
} | ||
} | ||
|
||
// @ts-expect-error use code generation to create node class or integrate bigint to autorest | ||
override async postTransaction( | ||
...args: Parameters<NodeDirect['postTransaction']> | ||
): ReturnType<NodeDirect['postTransaction']> { | ||
const res = super.postTransaction(...args); | ||
try { | ||
this.#saveNonce(args[0].tx as Encoded.Transaction); | ||
} catch (error) { | ||
console.warn('NodeGateway: failed to save nonce,', error); | ||
} | ||
return res; | ||
} | ||
|
||
async #retryNonceRequest<T>( | ||
address: string, | ||
doRequest: () => Promise<T>, | ||
getNonce: (t: T) => number, | ||
): Promise<T> { | ||
for (let attempt = 0; attempt < this.#retryIntervals.length; attempt += 1) { | ||
const result = await doRequest(); | ||
const nonce = getNonce(result); | ||
if (nonce >= (this.#nonces[address] ?? -1)) { | ||
return result; | ||
} | ||
await pause(this.#retryIntervals[attempt]); | ||
} | ||
return doRequest(); | ||
} | ||
|
||
// @ts-expect-error use code generation to create node class or integrate bigint to autorest | ||
override async getAccountByPubkey( | ||
...args: Parameters<NodeDirect['getAccountByPubkey']> | ||
): ReturnType<NodeDirect['getAccountByPubkey']> { | ||
return this.#retryNonceRequest( | ||
args[0], | ||
async () => super.getAccountByPubkey(...args), | ||
({ nonce, kind }) => (kind === 'generalized' ? Number.MAX_SAFE_INTEGER : nonce), | ||
); | ||
} | ||
|
||
// @ts-expect-error use code generation to create node class or integrate bigint to autorest | ||
override async getAccountNextNonce( | ||
...args: Parameters<NodeDirect['getAccountNextNonce']> | ||
): ReturnType<NodeDirect['getAccountNextNonce']> { | ||
return this.#retryNonceRequest( | ||
args[0], | ||
async () => super.getAccountNextNonce(...args), | ||
({ nextNonce }) => (nextNonce === 0 ? Number.MAX_SAFE_INTEGER : nextNonce - 1), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { describe, it, before } from 'mocha'; | ||
import { expect } from 'chai'; | ||
import { getSdk, url } from '.'; | ||
import { | ||
NodeGateway, AeSdk, Tag, buildTx, Encoded, | ||
} from '../../src'; | ||
import { bindRequestCounter } from '../utils'; | ||
|
||
describe('NodeGateway', () => { | ||
let aeSdk: AeSdk; | ||
const node = new NodeGateway(url, { retryCount: 2, retryOverallDelay: 500 }); | ||
node.pipeline.addPolicy({ | ||
name: 'swallow-post-tx-request', | ||
async sendRequest(request, next) { | ||
const suffix = 'transactions?int-as-string=true'; | ||
if (!request.url.endsWith(suffix)) return next(request); | ||
request.url = request.url.replace(suffix, 'status'); | ||
request.method = 'GET'; | ||
delete request.body; | ||
const response = await next(request); | ||
response.bodyAsText = '{"tx_hash": "fake"}'; | ||
return response; | ||
}, | ||
}); | ||
let spendTxHighNonce: Encoded.Transaction; | ||
|
||
before(async () => { | ||
aeSdk = await getSdk(); | ||
const spendTx = buildTx({ | ||
tag: Tag.SpendTx, recipientId: aeSdk.address, senderId: aeSdk.address, nonce: 1e10, | ||
}); | ||
spendTxHighNonce = await aeSdk.signTransaction(spendTx); | ||
}); | ||
|
||
it('doesn\'t retries getAccountByPubkey before seeing a transaction', async () => { | ||
const getCount = bindRequestCounter(node); | ||
await node.getAccountByPubkey(aeSdk.address); | ||
expect(getCount()).to.be.equal(1); | ||
}); | ||
|
||
it('doesn\'t retries getAccountNextNonce before seeing a transaction', async () => { | ||
const getCount = bindRequestCounter(node); | ||
await node.getAccountNextNonce(aeSdk.address); | ||
expect(getCount()).to.be.equal(1); | ||
}); | ||
|
||
it('retries getAccountByPubkey', async () => { | ||
await node.postTransaction({ tx: spendTxHighNonce }); | ||
const getCount = bindRequestCounter(node); | ||
await node.getAccountByPubkey(aeSdk.address); | ||
expect(getCount()).to.be.equal(3); | ||
}); | ||
|
||
it('retries getAccountNextNonce once for multiple calls', async () => { | ||
await node.postTransaction({ tx: spendTxHighNonce }); | ||
const getCount = bindRequestCounter(node); | ||
const nonces = await Promise.all( | ||
new Array(3).fill(undefined).map(async () => node.getAccountNextNonce(aeSdk.address)), | ||
); | ||
expect(getCount()).to.be.equal(3); | ||
expect(nonces).to.be.eql(nonces.map(() => ({ nextNonce: 1 }))); | ||
}); | ||
|
||
it('doesn\'t retries nonce for generalized account', async () => { | ||
const sourceCode = `contract BlindAuth = | ||
stateful entrypoint authorize() : bool = false`; | ||
await aeSdk.createGeneralizedAccount('authorize', [], { sourceCode }); | ||
await node.postTransaction({ tx: spendTxHighNonce }); | ||
|
||
const getCount = bindRequestCounter(node); | ||
await node.getAccountByPubkey(aeSdk.address); | ||
await node.getAccountNextNonce(aeSdk.address); | ||
expect(getCount()).to.be.equal(2); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters