Skip to content

Commit

Permalink
add caching to gas estimation
Browse files Browse the repository at this point in the history
  • Loading branch information
jonator committed May 31, 2024
1 parent eb1330b commit ea8df8d
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 32 deletions.
1 change: 0 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"@osmosis-labs/utils": "^1.0.0",
"@sentry/core": "^7.109.0",
"@vercel/kv": "^0.2.3",
"axios": "^0.27.2",
"cachified": "^3.5.4",
"dataloader": "^2.2.2",
"dayjs": "^1.10.7",
Expand Down
2 changes: 2 additions & 0 deletions packages/tx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"@osmosis-labs/types": "^1.0.0",
"@osmosis-labs/utils": "^1.0.0",
"buffer": "^6.0.3",
"cachified": "^3.5.4",
"cosmjs-types": "^0.5.2",
"lru-cache": "^10.0.1",
"utility-types": "^3.10.0"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/tx/src/__tests__/gas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1156,7 +1156,7 @@ describe("getGasPriceByFeeDenom", () => {
feeDenom,
gasMultiplier,
})
).rejects.toThrow("Invalid base fee: NaN");
).rejects.toThrow("Invalid base fee: invalid");
});

expect(queryFeesBaseGasPrice).not.toHaveBeenCalled();
Expand Down Expand Up @@ -1265,7 +1265,7 @@ describe("getDefaultGasPrice", () => {
chainId,
chainList,
})
).rejects.toThrow("Invalid base fee: NaN");
).rejects.toThrow("Invalid base fee: invalid");

expect(queryFeesBaseDenom).toHaveBeenCalledWith({
chainId,
Expand Down
128 changes: 99 additions & 29 deletions packages/tx/src/gas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Dec, Int } from "@keplr-wallet/unit";
import {
DEFAULT_LRU_OPTIONS,
queryBalances,
queryBaseAccount,
queryFeesBaseDenom,
Expand All @@ -11,6 +12,7 @@ import {
import type { Chain } from "@osmosis-labs/types";
import { ApiClientError } from "@osmosis-labs/utils";
import { Buffer } from "buffer/";
import cachified, { CacheEntry } from "cachified";
import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing";
import {
AuthInfo,
Expand All @@ -19,6 +21,7 @@ import {
TxBody,
TxRaw,
} from "cosmjs-types/cosmos/tx/v1beta1/tx";
import { LRUCache } from "lru-cache";

import { getSumTotalSpenderCoinsSpent } from "./events";

Expand Down Expand Up @@ -433,20 +436,18 @@ export async function getGasPriceByFeeDenom({
if (chainHasFeeMarketModule) {
// convert to alternative denom by querying spot price
// throws if given token does not have a spot price
const spotPrice = await queryFeeTokenSpotPrice({
const spotPrice = await getFeeTokenSpotPrice({
chainId,
chainList,
denom: feeDenom,
});

const spotPriceDec = new Dec(spotPrice.spot_price);

if (spotPriceDec.isZero() || spotPriceDec.isNegative()) {
if (spotPrice.isZero() || spotPrice.isNegative()) {
throw new Error(`Failed to fetch spot price for fee token ${feeDenom}.`);
}

return {
gasPrice: defaultFee.gasPrice.quo(spotPriceDec).mul(new Dec(1.01)),
gasPrice: defaultFee.gasPrice.quo(spotPrice).mul(new Dec(1.01)),
};
}

Expand Down Expand Up @@ -485,37 +486,28 @@ export async function getDefaultGasPrice({
);

let feeDenom: string;
let gasPrice: number;
let gasPrice: Dec;

if (chainHasFeeMarketModule) {
// fee market

const [baseDenom, baseFeePrice] = await Promise.all([
queryFeesBaseDenom({
chainId,
chainList,
}),
queryFeesBaseGasPrice({
chainId,
chainList,
}),
getFeesBaseDenom({ chainId, chainList }),
getBaseFeeSpotPrice({ chainId, chainList }),
]);

feeDenom = baseDenom.base_denom;

const baseFee = Number(baseFeePrice.base_fee);
if (isNaN(baseFee)) throw new Error("Invalid base fee: " + baseFee);

feeDenom = baseDenom;
// Add slippage multiplier to account for shifting gas prices in gas market
gasPrice = baseFee * gasMultiplier;
gasPrice = baseFeePrice.mul(new Dec(gasMultiplier));
} else {
// registry

feeDenom = chain.fees.fee_tokens[0].denom;
gasPrice = chain.fees.fee_tokens[0].average_gas_price || defaultGasPrice;
gasPrice = new Dec(
chain.fees.fee_tokens[0].average_gas_price || defaultGasPrice
);
}

return { gasPrice: new Dec(gasPrice), feeDenom };
return { gasPrice, feeDenom };
}

/**
Expand All @@ -538,15 +530,93 @@ export async function getChainSupportedFeeDenoms({
);

if (chainHasFeeMarketModule) {
const [{ base_denom }, alternativeFeeDenoms] = await Promise.all([
queryFeesBaseDenom({ chainId, chainList }),
queryFeeTokens({ chainId, chainList }).then(({ fee_tokens }) =>
fee_tokens.map((ft) => ft.denom)
),
const [baseDenom, alternativeFeeDenoms] = await Promise.all([
getFeesBaseDenom({ chainId, chainList }),
getFeeTokenDenoms({ chainId, chainList }),
]);

return [base_denom, ...alternativeFeeDenoms];
return [baseDenom, ...alternativeFeeDenoms];
}

return chain.fees.fee_tokens.map(({ denom }) => denom);
}

// cached query functions

const queryCache = new LRUCache<string, CacheEntry>(DEFAULT_LRU_OPTIONS);

export function getFeesBaseDenom({
chainId,
chainList,
}: {
chainId: string;
chainList: Chain[];
}) {
return cachified({
cache: queryCache,
key: "fees-base-denom-" + chainId,
ttl: process.env.NODE_ENV === "test" ? -1 : 1000 * 60 * 10, // 10 minutes since denoms don't change often
getFreshValue: () =>
queryFeesBaseDenom({ chainId, chainList }).then(
({ base_denom }) => base_denom
),
});
}

export function getFeeTokenDenoms({
chainId,
chainList,
}: {
chainId: string;
chainList: Chain[];
}) {
return cachified({
cache: queryCache,
key: "fee-token-denoms-" + chainId,
ttl: process.env.NODE_ENV === "test" ? -1 : 1000 * 60 * 10, // 10 minutes since denoms don't change often
getFreshValue: () =>
queryFeeTokens({ chainId, chainList }).then(({ fee_tokens }) =>
fee_tokens.map((ft) => ft.denom)
),
});
}

export function getFeeTokenSpotPrice({
chainId,
chainList,
denom,
}: {
chainId: string;
chainList: Chain[];
denom: string;
}) {
return cachified({
cache: queryCache,
key: `spot-price-${chainId}-${denom}`,
ttl: process.env.NODE_ENV === "test" ? -1 : 1000 * 5, // 5 seconds, shorter in case of swift price changes
getFreshValue: () =>
queryFeeTokenSpotPrice({ chainId, chainList, denom }).then(
({ spot_price }) => new Dec(spot_price)
),
});
}

export function getBaseFeeSpotPrice({
chainId,
chainList,
}: {
chainId: string;
chainList: Chain[];
}) {
return cachified({
cache: queryCache,
key: "base-fee-spot-price-" + chainId,
ttl: process.env.NODE_ENV === "test" ? -1 : 1000 * 5, // 5 seconds, shorter in case of swift price changes
getFreshValue: () =>
queryFeesBaseGasPrice({ chainId, chainList }).then(({ base_fee }) => {
if (isNaN(Number(base_fee)))
throw new Error("Invalid base fee: " + base_fee);
return new Dec(base_fee);
}),
});
}
2 changes: 2 additions & 0 deletions packages/web/pages/api/estimate-gas-fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,5 @@ export default async function handler(
}

// NOTE: `estimateGasFee` use of cosmjs-types makes it incompatible in edge runtime
// extend max duration to allow for more cache hits behind estimateGasFee
export const maxDuration = 300; // This function can run for a maximum of 300 seconds

0 comments on commit ea8df8d

Please sign in to comment.