Skip to content

Commit

Permalink
Implement iterative Qi tx fee estimation
Browse files Browse the repository at this point in the history
  • Loading branch information
rileystephens28 committed Oct 30, 2024
1 parent bc68b26 commit fb50564
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 36 deletions.
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

0 comments on commit fb50564

Please sign in to comment.