Skip to content

Commit

Permalink
feat: add estimation fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed Jan 6, 2023
1 parent f8ede21 commit 782a3c3
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 41 deletions.
61 changes: 46 additions & 15 deletions packages/transactions/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
ClarityVersion,
} from './constants';
import { ClarityAbi, validateContractCall } from './contract-abi';
import { NoEstimateAvailableError } from './errors';
import {
createStacksPrivateKey,
createStacksPublicKey,
Expand Down Expand Up @@ -100,7 +101,7 @@ export async function getNonce(
}

/**
* @deprecated Use the new {@link estimateTransaction} function insterad.
* @deprecated Use the new {@link estimateTransaction} function instead.
*
* Estimate the total transaction fee in microstacks for a token transfer
*
Expand All @@ -121,6 +122,17 @@ export async function estimateTransfer(
);
}

return estimateTransferUnsafe(transaction, network);
}

/**
* @deprecated Use the new {@link estimateTransaction} function instead.
* @internal
*/
export async function estimateTransferUnsafe(
transaction: StacksTransaction,
network?: StacksNetworkName | StacksNetwork
): Promise<bigint> {
const requestHeaders = {
Accept: 'application/text',
};
Expand Down Expand Up @@ -197,12 +209,14 @@ export async function estimateTransaction(
const response = await derivedNetwork.fetchFn(url, options);

if (!response.ok) {
let msg = '';
try {
msg = await response.text();
} catch (error) {}
const body = await response.json().catch(() => ({}));

if (body?.reason === 'NoEstimateAvailable') {
throw new NoEstimateAvailableError(body?.reason_data?.message ?? '');
}

throw new Error(
`Error estimating transaction fee. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"`
`Error estimating transaction fee. Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${body}"`
);
}

Expand Down Expand Up @@ -619,9 +633,8 @@ export async function makeUnsignedSTXTokenTransfer(
);

if (txOptions.fee === undefined || txOptions.fee === null) {
const estimatedLen = estimateTransactionByteLength(transaction);
const txFee = await estimateTransaction(payload, estimatedLen, options.network);
transaction.setFee(txFee[1].fee);
const fee = await estimateTransactionFeeWithFallback(transaction, network);
transaction.setFee(fee);
}

if (txOptions.nonce === undefined || txOptions.nonce === null) {
Expand Down Expand Up @@ -853,9 +866,8 @@ export async function makeUnsignedContractDeploy(
);

if (txOptions.fee === undefined || txOptions.fee === null) {
const estimatedLen = estimateTransactionByteLength(transaction);
const txFee = await estimateTransaction(payload, estimatedLen, options.network);
transaction.setFee(txFee[1].fee);
const fee = await estimateTransactionFeeWithFallback(transaction, network);
transaction.setFee(fee);
}

if (txOptions.nonce === undefined || txOptions.nonce === null) {
Expand Down Expand Up @@ -1061,9 +1073,8 @@ export async function makeUnsignedContractCall(
);

if (txOptions.fee === undefined || txOptions.fee === null) {
const estimatedLen = estimateTransactionByteLength(transaction);
const txFee = await estimateTransaction(payload, estimatedLen, network);
transaction.setFee(txFee[1].fee);
const fee = await estimateTransactionFeeWithFallback(transaction, network);
transaction.setFee(fee);
}

if (txOptions.nonce === undefined || txOptions.nonce === null) {
Expand Down Expand Up @@ -1476,3 +1487,23 @@ export function estimateTransactionByteLength(transaction: StacksTransaction): n
return transaction.serialize().byteLength;
}
}

/**
* Estimates the fee using {@link estimateTransfer} as a fallback if
* {@link estimateTransaction} does not get an estimation due to the
* {@link NoEstimateAvailableError} error.
*/
export async function estimateTransactionFeeWithFallback(
transaction: StacksTransaction,
network: StacksNetwork
): Promise<bigint | number> {
try {
const estimatedLen = estimateTransactionByteLength(transaction);
return (await estimateTransaction(transaction.payload, estimatedLen, network))[1].fee;
} catch (error) {
if (error instanceof NoEstimateAvailableError) {
return await estimateTransferUnsafe(transaction, network);
}
throw error;
}
}
49 changes: 24 additions & 25 deletions packages/transactions/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export class SerializationError extends Error {
class TransactionError extends Error {
constructor(message: string) {
super(message);
this.message = message;
Expand All @@ -9,45 +9,44 @@ export class SerializationError extends Error {
}
}

export class DeserializationError extends Error {
export class SerializationError extends TransactionError {
constructor(message: string) {
super(message);
this.message = message;
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}

export class NotImplementedError extends Error {
export class DeserializationError extends TransactionError {
constructor(message: string) {
super(message);
this.message = message;
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}

export class SigningError extends Error {
/**
* Thrown when `NoEstimateAvailable` is received as an error reason from a
* Stacks node. The Stacks node has not seen this kind of contract-call before,
* and it cannot provide an estimate yet.
* @see https://docs.hiro.so/api#tag/Fees/operation/post_fee_transaction
*/
export class NoEstimateAvailableError extends TransactionError {
constructor(message: string) {
super(message);
this.message = message;
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
export class VerificationError extends Error {

export class NotImplementedError extends TransactionError {
constructor(message: string) {
super(message);
}
}

export class SigningError extends TransactionError {
constructor(message: string) {
super(message);
}
}

export class VerificationError extends TransactionError {
constructor(message: string) {
super(message);
this.message = message;
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
64 changes: 63 additions & 1 deletion packages/transactions/tests/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
callReadOnlyFunction,
estimateTransaction,
estimateTransactionByteLength,
estimateTransactionFeeWithFallback,
getNonce,
makeContractCall,
makeContractDeploy,
Expand All @@ -43,7 +44,15 @@ import {
TxBroadcastResultRejected,
} from '../src/builders';
import { BytesReader } from '../src/bytesReader';
import { bufferCV, bufferCVFromString, serializeCV, standardPrincipalCV } from '../src/clarity';
import {
bufferCV,
bufferCVFromString,
noneCV,
serializeCV,
standardPrincipalCV,
uintCV,
} from '../src/clarity';
import { principalCV } from '../src/clarity/types/principalCV';
import { createMessageSignature } from '../src/common';
import {
AddressHashMode,
Expand Down Expand Up @@ -1134,6 +1143,59 @@ test('Estimate transaction transfer fee', async () => {
expect(resultEstimateFee2.map(f => f.fee)).toEqual([140, 17, 125]);
});

test('Estimate transaction fee fallback', async () => {
const privateKey = 'cb3df38053d132895220b9ce471f6b676db5b9bf0b4adefb55f2118ece2478df01';
const poolAddress = 'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y';
const network = new StacksTestnet({ url: 'http://localhost:3999' });

// http://localhost:3999/v2/fees/transaction
fetchMock.once(
`{"error":"Estimation could not be performed","reason":"NoEstimateAvailable","reason_data":{"message":"No estimate available for the provided payload."}}`,
{ status: 400 }
);

// http://localhost:3999/v2/fees/transfer
fetchMock.once('1');

const tx = await makeContractCall({
senderKey: privateKey,
contractAddress: 'ST000000000000000000002AMW42H',
contractName: 'pox-2',
functionName: 'delegate-stx',
functionArgs: [uintCV(100_000), principalCV(poolAddress), noneCV(), noneCV()],
anchorMode: AnchorMode.OnChainOnly,
nonce: 1,
network,
});

// http://localhost:3999/v2/fees/transaction
fetchMock.once(
`{"error":"Estimation could not be performed","reason":"NoEstimateAvailable","reason_data":{"message":"No estimate available for the provided payload."}}`,
{ status: 400 }
);

// http://localhost:3999/v2/fees/transfer
fetchMock.once('1');

const testnet = new StacksTestnet();
const resultEstimateFee = await estimateTransactionFeeWithFallback(tx, testnet);
expect(resultEstimateFee).toBe(201n);

// http://localhost:3999/v2/fees/transaction
fetchMock.once(
`{"error":"Estimation could not be performed","reason":"NoEstimateAvailable","reason_data":{"message":"No estimate available for the provided payload."}}`,
{ status: 400 }
);

// http://localhost:3999/v2/fees/transfer
fetchMock.once('2'); // double

const doubleRate = await estimateTransactionFeeWithFallback(tx, testnet);
expect(doubleRate).toBe(402n);

expect(fetchMock.mock.calls.length).toEqual(6);
});

test('Single-sig transaction byte length must include signature', async () => {
/*
* *** Context ***
Expand Down

1 comment on commit 782a3c3

@vercel
Copy link

@vercel vercel bot commented on 782a3c3 Jan 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.