diff --git a/apps/example/lib/stake.ts b/apps/example/lib/stake.ts index 0abe9c1..1e98412 100644 --- a/apps/example/lib/stake.ts +++ b/apps/example/lib/stake.ts @@ -132,22 +132,18 @@ export const StakerContractABI = [ export const STAKE_CONTRACT = { address: "0x733Debf51574c70CfCdb7918F032E16F686bd9f8", chain: optimism, - token: stakeToken, + token: { + address: "0x4200000000000000000000000000000000000006", // notice that ETH & WETH both have the same address + symbol: "ETH", + name: "Ether", + decimals: 18, + logoUrl: + "https://raw.githubusercontent.com/across-protocol/frontend/master/src/assets/token-logos/eth.svg", + }, multicallHandler: "0x924a9f036260DdD5808007E1AA95f08eD08aA569", abi: StakerContractABI, } as const; -export function generateStakeAction( - userAddress: Address, - value: bigint, -): CrossChainAction { - return { - target: STAKE_CONTRACT.address, - callData: generateStakeCallData(userAddress), - value, - }; -} - export function generateStakeCallData(userAddress: Address) { return encodeFunctionData({ abi: STAKE_CONTRACT.abi, diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 0ddf509..657507d 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -65,17 +65,20 @@ const client = createAcrossClient({ Now, you can retrieve a quote for a given route. ```ts -// USDC from Arbitrum -> Optimism +import { parseEther } from "viem"; + +// WETH from Arbitrum -> Optimism const route = { originChainId: arbitrum.id, destinationChainId: optimism.id, - inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", - outputToken: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + inputToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // WETH arb + outputToken: "0x4200000000000000000000000000000000000006", // WETH opt }; + const quote = await client.getQuote({ route, - inputAmount: parseUnits("1000", 6) // USDC decimals -}) + inputAmount: parseEther("1"), +}); ``` Note that we provide additional utilities for retrieving available routes, chain details, and token infos. @@ -94,7 +97,19 @@ await client.executeQuote({ walletClient: wallet, deposit: quote.deposit, // returned by `getQuote` onProgress: (progress) => { - // handle progress + if (progress.step === "approve" && progress.status === "txSuccess") { + // if approving an ERC20, you have access to the approval receipt + const { txReceipt } = progress; + } + if (progress.step === "deposit" && progress.status === "txSuccess") { + // once deposit is successful you have access to depositId and the receipt + const { depositId, txReceipt } = progress; + } + if (progress.step === "fill" && progress.status === "txSuccess") { + // if the fill is successful, you have access the following data + const { fillTxTimestamp, txReceipt, actionSuccess } = progress; + // actionSuccess is a boolean flag, telling us if your cross chain messages were successful + } }, }); ``` @@ -112,86 +127,90 @@ Have a look at our [example app](../../apps/example) for a more detailed usage o Across enables users to seamlessly interact with your dApp or chain using assets from other chains. -### 1. Craft a cross-chain message +### Example: Stake Native ETH on Destination Chain -To implement this feature, you first need to specify a `crossChainMessage`. -The example below shows a cross-chain message for staking USDC into a contract deployed -on Optimism by: +To stake native ETH in one step: -1. Approve USDC to be pulled into staking -2. Stake approved amount into contract +1. Bridge WETH to the destination chain, sending the output tokens (WETH) to Across's MulticallHandler contract (since contracts receive WETH). +2. Unwrap WETH to obtain native ETH. +3. Stake the ETH on your staking contract. -```ts -// Example staking contract on Optimism -const stakingContractAddress = "0x733Debf51574c70CfCdb7918F032E16F686bd9f8"; -// USDC on Optimism -const usdcAddress = "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"; -// Example user address -const userAddress = "0x924a9f036260DdD5808007E1AA95f08eD08aA569a"; -const inputAmount = parseUnits("200", 6); - -const crossChainMessage = { - // This address will receive the amount of bridged tokens on the destination chain if - // one of the cross-chain actions fail. Leftover tokens are here as well if all actions - // succeed. - fallbackRecipient: userAddress, - // List of actions that should be executed on the destination chain - actions: [ - { - // Address of target contract on destination chain, i.e. USDC on Optimism - target: usdcAddress, - // Encoded function data, i.e. calldata for approving USDC to be pulled in by - // staking contract - callData: generateApproveCallData( - stakingContractAddress, - inputAmount - ), - // Native msg.value, can be 0 in the context of USDC - value: 0n, - // Update call data callback - we need to provide a callback function to - // re-generate calldata because it depends on the `outputAmount`, i.e. - // `inputAmount` - `relayer fee`. This is the amount the user has available after a - // relayer filled the deposit on the destination chain. - updateCallData: (outputAmount) => generateApproveCallData( - stakingContractAddress, - outputAmount - ) - }, - { - // Address of target contract on destination chain, i.e. staking contract - // on Optimism - target: stakingContractAddress, - // Encoded function data, i.e. calldata for staking USDC on behalf of user - callData: generateStakeCallData( - userAddress, - inputAmount - ) - // Native msg.value, can be 0 in the context of USDC - value: 0n, - // Same reasoning as above in the approve step. - updateCallData: (outputAmount) => generateStakeCallData( - stakingContractAddress, - outputAmount - ) - } - ] -} +**Note**: If your calldata depends on the output amount, you must use the `update()` function. -function generateApproveCallData(spender: Address, amount: bigint) { - const approveCallData = encodeFunctionData({ - abi: [parseAbiItem("function approve(address spender, uint256 value)")], - args: [spender, amount], - }); +### 1. Craft a cross-chain message - return approveCallData; +```ts +import { type Amount } from "@across-protocol/app-sdk"; +import { encodeFunctionData } from "viem"; +import { optimism } from "viem/chains"; + +// constants +const userAddress = "0xFoo"; +const multicallHandlerOptimism = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; + +export const WETH_OPTIMISM = { + chain: optimism, + abi: WethAbi, + address: "0x4200000000000000000000000000000000000006", + decimals: 18, + logoUrl: + "https://raw.githubusercontent.com/across-protocol/frontend/master/src/assets/token-logos/weth.svg", + name: "Wrapped Ether", + symbol: "WETH", +} as const; + +export const STAKE_CONTRACT = { + address: "0x733Debf51574c70CfCdb7918F032E16F686bd9f8", + chain: optimism, + abi: StakerContractABI, +} as const; + +// WETH unwrap action +function generateUnwrapCallData(wethAmount: Amount) { + return encodeFunctionData({ + abi: WETH_OPTIMISM.abi, + functionName: "withdraw", + args: [BigInt(wethAmount)], + }); } -function generateStakeCallData(userAddress: Address, amount: bigint) { +// STAKE action +function generateStakeCallData(userAddress: Address) { return encodeFunctionData({ - abi: [parseAbiItem("function stake(address stakerAddress, uint256 amount)")], - args: [userAddress, amount], + abi: STAKE_CONTRACT.abi, + functionName: "stake", + args: [userAddress], }); } + +const unwrapAndStakeMessage = { + actions: [ + { + target: WETH_OPTIMISM.address, + callData: generateUnwrapCallData(inputAmount), + value: 0n, + // we only update the calldata since the unwrap call is non-payable, but we DO care about the output amount. + update: (updatedOutputAmount) => { + return { + callData: generateUnwrapCallData(updatedOutputAmount), + }; + }, + }, + { + target: STAKE_CONTRACT.address, + callData: generateStakeCallData(userAddress), + // 🔔 the initial value may be set equal to the output amount. This MUST be updated via the `update()` function below oir this call will fail! + value: inputAmount, + // now we MUST update msg.value since this last call is calling a payable function. + update: (updatedOutputAmount) => { + return { + value: updatedOutputAmount, + }; + }, + }, + ], + fallbackRecipient: userAddress, +}; ``` ### 2. Retrieve a quote @@ -202,13 +221,16 @@ After specifying a cross-chain message, you simply can fetch a quote the same wa const route = { originChainId: arbitrum.id, destinationChainId: optimism.id, - inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", - outputToken: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + inputToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // WETH arbitrum + outputToken: "0x4200000000000000000000000000000000000006", // WETH optimism }; + const quote = await client.getQuote({ route, - inputAmount, - crossChainMessage // created above + inputAmount: parseEther("1"), + // 🔔 Notice the recipient is not the staking contract itself or even the user, but the contract that will execute our cross chain messages + recipient: multicallHandlerOptimism, + crossChainMessage: unwrapAndStakeMessage, }); ```