Skip to content

Commit

Permalink
feat: bridge integration
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 9, 2025
1 parent bff8810 commit c11100b
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 7 deletions.
1 change: 1 addition & 0 deletions src/utils/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { getCurrentTime } from "@across-protocol/sdk/dist/esm/utils/TimeUtils";
export { isBridgedUsdc } from "@across-protocol/sdk/dist/esm/utils/TokenUtils";
export { BRIDGED_USDC_SYMBOLS } from "@across-protocol/sdk/dist/esm/constants";
export { getNativeTokenSymbol } from "@across-protocol/sdk/dist/esm/utils/NetworkUtils";
export { compareAddressesSimple } from "@across-protocol/sdk/dist/esm/utils/AddressUtils";

export function getUpdateV3DepositTypedData(
depositId: number,
Expand Down
3 changes: 3 additions & 0 deletions src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ export function getBridgeUrlWithQueryParams({
toChainId,
inputTokenSymbol,
outputTokenSymbol,
externalProjectId,
}: {
fromChainId: number;
toChainId: number;
inputTokenSymbol: string;
outputTokenSymbol?: string;
externalProjectId?: string;
}) {
const cleanParams = Object.entries({
from: fromChainId.toString(),
to: toChainId.toString(),
inputToken: inputTokenSymbol,
outputToken: outputTokenSymbol,
externalProjectId,
}).reduce((acc, [key, value]) => {
if (value) {
return { ...acc, [key]: value };
Expand Down
1 change: 0 additions & 1 deletion src/views/Bridge/Bridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ const Bridge = () => {
handleSetNewSlippage,
isQuoteLoading,
} = useBridge();

return (
<>
{toAccount && (
Expand Down
2 changes: 1 addition & 1 deletion src/views/Bridge/components/BridgeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ const BridgeForm = ({
/>
</TokenSelectorWrapper>
</RowWrapper>
{toAccount && (
{toAccount && selectedRoute.externalProjectId !== "hyper-liquid" && (
<RowWrapper>
<RecipientRow
onClickChangeToAddress={onClickChangeToAddress}
Expand Down
1 change: 1 addition & 0 deletions src/views/Bridge/hooks/useBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function useBridge() {

const { error: amountValidationError, warn: amountValidationWarning } =
validateBridgeAmount(
selectedRoute,
parsedAmount,
quotedFees,
maxBalance,
Expand Down
156 changes: 155 additions & 1 deletion src/views/Bridge/hooks/useBridgeAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
TransferQuoteReceivedProperties,
ampli,
} from "ampli";
import { BigNumber, constants, providers } from "ethers";
import { BigNumber, constants, providers, Signer, utils } from "ethers";
import {
useConnection,
useApprove,
Expand All @@ -23,12 +23,16 @@ import {
sendSpokePoolVerifierDepositTx,
sendDepositV3Tx,
sendSwapAndBridgeTx,
compareAddressesSimple,
getToken,
} from "utils";
import { TransferQuote } from "./useTransferQuote";
import { SelectedRoute } from "../utils";
import useReferrer from "hooks/useReferrer";
import { SwapQuoteApiResponse } from "utils/serverless-api/prod/swap-quote";
import { BridgeLimitInterface } from "utils/serverless-api/types";
import { CHAIN_IDs } from "@across-protocol/constants";
import { Contract } from "ethers";

const config = getConfig();

Expand Down Expand Up @@ -61,6 +65,10 @@ export function useBridgeAction(
const { isWrongNetworkHandler, isWrongNetwork } = useIsWrongNetwork(
selectedRoute.fromChain
);

const { isWrongNetworkHandler: isWrongNetworkHandlerHyperLiquid } =
useIsWrongNetwork(CHAIN_IDs.ARBITRUM);

const approveHandler = useApprove(selectedRoute.fromChain);
const { addToAmpliQueue } = useAmplitude();

Expand Down Expand Up @@ -98,6 +106,69 @@ export function useBridgeAction(
throw new Error("Missing required data for bridge action");
}

const externalProjectIsHyperLiquid =
frozenRoute.externalProjectId === "hyper-liquid";

let externalPayload: string | undefined;

if (externalProjectIsHyperLiquid) {
await isWrongNetworkHandlerHyperLiquid();

// External Project Inclusion Considerations:
//
// HyperLiquid:
// We need to set up our crosschain message to the hyperliquid bridge with
// the following considerations:
// 1. Our recipient address is the default multicall handler
// 2. The recipient and the signer must be the same address
// 3. We will first transfer funds to the true recipient EoA
// 4. We must construct a payload to send to HL's Bridge2 contract
// 5. The user must sign this signature

// For now let's assume a 0.05% loss in the amount
const amount = frozenDepositArgs.amount.mul(9995).div(10000);

// Build the payload
const hyperLiquidPayload = await generateHyperLiquidPayload(
signer,
frozenDepositArgs.toAddress,
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", [
frozenDepositArgs.toAddress,
amount,
]);

// Encode Instructions struct directly
externalPayload = 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: "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7", // Bridge2 contract
callData: hyperLiquidPayload,
value: 0,
},
],
fallbackRecipient: frozenDepositArgs.toAddress,
},
]
);
}

await isWrongNetworkHandler();

// If swap route then we need to approve the swap token for the `SwapAndBridge`
Expand Down Expand Up @@ -193,6 +264,10 @@ export function useBridgeAction(
inputTokenAddress: frozenRoute.fromTokenAddress,
outputTokenAddress: frozenRoute.toTokenAddress,
fillDeadline: frozenFeeQuote.fillDeadline,
message: externalPayload,
toAddress: externalProjectIsHyperLiquid
? "0x924a9f036260DdD5808007E1AA95f08eD08aA569" // Default multicall handler
: frozenDepositArgs.toAddress,
},
spokePool,
networkMismatchHandler
Expand Down Expand Up @@ -237,6 +312,9 @@ export function useBridgeAction(
: frozenRoute.fromTokenSymbol,
outputTokenSymbol: frozenRoute.toTokenSymbol,
referrer,
...(externalProjectIsHyperLiquid
? { externalProjectId: frozenRoute.externalProjectId }
: {}),
});
if (existingIntegrator) {
statusPageSearchParams.set("integrator", existingIntegrator);
Expand Down Expand Up @@ -283,6 +361,7 @@ type DepositArgs = {
exclusiveRelayer: string;
exclusivityDeadline: number;
integratorId: string;
externalProjectId?: string;
};
function getDepositArgs(
selectedRoute: SelectedRoute,
Expand Down Expand Up @@ -317,6 +396,7 @@ function getDepositArgs(
exclusiveRelayer: quotedFees.exclusiveRelayer,
exclusivityDeadline: quotedFees.exclusivityDeadline,
integratorId,
externalProjectId: selectedRoute.externalProjectId,
};
}

Expand All @@ -337,3 +417,77 @@ function getButtonLabel(args: {
}
return "Confirm transaction";
}

/**
* 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: "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7", // Bridge2 contract address
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]]);
}
12 changes: 11 additions & 1 deletion src/views/Bridge/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
nonEthChains,
GetBridgeFeesResult,
chainEndpointToId,
parseUnits,
} from "utils";
import { SwapQuoteApiResponse } from "utils/serverless-api/prod/swap-quote";

Expand Down Expand Up @@ -94,6 +95,7 @@ export function getReceiveTokenSymbol(
}

export function validateBridgeAmount(
selectedRoute: SelectedRoute,
parsedAmountInput?: BigNumber,
quoteFees?: GetBridgeFeesResult,
currentBalance?: BigNumber,
Expand Down Expand Up @@ -124,7 +126,12 @@ export function validateBridgeAmount(
};
}

if (quoteFees?.isAmountTooLow) {
if (
quoteFees?.isAmountTooLow ||
// HyperLiquid has a minimum deposit amount of 5 USDC
(selectedRoute.externalProjectId === "hyper-liquid" &&
parsedAmountInput.lt(parseUnits("5", 6)))
) {
return {
error: AmountInputError.AMOUNT_TOO_LOW,
};
Expand Down Expand Up @@ -441,6 +448,8 @@ export function getRouteFromUrl(overrides?: RouteFilter) {
overrides?.toChain
) || undefined;

const externalProjectId = params.get("externalProjectId") || undefined;

const inputTokenSymbol =
params.get("inputTokenSymbol") ??
params.get("inputToken") ??
Expand All @@ -459,6 +468,7 @@ export function getRouteFromUrl(overrides?: RouteFilter) {
toChain,
inputTokenSymbol,
outputTokenSymbol: outputTokenSymbol?.toUpperCase(),
externalProjectId,
};

const route =
Expand Down
4 changes: 3 additions & 1 deletion src/views/DepositStatus/DepositStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function DepositStatus() {
const destinationChainId = queryParams.get("destinationChainId");
const inputTokenSymbol = queryParams.get("inputTokenSymbol");
const outputTokenSymbol = queryParams.get("outputTokenSymbol");

const externalProjectId = queryParams.get("externalProjectId") || undefined;
if (
!depositTxHash ||
!originChainId ||
Expand All @@ -54,10 +54,12 @@ export default function DepositStatus() {
inputTokenSymbol={inputTokenSymbol}
outputTokenSymbol={outputTokenSymbol || inputTokenSymbol}
fromBridgePagePayload={state.fromBridgePagePayload}
externalProjectId={externalProjectId}
/>
<DepositStatusLowerCard
fromChainId={Number(originChainId)}
toChainId={Number(destinationChainId)}
externalProjectId={externalProjectId}
inputTokenSymbol={inputTokenSymbol}
outputTokenSymbol={outputTokenSymbol || inputTokenSymbol}
fromBridgePagePayload={state.fromBridgePagePayload}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DepositStatus } from "../types";
import styled from "@emotion/styled";
import { ReactComponent as CheckStarPending } from "assets/icons/check-star-ring-opaque-pending.svg";
import { ReactComponent as CheckStarCompleted } from "assets/icons/check-star-ring-opaque-completed.svg";
import { externConfigs } from "constants/chains/configs";

const BlurLoadingAnimation = () => (
<AnimatedBlurLoader>
Expand All @@ -15,15 +16,19 @@ type DepositStatusAnimatedIconsParams = {
status: DepositStatus;
fromChainId: number;
toChainId: number;
externalProjectId?: string;
};

const DepositStatusAnimatedIcons = ({
status,
fromChainId,
toChainId,
externalProjectId,
}: DepositStatusAnimatedIconsParams) => {
const GrayscaleLogoFromChain = getChainInfo(fromChainId).grayscaleLogoSvg;
const GrayscaleLogoToChain = getChainInfo(toChainId).grayscaleLogoSvg;
const GrayscaleLogoToChain = externalProjectId
? externConfigs[externalProjectId].grayscaleLogoSvg
: getChainInfo(toChainId).grayscaleLogoSvg;

return (
<>
Expand Down
3 changes: 3 additions & 0 deletions src/views/DepositStatus/components/DepositStatusLowerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { EarnByLpAndStakingCard } from "./EarnByLpAndStakingCard";
type Props = {
fromChainId: number;
toChainId: number;
externalProjectId?: string;
inputTokenSymbol: string;
outputTokenSymbol: string;
fromBridgePagePayload?: FromBridgePagePayload;
Expand All @@ -28,6 +29,7 @@ type Props = {
export function DepositStatusLowerCard({
fromChainId,
toChainId,
externalProjectId,
inputTokenSymbol,
outputTokenSymbol,
fromBridgePagePayload,
Expand Down Expand Up @@ -103,6 +105,7 @@ export function DepositStatusLowerCard({
toChainId,
inputTokenSymbol: baseToken.symbol,
outputTokenSymbol,
externalProjectId,
})
)
}
Expand Down
Loading

0 comments on commit c11100b

Please sign in to comment.