Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement iterative Qi tx fee estimation #341

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/_tests/integration/testcontract.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ describe('Test Contract Fallback', function () {
const { name, address, abi } = test;
const send = test[group];

const contract = new Contract(address, abi, provider);
const contract = new Contract(address, abi, provider as ContractRunner);
it(`test contract fallback checks: ${group} - ${name}`, async function () {
const func = async function () {
if (abi.length === 0) {
Expand Down
28 changes: 25 additions & 3 deletions src/providers/abstract-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { computeAddress, resolveAddress, formatMixedCaseChecksumAddress } from '../address/index.js';
import { Shard, toShard, toZone, Zone } from '../constants/index.js';
import { TxInput, TxOutput } from '../transaction/index.js';
import { Outpoint } from '../transaction/utxo.js';
import { Outpoint, TxInputJson, TxOutputJson } from '../transaction/utxo.js';
import {
hexlify,
isHexString,
Expand Down Expand Up @@ -494,15 +494,20 @@ export interface QuaiPerformActionTransaction extends QuaiPreparedTransactionReq
*/
// todo: write docs for this
export interface QiPerformActionTransaction extends QiPreparedTransactionRequest {
/**
* The transaction type. Always 2 for UTXO transactions.
*/
txType: number;

/**
* The `inputs` of the UTXO transaction.
*/
inputs?: Array<TxInput>;
txIn: Array<TxInputJson>;

/**
* The `outputs` of the UTXO transaction.
*/
outputs?: Array<TxOutput>;
txOut: Array<TxOutputJson>;

[key: string]: any;
}
Expand Down Expand Up @@ -534,6 +539,11 @@ export type PerformActionRequest =
transaction: PerformActionTransaction;
zone?: Zone;
}
| {
method: 'estimateFeeForQi';
transaction: QiPerformActionTransaction;
zone?: Zone;
}
| {
method: 'createAccessList';
transaction: PerformActionTransaction;
Expand Down Expand Up @@ -1507,6 +1517,18 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
);
}

async estimateFeeForQi(_tx: QiPerformActionTransaction): Promise<bigint> {
const zone = await this.zoneFromAddress(addressFromTransactionRequest(_tx));
return getBigInt(
await this.#perform({
method: 'estimateFeeForQi',
transaction: _tx,
zone: zone,
}),
'%response',
);
}

async createAccessList(_tx: TransactionRequest): Promise<AccessList> {
let tx = this._getTransactionRequest(_tx);
if (isPromise(tx)) {
Expand Down
7 changes: 7 additions & 0 deletions src/providers/provider-jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,13 @@ export abstract class JsonRpcApiProvider<C = FetchRequest> extends AbstractProvi
};
}

case 'estimateFeeForQi': {
return {
method: 'quai_estimateFeeForQi',
args: [req.transaction],
};
}

case 'createAccessList': {
return {
method: 'quai_createAccessList',
Expand Down
18 changes: 16 additions & 2 deletions src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { AccessList, AccessListish } from '../transaction/index.js';

import type { ContractRunner } from '../contract/index.js';
import type { Network } from './network.js';
import type { Outpoint } from '../transaction/utxo.js';
import type { Outpoint, TxInputJson } from '../transaction/utxo.js';
import type { TxInput, TxOutput } from '../transaction/utxo.js';
import type { Zone, Shard } from '../constants/index.js';
import type { txpoolContentResponse, txpoolInspectResponse } from './txpool.js';
Expand Down Expand Up @@ -56,6 +56,7 @@ import { QiTransactionLike } from '../transaction/qi-transaction.js';
import { QuaiTransactionLike } from '../transaction/quai-transaction.js';
import { toShard, toZone } from '../constants/index.js';
import { getZoneFromNodeLocation, getZoneForAddress } from '../utils/shards.js';
import { QiPerformActionTransaction } from './abstract-provider.js';

/**
* Get the value if it is not null or undefined.
Expand Down Expand Up @@ -146,7 +147,12 @@ export function addressFromTransactionRequest(tx: TransactionRequest): AddressLi
return tx.from;
}
if ('txInputs' in tx && !!tx.txInputs) {
return computeAddress(tx.txInputs[0].pubkey);
const inputs = tx.txInputs as TxInput[];
return computeAddress(inputs[0].pubkey);
}
if ('txIn' in tx && !!tx.txIn) {
const inputs = tx.txIn as TxInputJson[];
return computeAddress(inputs[0].pubkey);
}
if ('to' in tx && !!tx.to) {
return tx.to as AddressLike;
Expand Down Expand Up @@ -2857,6 +2863,14 @@ export interface Provider extends ContractRunner, EventEmitterable<ProviderEvent
*/
estimateGas(tx: TransactionRequest): Promise<bigint>;

/**
* Estimate the fee for a Qi transaction.
*
* @param {QiPerformActionTransaction} tx - The transaction to estimate the fee for.
* @returns {Promise<bigint>} A promise resolving to the estimated fee.
*/
estimateFeeForQi(tx: QiPerformActionTransaction): Promise<bigint>;

/**
* Required for populating access lists for state mutating calls
*
Expand Down
16 changes: 16 additions & 0 deletions src/transaction/utxo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ export type TxOutput = {
lock?: string;
};

type PreviousOutpointJson = {
txHash: string;
index: string;
};

export type TxInputJson = {
previousOutpoint: PreviousOutpointJson;
pubkey: string;
};

export type TxOutputJson = {
address: string;
denomination: string;
lock?: string;
};

/**
* List of supported Qi denominations.
*
Expand Down
124 changes: 94 additions & 30 deletions src/wallet/qi-hdwallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { HDNodeWallet } from './hdnodewallet.js';
import { QiTransactionRequest, Provider, TransactionResponse } from '../providers/index.js';
import { computeAddress, isQiAddress } from '../address/index.js';
import { getBytes, getZoneForAddress, hexlify } from '../utils/index.js';
import { getBytes, getZoneForAddress, hexlify, toQuantity } from '../utils/index.js';
import { TransactionLike, QiTransaction, TxInput, FewestCoinSelector } from '../transaction/index.js';
import { MuSigFactory } from '@brandonblack/musig';
import { schnorr } from '@noble/curves/secp256k1';
Expand All @@ -23,6 +23,7 @@ import { bs58check } from './bip32/crypto.js';
import { type BIP32API, HDNodeBIP32Adapter } from './bip32/types.js';
import ecc from '@bitcoinerlab/secp256k1';
import { SelectedCoinsResult } from '../transaction/abstract-coinselector.js';
import { QiPerformActionTransaction } from '../providers/abstract-provider.js';

/**
* @property {Outpoint} outpoint - The outpoint object.
Expand Down Expand Up @@ -550,43 +551,71 @@ export class QiHDWallet extends AbstractHDWallet {
throw new Error('Missing public key for input address');
}

const chainId = (await this.provider.getNetwork()).chainId;
let tx = await this.prepareTransaction(
selection,
inputPubKeys.map((pubkey) => pubkey!),
sendAddresses,
changeAddresses,
Number(chainId),
);
let attempts = 0;
let finalFee = 0n;
let satisfiedFeeEstimation = false;
const MAX_FEE_ESTIMATION_ATTEMPTS = 5;

while (attempts < MAX_FEE_ESTIMATION_ATTEMPTS) {
const feeEstimationTx = this.prepareFeeEstimationTransaction(
selection,
inputPubKeys.map((pubkey) => pubkey!),
sendAddresses,
changeAddresses,
);

const gasLimit = await this.provider.estimateGas(tx);
const gasPrice = denominations[1]; // 0.005 Qi
const minerTip = (gasLimit * gasPrice) / 100n; // 1% extra as tip
// const feeData = await this.provider.getFeeData(originZone, true);
// const conversionRate = await this.provider.getLatestQuaiRate(originZone, feeData.gasPrice!);
finalFee = await this.provider.estimateFeeForQi(feeEstimationTx);

// Get new selection with updated fee
selection = fewestCoinSelector.performSelection(spendTarget, finalFee);

// Determine if new addresses are needed for the change outputs
const changeAddressesNeeded = selection.changeOutputs.length - changeAddresses.length;
if (changeAddressesNeeded > 0) {
// Need more change addresses
const newChangeAddresses = await getChangeAddressesForOutputs(changeAddressesNeeded);
changeAddresses.push(...newChangeAddresses);
} else if (changeAddressesNeeded < 0) {
// Have extra change addresses, remove the addresses starting from the end
// TODO: Set the status of the addresses to UNUSED in _addressesMap. This fine for now as it will be fixed during next sync
changeAddresses.splice(changeAddressesNeeded);
}

// 5.6 Calculate total fee for the transaction using the gasLimit, gasPrice, and minerTip
const totalFee = gasLimit * gasPrice + minerTip;
// Determine if new addresses are needed for the spend outputs
const spendAddressesNeeded = selection.spendOutputs.length - sendAddresses.length;
if (spendAddressesNeeded > 0) {
// Need more send addresses
const newSendAddresses = await getDestinationAddresses(spendAddressesNeeded);
sendAddresses.push(...newSendAddresses);
} else if (spendAddressesNeeded < 0) {
// Have extra send addresses, remove the excess
// TODO: Set the status of the addresses to UNUSED in _addressesMap. This fine for now as it will be fixed during next sync
sendAddresses.splice(spendAddressesNeeded);
}

// Get new selection with fee
selection = fewestCoinSelector.performSelection(spendTarget, totalFee);
inputPubKeys = selection.inputs.map((input) => this.locateAddressInfo(input.address)?.pubKey);

// Determine if new addresses are needed for the change and spend outputs
const changeAddressesNeeded = selection.changeOutputs.length - changeAddresses.length;
if (changeAddressesNeeded > 0) {
const outpusChangeAddresses = await getChangeAddressesForOutputs(changeAddressesNeeded);
changeAddresses.push(...outpusChangeAddresses);
}
// Calculate total new outputs needed (absolute value)
const totalNewOutputsNeeded = Math.abs(changeAddressesNeeded) + Math.abs(spendAddressesNeeded);

const spendAddressesNeeded = selection.spendOutputs.length - sendAddresses.length;
if (spendAddressesNeeded > 0) {
const newSendAddresses = await getDestinationAddresses(spendAddressesNeeded);
sendAddresses.push(...newSendAddresses);
// If we need 5 or fewer new outputs, we can break the loop
if ((changeAddressesNeeded <= 0 && spendAddressesNeeded <= 0) || totalNewOutputsNeeded <= 5) {
finalFee *= 3n; // Increase the fee 3x to ensure it's accepted
satisfiedFeeEstimation = true;
break;
}

attempts++;
}

inputPubKeys = selection.inputs.map((input) => this.locateAddressInfo(input.address)?.pubKey);
// If we didn't satisfy the fee estimation, increase the fee 10x to ensure it's accepted
if (!satisfiedFeeEstimation) {
finalFee *= 10n;
}

tx = await this.prepareTransaction(
// Proceed with creating and signing the transaction
const chainId = (await this.provider.getNetwork()).chainId;
const tx = await this.prepareTransaction(
selection,
inputPubKeys.map((pubkey) => pubkey!),
sendAddresses,
Expand Down Expand Up @@ -706,6 +735,41 @@ export class QiHDWallet extends AbstractHDWallet {
return tx;
}

private prepareFeeEstimationTransaction(
selection: SelectedCoinsResult,
inputPubKeys: string[],
sendAddresses: string[],
changeAddresses: string[],
): QiPerformActionTransaction {
const txIn = selection.inputs.map((input, index) => ({
previousOutpoint: { txHash: input.txhash!, index: toQuantity(input.index!) },
pubkey: inputPubKeys[index],
}));

// 5.3 Create the "sender" outputs
const senderOutputs = selection.spendOutputs.map((output, index) => ({
address: sendAddresses[index],
denomination: output.denomination,
}));

// 5.4 Create the "change" outputs
const changeOutputs = selection.changeOutputs.map((output, index) => ({
address: changeAddresses[index],
denomination: output.denomination,
}));

const txOut = [...senderOutputs, ...changeOutputs].map((output) => ({
address: output.address,
denomination: toQuantity(output.denomination!),
}));

return {
txType: 2,
txIn,
txOut,
};
}

/**
* Checks the status of pending outpoints and updates the wallet's UTXO set accordingly.
*
Expand Down
Loading