aptos
Version:
176 lines (154 loc) • 6.06 kB
text/typescript
/**
* A wrapper that handles and manages an account sequence number.
*
* Submit up to `maximumInFlight` transactions per account in parallel with a timeout of `sleepTime`
* If local assumes `maximumInFlight` are in flight, determine the actual committed state from the network
* If there are less than `maximumInFlight` due to some being committed, adjust the window
* If `maximumInFlight` are in flight, wait `sleepTime` seconds before re-evaluating
* If ever waiting more than `maxWaitTime` restart the sequence number to the current on-chain state
*
* Assumptions:
* Accounts are expected to be managed by a single AccountSequenceNumber and not used otherwise.
* They are initialized to the current on-chain state, so if there are already transactions in
* flight, they may take some time to reset.
* Accounts are automatically initialized if not explicitly
*
* Notes:
* This is co-routine safe, that is many async tasks can be reading from this concurrently.
* The state of an account cannot be used across multiple AccountSequenceNumber services.
* The synchronize method will create a barrier that prevents additional nextSequenceNumber
* calls until it is complete.
* This only manages the distribution of sequence numbers it does not help handle transaction
* failures.
* If a transaction fails, you should call synchronize and wait for timeouts.
*/
import { AptosAccount } from "../account";
import { Provider } from "../providers";
import { sleep } from "../utils";
// returns `now` time in seconds
const now = () => Math.floor(Date.now() / 1000);
export class AccountSequenceNumber {
readonly provider: Provider;
readonly account: AptosAccount;
// sequence number on chain
lastUncommintedNumber: bigint | null = null;
// local sequence number
currentNumber: bigint | null = null;
/**
* We want to guarantee that we preserve ordering of workers to requests.
*
* `lock` is used to try to prevent multiple coroutines from accessing a shared resource at the same time,
* which can result in race conditions and data inconsistency.
* This code actually doesn't do it though, since we aren't giving out a slot, it is still somewhat a race condition.
*
* The ideal solution is likely that each thread grabs the next number from a incremental integer.
* When they complete, they increment that number and that entity is able to enter the `lock`.
* That would guarantee ordering.
*/
lock = false;
maxWaitTime: number;
maximumInFlight: number;
sleepTime: number;
constructor(
provider: Provider,
account: AptosAccount,
maxWaitTime: number,
maximumInFlight: number,
sleepTime: number,
) {
this.provider = provider;
this.account = account;
this.maxWaitTime = maxWaitTime;
this.maximumInFlight = maximumInFlight;
this.sleepTime = sleepTime;
}
/**
* Returns the next available sequence number for this account
*
* @returns next available sequence number
*/
async nextSequenceNumber(): Promise<bigint | null> {
/* eslint-disable no-await-in-loop */
while (this.lock) {
await sleep(this.sleepTime);
}
this.lock = true;
let nextNumber = BigInt(0);
try {
if (this.lastUncommintedNumber === null || this.currentNumber === null) {
await this.initialize();
}
if (this.currentNumber! - this.lastUncommintedNumber! >= this.maximumInFlight) {
await this.update();
const startTime = now();
while (this.currentNumber! - this.lastUncommintedNumber! >= this.maximumInFlight) {
await sleep(this.sleepTime);
if (now() - startTime > this.maxWaitTime) {
/* eslint-disable no-console */
console.warn(`Waited over 30 seconds for a transaction to commit, resyncing ${this.account.address()}`);
await this.initialize();
} else {
await this.update();
}
}
}
nextNumber = this.currentNumber!;
this.currentNumber! += BigInt(1);
} catch (e) {
console.error("error in getting next sequence number for this account", e);
} finally {
this.lock = false;
}
return nextNumber;
}
/**
* Initializes this account with the sequence number on chain
*/
async initialize(): Promise<void> {
const { sequence_number: sequenceNumber } = await this.provider.getAccount(this.account.address());
this.currentNumber = BigInt(sequenceNumber);
this.lastUncommintedNumber = BigInt(sequenceNumber);
}
/**
* Updates this account sequence number with the one on-chain
*
* @returns on-chain sequence number for this account
*/
async update(): Promise<bigint> {
const { sequence_number: sequenceNumber } = await this.provider.getAccount(this.account.address());
this.lastUncommintedNumber = BigInt(sequenceNumber);
return this.lastUncommintedNumber;
}
/**
* Synchronizes local sequence number with the seqeunce number on chain for this account.
*
* Poll the network until all submitted transactions have either been committed or until
* the maximum wait time has elapsed
*/
async synchronize(): Promise<void> {
if (this.lastUncommintedNumber === this.currentNumber) return;
/* eslint-disable no-await-in-loop */
while (this.lock) {
await sleep(this.sleepTime);
}
this.lock = true;
try {
await this.update();
const startTime = now();
while (this.lastUncommintedNumber !== this.currentNumber) {
if (now() - startTime > this.maxWaitTime) {
/* eslint-disable no-console */
console.warn(`Waited over 30 seconds for a transaction to commit, resyncing ${this.account.address()}`);
await this.initialize();
} else {
await sleep(this.sleepTime);
await this.update();
}
}
} catch (e) {
console.error("error in synchronizing this account sequence number with the one on chain", e);
} finally {
this.lock = false;
}
}
}