Skip to content

Commit

Permalink
feat(hyperliquid): estimate fees properly
Browse files Browse the repository at this point in the history
Signed-off-by: james-a-morris <[email protected]>
  • Loading branch information
james-a-morris committed Jan 22, 2025
1 parent 9c1f3b5 commit 5f819fc
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 96 deletions.
26 changes: 21 additions & 5 deletions src/hooks/useBridgeFees.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { BigNumber, ethers } from "ethers";
import { bridgeFeesQueryKey, getBridgeFees, ChainId } from "utils";
import {
bridgeFeesQueryKey,
getBridgeFees,
ChainId,
getBridgeFeesWithExternalProjectId,
} from "utils";
import { AxiosError } from "axios";

/**
Expand All @@ -10,6 +15,8 @@ import { AxiosError } from "axios";
* @param toChainId The chain Id of the receiving chain, its timestamp will be used to calculate the fees.
* @param inputTokenSymbol - The input token symbol to check bridge fees for.
* @param outputTokenSymbol - The output token symbol to check bridge fees for.
* @param externalProjectId - The external project id to check bridge fees for.
* @param recipientAddress - The recipient address to check bridge fees for.
* @returns The bridge fees for the given amount and token symbol and the UseQueryResult object.
*/
export function useBridgeFees(
Expand All @@ -18,14 +25,17 @@ export function useBridgeFees(
toChainId: ChainId,
inputTokenSymbol: string,
outputTokenSymbol: string,
externalProjectId?: string,
recipientAddress?: string
) {
const queryKey = bridgeFeesQueryKey(
amount,
inputTokenSymbol,
outputTokenSymbol,
fromChainId,
toChainId
toChainId,
externalProjectId,
recipientAddress
);
const { data: fees, ...delegated } = useQuery({
queryKey,
Expand All @@ -37,16 +47,22 @@ export function useBridgeFees(
amountToQuery,
fromChainIdToQuery,
toChainIdToQuery,
externalProjectIdToQuery,
recipientAddressToQuery,
] = queryKey;

return getBridgeFees({
const feeArgs = {
amount: BigNumber.from(amountToQuery),
inputTokenSymbol: inputTokenSymbolToQuery,
outputTokenSymbol: outputTokenSymbolToQuery,
toChainId: toChainIdToQuery,
fromChainId: fromChainIdToQuery,
recipientAddress,
});
recipientAddress: recipientAddressToQuery,
};

return externalProjectIdToQuery
? getBridgeFeesWithExternalProjectId(externalProjectIdToQuery, feeArgs)
: getBridgeFees(feeArgs);
},
enabled: Boolean(amount.gt(0)),
refetchInterval: 5000,
Expand Down
85 changes: 82 additions & 3 deletions src/utils/bridge.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { ethers, BigNumber } from "ethers";
import { ethers, BigNumber, utils } from "ethers";
import {
acrossPlusMulticallHandler,
ChainId,
fixedPointAdjustment,
getToken,
hyperLiquidBridge2Address,
referrerDelimiterHex,
} from "./constants";
import { DOMAIN_CALLDATA_DELIMITER, tagAddress, tagHex } from "./format";
import { getProvider } from "./providers";
import { getConfig, isContractDeployedToAddress } from "utils";
import {
generateHyperLiquidPayload,
getConfig,
isContractDeployedToAddress,
} from "utils";
import getApiEndpoint from "./serverless-api";
import { BridgeLimitInterface } from "./serverless-api/types";
import { DepositNetworkMismatchProperties } from "ampli";
import { SwapQuoteApiResponse } from "./serverless-api/prod/swap-quote";
import { SpokePool, SpokePoolVerifier } from "./typechain";
import { CHAIN_IDs } from "@across-protocol/constants";

export type Fee = {
total: ethers.BigNumber;
Expand Down Expand Up @@ -41,12 +49,81 @@ type GetBridgeFeesArgs = {
fromChainId: ChainId;
toChainId: ChainId;
recipientAddress?: string;
message?: string;
};

export type GetBridgeFeesResult = BridgeFees & {
isAmountTooLow: boolean;
};

/**
*
*/
export async function getBridgeFeesWithExternalProjectId(
externalProjectId: string,
args: GetBridgeFeesArgs
) {
let message = undefined;
let recipientAddress = args.recipientAddress;

if (externalProjectId === "hyper-liquid") {
const arbitrumProvider = getProvider(CHAIN_IDs.ARBITRUM);

const wallet = ethers.Wallet.createRandom();

const signer = new ethers.Wallet(wallet.privateKey, arbitrumProvider);

const recipient = await signer.getAddress();

// Build the payload
const hyperLiquidPayload = await generateHyperLiquidPayload(
signer,
recipient,
args.amount
);
// Create a txn calldata for transfering amount to recipient
const erc20Interface = new utils.Interface([
"function transfer(address to, uint256 amount) returns (bool)",
]);

const transferCalldata = erc20Interface.encodeFunctionData("transfer", [
recipient,
args.amount,
]);

// Encode Instructions struct directly
message = utils.defaultAbiCoder.encode(
[
"tuple(tuple(address target, bytes callData, uint256 value)[] calls, address fallbackRecipient)",
],
[
{
calls: [
{
target: getToken("USDC").addresses![CHAIN_IDs.ARBITRUM],
callData: transferCalldata,
value: 0,
},
{
target: hyperLiquidBridge2Address,
callData: hyperLiquidPayload,
value: 0,
},
],
fallbackRecipient: args.recipientAddress!,
},
]
);
recipientAddress = acrossPlusMulticallHandler[args.toChainId];
}

return getBridgeFees({
...args,
recipientAddress,
message,
});
}

/**
*
* @param amount - amount to bridge
Expand All @@ -63,6 +140,7 @@ export async function getBridgeFees({
fromChainId,
toChainId,
recipientAddress,
message,
}: GetBridgeFeesArgs): Promise<GetBridgeFeesResult> {
const timeBeforeRequests = Date.now();
const {
Expand All @@ -84,7 +162,8 @@ export async function getBridgeFees({
getConfig().getTokenInfoBySymbol(toChainId, outputTokenSymbol).address,
toChainId,
fromChainId,
recipientAddress
recipientAddress,
message
);
const timeAfterRequests = Date.now();

Expand Down
78 changes: 76 additions & 2 deletions src/utils/hyperliquid.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Deposit } from "hooks/useDeposits";
import { CHAIN_IDs } from "@across-protocol/constants";
import { utils } from "ethers";
import { BigNumber, Contract, providers, Signer, utils } from "ethers";
import { compareAddressesSimple } from "./sdk";
import { hyperLiquidBridge2Address } from "./constants";
import { getToken, hyperLiquidBridge2Address } from "./constants";

export function isHyperLiquidBoundDeposit(deposit: Deposit) {
if (deposit.destinationChainId !== CHAIN_IDs.ARBITRUM || !deposit.message) {
Expand Down Expand Up @@ -32,3 +32,77 @@ export function isHyperLiquidBoundDeposit(deposit: Deposit) {
return false;
}
}

/**
* Creates a payload that will be ingested by Bridge2/batchedDepositWithPermit of a single deposit
*/
export async function generateHyperLiquidPayload(
signer: Signer,
recipient: string,
amount: BigNumber
) {
const source = await signer.getAddress();

if (!compareAddressesSimple(source, recipient)) {
throw new Error("Source and recipient must be the same");
}

const timestamp = Date.now();
const deadline = Math.floor(timestamp / 1000) + 3600;

// Create USDC contract interface
const usdcInterface = new utils.Interface([
"function nonces(address owner) view returns (uint256)",
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
]);

const usdcContract = new Contract(
getToken("USDC").addresses![CHAIN_IDs.ARBITRUM],
usdcInterface,
signer
);

// USDC permit signature with verified domain parameters
const usdcDomain = {
name: "USD Coin",
version: "2",
chainId: CHAIN_IDs.ARBITRUM,
verifyingContract: getToken("USDC").addresses![CHAIN_IDs.ARBITRUM]!,
};

const permitTypes = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};

const permitValue = {
owner: source,
spender: hyperLiquidBridge2Address,
value: amount,
nonce: await usdcContract.nonces(source),
deadline,
};

const permitSignature = await (
signer as providers.JsonRpcSigner
)._signTypedData(usdcDomain, permitTypes, permitValue);
const { r, s, v } = utils.splitSignature(permitSignature);

const deposit = {
user: source,
usd: amount,
deadline,
signature: { r: BigNumber.from(r), s: BigNumber.from(s), v },
};

const iface = new utils.Interface([
"function batchedDepositWithPermit(tuple(address user, uint64 usd, uint64 deadline, tuple(uint256 r, uint256 s, uint8 v) signature)[] deposits)",
]);

return iface.encodeFunctionData("batchedDepositWithPermit", [[deposit]]);
}
8 changes: 7 additions & 1 deletion src/utils/query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,18 @@ export function balanceQueryKey(
* @param amount The amount to check bridge fees for.
* @param fromChainId The origin chain of this bridge action
* @param toChainId The destination chain of this bridge action
* @param externalProjectId The external project id to check bridge fees for.
* @param recipientAddress The recipient address to check bridge fees for.
* @returns An array of query keys for @tanstack/react-query `useQuery` hook.
*/
export function bridgeFeesQueryKey(
amount: ethers.BigNumber,
inputToken: string,
outputToken: string,
fromChainId: ChainId,
toChainId: ChainId
toChainId: ChainId,
externalProjectId?: string,
recipientAddress?: string
) {
return [
"bridgeFees",
Expand All @@ -47,6 +51,8 @@ export function bridgeFeesQueryKey(
amount.toString(),
fromChainId,
toChainId,
externalProjectId,
recipientAddress,
] as const;
}

Expand Down
3 changes: 2 additions & 1 deletion src/utils/serverless-api/mocked/suggested-fees.mocked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export async function suggestedFeesMockedApiCall(
_outputToken: string,
_toChainid: ChainId,
_fromChainid: ChainId,
_recipientAddress?: string
_recipientAddress?: string,
_message?: string
): Promise<SuggestedApiFeeReturnType> {
const token = getTokenByAddress(_inputToken);
const decimals = token?.decimals ?? 18;
Expand Down
4 changes: 3 additions & 1 deletion src/utils/serverless-api/prod/suggested-fees.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export async function suggestedFeesApiCall(
outputToken: string,
toChainid: ChainId,
fromChainid: ChainId,
recipientAddress?: string
recipientAddress?: string,
message?: string
): Promise<SuggestedApiFeeReturnType> {
const response = await axios.get(`${vercelApiBaseUrl}/api/suggested-fees`, {
params: {
Expand All @@ -29,6 +30,7 @@ export async function suggestedFeesApiCall(
amount: amount.toString(),
skipAmountLimit: true,
depositMethod: "depositExclusive",
message,
},
});
const result = response.data;
Expand Down
3 changes: 2 additions & 1 deletion src/utils/serverless-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export type SuggestedApiFeeType = (
outputToken: string,
toChainid: ChainId,
fromChainid: ChainId,
recipientAddress?: string
recipientAddress?: string,
message?: string
) => Promise<SuggestedApiFeeReturnType>;

export type RetrieveLinkedWalletType = (
Expand Down
Loading

0 comments on commit 5f819fc

Please sign in to comment.