UNPKG

@moonwell-fi/moonwell-sdk

Version:

TypeScript Interface for Moonwell

576 lines 28.4 kB
import { createAcrossClient } from "@across-protocol/app-sdk"; import { createMeeClient, greaterThanOrEqualTo, runtimeERC20BalanceOf, runtimeEncodeAbiParameters, toMultichainNexusAccount, } from "@biconomy/abstractjs"; import { http, createPublicClient, encodeFunctionData, getContract, parseAbi, zeroAddress, } from "viem"; import { getEnvironmentsFromArgs } from "../../common/index.js"; import marketTokenAbi from "../../environments/abis/marketTokenAbi.js"; import morphoBlueAbi from "../../environments/abis/morphoBlueAbi.js"; import morphoVaultAbi from "../../environments/abis/morphoVaultAbi.js"; import { moonbeam, moonriver, } from "../../environments/index.js"; const ACROSS_ABI = parseAbi([ "function deposit(bytes32 depositor, bytes32 recipient, bytes32 inputToken, bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, bytes32 exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityParameter, bytes message)", "function depositV3(address depositor,address recipient,address inputToken,address outputToken,uint256 inputAmount,uint256 outputAmount,uint256 destinationChainId,address exclusiveRelayer,uint32 quoteTimestamp,uint32 fillDeadline,uint32 exclusivityParameter,bytes message)", ]); const WRAP_ABI = parseAbi(["function deposit()"]); const MEE_CLIENT_API_KEY = "mee_3ZkX3T823ZDfwsNiFsqj5oZS"; const ACROSS_INTEGRATOR_ID = "0x008e"; const findMarketToken = (environments, chainId, underlyingTokenAddress) => { const originEnvironment = environments.find((env) => chainId === env.chainId); if (originEnvironment) { const underlyingTokenConfig = Object.keys(originEnvironment.config.tokens) .map((token) => { const marketToken = originEnvironment.config.tokens[token]; return { ...marketToken, key: token, }; }) .find((token) => token.address === underlyingTokenAddress); if (underlyingTokenConfig) { const originMarket = Object.values(originEnvironment.config.markets).find((market) => market.underlyingToken === underlyingTokenConfig.key); if (originMarket) { return { underlyingToken: originEnvironment.config.tokens[originMarket.underlyingToken], marketToken: originEnvironment.config.tokens[originMarket.marketToken], }; } } } return undefined; }; const findIsolatedMarketToken = (environments, chainId, underlyingTokenAddress) => { const originEnvironment = environments.find((env) => chainId === env.chainId); if (originEnvironment) { const underlyingTokenConfig = Object.keys(originEnvironment.config.tokens) .map((token) => { const marketToken = originEnvironment.config.tokens[token]; return { ...marketToken, key: token, }; }) .find((token) => token.address === underlyingTokenAddress); if (underlyingTokenConfig) { const originMarket = Object.values(originEnvironment.config.morphoMarkets).find((market) => market.collateralToken === underlyingTokenConfig.key); if (originMarket) { return { underlyingToken: originEnvironment.config.tokens[originMarket.collateralToken], marketToken: originEnvironment.config.tokens[originMarket.loanToken], }; } } } return undefined; }; const findVaultToken = (environments, chainId, underlyingTokenAddress) => { const originEnvironment = environments.find((env) => chainId === env.chainId); if (originEnvironment) { const underlyingTokenConfig = Object.keys(originEnvironment.config.tokens) .map((token) => { const marketToken = originEnvironment.config.tokens[token]; return { ...marketToken, key: token, }; }) .find((token) => token.address === underlyingTokenAddress); if (underlyingTokenConfig) { const originMarket = Object.values(originEnvironment.config.vaults).find((market) => market.underlyingToken === underlyingTokenConfig.key); if (originMarket) { return { underlyingToken: originEnvironment.config.tokens[originMarket.underlyingToken], marketToken: originEnvironment.config.tokens[originMarket.vaultToken], }; } } } return undefined; }; export async function getQuote(client, args) { const envs = getEnvironmentsFromArgs(client, undefined, false).filter((env) => env.chainId !== moonbeam.id && env.chainId !== moonriver.id); const chains = Object.values(envs).map((env) => env.chain); const transports = chains.map((chain) => http(chain.rpcUrls.default.http[0])); const smartAccount = await toMultichainNexusAccount({ signer: args.wallet, chains, transports, }); const meeClient = await createMeeClient({ account: smartAccount, apiKey: MEE_CLIENT_API_KEY, }); const accrossClient = createAcrossClient({ integratorId: ACROSS_INTEGRATOR_ID, // 2-byte hex string chains, useTestnet: false, }); if (args.type === "supply" || args.type === "morpho-supply" || args.type === "vault-deposit") { const tokenConfig = args.type === "supply" ? findMarketToken(envs, args.destination.chainId, args.destination.address) : args.type === "morpho-supply" ? findIsolatedMarketToken(envs, args.destination.chainId, args.destination.address) : findVaultToken(envs, args.destination.chainId, args.destination.address); if (tokenConfig) { const quotes = []; const instructions = []; const approvals = []; const transfers = []; let totalOutput = 0n; const nativeAmounts = []; const cleanUps = []; for (const source of args.sources) { const sourceChain = chains.find((r) => r.id === source.chainId); const publicClient = createPublicClient({ chain: sourceChain, transport: http(sourceChain.rpcUrls.default.http[0]), }); const nativeBalance = await publicClient.getBalance({ address: args.wallet.account?.address, }); nativeAmounts.push({ chainId: source.chainId, amount: nativeBalance, }); if (source.address === zeroAddress) { const nativeBalanceSmartAccount = await publicClient.getBalance({ address: smartAccount.addressOn(source.chainId), }); if (nativeBalanceSmartAccount < source.amount) { transfers.push({ amount: source.amount - nativeBalanceSmartAccount, chainId: source.chainId, to: smartAccount.addressOn(source.chainId), }); } cleanUps.push({ tokenAddress: source.address, chainId: source.chainId, recipientAddress: args.wallet.account.address, }); } else { const erc20Abi = parseAbi([ "function allowance(address owner, address spender) view returns (uint256)", ]); const erc20Contract = getContract({ address: source.routeTokenAddress, abi: erc20Abi, client: publicClient, }); const allowance = await erc20Contract.read.allowance([ args.wallet.account?.address, smartAccount.addressOn(source.chainId), ]); if (allowance < source.amount) { approvals.push({ spender: smartAccount.addressOn(source.chainId), tokenAddress: source.routeTokenAddress, chainId: source.chainId, amount: source.amount, }); } } let transferOrWrapInstruction = []; if (source.address === zeroAddress) { const wrapData = encodeFunctionData({ abi: WRAP_ABI, functionName: "deposit", args: [], }); transferOrWrapInstruction = await smartAccount.buildComposable({ type: "rawCalldata", data: { calldata: wrapData, to: source.routeTokenAddress, chainId: source.chainId, value: source.amount, }, }); } else { transferOrWrapInstruction = await smartAccount.buildComposable({ type: "transferFrom", data: { sender: args.wallet.account?.address, recipient: smartAccount.addressOn(source.chainId), tokenAddress: source.routeTokenAddress, amount: source.amount, chainId: source.chainId, }, }); } if (source.chainId !== args.destination.chainId) { const acrossQuote = await accrossClient.getQuote({ route: { originChainId: source.chainId, inputToken: source.routeTokenAddress, destinationChainId: args.destination.chainId, outputToken: args.destination.routeTokenAddress, }, inputAmount: source.amount.toString(), recipient: smartAccount.addressOn(args.destination.chainId), }); const acrossChainInfo = await accrossClient.getChainInfo(source.chainId); const approveAcrossSpendInstructions = await smartAccount.buildComposable({ type: "approve", data: { tokenAddress: source.routeTokenAddress, amount: source.amount, chainId: source.chainId, spender: acrossChainInfo.spokePool, }, }); const bridgeData = encodeFunctionData({ abi: ACROSS_ABI, functionName: "depositV3", args: [ smartAccount.addressOn(source.chainId), smartAccount.addressOn(args.destination.chainId), acrossQuote.deposit.inputToken, acrossQuote.deposit.outputToken, acrossQuote.deposit.inputAmount, acrossQuote.deposit.outputAmount, acrossQuote.deposit.destinationChainId, acrossQuote.deposit.exclusiveRelayer, acrossQuote.deposit.quoteTimestamp, acrossQuote.deposit.fillDeadline, acrossQuote.deposit.exclusivityDeadline, acrossQuote.deposit.message, ], }); totalOutput += acrossQuote.deposit.outputAmount; const bridgeInstructions = await smartAccount.buildComposable({ type: "rawCalldata", data: { calldata: bridgeData, to: acrossChainInfo.spokePool, chainId: source.chainId, }, }); quotes.push(acrossQuote); instructions.push(await smartAccount.buildComposable({ type: "batch", data: { instructions: [ transferOrWrapInstruction, approveAcrossSpendInstructions, bridgeInstructions, ], }, })); } else { totalOutput += source.amount; instructions.push(transferOrWrapInstruction); } } // Approve the contract to spend USDC const approveSupplyInstructions = await smartAccount.buildComposable({ type: "approve", data: { tokenAddress: args.destination.routeTokenAddress, amount: runtimeERC20BalanceOf({ tokenAddress: args.destination.routeTokenAddress, targetAddress: smartAccount.addressOn(args.destination.chainId, true), constraints: [greaterThanOrEqualTo(totalOutput)], }), chainId: args.destination.chainId, spender: args.type === "morpho-supply" ? args.destination.morphoBlue : tokenConfig.marketToken.address, }, }); if (args.type === "supply") { const supplyInstructions = await smartAccount.buildComposable({ type: "default", data: { to: tokenConfig.marketToken.address, abi: marketTokenAbi, functionName: "mint", args: [ runtimeEncodeAbiParameters([{ name: "amount", type: "uint256" }], [ runtimeERC20BalanceOf({ targetAddress: smartAccount.addressOn(args.destination.chainId, true), tokenAddress: args.destination .routeTokenAddress, constraints: [greaterThanOrEqualTo(totalOutput)], }), ]), ], // Pass as a single bytes parameter chainId: args.destination.chainId, }, }); const transferBack = await smartAccount.buildComposable({ type: "transfer", data: { amount: runtimeERC20BalanceOf({ tokenAddress: tokenConfig.marketToken.address, targetAddress: smartAccount.addressOn(args.destination.chainId, true), constraints: [greaterThanOrEqualTo(1n)], }), chainId: args.destination.chainId, recipient: args.wallet.account.address, tokenAddress: tokenConfig.marketToken.address, }, }); const batchedInstructions = await smartAccount.buildComposable({ type: "batch", data: { instructions: [ approveSupplyInstructions, supplyInstructions, transferBack, ], }, }); instructions.push(batchedInstructions); } else if (args.type === "morpho-supply") { const supplyCollateralData = encodeFunctionData({ abi: morphoBlueAbi, functionName: "supplyCollateral", args: [ { loanToken: args.destination.marketParams?.loanToken, collateralToken: args.destination.marketParams?.collateralToken, oracle: args.destination.marketParams?.oracle, irm: args.destination.marketParams?.irm, lltv: args.destination.marketParams?.lltv, }, totalOutput, args.wallet.account.address, "0x", ], }); const supplyInstructions = await smartAccount.buildComposable({ type: "rawCalldata", data: { calldata: supplyCollateralData, to: args.destination.morphoBlue, chainId: args.destination.chainId, value: 0n, }, }); // const supplyInstructions = await smartAccount.buildComposable({ // type: "default", // data: { // to: args.destination.morphoBlue, // abi: morphoBlueAbi, // functionName: "supplyCollateral", // args: [runtimeEncodeAbiParameters( // [ // { // components: [ // { // internalType: "address", // name: "loanToken", // type: "address", // }, // { // internalType: "address", // name: "collateralToken", // type: "address", // }, // { // internalType: "address", // name: "oracle", // type: "address", // }, // { // internalType: "address", // name: "irm", // type: "address", // }, // { // internalType: "uint256", // name: "lltv", // type: "uint256", // }, // ], // internalType: "struct MarketParams", // name: "marketParams", // type: "tuple", // }, // { // internalType: "uint256", // name: "assets", // type: "uint256", // }, // { // internalType: "address", // name: "onBehalf", // type: "address", // }, // { // internalType: "bytes", // name: "data", // type: "bytes", // }, // ], // [ // { // loanToken: args.destination.marketParams?.loanToken, // collateralToken: args.destination.marketParams?.collateralToken, // oracle: args.destination.marketParams?.oracle, // irm: args.destination.marketParams?.irm, // lltv: args.destination.marketParams?.lltv, // }, // runtimeERC20BalanceOf({ // targetAddress: smartAccount.addressOn(args.destination.chainId, true), // tokenAddress: args.destination.routeTokenAddress as `0x${string}`, // constraints: [greaterThanOrEqualTo(totalOutput)] // }), // args.wallet.account!.address, // "0x", // ] // )], // chainId: args.destination.chainId, // } // }); const batchedInstructions = await smartAccount.buildComposable({ type: "batch", data: { instructions: [approveSupplyInstructions, supplyInstructions], }, }); instructions.push(batchedInstructions); } else if (args.type === "vault-deposit") { const depositData = encodeFunctionData({ abi: morphoVaultAbi, functionName: "deposit", args: [totalOutput, args.wallet.account.address], }); const depositInstructions = await smartAccount.buildComposable({ type: "rawCalldata", data: { calldata: depositData, to: tokenConfig.marketToken.address, chainId: args.destination.chainId, value: 0n, }, }); const batchedInstructions = await smartAccount.buildComposable({ type: "batch", data: { instructions: [approveSupplyInstructions, depositInstructions], }, }); instructions.push(batchedInstructions); } const fusionQuote = await meeClient.getFusionQuote({ trigger: { tokenAddress: zeroAddress, chainId: args.destination.chainId, amount: 1n, }, feeToken: { address: zeroAddress, chainId: args.destination.chainId, }, instructions, cleanUps: [ ...cleanUps, { tokenAddress: args.destination.routeTokenAddress, chainId: args.destination.chainId, recipientAddress: args.wallet.account.address, }, ], }); return { status: "success", checks: { approvals, transfers, }, account: args.wallet.account, instructions, quote: { hash: fusionQuote.quote.hash, node: fusionQuote.quote.node, commitment: fusionQuote.quote.commitment, fee: { tokenAmount: fusionQuote.quote.paymentInfo.tokenAmount, tokenWeiAmount: fusionQuote.quote.paymentInfo.tokenWeiAmount, tokenValue: fusionQuote.quote.paymentInfo.tokenValue, }, userOps: fusionQuote.quote.userOps.map((operation) => ({ sender: operation.userOp.sender, nonce: operation.userOp.nonce, initCode: operation.userOp.initCode, callData: operation.userOp.callData, })), fusionQuote, }, execute: async () => { try { const superTx = await meeClient.executeFusionQuote({ fusionQuote, }); return { hash: superTx.hash, wait: async (confirmations) => { await meeClient.waitForSupertransactionReceipt({ hash: superTx.hash, confirmations, }); return; }, status: async () => { const receipt = await meeClient.getSupertransactionReceipt({ hash: superTx.hash, }); if (args.type === "supply" || args.type === "morpho-supply" || args.type === "vault-deposit") { try { //cleanup ops receipt.userOps.splice(receipt.userOps.length - (1 + cleanUps.length)); const supplyOp = receipt.userOps.splice(receipt.userOps.length - 1); const acrossOps = [...receipt.userOps]; const acrossOpsPending = acrossOps.filter((op) => op.executionStatus === "MINING" || op.executionStatus === "PENDING").length > 0; const acrossOpsFailed = acrossOps.filter((op) => op.executionStatus === "FAILED" || op.executionStatus === "MINED_FAIL").length > 0; const acrossOpsSucceed = acrossOps.filter((op) => op.executionStatus === "MINED_SUCCESS" || op.executionStatus === "SUCCESS").length === acrossOps.length; const supplyOpPending = supplyOp.filter((op) => op.executionStatus === "MINING" || op.executionStatus === "PENDING").length > 0; const supplyOpFailed = supplyOp.filter((op) => op.executionStatus === "FAILED" || op.executionStatus === "MINED_FAIL").length > 0; const supplyOpSucceed = supplyOp.filter((op) => op.executionStatus === "MINED_SUCCESS" || op.executionStatus === "SUCCESS").length === supplyOp.length; if (acrossOpsFailed || supplyOpFailed) { return "reverted"; } if (acrossOpsPending || supplyOpPending) { return "processing"; } if (acrossOpsSucceed || supplyOpSucceed) { return "success"; } } catch (ex) { return "pending"; } } return "pending"; }, }; } catch (ex) { console.log(ex); throw ex; } }, }; } } return { status: "error", error: "Token not found", }; } //# sourceMappingURL=common.js.map