diff --git a/README.md b/README.md index 565bb9b5..faeb9c28 100644 --- a/README.md +++ b/README.md @@ -531,6 +531,21 @@ const inference = await agent.getInferenceByTopicId(42); console.log("Allora inference for topic 42:", inference); ``` +### Cross-Chain Swap + +```typescript +import { PublicKey } from "@solana/web3.js"; + +const signature = await agent.swap( + amount: "10", + fromChain: "bsc", + fromToken: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + toChain: "solana", + toToken: "0x0000000000000000000000000000000000000000", + dstAddr: "0xc2d3024d64f27d85e05c40056674Fd18772dd922", +); +``` + ## Examples ### LangGraph Multi-Agent System diff --git a/package.json b/package.json index ff0e1f7b..2e864e82 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@meteora-ag/alpha-vault": "^1.1.7", "@meteora-ag/dlmm": "^1.3.0", "@onsol/tldparser": "^0.6.7", + "@openzeppelin/contracts": "^5.2.0", "@orca-so/common-sdk": "0.6.4", "@orca-so/whirlpools-sdk": "^0.13.12", "@pythnetwork/hermes-client": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdd38630..26aae15f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: '@onsol/tldparser': specifier: ^0.6.7 version: 0.6.7(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bn.js@5.2.1)(borsh@2.0.0)(buffer@6.0.3)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@openzeppelin/contracts': + specifier: ^5.2.0 + version: 5.2.0 '@orca-so/common-sdk': specifier: 0.6.4 version: 0.6.4(@solana/spl-token@0.4.9(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10))(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(decimal.js@10.4.3) @@ -1092,6 +1095,9 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@openzeppelin/contracts@5.2.0': + resolution: {integrity: sha512-bxjNie5z89W1Ea0NZLZluFh8PrFNn9DH8DQlujEok2yjsOlraUPKID5p1Wk3qdNbf6XkQ1Os2RvfiHrrXLHWKA==} + '@orca-so/common-sdk@0.6.4': resolution: {integrity: sha512-iOiC6exTA9t2CEOaUPoWlNP3soN/1yZFjoz1mSf7NvOqo/PJZeIdWpB7BRXwU0mGGatjxU4SFgMGQ8NrSx+ONw==} peerDependencies: @@ -6748,6 +6754,8 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@openzeppelin/contracts@5.2.0': {} + '@orca-so/common-sdk@0.6.4(@solana/spl-token@0.4.9(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10))(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(decimal.js@10.4.3)': dependencies: '@solana/spl-token': 0.4.9(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) diff --git a/src/actions/mayan/swap.ts b/src/actions/mayan/swap.ts index fac736dc..8c6f593e 100644 --- a/src/actions/mayan/swap.ts +++ b/src/actions/mayan/swap.ts @@ -1,4 +1,3 @@ -import { PublicKey } from "@solana/web3.js"; import { Action } from "../../types/action"; import { SolanaAgentKit } from "../../agent"; import { z } from "zod"; @@ -27,8 +26,6 @@ const swapAction: Action = { explanation: "swap 0.02 0x0000000000000000000000000000000000000000 from solana to 0x0000000000000000000000000000000000000000 polygon destination 0x0cae42c0ce52e6e64c1e384ff98e686c6ee225f0", }, - ], - [ { input: { amount: "0.02", @@ -47,6 +44,25 @@ const swapAction: Action = { "swap 0.02 0x0000000000000000000000000000000000000000 from solana to HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3 solana destination 4ZgCP2idpqrxuQNfsjakJEm9nFyZ2xnT4CrDPKPULJPk", }, ], + [ + { + input: { + amount: "0.02", + fromChain: "solana", + fromToken: "sol", + toChain: "solana", + toToken: "HNT", + dstAddr: "4ZgCP2idpqrxuQNfsjakJEm9nFyZ2xnT4CrDPKPULJPk", + }, + output: { + status: "success", + message: "Swap executed successfully", + url: "https://explorer.mayan.finance/swap/2GLNqs5gXCBSwRt6VjtfQRnLWYbcU1gzkgjWMWautv1RUj13Di4qJPjV29YRpoAdMYxgXj8ArMLzF3bCCZmVUXHz", + }, + explanation: + "swap 0.02 sol from solana to hnt solana destination 4ZgCP2idpqrxuQNfsjakJEm9nFyZ2xnT4CrDPKPULJPk", + }, + ], ], schema: z.object({ amount: z @@ -65,7 +81,7 @@ const swapAction: Action = { "optimism", "base", ]), - fromToken: z.string().min(32, "Invalid from mint address"), + fromToken: z.string(), toChain: z.enum([ "solana", "ethereum", @@ -76,7 +92,7 @@ const swapAction: Action = { "optimism", "base", ]), - toToken: z.string().min(32, "Invalid to mint address"), + toToken: z.string(), dstAddr: z.string().min(32, "Invalid destination address"), inputAmount: z.number().positive("Input amount must be positive"), slippageBps: z.number().min(0).max(10000).optional(), @@ -87,7 +103,7 @@ const swapAction: Action = { throw new Error("one of the from or to chain should be solana."); } - const tx = await swap( + const url = await swap( agent, input.amount, input.fromChain, @@ -101,10 +117,7 @@ const swapAction: Action = { return { status: "success", message: "Swap executed successfully", - transaction: tx, - inputAmount: input.inputAmount, - inputToken: input.inputMint || "SOL", - outputToken: input.outputMint, + url, }; }, }; diff --git a/src/langchain/mayan/swap.ts b/src/langchain/mayan/swap.ts index aa998fa4..f5771f75 100644 --- a/src/langchain/mayan/swap.ts +++ b/src/langchain/mayan/swap.ts @@ -8,9 +8,9 @@ export class SolanaCrossChainSwapTool extends Tool { Inputs ( input is a JSON string): amount: string, eg "0.02" or "7" fromChain: string, eg "solana" or "ethereum" - fromToken: string, eg "0x0000000000000000000000000000000000000000" or "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux" + fromToken: string, eg "0x0000000000000000000000000000000000000000" or "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux" or "sol" or "Pol" toChain: string, eg "solana" or "ethereum" - toToken: string, eg "0x0000000000000000000000000000000000000000" or "0x6b175474e89094c44da98b954eedeac495271d0f" + toToken: string, eg "0x0000000000000000000000000000000000000000" or "0x6b175474e89094c44da98b954eedeac495271d0f" or "SOL" or "eth" dstAddr: string, eg "4ZgCP2idpqrxuQNfsjakJEm9nFyZ2xnT4CrDPKPULJPk" or "0x0cae42c0cE52E6E64C1e384fF98e686C6eE225f0" slippageBps: number, eg 10 (optional)`; diff --git a/src/tools/mayan/MayanForwarderArtifact.ts b/src/tools/mayan/MayanForwarderArtifact.ts new file mode 100644 index 00000000..e38ee249 --- /dev/null +++ b/src/tools/mayan/MayanForwarderArtifact.ts @@ -0,0 +1,552 @@ +export default { + _format: "hh-sol-artifact-1", + contractName: "MayanForwarder", + sourceName: "src/MayanForwarder.sol", + abi: [ + { + inputs: [ + { + internalType: "address", + name: "_guardian", + type: "address", + }, + { + internalType: "address[]", + name: "_swapProtocols", + type: "address[]", + }, + { + internalType: "address[]", + name: "_mayanProtocols", + type: "address[]", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "UnsupportedProtocol", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "mayanProtocol", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "protocolData", + type: "bytes", + }, + ], + name: "ForwardedERC20", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "mayanProtocol", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "protocolData", + type: "bytes", + }, + ], + name: "ForwardedEth", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "SwapAndForwarded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "tokenIn", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "swapProtocol", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "middleToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "middleAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "mayanProtocol", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "mayanData", + type: "bytes", + }, + ], + name: "SwapAndForwardedERC20", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "swapProtocol", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "middleToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "middleAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "mayanProtocol", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "mayanData", + type: "bytes", + }, + ], + name: "SwapAndForwardedEth", + type: "event", + }, + { + inputs: [ + { + internalType: "address", + name: "newGuardian", + type: "address", + }, + ], + name: "changeGuardian", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "claimGuardian", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "tokenIn", + type: "address", + }, + { + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + internalType: "struct MayanForwarder.PermitParams", + name: "permitParams", + type: "tuple", + }, + { + internalType: "address", + name: "mayanProtocol", + type: "address", + }, + { + internalType: "bytes", + name: "protocolData", + type: "bytes", + }, + ], + name: "forwardERC20", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "mayanProtocol", + type: "address", + }, + { + internalType: "bytes", + name: "protocolData", + type: "bytes", + }, + ], + name: "forwardEth", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "guardian", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "mayanProtocols", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "nextGuardian", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address payable", + name: "to", + type: "address", + }, + ], + name: "rescueEth", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "token", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + ], + name: "rescueToken", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "mayanProtocol", + type: "address", + }, + { + internalType: "bool", + name: "enabled", + type: "bool", + }, + ], + name: "setMayanProtocol", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "swapProtocol", + type: "address", + }, + { + internalType: "bool", + name: "enabled", + type: "bool", + }, + ], + name: "setSwapProtocol", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "tokenIn", + type: "address", + }, + { + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + internalType: "struct MayanForwarder.PermitParams", + name: "permitParams", + type: "tuple", + }, + { + internalType: "address", + name: "swapProtocol", + type: "address", + }, + { + internalType: "bytes", + name: "swapData", + type: "bytes", + }, + { + internalType: "address", + name: "middleToken", + type: "address", + }, + { + internalType: "uint256", + name: "minMiddleAmount", + type: "uint256", + }, + { + internalType: "address", + name: "mayanProtocol", + type: "address", + }, + { + internalType: "bytes", + name: "mayanData", + type: "bytes", + }, + ], + name: "swapAndForwardERC20", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + internalType: "address", + name: "swapProtocol", + type: "address", + }, + { + internalType: "bytes", + name: "swapData", + type: "bytes", + }, + { + internalType: "address", + name: "middleToken", + type: "address", + }, + { + internalType: "uint256", + name: "minMiddleAmount", + type: "uint256", + }, + { + internalType: "address", + name: "mayanProtocol", + type: "address", + }, + { + internalType: "bytes", + name: "mayanData", + type: "bytes", + }, + ], + name: "swapAndForwardEth", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "swapProtocols", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + ], + linkReferences: {}, + deployedLinkReferences: {}, +}; diff --git a/src/tools/mayan/swap.ts b/src/tools/mayan/swap.ts index 7785841b..c768fc5c 100644 --- a/src/tools/mayan/swap.ts +++ b/src/tools/mayan/swap.ts @@ -1,13 +1,42 @@ import { + addresses, ChainName, + Erc20Permit, fetchQuote, + fetchTokenList, Quote, swapFromEvm, swapFromSolana, } from "@mayanfinance/swap-sdk"; import { SolanaAgentKit } from "../../agent"; -import { getDefaultProvider, TransactionResponse, Wallet } from "ethers"; +import { + Contract, + getDefaultProvider, + parseUnits, + Signature, + Signer, + TransactionResponse, + TypedDataEncoder, + Wallet, +} from "ethers"; +import { abi as ERC20Permit_ABI } from "@openzeppelin/contracts/build/contracts/ERC20Permit.json"; import { VersionedTransaction, Transaction } from "@solana/web3.js"; +import MayanForwarderArtifact from "./MayanForwarderArtifact"; + +async function findTokenContract( + symbol: string, + chain: string, +): Promise { + const tokens = await fetchTokenList(chain as ChainName, true); + const token = tokens.find( + (t) => t.symbol.toLowerCase() === symbol.toLowerCase(), + ); + if (!token) { + throw new Error(`Couldn't find token with ${symbol} symbol`); + } + + return token.contract; +} export async function swap( agent: SolanaAgentKit, @@ -19,6 +48,13 @@ export async function swap( dstAddr: string, slippageBps: "auto" | number = "auto", ): Promise { + if (fromToken.length < 32) { + fromToken = await findTokenContract(fromToken, fromChain); + } + if (toToken.length < 32) { + toToken = await findTokenContract(toToken, toChain); + } + const quotes = await fetchQuote({ amount: +amount, fromChain: fromChain as ChainName, @@ -145,6 +181,34 @@ async function swapEVM( } const signer = evmWallet.connect(getDefaultProvider(quote.fromToken.chainId)); + const amountIn = getAmountOfFractionalAmount( + quote.effectiveAmountIn, + quote.fromToken.decimals, + ); + const tokenContract = new Contract( + quote.fromToken.contract, + ERC20Permit_ABI, + signer, + ); + + const allowance: bigint = await tokenContract.allowance( + evmWallet.address, + addresses.MAYAN_FORWARDER_CONTRACT, + ); + if (allowance < amountIn) { + // Approve the spender to spend the tokens + const approveTx = await tokenContract.approve( + addresses.MAYAN_FORWARDER_CONTRACT, + amountIn, + ); + await approveTx.wait(); + } + + let permit: Erc20Permit | undefined; + if (quote.fromToken.supportsPermit) { + permit = await getERC20Permit(quote, tokenContract, amountIn, signer); + } + const swapRes = await swapFromEvm( quote, evmWallet.address, @@ -189,3 +253,102 @@ async function getJitoTipLamports() { : null; return tip ? Math.floor(Number(tip) * 10 ** 9) : null; } + +function getAmountOfFractionalAmount( + amount: string | number, + decimals: string | number, +): bigint { + const cutFactor = Math.min(8, Number(decimals)); + const numStr = Number(amount).toFixed(cutFactor + 1); + const reg = new RegExp(`^-?\\d+(?:\\.\\d{0,${cutFactor}})?`); + const matchResult = numStr.match(reg); + if (!matchResult) { + throw new Error("getAmountOfFractionalAmount: fixedAmount is null"); + } + const fixedAmount = matchResult[0]; + return parseUnits(fixedAmount, Number(decimals)); +} + +async function getERC20Permit( + quote: Quote, + tokenContract: Contract, + amountIn: bigint, + signer: Signer, +): Promise { + const walletSrcAddr = await signer.getAddress(); + const nonce = await tokenContract.nonces(walletSrcAddr); + const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now + + const domain = { + name: await tokenContract.name(), + version: "1", + chainId: quote.fromToken.chainId, + verifyingContract: await tokenContract.getAddress(), + }; + const domainSeparator = await tokenContract.DOMAIN_SEPARATOR(); + for (let i = 1; i < 11; i++) { + domain.version = String(i); + const hash = TypedDataEncoder.hashDomain(domain); + if (hash.toLowerCase() === domainSeparator.toLowerCase()) { + break; + } + } + + let spender = addresses.MAYAN_FORWARDER_CONTRACT; + if (quote.type === "SWIFT" && quote.gasless) { + const forwarderContract = new Contract( + addresses.MAYAN_FORWARDER_CONTRACT, + MayanForwarderArtifact.abi, + signer.provider, + ); + const isValidSwiftContract = await forwarderContract.mayanProtocols( + quote.swiftMayanContract, + ); + if (!isValidSwiftContract) { + throw new Error("Invalid Swift contract for gasless swap"); + } + if (!quote.swiftMayanContract) { + throw new Error("Swift contract not found"); + } + spender = quote.swiftMayanContract; + } + + const types = { + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + }; + + const value = { + owner: walletSrcAddr, + spender, + value: amountIn, + nonce: nonce, + deadline: deadline, + }; + + const signature = await signer.signTypedData(domain, types, value); + const { v, r, s } = Signature.from(signature); + + const permitTx = await tokenContract.permit( + walletSrcAddr, + spender, + amountIn, + deadline, + v, + r, + s, + ); + await permitTx.wait(); + return { + value: amountIn, + deadline, + v, + r, + s, + }; +} diff --git a/tsconfig.json b/tsconfig.json index 0df003a7..e79de5fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, + "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"]