0xweb
Version:
Contract package manager and other web3 tools
309 lines (269 loc) • 10.1 kB
text/typescript
import { type TAbiItem } from '@dequanto/types/TAbi';
import { File } from 'atma-io';
import type { TAccount } from "@dequanto/models/TAccount";
import type { Web3Client } from '@dequanto/clients/Web3Client';
import type { TAddress } from '@dequanto/models/TAddress';
import { $account } from '@dequanto/utils/$account';
import { $bigint } from '@dequanto/utils/$bigint';
import { ITxBuilderNonceOptions, ITxBuilderOptions } from './ITxBuilderOptions';
import { $number } from '@dequanto/utils/$number';
import { TEth } from '@dequanto/models/TEth';
import { $sig } from '@dequanto/utils/$sig';
import { $abiUtils } from '@dequanto/utils/$abiUtils';
import { $hex } from '@dequanto/utils/$hex';
import { $contract } from '@dequanto/utils/$contract';
import { TxNonceManager } from './TxNonceManager';
export class TxDataBuilder {
public abi: TAbiItem[] = null;
constructor(
public client: Web3Client,
public account: { address?: TAddress },
public data: TEth.TxLike,
public config: ITxBuilderOptions = null,
) {
this.data ??= {};
this.data.value = this.data.value ?? 0;
this.data.chainId = client.chainId;
this.abi = config?.abi;
}
setInputDataWithABI(abi: string | TAbiItem, ...params): this {
try {
this.data.data = $abiUtils.serializeMethodCallData(abi, params);
} catch (error) {
error.message = `${JSON.stringify(abi)}\n${error.message}`;
throw error;
}
return this;
}
setValue(value: number | string | bigint): this {
if (value == null) {
return this;
}
if (typeof value === 'number') {
value = $bigint.toWei(value);
}
if (typeof value === 'bigint') {
this.data.value = `0x${value.toString(16)}`;
return this;
}
this.data.value = value;
return this;
}
setConfig (config: ITxBuilderOptions): this {
this.config = config;
return this;
}
async ensureNonce (options?: ITxBuilderNonceOptions) {
if (this.data.nonce != null) {
// was already set
return;
}
await this.setNonce(options);
}
async setNonce(local?: ITxBuilderNonceOptions) {
let opts = {
...(this.config ?? {}),
...(local ?? {})
};
let nonce: bigint;
if (opts.nonce != null) {
if (typeof opts.nonce === 'number' || typeof opts.nonce === 'bigint') {
nonce = BigInt(opts.nonce)
} else if (opts.nonce instanceof TxNonceManager) {
nonce = await opts.nonce.pickNonce(this.client);
} else {
console.error(opts.nonce);
throw new Error(`Invalid nonce ${typeof opts.nonce}`);
}
} else if (opts.overriding) {
nonce = await this.client.getTransactionCount(this.account.address);
// override first pending TX:
} else if (opts.noncePending != null) {
let pendingIndex = BigInt(opts.noncePending) - 1n;
let submitted = await this.client.getTransactionCount(this.account.address);
let next = pendingIndex;
if (next > 0) {
let total = await this.client.getTransactionCount(this.account.address, 'pending');
let pendingCount = total - submitted;
if (pendingCount > 0n && next > pendingCount - 1n) {
next = pendingCount - 1n;
}
}
nonce = submitted + next;
} else {
nonce = await TxNonceManager.loadNonce(this.client, this.account.address);
}
this.data.nonce = Number(nonce);
}
async ensureGas () {
if (this.data.gasPrice == null && this.data.maxFeePerGas == null) {
await this.setGas();
}
}
async setGas({
price,
priceRatio,
gasLimitRatio,
gasLimit,
gasEstimation,
from,
type,
}: {
price?: bigint
priceRatio?: number
gasLimitRatio?: number
gasLimit?: string | number
gasEstimation?: boolean
from?: TAddress
type?: 0 | 1 | 2
} = {}): Promise<this> {
let [ gasPrice, gasUsage ] = await Promise.all([
price != null ?
{ price, base: price, priority: 10n**9n }
: this.client.getGasPrice(),
gasEstimation == null || gasEstimation === true
? this.getGasEstimation(from ?? this.account.address)
: (gasLimit ?? this.client.defaultGasLimit ?? 2_000_000)
]);
let hasPriceRatio = priceRatio != null;
let hasPriceFixed = price != null;
let $priceRatio = 1;
if (hasPriceRatio) {
$priceRatio = priceRatio;
} else if (hasPriceFixed === false) {
$priceRatio = this.client.defaultGasPriceRatio;
}
type ??= this.client.defaultTxType;
if (type === 0 || type === 1) {
let $baseFee = $bigint.multWithFloat(gasPrice.price, $priceRatio);
this.data.gasPrice = $bigint.toHex($baseFee);
this.data.type = type;
} else {
let $baseFee = $bigint.multWithFloat(gasPrice.base ?? gasPrice.price, $priceRatio);
let $priorityFee = gasPrice.priority;
if ($priorityFee == null) {
$priorityFee = await this.client.getGasPriorityFee();
$priorityFee = $bigint.multWithFloat($priorityFee, $priceRatio);
}
this.data.maxFeePerGas = $bigint.toHex($baseFee + $priorityFee);
this.data.maxPriorityFeePerGas = $bigint.toHex($priorityFee);
this.data.type = 2;
}
let hasLimitRatio = gasLimitRatio != null;
let hasLimitFixed = gasLimit != null;
let $gasLimitRatio = 1;
if (hasLimitRatio) {
$gasLimitRatio = gasLimitRatio;
} else if (hasLimitFixed === false) {
$gasLimitRatio = 1.5;
}
this.data.gas = gasLimit ?? Math.floor(Number(gasUsage) * $gasLimitRatio);
return this;
}
increaseGas (ratio: number) {
let { gasPrice, maxFeePerGas } = this.data;
if (gasPrice != null) {
let price = BigInt(gasPrice as any);
let priceNew = $bigint.multWithFloat(price, ratio);
this.data.gasPrice = $bigint.toHex(priceNew);
return;
}
if (maxFeePerGas != null) {
let price = BigInt(maxFeePerGas as any);
let priceNew = $bigint.multWithFloat(price, ratio);
this.data.maxFeePerGas = $bigint.toHex(priceNew);
return;
}
throw new Error(`Not possible to increase the gas price, the price not set yet`);
}
getTxData (client?: Web3Client) {
let txData = {
...this.data,
from: this.account?.address ?? void 0,
chainId: $number.toHex(this.data.chainId ?? client?.chainId ?? this.client?.chainId),
};
for (let key in txData) {
if (key === 'type') {
continue;
}
txData[key] = $hex.ensure(txData[key]);
}
return txData as TEth.TxLike;
}
/** Returns raw signed transaction */
async signToString(privateKey: TEth.EoAccount['key']): Promise<TEth.Hex> {
let address = await $sig.$account.getAddressFromKey(privateKey);
let rpc = await this.client.getRpc();
let txSig = await $sig.signTx(this.data, { address, key: privateKey }, rpc);
return txSig;
}
toJSON () {
return {
account: {
address: this.account?.address,
},
tx: this.data,
config: this.config,
};
}
async save (path: string, additionalProperties?) {
let json = this.toJSON();
await File.writeAsync(path, {
...json,
...(additionalProperties ?? {})
});
}
private async getGasEstimation (from: TAddress) {
try {
return await this.client.getGasEstimation(from, this.data)
} catch (error) {
let message = error.message;
if (error.data?.type != null) {
let data = error.data;
if (error.data.type === `Unknown` && error.data.params) {
let parsed = $contract.parseInputData(error.data.params, this.abi ?? $contract.store.getFlattened());
if (parsed) {
data = parsed;
}
}
message += `\nError: ` + $contract.formatCall(data);
}
let parsed = $contract.parseInputData(this.data.data, this.abi ?? $contract.store.getFlattened());
if (parsed) {
message += `\nMethod: ` + $contract.formatCall(parsed);
}
throw new Error(message);
}
}
static fromJSON (client: Web3Client, account: TAccount, json: {
config: ITxBuilderOptions,
tx: TEth.TxLike,
}) {
let sender = $account.getSender(account);
return new TxDataBuilder(
client,
sender,
json.tx,
json.config
);
}
static normalize(data: Partial<TEth.TxLike>) {
for (let key in data) {
let v = data[key];
if (typeof v === 'string' && /^\d+$/.test(v)) {
data[key] = BigInt(v);
}
}
return data;
}
static getGasPrice (builder: TxDataBuilder): bigint {
let { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = builder.data;
if (gasPrice != null) {
return BigInt(gasPrice as any);
}
if (maxFeePerGas != null) {
return BigInt(maxFeePerGas as any) + BigInt(<any> maxPriorityFeePerGas ?? 0);
}
return null;
}
}