diff --git a/.env.example b/.env.example index ff842c55..f48add42 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ JUPITER_FEE_BPS= FLASH_PRIVILEGE= referral | nft | none FLEXLEND_API_KEY= HELIUS_API_KEY= +ETHEREUM_PRIVATE_KEY= ALLORA_API_KEY= ALLORA_API_URL= ALLORA_NETWORK= testnet | mainnet diff --git a/README.md b/README.md index 4156248f..ac5e0f17 100644 --- a/README.md +++ b/README.md @@ -540,6 +540,21 @@ const value = await agent.simulateSwitchboardFeed( "9wcBMATS8bGLQ2UcRuYjsRAD7TPqB1CMhqfueBx78Uj2", // TRUMP/USD "http://crossbar.switchboard.xyz");; console.log("Simulation resulted in the following value:", value); + +### 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 diff --git a/package.json b/package.json index ff400fc5..371fc9bf 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@langchain/openai": "^0.3.16", "@lightprotocol/compressed-token": "^0.17.1", "@lightprotocol/stateless.js": "^0.17.1", + "@mayanfinance/swap-sdk": "^9.8.0", "@mercurial-finance/dynamic-amm-sdk": "^1.1.19", "@metaplex-foundation/digital-asset-standard-api": "^1.0.4", "@metaplex-foundation/mpl-core": "^1.1.1", @@ -55,6 +56,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", @@ -72,6 +74,7 @@ "chai": "^5.1.2", "decimal.js": "^10.4.3", "dotenv": "^16.4.7", + "ethers": "^6.13.5", "flash-sdk": "^2.24.3", "form-data": "^4.0.1", "langchain": "^0.3.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f2fbf8e..fda8d1e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: '@lightprotocol/stateless.js': specifier: ^0.17.1 version: 0.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@mayanfinance/swap-sdk': + specifier: ^9.8.0 + version: 9.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@mercurial-finance/dynamic-amm-sdk': specifier: ^1.1.19 version: 1.1.23(@solana/buffer-layout@4.0.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) @@ -83,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) @@ -134,6 +140,9 @@ importers: dotenv: specifier: ^16.4.7 version: 16.4.7 + ethers: + specifier: ^6.13.5 + version: 6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) flash-sdk: specifier: ^2.24.3 version: 2.24.3(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10) @@ -198,6 +207,9 @@ packages: '@3land/listings-sdk@0.0.7': resolution: {integrity: sha512-7hEgqBcYFTF15OzXKliG8JuO3SKKDTkFZDgyldxXwPX/HSwvVEGjnX+LorGDhhWt/9YazV7K5iEC3qyeuqibIA==} + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + '@ai-sdk/openai@1.0.11': resolution: {integrity: sha512-qI9s7Slma5i5bB4yYVlFdcG3PNDwdqivPT1Dr8adDX92nSSpILjgFIooS5yys9sXjvvcfOi/WXbDvVhLSRRlvg==} engines: {node: '>=18'} @@ -787,6 +799,9 @@ packages: '@lightprotocol/stateless.js@0.17.1': resolution: {integrity: sha512-EjId1n33A6dBwpce33Wsa/fs/CDKtMtRrkxbApH0alXrnEXmbW6QhIViXOrKYXjZ4uJQM1xsBtsKe0vqJ4nbtQ==} + '@mayanfinance/swap-sdk@9.8.0': + resolution: {integrity: sha512-K+xuK+Ot5u1olFRHhSIi28zCMIMrolxdy/sGcJFCQz3kfCkULIBtp6+oWZ5dhHc1RJ3J+VQUEmX476GkDYRh7A==} + '@mercurial-finance/dynamic-amm-sdk@1.1.19': resolution: {integrity: sha512-e828SZkwSdzLKrQjOyr+/dyKdLKebwwR+rQj/CC3m3HvzaM1E1PgAaafjv4C/Z4eTR1TVdCNIq5p2bJtEDTPiQ==} peerDependencies: @@ -1023,6 +1038,9 @@ packages: '@near-js/utils@0.0.4': resolution: {integrity: sha512-mPUEPJbTCMicGitjEGvQqOe8AS7O4KkRCxqd0xuE/X6gXF1jz1pYMZn4lNUeUz2C84YnVSGLAM0o9zcN6Y4hiA==} + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + '@noble/curves@1.4.2': resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} @@ -1036,6 +1054,10 @@ packages: '@noble/hashes@1.1.3': resolution: {integrity: sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==} + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -1076,6 +1098,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: @@ -1597,6 +1622,9 @@ packages: '@types/node@22.10.7': resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/promise-retry@1.1.6': resolution: {integrity: sha512-EC1+OMXV0PZb0pf+cmyxc43MEP2CDumZe4AfuxWboxxEixztIebknpJPZAX5XlodGF1OY+C1E/RAeNGzxf+bJA==} @@ -1738,6 +1766,9 @@ packages: aes-js@3.0.0: resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + agentkeepalive@4.6.0: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} @@ -2551,6 +2582,10 @@ packages: ethereum-cryptography@2.2.1: resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + ethers@6.13.5: + resolution: {integrity: sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==} + engines: {node: '>=14.0.0'} + ethjs-unit@0.1.6: resolution: {integrity: sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==} engines: {node: '>=6.5.0', npm: '>=3'} @@ -4457,6 +4492,9 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -4726,6 +4764,18 @@ packages: utf-8-validate: optional: true + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -4823,6 +4873,8 @@ snapshots: - typescript - utf-8-validate + '@adraffy/ens-normalize@1.10.1': {} + '@ai-sdk/openai@1.0.11(zod@3.24.1)': dependencies: '@ai-sdk/provider': 1.0.3 @@ -5954,6 +6006,20 @@ snapshots: - encoding - utf-8-validate + '@mayanfinance/swap-sdk@9.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/web3.js': 1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + bs58: 6.0.0 + cross-fetch: 3.2.0 + ethers: 6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + js-sha256: 0.9.0 + js-sha3: 0.8.0 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + '@mercurial-finance/dynamic-amm-sdk@1.1.19(@solana/buffer-layout@4.0.1)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.2)(utf-8-validate@5.0.10)': dependencies: '@coral-xyz/anchor': 0.28.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -6613,6 +6679,10 @@ snapshots: depd: 2.0.0 mustache: 4.2.0 + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + '@noble/curves@1.4.2': dependencies: '@noble/hashes': 1.4.0 @@ -6625,6 +6695,8 @@ snapshots: '@noble/hashes@1.1.3': {} + '@noble/hashes@1.3.2': {} + '@noble/hashes@1.4.0': {} '@noble/hashes@1.5.0': {} @@ -6685,6 +6757,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) @@ -7982,6 +8056,10 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + '@types/promise-retry@1.1.6': dependencies: '@types/retry': 0.12.5 @@ -8157,6 +8235,8 @@ snapshots: aes-js@3.0.0: {} + aes-js@4.0.0-beta.5: {} + agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 @@ -9111,6 +9191,19 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 + ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + ethjs-unit@0.1.6: dependencies: bn.js: 4.11.6 @@ -11257,6 +11350,8 @@ snapshots: tslib@1.14.1: {} + tslib@2.7.0: {} + tslib@2.8.1: {} tsx@4.19.2: @@ -11523,6 +11618,11 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 + ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.0.9 diff --git a/src/actions/index.ts b/src/actions/index.ts index f825c3f3..9c4f460e 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -77,6 +77,7 @@ import getAssetsByAuthorityAction from "./metaplex/getAssetsByAuthority"; import getAssetsByCreatorAction from "./metaplex/getAssetsByCreator"; import getInfoAction from "./agent/get_info"; import switchboardSimulateFeedAction from "./switchboard/simulate_feed"; +import swapAction from "./mayan/swap"; import getPriceInferenceAction from "./allora/getPriceInference"; import getAllTopicsAction from "./allora/getAllTopics"; import getInferenceByTopicIdAction from "./allora/getInferenceByTopicId"; @@ -85,6 +86,7 @@ import createOrcaCLMMAction from "./orca/createOrcaCLMM"; import fetchOrcaPositionsAction from "./orca/fetchOrcaPositions"; import openOrcaCenteredPositionWithLiquidityAction from "./orca/openOrcaCenteredPositionWithLiquidity"; import openOrcaSingleSidedPositionAction from "./orca/openOrcaSingleSidedPosition"; + export const ACTIONS = { GET_INFO_ACTION: getInfoAction, WALLET_ADDRESS_ACTION: getWalletAddressAction, @@ -173,6 +175,7 @@ export const ACTIONS = { GET_ASSETS_BY_AUTHORITY_ACTION: getAssetsByAuthorityAction, SWITCHBOARD_FEED_ACTION: switchboardSimulateFeedAction, GET_ASSETS_BY_CREATOR_ACTION: getAssetsByCreatorAction, + SWAP_ACTION: swapAction, GET_PRICE_INFERENCE_ACTION: getPriceInferenceAction, GET_ALL_TOPICS_ACTION: getAllTopicsAction, GET_INFERENCE_BY_TOPIC_ID_ACTION: getInferenceByTopicIdAction, diff --git a/src/actions/mayan/swap.ts b/src/actions/mayan/swap.ts new file mode 100644 index 00000000..4a66c57b --- /dev/null +++ b/src/actions/mayan/swap.ts @@ -0,0 +1,125 @@ +import { Action } from "../../types/action"; +import { SolanaAgentKit } from "../../agent"; +import { z } from "zod"; +import { swap } from "../../tools"; + +const swapAction: Action = { + name: "SWAP", + similes: ["swap tokens", "exchange tokens", "cross-chain swap"], + description: `This tool can be used to swap tokens to another token cross-chain (It uses Mayan Swap SDK).`, + examples: [ + [ + { + input: { + amount: "0.02", + fromChain: "solana", + fromToken: "SOL", + toChain: "polygan", + toToken: "pol", + dstAddr: "0x0cae42c0ce52e6e64c1e384ff98e686c6ee225f0", + }, + output: { + status: "success", + message: "Swap executed successfully", + url: "https://explorer.mayan.finance/swap/3JywZA6om5t1c5gT1bkFX91bEewHGmntJAqRZniEzETDEBMERvzxBeXVUUMFaernRCmvniZTKsAM7TVG3CTumc12", + }, + explanation: + "swap 0.02 SOL from solana to pol polygon destination 0x0cae42c0ce52e6e64c1e384ff98e686c6ee225f0", + }, + { + 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", + }, + ], + [ + { + 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 + .string() + .refine( + (val) => !isNaN(+val) && Number(val).toString() === val, + "amount is not a valid number", + ), + fromChain: z.enum([ + "solana", + "ethereum", + "bsc", + "polygon", + "avalanche", + "arbitrum", + "optimism", + "base", + ]), + fromToken: z.string(), + toChain: z.enum([ + "solana", + "ethereum", + "bsc", + "polygon", + "avalanche", + "arbitrum", + "optimism", + "base", + ]), + 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(), + }), + handler: async (agent: SolanaAgentKit, input: Record) => { + // TODO: should we allow evm to evm? + if (input.fromChain !== "solana" && input.toChain !== "solana") { + throw new Error("one of the from or to chain should be solana."); + } + + const url = await swap( + agent, + input.amount, + input.fromChain, + input.fromToken, + input.toChain, + input.toToken, + input.dstAddr, + input.slippageBps, + ); + + return { + status: "success", + message: "Swap executed successfully", + url, + }; + }, +}; + +export default swapAction; diff --git a/src/agent/index.ts b/src/agent/index.ts index 98b6b014..3648853e 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -118,6 +118,7 @@ import { get_assets_by_authority, get_assets_by_creator, simulate_switchboard_feed, + swap, getPriceInference, getAllTopics, getInferenceByTopicId, @@ -1034,6 +1035,28 @@ export class SolanaAgentKit { ): Promise { return get_assets_by_creator(this, params); } + + async swap( + amount: string, + fromChain: string, + fromToken: string, + toChain: string, + toToken: string, + dstAddr: string, + slippageBps?: number, + ): Promise { + return swap( + this, + amount, + fromChain, + fromToken, + toChain, + toToken, + dstAddr, + slippageBps, + ); + } + async getPriceInference( tokenSymbol: string, timeframe: string, diff --git a/src/langchain/index.ts b/src/langchain/index.ts index ac6d973c..12e659c9 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -28,6 +28,7 @@ export * from "./meteora"; export * from "./helius"; export * from "./drift"; export * from "./voltr"; +export * from "./mayan"; export * from "./allora"; export * from "./switchboard"; @@ -136,6 +137,7 @@ import { SolanaGetAssetsByAuthorityTool, SolanaGetAssetsByCreatorTool, SolanaGetInfoTool, + SolanaCrossChainSwapTool, SolanaAlloraGetPriceInference, SolanaAlloraGetAllTopics, SolanaAlloraGetInferenceByTopicId, @@ -248,6 +250,7 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaGetAssetsByAuthorityTool(solanaKit), new SolanaGetAssetsByCreatorTool(solanaKit), new SolanaSwitchboardSimulateFeed(solanaKit), + new SolanaCrossChainSwapTool(solanaKit), new SolanaAlloraGetAllTopics(solanaKit), new SolanaAlloraGetInferenceByTopicId(solanaKit), new SolanaAlloraGetPriceInference(solanaKit), diff --git a/src/langchain/mayan/index.ts b/src/langchain/mayan/index.ts new file mode 100644 index 00000000..47b3e6ff --- /dev/null +++ b/src/langchain/mayan/index.ts @@ -0,0 +1 @@ +export * from "./swap"; diff --git a/src/langchain/mayan/swap.ts b/src/langchain/mayan/swap.ts new file mode 100644 index 00000000..65879693 --- /dev/null +++ b/src/langchain/mayan/swap.ts @@ -0,0 +1,47 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaCrossChainSwapTool extends Tool { + name = "cross_chain_swap"; + description = `This tool can be used to swap tokens between different chains using Mayan SDK. + + Inputs ( input is a JSON string): + amount: string, eg "0.02" or "7" + fromChain: string, eg "solana" or "ethereum" + fromToken: string, eg "sol" or "Pol" or "0x0000000000000000000000000000000000000000" or "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux" + toChain: string, eg "solana" or "ethereum" + toToken: string, eg "SOL" or "eth" or "0x0000000000000000000000000000000000000000" or "0x6b175474e89094c44da98b954eedeac495271d0f" + dstAddr: string, eg "4ZgCP2idpqrxuQNfsjakJEm9nFyZ2xnT4CrDPKPULJPk" or "0x0cae42c0cE52E6E64C1e384fF98e686C6eE225f0" + slippageBps: number, eg 10 (optional)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + + const url = await this.solanaKit.swap( + parsedInput.amount, + parsedInput.fromChain, + parsedInput.fromToken, + parsedInput.toChain, + parsedInput.toToken, + parsedInput.dstAddr, + parsedInput.slippageBps ?? "auto", + ); + return JSON.stringify({ + status: "success", + message: "Swap executed successfully", + url, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index b9a520f0..eff0e276 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -27,5 +27,6 @@ export * from "./squads"; export * from "./meteora"; export * from "./helius"; export * from "./voltr"; +export * from "./mayan"; export * from "./allora"; export * from "./switchboard"; 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/index.ts b/src/tools/mayan/index.ts new file mode 100644 index 00000000..47b3e6ff --- /dev/null +++ b/src/tools/mayan/index.ts @@ -0,0 +1 @@ +export * from "./swap"; diff --git a/src/tools/mayan/swap.ts b/src/tools/mayan/swap.ts new file mode 100644 index 00000000..1644a3be --- /dev/null +++ b/src/tools/mayan/swap.ts @@ -0,0 +1,353 @@ +import { + addresses, + ChainName, + Erc20Permit, + fetchQuote, + fetchTokenList, + Quote, + swapFromEvm, + swapFromSolana, +} from "@mayanfinance/swap-sdk"; +import { SolanaAgentKit } from "../../agent"; +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, + amount: string, + fromChain: string, + fromToken: string, + toChain: string, + toToken: string, + 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, + toChain: toChain as ChainName, + fromToken, + toToken, + slippageBps, + }); + if (quotes.length === 0) { + throw new Error( + "There is no quote available for the tokens you requested.", + ); + } + + let txHash; + if (fromChain === "solana") { + txHash = await swapSolana(quotes[0], agent, dstAddr); + } else { + txHash = await swapEVM(quotes[0], agent, dstAddr); + } + + return `https://explorer.mayan.finance/swap/${txHash}`; +} + +async function swapSolana( + quote: Quote, + agent: SolanaAgentKit, + dstAddr: string, +): Promise { + const jitoConfig = await getJitoConfig(); + const jitoTipLamports = await getJitoTipLamports(); + const jitoTip = jitoConfig?.enable + ? Math.min( + jitoTipLamports || jitoConfig?.defaultTipLamports, + jitoConfig?.maxTipLamports, + ) + : 0; + + const jitoOptions = { + tipLamports: jitoTip, + jitoAccount: jitoConfig.jitoAccount, + jitoSendUrl: jitoConfig.sendBundleUrl, + signAllTransactions: async ( + trxs: T[], + ): Promise => { + for (let i = 0; i < trxs.length; i++) { + if ("version" in trxs[i]) { + (trxs[i] as VersionedTransaction).sign([agent.wallet]); + } else { + (trxs[i] as Transaction).partialSign(agent.wallet); + } + } + return trxs; + }, + }; + + const signer = async ( + trx: T, + ): Promise => { + if ("version" in trx) { + (trx as VersionedTransaction).sign([agent.wallet]); + } else { + (trx as Transaction).partialSign(agent.wallet); + } + return trx; + }; + + const swapRes = await swapFromSolana( + quote, + agent.wallet.publicKey.toString(), + dstAddr, + null, + signer, + agent.connection, + [], + { skipPreflight: true }, + jitoOptions, + ); + if (!swapRes.signature) { + throw new Error("Error on swap from solana. Try again."); + } + + try { + const { blockhash, lastValidBlockHeight } = + await agent.connection.getLatestBlockhash(); + const result = await agent.connection.confirmTransaction( + { + signature: swapRes.signature, + blockhash: blockhash, + lastValidBlockHeight: lastValidBlockHeight, + }, + "confirmed", + ); + if (result?.value.err) { + throw new Error(`Transaction ${swapRes.serializedTrx} reverted!`); + } + return swapRes.signature; + } catch (error) { + // Wait for 3 sec and check mayan explorer + await new Promise((resolve) => setTimeout(resolve, 3000)); + const res = await fetch( + `https://explorer-api.mayan.finance/v3/swap/trx/${swapRes.signature}`, + ); + if (res.status !== 200) { + throw error; + } + return swapRes.signature; + } +} + +let evmWallet: Wallet | null; + +async function swapEVM( + quote: Quote, + agent: SolanaAgentKit, + dstAddr: string, +): Promise { + if (!evmWallet) { + if (agent.config.ETHEREUM_PRIVATE_KEY) { + evmWallet = new Wallet(agent.config.ETHEREUM_PRIVATE_KEY); + } else { + throw new Error("You haven't provided EVM wallet private key."); + } + } + 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, + dstAddr, + null, + signer, + permit, + null, + null, + ); + if (typeof swapRes === "string") { + return swapRes; + } + return (swapRes as TransactionResponse).hash; +} + +type SolanaJitoConfig = { + enable: boolean; + defaultTipLamports: number; + maxTipLamports: number; + sendBundleUrl: string; + jitoAccount: string; +}; + +type SiaResponse = Readonly<{ + solanaJitoConfig: SolanaJitoConfig; +}>; + +async function getJitoConfig(): Promise { + const res = await fetch(`https://sia.mayan.finance/v4/init`); + const data: SiaResponse = await res.json(); + return data.solanaJitoConfig; +} + +async function getJitoTipLamports() { + const res = await fetch(`https://price-api.mayan.finance/jito-tips/suggest`); + const data = await res.json(); + const tip = + typeof data?.default === "number" && Number.isFinite(data.default) + ? data?.default?.toFixed(9) + : 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/src/types/index.ts b/src/types/index.ts index 859340ee..5729a778 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,6 +12,7 @@ export interface Config { FLEXLEND_API_KEY?: string; HELIUS_API_KEY?: string; PRIORITY_LEVEL?: string; // medium, high, or veryHigh + ETHEREUM_PRIVATE_KEY?: string; ALLORA_API_KEY?: string; ALLORA_API_URL?: string; ALLORA_NETWORK?: string; 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"]