Skip to content

Commit e2b88d2

Browse files
committed
chore: added gas estimation api to planner
1 parent 61d9708 commit e2b88d2

File tree

6 files changed

+244
-8
lines changed

6 files changed

+244
-8
lines changed

packages/portfolio-contract/tools/portfolio-actors.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ export const planTransfer = (
348348
dest: PoolKey,
349349
amount: NatAmount,
350350
feeBrand: Brand<'nat'>,
351+
gmpFees: { acct: bigint; wallet: bigint; return: bigint },
351352
): MovementDesc[] => {
352353
const { protocol: p, chainName: evm } = PoolPlaces[dest];
353354
const steps: MovementDesc[] = [];
@@ -366,16 +367,14 @@ export const planTransfer = (
366367
src: '@noble',
367368
dest: `@${evm}`,
368369
amount,
369-
// TODO: Rather than hard-code, derive from Axelar `estimateGasFee`.
370-
// https://docs.axelar.dev/dev/axelarjs-sdk/axelar-query-api#estimategasfee
371-
fee: make(feeBrand, 15_000_000n),
370+
fee: make(feeBrand, gmpFees.acct),
371+
detail: { evmGas: gmpFees.return },
372372
});
373-
console.warn('TODO: stop hard-coding fees!');
374373
steps.push({
375374
src: `@${evm}`,
376375
dest: `${p}_${evm}`,
377376
amount,
378-
fee: make(feeBrand, 15_000_000n), // KLUDGE.
377+
fee: make(feeBrand, gmpFees.wallet), // KLUDGE.
379378
});
380379
break;
381380
default:

services/ymax-planner/src/engine.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,12 @@ export const startEngine = async (
646646
unprefixedPortfolioPath as any,
647647
amount,
648648
feeAsset.brand as Brand<'nat'>,
649-
{ readPublished: query.readPublished, spectrum, cosmosRest },
649+
evmCtx,
650+
{
651+
readPublished: query.readPublished,
652+
spectrum,
653+
cosmosRest,
654+
},
650655
);
651656

652657
// TODO: consolidate with portfolioIdOfPath
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import {
2+
axelarConfig,
3+
axelarConfigTestnet,
4+
} from '@aglocal/portfolio-deploy/src/axelar-configs.js';
5+
import { buildGMPPayload } from '@agoric/orchestration/src/utils/gmp.js';
6+
import type { AxelarChain } from '@agoric/portfolio-api/src/constants';
7+
import { ethers } from 'ethers';
8+
import { getEvmRpcMap } from './support.ts';
9+
10+
// arbitrary value. it just mimicks a valid command id for axelar
11+
const COMMAND_ID =
12+
'0xddea323337dfb73c82df7393d76b2f38835d5c2bda0123c47d1a2fc06527d19f';
13+
const AGORIC_CHAIN = 'agoric';
14+
const BLD_TOKEN = 'ubld';
15+
const GAS_ESTIMATOR_CONTRACT_ABI = [
16+
'function executeFactory(bytes32 commandId, string sourceChain, string sourceAddress, bytes payload)',
17+
'function executeWallet(bytes32 commandId, string sourceChain, string sourceAddress, bytes payload)',
18+
];
19+
const GAS_ESTIMATOR_OWNER = 'agoric1owner';
20+
21+
const bytesToHex = (bytes: number[]): string =>
22+
'0x' + bytes.map(n => n.toString(16).padStart(2, '0')).join('');
23+
24+
const gasEstimatorContracts = {
25+
mainnet: {},
26+
testnet: {
27+
Avalanche: '0x0010F196F5CD0314f68FF665b8c8eD93531112Fe',
28+
},
29+
};
30+
31+
export const makeGasEstimatorKit = ({
32+
alchemyApiKey,
33+
clusterName,
34+
chainName,
35+
}: {
36+
alchemyApiKey: string;
37+
clusterName: 'mainnet' | 'testnet';
38+
chainName: AxelarChain;
39+
}) => {
40+
const axelarConf =
41+
clusterName === 'mainnet' ? axelarConfig : axelarConfigTestnet;
42+
const chainConfig = axelarConf[chainName];
43+
44+
const evmContracts = chainConfig.contracts;
45+
const gasEstimatorContract = gasEstimatorContracts[clusterName][chainName];
46+
47+
const axelarApiAddress =
48+
clusterName === 'mainnet'
49+
? 'https://api.axelarscan.io/gmp/estimateGasFee'
50+
: 'https://testnet.api.axelarscan.io/gmp/estimateGasFee';
51+
52+
const evmRpcMap = getEvmRpcMap(clusterName, alchemyApiKey);
53+
const evmRpcAddress =
54+
evmRpcMap[
55+
`${chainConfig.chainInfo.namespace}:${chainConfig.chainInfo.reference}`
56+
];
57+
58+
const generateAaveSupplyPayload = (
59+
chainName: AxelarChain,
60+
contractAddress: string,
61+
payment: string,
62+
) => {
63+
const contractCalls = [
64+
{
65+
target: evmContracts.usdc,
66+
functionSignature: 'approve(address,uint256)',
67+
args: [evmContracts.aavePool, payment],
68+
},
69+
{
70+
target: evmContracts.aavePool,
71+
functionSignature: 'supply(address,uint256,address,uint16)',
72+
args: [evmContracts.usdc, payment, contractAddress, '0'],
73+
},
74+
];
75+
return bytesToHex(buildGMPPayload(contractCalls));
76+
};
77+
78+
const queryAxelarGasAPI = async (
79+
sourceChain: AxelarChain | 'agoric',
80+
destinationChain: AxelarChain | 'agoric',
81+
gasLimit: bigint,
82+
gasToken?: string,
83+
) => {
84+
const response = await fetch(axelarApiAddress, {
85+
method: 'POST',
86+
headers: {
87+
'Content-Type': 'application/json',
88+
},
89+
body: JSON.stringify({
90+
sourceChain,
91+
destinationChain,
92+
gasLimit: gasLimit.toString(),
93+
sourceTokenSymbol: gasToken,
94+
gasMultiplier: '1',
95+
}),
96+
});
97+
98+
if (!response.ok) {
99+
throw new Error(`HTTP error! status: ${response.status}`);
100+
}
101+
102+
const data = await response.json();
103+
return data;
104+
};
105+
106+
const queryEstimateGas = async (
107+
provider: ethers.Provider,
108+
contractAddress: string,
109+
abi: ethers.InterfaceAbi,
110+
functionName: string,
111+
params: any[] = [],
112+
fromAddress?: string,
113+
): Promise<bigint> => {
114+
const contract = new ethers.Contract(contractAddress, abi, provider);
115+
116+
const gasEstimate = await contract[functionName].estimateGas(
117+
...params,
118+
fromAddress ? { from: fromAddress } : {},
119+
);
120+
121+
return gasEstimate;
122+
};
123+
124+
const createGasEstimator = (rpcUrl: string, contractAddress: string) => {
125+
return async (
126+
functionName: string,
127+
params: any[] = [],
128+
fromAddress?: string,
129+
): Promise<bigint> => {
130+
const provider = new ethers.JsonRpcProvider(rpcUrl);
131+
132+
return queryEstimateGas(
133+
provider,
134+
contractAddress,
135+
GAS_ESTIMATOR_CONTRACT_ABI,
136+
functionName,
137+
params,
138+
fromAddress,
139+
);
140+
};
141+
};
142+
143+
const getWalletEstimate = async () => {
144+
const ethGasEstimator = createGasEstimator(
145+
evmRpcAddress,
146+
gasEstimatorContract,
147+
);
148+
149+
const payload = generateAaveSupplyPayload(
150+
chainName,
151+
gasEstimatorContract,
152+
'1', // Arbitrary minimum value needed to execute successfully
153+
);
154+
155+
const gasLimit = await ethGasEstimator('executeWallet', [
156+
COMMAND_ID,
157+
AGORIC_CHAIN,
158+
GAS_ESTIMATOR_OWNER,
159+
payload,
160+
]);
161+
162+
const estimate = (await queryAxelarGasAPI(
163+
AGORIC_CHAIN,
164+
chainName,
165+
gasLimit,
166+
BLD_TOKEN,
167+
)) as string;
168+
169+
return BigInt(estimate);
170+
};
171+
172+
const getFactoryContractEstimate = async () => {
173+
const ethGasEstimator = createGasEstimator(
174+
evmRpcAddress,
175+
gasEstimatorContract,
176+
);
177+
178+
// Payload should be 0 since no eth will be present on the gas estimator contract
179+
const payload = ethers.AbiCoder.defaultAbiCoder().encode(['uint256'], [0]);
180+
181+
const gasLimit = await ethGasEstimator('executeFactory', [
182+
COMMAND_ID,
183+
AGORIC_CHAIN,
184+
GAS_ESTIMATOR_OWNER,
185+
payload,
186+
]);
187+
188+
const estimate = (await queryAxelarGasAPI(
189+
AGORIC_CHAIN,
190+
chainName,
191+
gasLimit,
192+
BLD_TOKEN,
193+
)) as string;
194+
195+
return BigInt(estimate);
196+
};
197+
198+
const getReturnFeeEstimate = async () => {
199+
const fee = (await queryAxelarGasAPI(chainName, 'agoric', 1n)) as string;
200+
201+
return BigInt(fee);
202+
};
203+
204+
return {
205+
getWalletEstimate,
206+
getFactoryContractEstimate,
207+
getReturnFeeEstimate,
208+
};
209+
};

services/ymax-planner/src/pending-tx-manager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export type EvmContext = {
3333
evmProviders: EvmProviders;
3434
signingSmartWalletKit: SigningSmartWalletKit;
3535
fetch: typeof fetch;
36+
alchemyApiKey: string;
37+
clusterName: 'mainnet' | 'testnet';
3638
};
3739

3840
export type GmpTransfer = {

services/ymax-planner/src/plan-deposit.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
} from '@aglocal/portfolio-contract/tools/portfolio-actors.js';
1616
import type { CosmosRestClient } from './cosmos-rest-client.js';
1717
import type { Chain, Pool, SpectrumClient } from './spectrum-client.js';
18+
import {
19+
makeGasEstimatorKit
20+
} from './gas-estimation.js';
21+
import type { EvmContext } from './pending-tx-manager.js';
1822

1923
const getOwn = <O, K extends PropertyKey>(
2024
obj: O,
@@ -63,6 +67,7 @@ export const handleDeposit = async (
6367
portfolioKey: `${string}.portfolios.portfolio${number}`,
6468
amount: NatAmount,
6569
feeBrand: Brand<'nat'>,
70+
evmCtx: Omit<EvmContext, 'cosmosRest' | 'signingSmartWalletKit' | 'fetch'>,
6671
powers: {
6772
readPublished: VstorageKit['readPublished'];
6873
spectrum: SpectrumClient;
@@ -96,13 +101,27 @@ export const handleDeposit = async (
96101
if (errors.length) {
97102
throw AggregateError(errors, 'Could not get balances');
98103
}
104+
const gasEstimator = makeGasEstimatorKit({
105+
alchemyApiKey: evmCtx.alchemyApiKey,
106+
clusterName: evmCtx.clusterName,
107+
chainName: 'Avalanche',
108+
});
109+
const [gmpAccountFee, gmpWalletFee, gmpReturnFee] = await Promise.all([
110+
gasEstimator.getFactoryContractEstimate(),
111+
gasEstimator.getWalletEstimate(),
112+
gasEstimator.getReturnFeeEstimate(),
113+
]);
99114
const balances = Object.fromEntries(balanceEntries);
100115
const transfers = planDepositTransfers(amount, balances, targetAllocation);
101116
const steps = [
102117
{ src: '+agoric', dest: '@agoric', amount },
103118
{ src: '@agoric', dest: '@noble', amount },
104119
...Object.entries(transfers).flatMap(([dest, amt]) =>
105-
planTransfer(dest as PoolKey, amt, feeBrand),
120+
planTransfer(dest as PoolKey, amt, feeBrand, {
121+
acct: gmpAccountFee,
122+
wallet: gmpWalletFee,
123+
return: gmpReturnFee,
124+
}),
106125
),
107126
];
108127
return steps;

services/ymax-planner/src/support.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export const createEVMContext = async ({
8181
clusterName,
8282
alchemyApiKey,
8383
}: CreateContextParams): Promise<
84-
Pick<EvmContext, 'evmProviders' | 'usdcAddresses'>
84+
Omit<EvmContext, 'cosmosRest' | 'signingSmartWalletKit' | 'fetch'>
8585
> => {
8686
if (clusterName === 'local') clusterName = 'testnet';
8787
if (!alchemyApiKey) throw Error('missing alchemyApiKey');
@@ -97,6 +97,8 @@ export const createEVMContext = async ({
9797
return {
9898
evmProviders,
9999
usdcAddresses: usdcAddresses[clusterName],
100+
alchemyApiKey,
101+
clusterName,
100102
};
101103
};
102104

0 commit comments

Comments
 (0)