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'; import { $traces } from '@dequanto/utils/$traces'; import { $abiProvider } from '@dequanto/utils/$abiProvider'; import { $require } from '@dequanto/utils/$require'; 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 { 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); } const minGasPriorityFee = this.client.minGasPriorityFee; if (minGasPriorityFee > 0) { $priorityFee = $bigint.max( $bigint.toGweiFromEther(minGasPriorityFee), $priorityFee ); } const maxFeePerGas = $baseFee + $priorityFee; const maxPriorityFeePerGas = $priorityFee; $require.lte(maxFeePerGas, BigInt(200e9), `maxFeePerGas exceeds maximum`); $require.lte(maxPriorityFeePerGas, BigInt(5e9), `maxPriorityFeePerGas exceeds maximum`); this.data.maxFeePerGas = $bigint.toHex(maxFeePerGas); this.data.maxPriorityFeePerGas = $bigint.toHex(maxPriorityFeePerGas); 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 { 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 ?? {}) }); } async getInputDataInfo () { return $contract.parseInputData(this.data.data, this.abi ?? $contract.store.getFlattened()); } private async getGasEstimation (from: TAddress) { try { return await this.client.getGasEstimation(from, this.data) } catch (error) { let message = error.message; let tracesF = null; if (this.client.debug && this.client.debug.traceCall) { // Process traces earlier to capture all ABIs for the error parser try { let { client } = this; let txData = { from, ...this.data }; let traces = await client.debug.traceCall(txData); let output = await $traces.format({ tx: txData, traces, color: this.config.color ?? true, shortAddress: false, async getABI(address) { return $abiProvider.getAbi(address, client.network); } }); tracesF = `\nTraces: \n` + output; } catch (error) { // ignore } } 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); } if (tracesF != null) { message += `\nTraces: \n` + tracesF; } 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) { 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( maxPriorityFeePerGas ?? 0); } return null; } }