├── .editorconfig ├── .gitignore ├── .node-version ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── addresses.ts ├── api.ts ├── cctp.ts ├── evm │ ├── ERC20Artifact.ts │ ├── HCDepositInitiatorArtifact.ts │ ├── MayanCircleArtifact.ts │ ├── MayanFastMctpArtifact.ts │ ├── MayanForwarderArtifact.ts │ ├── MayanMonoChainArtifact.ts │ ├── MayanSwapArtifact.ts │ ├── MayanSwiftArtifact.ts │ ├── ShuttleArtifact.ts │ ├── evmFastMctp.ts │ ├── evmHyperCore.ts │ ├── evmMctp.ts │ ├── evmMonoChain.ts │ ├── evmShuttle.ts │ ├── evmSwap.ts │ ├── evmSwift.ts │ └── index.ts ├── index.ts ├── solana │ ├── index.ts │ ├── jupiter.ts │ ├── solanaHyperCore.ts │ ├── solanaMctp.ts │ ├── solanaMonoChain.ts │ ├── solanaSwap.ts │ ├── solanaSwift.ts │ └── utils.ts ├── sui │ ├── index.ts │ ├── suiHyperCore.ts │ ├── suiMctp.ts │ ├── suiSwap.ts │ └── utils.ts ├── types.ts ├── utils.ts └── wormhole.ts ├── tsconfig.json └── tsup.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # The JSON files contain newlines inconsistently 13 | [*.json] 14 | insert_final_newline = ignore 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .idea 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | dist 90 | .rts2_cache_* 91 | schema.json 92 | _gen 93 | lib 94 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019-2020 Hugo Di Francesco 3 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Mayan Cross-Chain Swap SDK 3 | A minimal package for sending cross-chain swap transactions 4 | 5 | ## Installation: 6 | 7 | ```bash 8 | npm install --save @mayanfinance/swap-sdk 9 | ``` 10 | 11 | ## Usage: 12 | 13 | Import the necessary functions and models: 14 | 15 | ```javascript 16 | import { fetchQuote, swapFromEvm, swapFromSolana, Quote, createSwapFromSuiMoveCalls } from '@mayanfinance/swap-sdk' 17 | ``` 18 | 19 | Then you will need to get a quote: 20 | 21 | ### Getting Quote: 22 | ```javascript 23 | const quotes = await fetchQuote({ 24 | amountIn64: "250000000", // if fromToken is USDC means 250 USDC 25 | fromToken: fromToken.contract, 26 | toToken: toToken.contract, 27 | fromChain: "avalanche", 28 | toChain: "solana", 29 | slippageBps: "auto", 30 | gasDrop: 0.04, // optional 31 | referrer: "YOUR SOLANA WALLET ADDRESS", // optional 32 | referrerBps: 5, // optional 33 | }); 34 | ``` 35 | > `slippageBps` can either be a specific basis point number or the string **"auto"**. When set to "auto", the system determines the safest slippage based on the input and output tokens. 36 | You can also provide slippageBps directly as a number in basis points; for example, 300 means 3%. Regardless of whether you pass "auto" or a basis point number, the `slippageBps` field in the quote response will always be returned as a **basis point number**. 37 | 38 | > see the list of supported chains [here](./src/types.ts#L13). 39 | 40 | You can get the list of supported tokens using [Tokens API](https://price-api.mayan.finance/swagger/) 41 | 42 | #### Gas on destination: 43 | To enable [Gas on destination](https://docs.mayan.finance/dapp/gas-on-destination) set the gasDrop param to the amount of native token (e.g. ETH, BNB..) you want to receive on the destination chain. 44 | 45 | 46 | ``` 47 | Maximum supported amount of gasDrop for each destination chain: 48 | 49 | ethereum: 0.05 ETH 50 | bsc: 0.02 BNB 51 | polygon: 0.2 MATIC 52 | avalanche: 0.2 AVAX 53 | solana: 0.2 SOL 54 | arbitrum: 0.01 ETH 55 | optimism: 0.01 ETH 56 | unichain: 0.01 ETH 57 | base: 0.01 ETH 58 | ``` 59 | 60 | #### Referrer fee: 61 | > If you want to receive [referrer fee](https://docs.mayan.finance/integration/referral), set the `referrer` param to your wallet address. 62 | 63 | #### Slippage: 64 | > Slippage is in bps (basis points), so 300 means "up to three percent slippage". 65 | 66 |
67 | After you get the quote, you can build and send the swap transaction: 68 | 69 | ### Bridge from Solana: 70 | 71 | ```javascript 72 | swapTrx = await swapFromSolana(quotes[0], originWalletAddress, destinationWalletAddress, referrerAddresses, signSolanaTransaction, solanaConnection) 73 | ``` 74 |
75 | 76 | `referrerAddresses` is an optional object with two keys `evm` and `solana` that contains the referrer addresses for each network type. 77 |
78 | example: 79 | 80 | ```javascript 81 | { 82 | evm: "YOUR EVM WALLET", 83 | solana: "YOUR SOLANA WALLET", 84 | sui: "YOUR SUI WALLET" 85 | } 86 | ``` 87 |
88 | 89 | If you need more control over the transaction and manually send the trx you can use `createSwapFromSolanaInstructions` function to build the solana instruction. 90 | 91 | 92 | ### Bridge from EVM: 93 | 94 | ```javascript 95 | swapTrx = await swapFromEvm(quotes[0], destinationWalletAddress, referrerAddress, provider, signer, permit?) 96 | ``` 97 | 98 | #### ERC20 Allowance 99 | 100 | * If you want to initiate a swap using an ERC20 token as the input, ensure that you have already approved sufficient allowance for the Mayan Forwarder contract. The Forwarder's address can be accessed via `addresses.MAYAN_FORWARDER_CONTRACT`. 101 | 102 | 103 | * Alternatively, the user can sign a permit message ([EIP-2612](https://eips.ethereum.org/EIPS/eip-2612)). The permit parameter is optional; you can pass the permit object to the function if the input token supports the permit standard. The permit object should contain the following fields: 104 | 105 | ```javascript 106 | { 107 | value: bigint, 108 | deadline: number, 109 | v: number, 110 | r: string, 111 | s: string, 112 | } 113 | ``` 114 |
115 | 116 | ### Bridge from Sui 117 | The `createSwapFromSuiMoveCalls` function returns a Transaction instance containing all the required Move calls. This transaction should then be signed by the user's wallet and broadcast to the Sui network. 118 | 119 | ```javascript 120 | const bridgeFromSuiMoveCalls = await createSwapFromSuiMoveCalls( 121 | quote, // Quote 122 | originWalletAddress, // string 123 | destinationWalletAddress, // string 124 | referrerAddresses, // Optional(ReferrerAddresses) 125 | customPayload, // Optional(Uint8Array | Buffer) 126 | suiClient, // SuiClient 127 | options, // Optional(ComposableSuiMoveCallsOptions) 128 | ); 129 | 130 | await suiClient.signAndExecuteTransaction({ 131 | signer: suiKeypair, 132 | transaction: bridgeFromSuiMoveCalls, 133 | }); 134 | ``` 135 | 136 | #### Composability on Move Calls and Input Coin 137 | 138 | The SDK offers composability for advanced use cases where you want to integrate bridge Move calls into an existing Sui transaction or use a specific coin as the input for bridging. 139 | 140 | - **Custom Move Calls**: To compose the bridge logic into an existing transaction, pass your transaction through the `builtTransaction` parameter. The bridge Move calls will be appended, allowing you to sign and send the combined transaction. 141 | 142 | - **Custom Input Coin**: If you'd like to use a specific coin (e.g., one returned from earlier Move calls) as the input for the bridge, provide it via the `inputCoin` parameter. 143 | 144 | ```javascript 145 | type ComposableSuiMoveCallsOptions = { 146 | builtTransaction?: SuiTransaction; 147 | inputCoin?: SuiFunctionParameter; 148 | } 149 | ``` 150 |
151 | 152 | ### Depositing on HyperCore (Hyperliquid Core) as a Destination 153 | 154 | 155 | To deposit into HyperCore, start by fetching a quote as described earlier, just set the `toChain` parameter to `hypercore`. Then, depending on the source chain, use the appropriate transaction-building method, also covered above. 156 | 157 | The key difference when depositing **USDC on HyperCore** is that you must pass a `usdcPermitSignature` in the options object when building the transaction. 158 | 159 | You can generate this signature using the `getHyperCoreUSDCDepositPermitParams` helper function as shown below: 160 | 161 | ```javascript 162 | import { getHyperCoreUSDCDepositPermitParams } from '@mayanfinance/swap-sdk'; 163 | 164 | const arbitrumProvider = new ethers.providers.JsonRpcProvider('ARBITRUM_RPC_URL'); 165 | const arbDestUserWallet = new ethers.Wallet('USER_ARBITRUM_WALLET_PRIVATE_KEY', arbitrumProvider); 166 | 167 | const { value, domain, types } = await getHyperCoreUSDCDepositPermitParams( 168 | quote, 169 | userDestAddress, 170 | arbitrumProvider 171 | ); 172 | 173 | const permitSignature = await arbDestUserWallet.signTypedData(domain, types, value); 174 | ``` 175 |
176 | 177 | #### Gasless Transaction: 178 | > If the selected quote's `gasless` parameter is set to true (`quote.gasless == true`), the return value of the `swapFromEvm` function will be the order hash of the `string` type. This hash can be queried on the Mayan Explorer API, similar to a transaction hash. 179 | 180 | 181 | 182 | 183 | If you need to get the transaction payload and send it manually, you can use `getSwapFromEvmTxPayload` function to build the EVM transaction payload. 184 | 185 | #### Contract Level Integration: 186 | >If you aim to integrate the Mayan protocol at the contract level, you can use the `_forwarder` object returned from the `getSwapFromEvmTxPayload`. It contains the method name and parameters for a contract level method call. 187 | 188 | ### Tracking: 189 | To track the progress of swaps, you can use [Mayan Explorer API](https://explorer-api.mayan.finance/swagger/#/default/SwapDetailsController_getSwapByTrxHash) by passing the transaction hash of the swap transaction. 190 | 191 |
192 | The response contains a lot of info about the swap but the important field is `clientStatus` which can be one of the following values: 193 | 194 | - `INPROGRESS` - the swap is being processed 195 | - `COMPLETED` - the swap is completed 196 | - `REFUNDED` - the swap has refunded 197 | 198 |
199 | 200 | ## 📱 React Native Support (Solana Mobile SDK): 201 | 202 | You can also use this SDK in your react native app: 203 |
204 | ```javascript 205 | import { transact, Web3MobileWallet } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js'; 206 | ``` 207 | 208 | For swaps from solana after importing the above functions from Solana Mobile SDK you have to pass a callback function that calls `transact` function as the `signSolanaTransaction` parameter of `swapFromSolana` function: 209 | 210 | 211 | ```javascript 212 | const signSolanaTransaction = useCallback( 213 | async (tx: Transaction) => { 214 | return await transact(async (wallet: Web3MobileWallet) => { 215 | authorizeSession(wallet); 216 | const signedTransactions = await wallet.signTransactions({ 217 | transactions: [tx], 218 | }); 219 | 220 | return signedTransactions[0]; 221 | }); 222 | }, 223 | [authorizeSession], 224 | ); 225 | ``` 226 | 227 | For swaps from EVM you can use `useWalletConnectModal` hook from [WalletConnet](https://github.com/WalletConnect/modal-react-native) to get the provider and pass it to `swapFromEvm` function as the `signer`: 228 | 229 | ```javascript 230 | import {useWalletConnectModal} from '@walletconnect/modal-react-native'; 231 | ... 232 | const { provider: evmWalletProvider} = 233 | useWalletConnectModal(); 234 | ... 235 | const web3Provider = new ethers.providers.Web3Provider( 236 | evmWalletProvider, 237 | ); 238 | const signer = web3Provider.getSigner(0); 239 | ``` 240 | 241 | To learn more about how to use Mayan SDK in a react-native project, you can check [this scaffold](https://github.com/mayan-finance/react-native-scaffold). 242 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mayanfinance/swap-sdk", 3 | "description": "A SDK to swap with Mayan", 4 | "version": "11.4.0", 5 | "source": "src/main.ts", 6 | "main": "dist/index.js", 7 | "module": "dist/index.esm.min.js", 8 | "unpkg": "dist/index.iife.min.js", 9 | "types": "dist/index.d.ts", 10 | "homepage": "https://github.com/mayan-finance/swap-sdk#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mayan-finance/swap-sdk.git" 14 | }, 15 | "keywords": [ 16 | "mayan", 17 | "mayanfinance", 18 | "mayansdk", 19 | "swap-sdk" 20 | ], 21 | "exports": { 22 | ".": { 23 | "require": "./dist/index.js", 24 | "import": "./dist/index.mjs", 25 | "types": "./dist/index.d.ts" 26 | } 27 | }, 28 | "files": [ 29 | "dist/**/*", 30 | "LICENSE", 31 | "README.md" 32 | ], 33 | "scripts": { 34 | "build": "tsup --clean && tsup --minify", 35 | "watch": "tsup --watch", 36 | "lint": "prettier src -c", 37 | "format": "prettier src --write", 38 | "prepack": "npm run build", 39 | "release": "npm run prepack && npx np --no-tests" 40 | }, 41 | "devDependencies": { 42 | "prettier": "^2.6.2", 43 | "tsup": "^6.7.0", 44 | "typescript": "^5.8.3" 45 | }, 46 | "prettier": { 47 | "singleQuote": true 48 | }, 49 | "license": "MIT", 50 | "publishConfig": { 51 | "registry": "https://registry.npmjs.org", 52 | "access": "public" 53 | }, 54 | "author": "Jiri", 55 | "dependencies": { 56 | "@mysten/sui": "^1.34.0", 57 | "@noble/hashes": "1.8.0", 58 | "@solana/buffer-layout": "^4 || ^3", 59 | "@solana/web3.js": "^1.87.6", 60 | "bs58": "^6.0.0", 61 | "cross-fetch": "^3.1.5", 62 | "ethers": "^6", 63 | "js-sha3": "^0.8.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/addresses.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "MAYAN_FORWARDER_CONTRACT": "0x337685fdaB40D39bd02028545a4FfA7D287cC3E2", 3 | "MAYAN_PROGRAM_ID": "FC4eXxkyrMPTjiYUpp4EAnkmwMbQyZ6NDCh1kfLn6vsf", 4 | "AUCTION_PROGRAM_ID": "8QJmxZcEzwuYmCPy6XqgN2sHcYCcFq6AEfBMJZZuLo5a", 5 | "MCTP_PROGRAM_ID": "dkpZqrxHFrhziEMQ931GLtfy11nFkCsfMftH9u6QwBU", 6 | "SWIFT_PROGRAM_ID": "BLZRi6frs4X4DNLw56V4EXai1b6QVESN1BhHBTYM9VcY", 7 | "FEE_MANAGER_PROGRAM_ID": "5VtQHnhs2pfVEr68qQsbTRwKh4JV5GTu9mBHgHFxpHeQ", 8 | "WORMHOLE_PROGRAM_ID": "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth", 9 | "WORMHOLE_SHIM_POST_MESSAGE_PROGRAM_ID": "EtZMZM22ViKMo4r5y4Anovs3wKQ2owUmDpjygnMMcdEX", 10 | "CCTP_CORE_PROGRAM_ID": "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd", 11 | "CCTP_TOKEN_PROGRAM_ID": "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3", 12 | "TOKEN_PROGRAM_ID": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 13 | "TOKEN_2022_PROGRAM_ID": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", 14 | "ASSOCIATED_TOKEN_PROGRAM_ID": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", 15 | "SPL_UTILS_PROGRAM_ID": "B96dV3Luxzo6SokJx3xt8i5y8Mb7HRR6Eec8hCjJDT69", 16 | "LOOKUP_TABLE": "Ff3yi1meWQQ19VPZMzGg6H8JQQeRudiV7QtVtyzJyoht", 17 | "SUI_MCTP_STATE": "0xb787fe0f7530b4fd2162fa0cc92f4f6c5a97c54b4c5c55eb04ab29f4b803ac9c", 18 | "SUI_MCTP_FEE_MANAGER_STATE": "0xa1b4a96ce93d36dd0bbce0adc39533a07d2f32928918c80cd6fe7868320978f2", 19 | "SUI_CCTP_CORE_PACKAGE_ID": "0x08d87d37ba49e785dde270a83f8e979605b03dc552b5548f26fdf2f49bf7ed1b", 20 | "SUI_CCTP_CORE_STATE": "0xf68268c3d9b1df3215f2439400c1c4ea08ac4ef4bb7d6f3ca6a2a239e17510af", 21 | "SUI_CCTP_TOKEN_PACKAGE_ID": "0x2aa6c5d56376c371f88a6cc42e852824994993cb9bab8d3e6450cbe3cb32b94e", 22 | "SUI_CCTP_TOKEN_STATE": "0x45993eecc0382f37419864992c12faee2238f5cfe22b98ad3bf455baf65c8a2f", 23 | "SUI_WORMHOLE_PACKAGE_ID": "0x5306f64e312b581766351c07af79c72fcb1cd25147157fdc2f8ad76de9a3fb6a", 24 | "SUI_WORMHOLE_STATE": "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c", 25 | "SUI_LOGGER_PACKAGE_ID": "0x05680e9030c147b413a489f7891273acc221d49bd061c433e5771bc170fc37ac", 26 | "EXPLORER_URL": "https://explorer-api.mayan.finance/v3", 27 | "PRICE_URL": "https://price-api.mayan.finance/v3", 28 | "RELAYER_URL": "https://relayer-api.mayan.finance/v3", 29 | "GAS_ESTIMATE_URL": "https://gas-estimate.mayan.finance/v2", 30 | "HC_ARBITRUM_BRIDGE": "0x2df1c51e09aecf9cacb7bc98cb1742757f163df7", 31 | "HC_ARBITRUM_DEPOSIT_PROCESSOR": "0xdDd77e62C848C3334f1148d4F1457FC59ede4E4B", 32 | "ARBITRUM_USDC_CONTRACT": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", 33 | "PAYLOAD_WRITER_PROGRAM_ID": "DwMLtdtJqJQkHzNcrdTBuWHJByJfgpKBnvFvzyKdy3cU", 34 | "CPI_PROXY_PROGRAM_ID": "D8C8iW6zmoKg5TRr8nQ7h14TMWqQX8FiBdj2ju5MF3wa", 35 | } 36 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | import { 3 | Token, 4 | ChainName, 5 | QuoteParams, 6 | Quote, 7 | QuoteOptions, 8 | QuoteError, 9 | SolanaClientSwap, 10 | GetSolanaSwapParams, 11 | TokenStandard, 12 | GetSuiSwapParams, 13 | SuiClientSwap, 14 | EstimateGasEvmParams 15 | } from './types'; 16 | import addresses from './addresses'; 17 | import { checkSdkVersionSupport, getSdkVersion } from './utils'; 18 | import { SwiftEvmGasLessParams } from './evm/evmSwift'; 19 | 20 | function toQueryString(params: Record): string { 21 | return Object.entries(params) 22 | .filter(([_, value]) => value !== undefined && value !== null && !Array.isArray(value)) 23 | .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) 24 | .join('&'); 25 | } 26 | 27 | async function check5xxError(res: Response): Promise { 28 | if (res.status.toString().startsWith('5')) { 29 | let error: Error | QuoteError; 30 | try { 31 | const err = await res.json(); 32 | if ((err?.code || err?.statusCode) && (err?.message || err?.msg)) { 33 | error = { 34 | code: err?.code || err?.statusCode, 35 | message: err?.message || err?.msg, 36 | } as QuoteError 37 | } 38 | } catch (err) { 39 | error = new Error('Internal server error'); 40 | } 41 | throw error; 42 | } 43 | } 44 | 45 | export async function fetchAllTokenList(tokenStandards?: TokenStandard[]): Promise<{[index: string]: Token[]}> { 46 | const query = tokenStandards ? `?standard=${tokenStandards.join(',')}` : ''; 47 | const res = await fetch(`${addresses.PRICE_URL}/tokens${query}`, { 48 | method: 'GET', 49 | redirect: 'follow', 50 | }); 51 | await check5xxError(res); 52 | if (res.status === 200) { 53 | const result = await res.json(); 54 | return result as { [index: string]: Token[] }; 55 | } 56 | throw new Error('Cannot fetch Mayan tokens!'); 57 | } 58 | 59 | export async function fetchTokenList(chain: ChainName, nonPortal: boolean = false, tokenStandards?: TokenStandard[]): Promise { 60 | const queryParams = { 61 | chain, 62 | nonPortal, 63 | standard: tokenStandards ? tokenStandards?.join(',') : undefined, 64 | }; 65 | const res = await fetch(`${addresses.PRICE_URL}/tokens?${toQueryString(queryParams)}`); 66 | await check5xxError(res); 67 | if (res.status === 200) { 68 | const result = await res.json(); 69 | return result[chain] as Token[]; 70 | } 71 | throw new Error('Cannot fetch Mayan tokens!'); 72 | } 73 | 74 | export function generateFetchQuoteUrl(params: QuoteParams, quoteOptions: QuoteOptions = { 75 | wormhole: true, 76 | swift: true, 77 | mctp: true, 78 | shuttle: true, 79 | gasless: false, 80 | onlyDirect: false, 81 | fastMctp: true, 82 | fullList: false, 83 | payload: undefined, 84 | monoChain: true, 85 | }): string { 86 | const { gasDrop, referrerBps } = params; 87 | let slippageBps = params.slippageBps; 88 | if (slippageBps !== 'auto' && !Number.isFinite(slippageBps)) { 89 | slippageBps = params.slippage * 100; 90 | } 91 | const _quoteOptions: QuoteOptions = { 92 | wormhole: quoteOptions.wormhole !== false, // default to true 93 | swift: quoteOptions.swift !== false, // default to true 94 | mctp: quoteOptions.mctp !== false, // default to true 95 | shuttle: quoteOptions.shuttle === true, // default to false 96 | fastMctp: quoteOptions.fastMctp !== false, // default to true 97 | gasless: quoteOptions.gasless === true, // default to false 98 | onlyDirect: quoteOptions.onlyDirect === true, // default to false 99 | fullList: quoteOptions.fullList === true, // default to false 100 | payload: typeof quoteOptions.payload === 'string' ? quoteOptions.payload : undefined, 101 | monoChain: quoteOptions.monoChain !== false, // default to true 102 | } 103 | const queryParams: Record = { 104 | ..._quoteOptions, 105 | solanaProgram: addresses.MAYAN_PROGRAM_ID, 106 | forwarderAddress: addresses.MAYAN_FORWARDER_CONTRACT, 107 | amountIn: !params.amountIn64 && Number.isFinite(params.amount) ? params.amount : undefined, 108 | amountIn64: params.amountIn64, 109 | fromToken: params.fromToken, 110 | fromChain: params.fromChain, 111 | toToken: params.toToken, 112 | toChain: params.toChain, 113 | slippageBps, 114 | referrer: params.referrer, 115 | referrerBps: Number.isFinite(referrerBps) ? referrerBps : undefined, 116 | gasDrop: Number.isFinite(gasDrop) ? gasDrop : undefined, 117 | sdkVersion: getSdkVersion(), 118 | }; 119 | const baseUrl = `${addresses.PRICE_URL}/quote?`; 120 | const queryString = toQueryString(queryParams); 121 | return (baseUrl + queryString); 122 | } 123 | export async function fetchQuote(params: QuoteParams, quoteOptions: QuoteOptions = { 124 | swift: true, 125 | mctp: true, 126 | gasless: false, 127 | onlyDirect: false, 128 | }): Promise { 129 | const url = generateFetchQuoteUrl(params, quoteOptions); 130 | const res = await fetch(url, { 131 | method: 'GET', 132 | redirect: 'follow', 133 | }); 134 | await check5xxError(res); 135 | const result = await res.json(); 136 | if (res.status !== 200 && res.status !== 201) { 137 | throw { 138 | code: result?.code || 0, 139 | message: result?.msg || result?.message || 'Route not found', 140 | data: result?.data, 141 | } as QuoteError 142 | } 143 | if (!checkSdkVersionSupport(result.minimumSdkVersion)) { 144 | throw { 145 | code: 9999, 146 | message: 'Swap SDK is outdated!', 147 | } as QuoteError 148 | } 149 | return result.quotes as Quote[]; 150 | } 151 | 152 | export async function getCurrentChainTime(chain: ChainName): Promise { 153 | const res = await fetch(`${addresses.PRICE_URL}/clock/${chain}`, { 154 | method: 'GET', 155 | redirect: 'follow', 156 | }); 157 | await check5xxError(res); 158 | const result = await res.json(); 159 | if (res.status !== 200 && res.status !== 201) { 160 | throw result; 161 | } 162 | return result.clock; 163 | } 164 | 165 | export async function getSuggestedRelayer(): Promise { 166 | const res = await fetch(`${addresses.RELAYER_URL}/active-relayers?solanaProgram=${addresses.MAYAN_PROGRAM_ID}`, { 167 | method: 'GET', 168 | redirect: 'follow', 169 | }); 170 | await check5xxError(res); 171 | const result = await res.json(); 172 | if (res.status !== 200 && res.status !== 201) { 173 | throw result; 174 | } 175 | return result.suggested; 176 | } 177 | 178 | 179 | export async function getSwapSolana(params : GetSolanaSwapParams): Promise { 180 | const query = toQueryString({ 181 | ...params, 182 | sdkVersion: getSdkVersion(), 183 | }); 184 | const res = await fetch(`${addresses.PRICE_URL}/get-swap/solana?${query}`, { 185 | method: 'GET', 186 | redirect: 'follow', 187 | }); 188 | await check5xxError(res); 189 | const result = await res.json(); 190 | if (res.status !== 200 && res.status !== 201) { 191 | throw result; 192 | } 193 | return result; 194 | } 195 | 196 | export async function getSwapSui(params : GetSuiSwapParams): Promise { 197 | const requestBody = JSON.stringify({ 198 | ...params, 199 | sdkVersion: getSdkVersion(), 200 | }); 201 | const requestUrl = `${addresses.PRICE_URL}/get-swap/sui`; 202 | 203 | const res = await fetch(requestUrl, { 204 | method: 'POST', 205 | redirect: 'follow', 206 | body: requestBody, 207 | headers: { 208 | 'Content-Type': 'application/json', 209 | }, 210 | }); 211 | await check5xxError(res); 212 | const result = await res.json(); 213 | if (res.status !== 200 && res.status !== 201) { 214 | throw result; 215 | } 216 | return result; 217 | } 218 | 219 | export async function submitSwiftEvmSwap(params: SwiftEvmGasLessParams, signature: string): Promise { 220 | const res = await fetch(`${addresses.EXPLORER_URL}/submit/evm`, { 221 | method: 'POST', 222 | headers: { 223 | 'Content-Type': 'application/json', 224 | }, 225 | body: JSON.stringify({ 226 | ...params, 227 | signature, 228 | }, (_key, value) => { 229 | if (typeof value === 'bigint') { 230 | return value.toString(); 231 | } 232 | return value; 233 | }), 234 | }); 235 | await check5xxError(res); 236 | } 237 | 238 | export async function submitSwiftSolanaSwap(signedTx: string): Promise<{ orderHash: string }> { 239 | const res = await fetch(`${addresses.EXPLORER_URL}/submit/solana`, { 240 | method: 'POST', 241 | headers: { 242 | 'Content-Type': 'application/json', 243 | }, 244 | body: JSON.stringify({ 245 | signedTx, 246 | }), 247 | }); 248 | await check5xxError(res); 249 | const result = await res.json(); 250 | if (res.status !== 200 && res.status !== 201) { 251 | throw result; 252 | } 253 | return result; 254 | } 255 | 256 | 257 | export async function checkHyperCoreDeposit(destinationAddress: string, tokenAddress: string): Promise { 258 | const query = toQueryString({ 259 | destWallet: destinationAddress, 260 | destToken: tokenAddress, 261 | sdkVersion: getSdkVersion(), 262 | }); 263 | const res = await fetch(`${addresses.EXPLORER_URL}/hypercore/is-allowed?${query}`, { 264 | method: 'GET', 265 | redirect: 'follow', 266 | }); 267 | await check5xxError(res); 268 | const result = await res.json(); 269 | if (res.status !== 200 && res.status !== 201) { 270 | throw result; 271 | } 272 | return result.allowed === true; 273 | } 274 | 275 | export async function getEstimateGasEvm( 276 | params: EstimateGasEvmParams 277 | ): Promise<{ 278 | estimatedGas: bigint; 279 | gasPrice: bigint; 280 | }> { 281 | const res = await fetch(`${addresses.GAS_ESTIMATE_URL}/evm`, { 282 | method: 'POST', 283 | headers: { 284 | 'Content-Type': 'application/json', 285 | }, 286 | body: JSON.stringify({ 287 | ...params, 288 | sdkVersion: getSdkVersion(), 289 | }), 290 | }); 291 | await check5xxError(res); 292 | const result = await res.json(); 293 | if (res.status !== 200 && res.status !== 201) { 294 | throw result; 295 | } 296 | return { 297 | estimatedGas: BigInt(result.estimatedGas), 298 | gasPrice: BigInt(result.gasPrice), 299 | }; 300 | } 301 | -------------------------------------------------------------------------------- /src/cctp.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import addresses from './addresses'; 3 | import { Buffer } from 'buffer'; 4 | import {ChainName} from "./types"; 5 | 6 | export const CCTP_TOKEN_DECIMALS = 6; 7 | export function getCCTPDomain(chain: ChainName): number { 8 | switch (chain) { 9 | case 'ethereum': 10 | return 0; 11 | case 'avalanche': 12 | return 1; 13 | case 'optimism': 14 | return 2; 15 | case 'arbitrum': 16 | return 3; 17 | case 'solana': 18 | return 5; 19 | case 'base': 20 | return 6; 21 | case 'polygon': 22 | return 7; 23 | case 'sui': 24 | return 8; 25 | case 'unichain': 26 | return 10; 27 | case 'linea': 28 | return 11; 29 | case 'sonic': 30 | return 13; 31 | case 'hyperevm': 32 | return 19; 33 | default: 34 | throw new Error('unsupported chain for cctp'); 35 | } 36 | } 37 | 38 | export function getCCTPBridgePDAs(mint: PublicKey, destinationChain: ChainName): { 39 | messageTransmitter: PublicKey, 40 | senderAuthority: PublicKey, 41 | localToken: PublicKey, 42 | tokenMessenger: PublicKey, 43 | tokenMinter: PublicKey, 44 | remoteTokenMessengerKey: PublicKey, 45 | eventAuthCore: PublicKey, 46 | eventAuthToken: PublicKey, 47 | } { 48 | const cctpCoreProgramId = new PublicKey(addresses.CCTP_CORE_PROGRAM_ID); 49 | const cctpTokenProgramId = new PublicKey(addresses.CCTP_TOKEN_PROGRAM_ID); 50 | 51 | const [messageTransmitter] = PublicKey.findProgramAddressSync( 52 | [Buffer.from('message_transmitter')], 53 | cctpCoreProgramId, 54 | ); 55 | 56 | const [senderAuthority] = PublicKey.findProgramAddressSync( 57 | [Buffer.from('sender_authority')], 58 | cctpTokenProgramId, 59 | ); 60 | 61 | const [localToken] = PublicKey.findProgramAddressSync( 62 | [Buffer.from('local_token'), mint.toBytes()], 63 | cctpTokenProgramId, 64 | ); 65 | 66 | const [tokenMessenger] = PublicKey.findProgramAddressSync( 67 | [Buffer.from('token_messenger')], 68 | cctpTokenProgramId, 69 | ); 70 | const [tokenMinter] = PublicKey.findProgramAddressSync( 71 | [Buffer.from('token_minter')], 72 | cctpTokenProgramId, 73 | ); 74 | 75 | const destinationDomain = getCCTPDomain(destinationChain); 76 | 77 | const [remoteTokenMessengerKey] = PublicKey.findProgramAddressSync( 78 | [Buffer.from('remote_token_messenger'), Buffer.from(destinationDomain.toString())], 79 | cctpTokenProgramId, 80 | ); 81 | 82 | const [eventAuthCore] = PublicKey.findProgramAddressSync( 83 | [Buffer.from('__event_authority')], 84 | cctpCoreProgramId, 85 | ); 86 | 87 | const [eventAuthToken] = PublicKey.findProgramAddressSync( 88 | [Buffer.from('__event_authority')], 89 | cctpTokenProgramId, 90 | ); 91 | 92 | return { 93 | messageTransmitter, 94 | senderAuthority, 95 | remoteTokenMessengerKey, 96 | tokenMessenger, 97 | tokenMinter, 98 | eventAuthToken, 99 | eventAuthCore, 100 | localToken, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/evm/ERC20Artifact.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "_format": "hh-sol-artifact-1", 3 | "contractName": "ERC20Permit", 4 | "sourceName": "contracts/token/ERC20/extensions/ERC20Permit.sol", 5 | "abi": [ 6 | { 7 | "inputs": [], 8 | "name": "ECDSAInvalidSignature", 9 | "type": "error" 10 | }, 11 | { 12 | "inputs": [ 13 | { 14 | "internalType": "uint256", 15 | "name": "length", 16 | "type": "uint256" 17 | } 18 | ], 19 | "name": "ECDSAInvalidSignatureLength", 20 | "type": "error" 21 | }, 22 | { 23 | "inputs": [ 24 | { 25 | "internalType": "bytes32", 26 | "name": "s", 27 | "type": "bytes32" 28 | } 29 | ], 30 | "name": "ECDSAInvalidSignatureS", 31 | "type": "error" 32 | }, 33 | { 34 | "inputs": [ 35 | { 36 | "internalType": "address", 37 | "name": "spender", 38 | "type": "address" 39 | }, 40 | { 41 | "internalType": "uint256", 42 | "name": "allowance", 43 | "type": "uint256" 44 | }, 45 | { 46 | "internalType": "uint256", 47 | "name": "needed", 48 | "type": "uint256" 49 | } 50 | ], 51 | "name": "ERC20InsufficientAllowance", 52 | "type": "error" 53 | }, 54 | { 55 | "inputs": [ 56 | { 57 | "internalType": "address", 58 | "name": "sender", 59 | "type": "address" 60 | }, 61 | { 62 | "internalType": "uint256", 63 | "name": "balance", 64 | "type": "uint256" 65 | }, 66 | { 67 | "internalType": "uint256", 68 | "name": "needed", 69 | "type": "uint256" 70 | } 71 | ], 72 | "name": "ERC20InsufficientBalance", 73 | "type": "error" 74 | }, 75 | { 76 | "inputs": [ 77 | { 78 | "internalType": "address", 79 | "name": "approver", 80 | "type": "address" 81 | } 82 | ], 83 | "name": "ERC20InvalidApprover", 84 | "type": "error" 85 | }, 86 | { 87 | "inputs": [ 88 | { 89 | "internalType": "address", 90 | "name": "receiver", 91 | "type": "address" 92 | } 93 | ], 94 | "name": "ERC20InvalidReceiver", 95 | "type": "error" 96 | }, 97 | { 98 | "inputs": [ 99 | { 100 | "internalType": "address", 101 | "name": "sender", 102 | "type": "address" 103 | } 104 | ], 105 | "name": "ERC20InvalidSender", 106 | "type": "error" 107 | }, 108 | { 109 | "inputs": [ 110 | { 111 | "internalType": "address", 112 | "name": "spender", 113 | "type": "address" 114 | } 115 | ], 116 | "name": "ERC20InvalidSpender", 117 | "type": "error" 118 | }, 119 | { 120 | "inputs": [ 121 | { 122 | "internalType": "uint256", 123 | "name": "deadline", 124 | "type": "uint256" 125 | } 126 | ], 127 | "name": "ERC2612ExpiredSignature", 128 | "type": "error" 129 | }, 130 | { 131 | "inputs": [ 132 | { 133 | "internalType": "address", 134 | "name": "signer", 135 | "type": "address" 136 | }, 137 | { 138 | "internalType": "address", 139 | "name": "owner", 140 | "type": "address" 141 | } 142 | ], 143 | "name": "ERC2612InvalidSigner", 144 | "type": "error" 145 | }, 146 | { 147 | "inputs": [ 148 | { 149 | "internalType": "address", 150 | "name": "account", 151 | "type": "address" 152 | }, 153 | { 154 | "internalType": "uint256", 155 | "name": "currentNonce", 156 | "type": "uint256" 157 | } 158 | ], 159 | "name": "InvalidAccountNonce", 160 | "type": "error" 161 | }, 162 | { 163 | "inputs": [], 164 | "name": "InvalidShortString", 165 | "type": "error" 166 | }, 167 | { 168 | "inputs": [ 169 | { 170 | "internalType": "string", 171 | "name": "str", 172 | "type": "string" 173 | } 174 | ], 175 | "name": "StringTooLong", 176 | "type": "error" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "internalType": "address", 184 | "name": "owner", 185 | "type": "address" 186 | }, 187 | { 188 | "indexed": true, 189 | "internalType": "address", 190 | "name": "spender", 191 | "type": "address" 192 | }, 193 | { 194 | "indexed": false, 195 | "internalType": "uint256", 196 | "name": "value", 197 | "type": "uint256" 198 | } 199 | ], 200 | "name": "Approval", 201 | "type": "event" 202 | }, 203 | { 204 | "anonymous": false, 205 | "inputs": [], 206 | "name": "EIP712DomainChanged", 207 | "type": "event" 208 | }, 209 | { 210 | "anonymous": false, 211 | "inputs": [ 212 | { 213 | "indexed": true, 214 | "internalType": "address", 215 | "name": "from", 216 | "type": "address" 217 | }, 218 | { 219 | "indexed": true, 220 | "internalType": "address", 221 | "name": "to", 222 | "type": "address" 223 | }, 224 | { 225 | "indexed": false, 226 | "internalType": "uint256", 227 | "name": "value", 228 | "type": "uint256" 229 | } 230 | ], 231 | "name": "Transfer", 232 | "type": "event" 233 | }, 234 | { 235 | "inputs": [], 236 | "name": "DOMAIN_SEPARATOR", 237 | "outputs": [ 238 | { 239 | "internalType": "bytes32", 240 | "name": "", 241 | "type": "bytes32" 242 | } 243 | ], 244 | "stateMutability": "view", 245 | "type": "function" 246 | }, 247 | { 248 | "inputs": [ 249 | { 250 | "internalType": "address", 251 | "name": "owner", 252 | "type": "address" 253 | }, 254 | { 255 | "internalType": "address", 256 | "name": "spender", 257 | "type": "address" 258 | } 259 | ], 260 | "name": "allowance", 261 | "outputs": [ 262 | { 263 | "internalType": "uint256", 264 | "name": "", 265 | "type": "uint256" 266 | } 267 | ], 268 | "stateMutability": "view", 269 | "type": "function" 270 | }, 271 | { 272 | "inputs": [ 273 | { 274 | "internalType": "address", 275 | "name": "spender", 276 | "type": "address" 277 | }, 278 | { 279 | "internalType": "uint256", 280 | "name": "value", 281 | "type": "uint256" 282 | } 283 | ], 284 | "name": "approve", 285 | "outputs": [ 286 | { 287 | "internalType": "bool", 288 | "name": "", 289 | "type": "bool" 290 | } 291 | ], 292 | "stateMutability": "nonpayable", 293 | "type": "function" 294 | }, 295 | { 296 | "inputs": [ 297 | { 298 | "internalType": "address", 299 | "name": "account", 300 | "type": "address" 301 | } 302 | ], 303 | "name": "balanceOf", 304 | "outputs": [ 305 | { 306 | "internalType": "uint256", 307 | "name": "", 308 | "type": "uint256" 309 | } 310 | ], 311 | "stateMutability": "view", 312 | "type": "function" 313 | }, 314 | { 315 | "inputs": [], 316 | "name": "decimals", 317 | "outputs": [ 318 | { 319 | "internalType": "uint8", 320 | "name": "", 321 | "type": "uint8" 322 | } 323 | ], 324 | "stateMutability": "view", 325 | "type": "function" 326 | }, 327 | { 328 | "inputs": [], 329 | "name": "eip712Domain", 330 | "outputs": [ 331 | { 332 | "internalType": "bytes1", 333 | "name": "fields", 334 | "type": "bytes1" 335 | }, 336 | { 337 | "internalType": "string", 338 | "name": "name", 339 | "type": "string" 340 | }, 341 | { 342 | "internalType": "string", 343 | "name": "version", 344 | "type": "string" 345 | }, 346 | { 347 | "internalType": "uint256", 348 | "name": "chainId", 349 | "type": "uint256" 350 | }, 351 | { 352 | "internalType": "address", 353 | "name": "verifyingContract", 354 | "type": "address" 355 | }, 356 | { 357 | "internalType": "bytes32", 358 | "name": "salt", 359 | "type": "bytes32" 360 | }, 361 | { 362 | "internalType": "uint256[]", 363 | "name": "extensions", 364 | "type": "uint256[]" 365 | } 366 | ], 367 | "stateMutability": "view", 368 | "type": "function" 369 | }, 370 | { 371 | "inputs": [], 372 | "name": "name", 373 | "outputs": [ 374 | { 375 | "internalType": "string", 376 | "name": "", 377 | "type": "string" 378 | } 379 | ], 380 | "stateMutability": "view", 381 | "type": "function" 382 | }, 383 | { 384 | "inputs": [ 385 | { 386 | "internalType": "address", 387 | "name": "owner", 388 | "type": "address" 389 | } 390 | ], 391 | "name": "nonces", 392 | "outputs": [ 393 | { 394 | "internalType": "uint256", 395 | "name": "", 396 | "type": "uint256" 397 | } 398 | ], 399 | "stateMutability": "view", 400 | "type": "function" 401 | }, 402 | { 403 | "inputs": [ 404 | { 405 | "internalType": "address", 406 | "name": "owner", 407 | "type": "address" 408 | }, 409 | { 410 | "internalType": "address", 411 | "name": "spender", 412 | "type": "address" 413 | }, 414 | { 415 | "internalType": "uint256", 416 | "name": "value", 417 | "type": "uint256" 418 | }, 419 | { 420 | "internalType": "uint256", 421 | "name": "deadline", 422 | "type": "uint256" 423 | }, 424 | { 425 | "internalType": "uint8", 426 | "name": "v", 427 | "type": "uint8" 428 | }, 429 | { 430 | "internalType": "bytes32", 431 | "name": "r", 432 | "type": "bytes32" 433 | }, 434 | { 435 | "internalType": "bytes32", 436 | "name": "s", 437 | "type": "bytes32" 438 | } 439 | ], 440 | "name": "permit", 441 | "outputs": [], 442 | "stateMutability": "nonpayable", 443 | "type": "function" 444 | }, 445 | { 446 | "inputs": [], 447 | "name": "symbol", 448 | "outputs": [ 449 | { 450 | "internalType": "string", 451 | "name": "", 452 | "type": "string" 453 | } 454 | ], 455 | "stateMutability": "view", 456 | "type": "function" 457 | }, 458 | { 459 | "inputs": [], 460 | "name": "totalSupply", 461 | "outputs": [ 462 | { 463 | "internalType": "uint256", 464 | "name": "", 465 | "type": "uint256" 466 | } 467 | ], 468 | "stateMutability": "view", 469 | "type": "function" 470 | }, 471 | { 472 | "inputs": [ 473 | { 474 | "internalType": "address", 475 | "name": "to", 476 | "type": "address" 477 | }, 478 | { 479 | "internalType": "uint256", 480 | "name": "value", 481 | "type": "uint256" 482 | } 483 | ], 484 | "name": "transfer", 485 | "outputs": [ 486 | { 487 | "internalType": "bool", 488 | "name": "", 489 | "type": "bool" 490 | } 491 | ], 492 | "stateMutability": "nonpayable", 493 | "type": "function" 494 | }, 495 | { 496 | "inputs": [ 497 | { 498 | "internalType": "address", 499 | "name": "from", 500 | "type": "address" 501 | }, 502 | { 503 | "internalType": "address", 504 | "name": "to", 505 | "type": "address" 506 | }, 507 | { 508 | "internalType": "uint256", 509 | "name": "value", 510 | "type": "uint256" 511 | } 512 | ], 513 | "name": "transferFrom", 514 | "outputs": [ 515 | { 516 | "internalType": "bool", 517 | "name": "", 518 | "type": "bool" 519 | } 520 | ], 521 | "stateMutability": "nonpayable", 522 | "type": "function" 523 | } 524 | ], 525 | "bytecode": "0x", 526 | "deployedBytecode": "0x", 527 | "linkReferences": {}, 528 | "deployedLinkReferences": {} 529 | } 530 | -------------------------------------------------------------------------------- /src/evm/HCDepositInitiatorArtifact.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | abi: [ 3 | { 4 | type: 'constructor', 5 | inputs: [ 6 | { name: '_hcProcessor', type: 'address', internalType: 'address' }, 7 | { 8 | name: '_usdc', 9 | type: 'address', 10 | internalType: 'address', 11 | }, 12 | ], 13 | stateMutability: 'nonpayable', 14 | }, 15 | { 16 | type: 'function', 17 | name: 'changeGuardian', 18 | inputs: [ 19 | { name: 'newGuardian', type: 'address', internalType: 'address' }, 20 | ], 21 | outputs: [], 22 | stateMutability: 'nonpayable', 23 | }, 24 | { 25 | type: 'function', 26 | name: 'claimGuardian', 27 | inputs: [], 28 | outputs: [], 29 | stateMutability: 'nonpayable', 30 | }, 31 | { 32 | type: 'function', 33 | name: 'deposit', 34 | inputs: [ 35 | { name: 'tokenIn', type: 'address', internalType: 'address' }, 36 | { 37 | name: 'amountIn', 38 | type: 'uint256', 39 | internalType: 'uint256', 40 | }, 41 | { name: 'trader', type: 'address', internalType: 'address' }, 42 | { 43 | name: 'gasDrop', 44 | type: 'uint64', 45 | internalType: 'uint64', 46 | }, 47 | { name: 'bridgeAmount', type: 'uint256', internalType: 'uint256' }, 48 | { 49 | name: 'depositPayload', 50 | type: 'tuple', 51 | internalType: 'struct HCDepositInitiator.DepositPayload', 52 | components: [ 53 | { name: 'relayerFee', type: 'uint64', internalType: 'uint64' }, 54 | { 55 | name: 'permit', 56 | type: 'tuple', 57 | internalType: 'struct IHCBridge.DepositWithPermit', 58 | components: [ 59 | { name: 'user', type: 'address', internalType: 'address' }, 60 | { 61 | name: 'usd', 62 | type: 'uint64', 63 | internalType: 'uint64', 64 | }, 65 | { name: 'deadline', type: 'uint64', internalType: 'uint64' }, 66 | { 67 | name: 'signature', 68 | type: 'tuple', 69 | internalType: 'struct IHCBridge.Signature', 70 | components: [ 71 | { name: 'r', type: 'uint256', internalType: 'uint256' }, 72 | { 73 | name: 's', 74 | type: 'uint256', 75 | internalType: 'uint256', 76 | }, 77 | { name: 'v', type: 'uint8', internalType: 'uint8' }, 78 | ], 79 | }, 80 | ], 81 | }, 82 | ], 83 | }, 84 | ], 85 | outputs: [], 86 | stateMutability: 'nonpayable', 87 | }, 88 | { 89 | type: 'function', 90 | name: 'fastDeposit', 91 | inputs: [ 92 | { name: 'tokenIn', type: 'address', internalType: 'address' }, 93 | { 94 | name: 'amountIn', 95 | type: 'uint256', 96 | internalType: 'uint256', 97 | }, 98 | { name: 'trader', type: 'address', internalType: 'address' }, 99 | { 100 | name: 'circleMaxFee', 101 | type: 'uint256', 102 | internalType: 'uint256', 103 | }, 104 | { name: 'gasDrop', type: 'uint64', internalType: 'uint64' }, 105 | { 106 | name: 'referrerAddress', 107 | type: 'bytes32', 108 | internalType: 'bytes32', 109 | }, 110 | { name: 'referrerBps', type: 'uint8', internalType: 'uint8' }, 111 | { 112 | name: 'minFinalityThreshold', 113 | type: 'uint32', 114 | internalType: 'uint32', 115 | }, 116 | { name: 'bridgeAmount', type: 'uint256', internalType: 'uint256' }, 117 | { 118 | name: 'depositPayload', 119 | type: 'tuple', 120 | internalType: 'struct HCDepositInitiator.DepositPayload', 121 | components: [ 122 | { name: 'relayerFee', type: 'uint64', internalType: 'uint64' }, 123 | { 124 | name: 'permit', 125 | type: 'tuple', 126 | internalType: 'struct IHCBridge.DepositWithPermit', 127 | components: [ 128 | { name: 'user', type: 'address', internalType: 'address' }, 129 | { 130 | name: 'usd', 131 | type: 'uint64', 132 | internalType: 'uint64', 133 | }, 134 | { name: 'deadline', type: 'uint64', internalType: 'uint64' }, 135 | { 136 | name: 'signature', 137 | type: 'tuple', 138 | internalType: 'struct IHCBridge.Signature', 139 | components: [ 140 | { name: 'r', type: 'uint256', internalType: 'uint256' }, 141 | { 142 | name: 's', 143 | type: 'uint256', 144 | internalType: 'uint256', 145 | }, 146 | { name: 'v', type: 'uint8', internalType: 'uint8' }, 147 | ], 148 | }, 149 | ], 150 | }, 151 | ], 152 | }, 153 | ], 154 | outputs: [], 155 | stateMutability: 'nonpayable', 156 | }, 157 | { 158 | type: 'function', 159 | name: 'fastMCTP', 160 | inputs: [], 161 | outputs: [{ name: '', type: 'address', internalType: 'address' }], 162 | stateMutability: 'view', 163 | }, 164 | { 165 | type: 'function', 166 | name: 'guardian', 167 | inputs: [], 168 | outputs: [{ name: '', type: 'address', internalType: 'address' }], 169 | stateMutability: 'view', 170 | }, 171 | { 172 | type: 'function', 173 | name: 'mayanCircle', 174 | inputs: [], 175 | outputs: [{ name: '', type: 'address', internalType: 'address' }], 176 | stateMutability: 'view', 177 | }, 178 | { 179 | type: 'function', 180 | name: 'nextGuardian', 181 | inputs: [], 182 | outputs: [{ name: '', type: 'address', internalType: 'address' }], 183 | stateMutability: 'view', 184 | }, 185 | { 186 | type: 'function', 187 | name: 'setFastMCTP', 188 | inputs: [{ name: '_fastMCTP', type: 'address', internalType: 'address' }], 189 | outputs: [], 190 | stateMutability: 'nonpayable', 191 | }, 192 | { 193 | type: 'function', 194 | name: 'setMayanCircle', 195 | inputs: [ 196 | { name: '_mayanCircle', type: 'address', internalType: 'address' }, 197 | ], 198 | outputs: [], 199 | stateMutability: 'nonpayable', 200 | }, 201 | { type: 'error', name: 'AlreadySet', inputs: [] }, 202 | { 203 | type: 'error', 204 | name: 'InsufficientAmount', 205 | inputs: [], 206 | }, 207 | { type: 'error', name: 'Unauthorized', inputs: [] }, 208 | ], 209 | }; 210 | -------------------------------------------------------------------------------- /src/evm/MayanForwarderArtifact.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "_format": "hh-sol-artifact-1", 3 | "contractName": "MayanForwarder", 4 | "sourceName": "src/MayanForwarder.sol", 5 | "abi": [ 6 | { 7 | "inputs": [ 8 | { 9 | "internalType": "address", 10 | "name": "_guardian", 11 | "type": "address" 12 | }, 13 | { 14 | "internalType": "address[]", 15 | "name": "_swapProtocols", 16 | "type": "address[]" 17 | }, 18 | { 19 | "internalType": "address[]", 20 | "name": "_mayanProtocols", 21 | "type": "address[]" 22 | } 23 | ], 24 | "stateMutability": "nonpayable", 25 | "type": "constructor" 26 | }, 27 | { 28 | "inputs": [], 29 | "name": "UnsupportedProtocol", 30 | "type": "error" 31 | }, 32 | { 33 | "anonymous": false, 34 | "inputs": [ 35 | { 36 | "indexed": false, 37 | "internalType": "address", 38 | "name": "token", 39 | "type": "address" 40 | }, 41 | { 42 | "indexed": false, 43 | "internalType": "uint256", 44 | "name": "amount", 45 | "type": "uint256" 46 | }, 47 | { 48 | "indexed": false, 49 | "internalType": "address", 50 | "name": "mayanProtocol", 51 | "type": "address" 52 | }, 53 | { 54 | "indexed": false, 55 | "internalType": "bytes", 56 | "name": "protocolData", 57 | "type": "bytes" 58 | } 59 | ], 60 | "name": "ForwardedERC20", 61 | "type": "event" 62 | }, 63 | { 64 | "anonymous": false, 65 | "inputs": [ 66 | { 67 | "indexed": false, 68 | "internalType": "address", 69 | "name": "mayanProtocol", 70 | "type": "address" 71 | }, 72 | { 73 | "indexed": false, 74 | "internalType": "bytes", 75 | "name": "protocolData", 76 | "type": "bytes" 77 | } 78 | ], 79 | "name": "ForwardedEth", 80 | "type": "event" 81 | }, 82 | { 83 | "anonymous": false, 84 | "inputs": [ 85 | { 86 | "indexed": false, 87 | "internalType": "uint256", 88 | "name": "amount", 89 | "type": "uint256" 90 | } 91 | ], 92 | "name": "SwapAndForwarded", 93 | "type": "event" 94 | }, 95 | { 96 | "anonymous": false, 97 | "inputs": [ 98 | { 99 | "indexed": false, 100 | "internalType": "address", 101 | "name": "tokenIn", 102 | "type": "address" 103 | }, 104 | { 105 | "indexed": false, 106 | "internalType": "uint256", 107 | "name": "amountIn", 108 | "type": "uint256" 109 | }, 110 | { 111 | "indexed": false, 112 | "internalType": "address", 113 | "name": "swapProtocol", 114 | "type": "address" 115 | }, 116 | { 117 | "indexed": false, 118 | "internalType": "address", 119 | "name": "middleToken", 120 | "type": "address" 121 | }, 122 | { 123 | "indexed": false, 124 | "internalType": "uint256", 125 | "name": "middleAmount", 126 | "type": "uint256" 127 | }, 128 | { 129 | "indexed": false, 130 | "internalType": "address", 131 | "name": "mayanProtocol", 132 | "type": "address" 133 | }, 134 | { 135 | "indexed": false, 136 | "internalType": "bytes", 137 | "name": "mayanData", 138 | "type": "bytes" 139 | } 140 | ], 141 | "name": "SwapAndForwardedERC20", 142 | "type": "event" 143 | }, 144 | { 145 | "anonymous": false, 146 | "inputs": [ 147 | { 148 | "indexed": false, 149 | "internalType": "uint256", 150 | "name": "amountIn", 151 | "type": "uint256" 152 | }, 153 | { 154 | "indexed": false, 155 | "internalType": "address", 156 | "name": "swapProtocol", 157 | "type": "address" 158 | }, 159 | { 160 | "indexed": false, 161 | "internalType": "address", 162 | "name": "middleToken", 163 | "type": "address" 164 | }, 165 | { 166 | "indexed": false, 167 | "internalType": "uint256", 168 | "name": "middleAmount", 169 | "type": "uint256" 170 | }, 171 | { 172 | "indexed": false, 173 | "internalType": "address", 174 | "name": "mayanProtocol", 175 | "type": "address" 176 | }, 177 | { 178 | "indexed": false, 179 | "internalType": "bytes", 180 | "name": "mayanData", 181 | "type": "bytes" 182 | } 183 | ], 184 | "name": "SwapAndForwardedEth", 185 | "type": "event" 186 | }, 187 | { 188 | "inputs": [ 189 | { 190 | "internalType": "address", 191 | "name": "newGuardian", 192 | "type": "address" 193 | } 194 | ], 195 | "name": "changeGuardian", 196 | "outputs": [], 197 | "stateMutability": "nonpayable", 198 | "type": "function" 199 | }, 200 | { 201 | "inputs": [], 202 | "name": "claimGuardian", 203 | "outputs": [], 204 | "stateMutability": "nonpayable", 205 | "type": "function" 206 | }, 207 | { 208 | "inputs": [ 209 | { 210 | "internalType": "address", 211 | "name": "tokenIn", 212 | "type": "address" 213 | }, 214 | { 215 | "internalType": "uint256", 216 | "name": "amountIn", 217 | "type": "uint256" 218 | }, 219 | { 220 | "components": [ 221 | { 222 | "internalType": "uint256", 223 | "name": "value", 224 | "type": "uint256" 225 | }, 226 | { 227 | "internalType": "uint256", 228 | "name": "deadline", 229 | "type": "uint256" 230 | }, 231 | { 232 | "internalType": "uint8", 233 | "name": "v", 234 | "type": "uint8" 235 | }, 236 | { 237 | "internalType": "bytes32", 238 | "name": "r", 239 | "type": "bytes32" 240 | }, 241 | { 242 | "internalType": "bytes32", 243 | "name": "s", 244 | "type": "bytes32" 245 | } 246 | ], 247 | "internalType": "struct MayanForwarder.PermitParams", 248 | "name": "permitParams", 249 | "type": "tuple" 250 | }, 251 | { 252 | "internalType": "address", 253 | "name": "mayanProtocol", 254 | "type": "address" 255 | }, 256 | { 257 | "internalType": "bytes", 258 | "name": "protocolData", 259 | "type": "bytes" 260 | } 261 | ], 262 | "name": "forwardERC20", 263 | "outputs": [], 264 | "stateMutability": "payable", 265 | "type": "function" 266 | }, 267 | { 268 | "inputs": [ 269 | { 270 | "internalType": "address", 271 | "name": "mayanProtocol", 272 | "type": "address" 273 | }, 274 | { 275 | "internalType": "bytes", 276 | "name": "protocolData", 277 | "type": "bytes" 278 | } 279 | ], 280 | "name": "forwardEth", 281 | "outputs": [], 282 | "stateMutability": "payable", 283 | "type": "function" 284 | }, 285 | { 286 | "inputs": [], 287 | "name": "guardian", 288 | "outputs": [ 289 | { 290 | "internalType": "address", 291 | "name": "", 292 | "type": "address" 293 | } 294 | ], 295 | "stateMutability": "view", 296 | "type": "function" 297 | }, 298 | { 299 | "inputs": [ 300 | { 301 | "internalType": "address", 302 | "name": "", 303 | "type": "address" 304 | } 305 | ], 306 | "name": "mayanProtocols", 307 | "outputs": [ 308 | { 309 | "internalType": "bool", 310 | "name": "", 311 | "type": "bool" 312 | } 313 | ], 314 | "stateMutability": "view", 315 | "type": "function" 316 | }, 317 | { 318 | "inputs": [], 319 | "name": "nextGuardian", 320 | "outputs": [ 321 | { 322 | "internalType": "address", 323 | "name": "", 324 | "type": "address" 325 | } 326 | ], 327 | "stateMutability": "view", 328 | "type": "function" 329 | }, 330 | { 331 | "inputs": [ 332 | { 333 | "internalType": "uint256", 334 | "name": "amount", 335 | "type": "uint256" 336 | }, 337 | { 338 | "internalType": "address payable", 339 | "name": "to", 340 | "type": "address" 341 | } 342 | ], 343 | "name": "rescueEth", 344 | "outputs": [], 345 | "stateMutability": "nonpayable", 346 | "type": "function" 347 | }, 348 | { 349 | "inputs": [ 350 | { 351 | "internalType": "address", 352 | "name": "token", 353 | "type": "address" 354 | }, 355 | { 356 | "internalType": "uint256", 357 | "name": "amount", 358 | "type": "uint256" 359 | }, 360 | { 361 | "internalType": "address", 362 | "name": "to", 363 | "type": "address" 364 | } 365 | ], 366 | "name": "rescueToken", 367 | "outputs": [], 368 | "stateMutability": "nonpayable", 369 | "type": "function" 370 | }, 371 | { 372 | "inputs": [ 373 | { 374 | "internalType": "address", 375 | "name": "mayanProtocol", 376 | "type": "address" 377 | }, 378 | { 379 | "internalType": "bool", 380 | "name": "enabled", 381 | "type": "bool" 382 | } 383 | ], 384 | "name": "setMayanProtocol", 385 | "outputs": [], 386 | "stateMutability": "nonpayable", 387 | "type": "function" 388 | }, 389 | { 390 | "inputs": [ 391 | { 392 | "internalType": "address", 393 | "name": "swapProtocol", 394 | "type": "address" 395 | }, 396 | { 397 | "internalType": "bool", 398 | "name": "enabled", 399 | "type": "bool" 400 | } 401 | ], 402 | "name": "setSwapProtocol", 403 | "outputs": [], 404 | "stateMutability": "nonpayable", 405 | "type": "function" 406 | }, 407 | { 408 | "inputs": [ 409 | { 410 | "internalType": "address", 411 | "name": "tokenIn", 412 | "type": "address" 413 | }, 414 | { 415 | "internalType": "uint256", 416 | "name": "amountIn", 417 | "type": "uint256" 418 | }, 419 | { 420 | "components": [ 421 | { 422 | "internalType": "uint256", 423 | "name": "value", 424 | "type": "uint256" 425 | }, 426 | { 427 | "internalType": "uint256", 428 | "name": "deadline", 429 | "type": "uint256" 430 | }, 431 | { 432 | "internalType": "uint8", 433 | "name": "v", 434 | "type": "uint8" 435 | }, 436 | { 437 | "internalType": "bytes32", 438 | "name": "r", 439 | "type": "bytes32" 440 | }, 441 | { 442 | "internalType": "bytes32", 443 | "name": "s", 444 | "type": "bytes32" 445 | } 446 | ], 447 | "internalType": "struct MayanForwarder.PermitParams", 448 | "name": "permitParams", 449 | "type": "tuple" 450 | }, 451 | { 452 | "internalType": "address", 453 | "name": "swapProtocol", 454 | "type": "address" 455 | }, 456 | { 457 | "internalType": "bytes", 458 | "name": "swapData", 459 | "type": "bytes" 460 | }, 461 | { 462 | "internalType": "address", 463 | "name": "middleToken", 464 | "type": "address" 465 | }, 466 | { 467 | "internalType": "uint256", 468 | "name": "minMiddleAmount", 469 | "type": "uint256" 470 | }, 471 | { 472 | "internalType": "address", 473 | "name": "mayanProtocol", 474 | "type": "address" 475 | }, 476 | { 477 | "internalType": "bytes", 478 | "name": "mayanData", 479 | "type": "bytes" 480 | } 481 | ], 482 | "name": "swapAndForwardERC20", 483 | "outputs": [], 484 | "stateMutability": "payable", 485 | "type": "function" 486 | }, 487 | { 488 | "inputs": [ 489 | { 490 | "internalType": "uint256", 491 | "name": "amountIn", 492 | "type": "uint256" 493 | }, 494 | { 495 | "internalType": "address", 496 | "name": "swapProtocol", 497 | "type": "address" 498 | }, 499 | { 500 | "internalType": "bytes", 501 | "name": "swapData", 502 | "type": "bytes" 503 | }, 504 | { 505 | "internalType": "address", 506 | "name": "middleToken", 507 | "type": "address" 508 | }, 509 | { 510 | "internalType": "uint256", 511 | "name": "minMiddleAmount", 512 | "type": "uint256" 513 | }, 514 | { 515 | "internalType": "address", 516 | "name": "mayanProtocol", 517 | "type": "address" 518 | }, 519 | { 520 | "internalType": "bytes", 521 | "name": "mayanData", 522 | "type": "bytes" 523 | } 524 | ], 525 | "name": "swapAndForwardEth", 526 | "outputs": [], 527 | "stateMutability": "payable", 528 | "type": "function" 529 | }, 530 | { 531 | "inputs": [ 532 | { 533 | "internalType": "address", 534 | "name": "", 535 | "type": "address" 536 | } 537 | ], 538 | "name": "swapProtocols", 539 | "outputs": [ 540 | { 541 | "internalType": "bool", 542 | "name": "", 543 | "type": "bool" 544 | } 545 | ], 546 | "stateMutability": "view", 547 | "type": "function" 548 | } 549 | ], 550 | "linkReferences": {}, 551 | "deployedLinkReferences": {} 552 | } 553 | -------------------------------------------------------------------------------- /src/evm/MayanMonoChainArtifact.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | abi: [ 3 | { 4 | type: 'constructor', 5 | inputs: [ 6 | { name: '_forwarderAddress', type: 'address', internalType: 'address' }, 7 | ], 8 | stateMutability: 'nonpayable', 9 | }, 10 | { type: 'receive', stateMutability: 'payable' }, 11 | { 12 | type: 'function', 13 | name: 'ForwarderAddress', 14 | inputs: [], 15 | outputs: [{ name: '', type: 'address', internalType: 'address' }], 16 | stateMutability: 'view', 17 | }, 18 | { 19 | type: 'function', 20 | name: 'MAX_REFERRER_BPS', 21 | inputs: [], 22 | outputs: [{ name: '', type: 'uint8', internalType: 'uint8' }], 23 | stateMutability: 'view', 24 | }, 25 | { 26 | type: 'function', 27 | name: 'changeGuardian', 28 | inputs: [ 29 | { name: 'newGuardian', type: 'address', internalType: 'address' }, 30 | ], 31 | outputs: [], 32 | stateMutability: 'nonpayable', 33 | }, 34 | { 35 | type: 'function', 36 | name: 'claimGuardian', 37 | inputs: [], 38 | outputs: [], 39 | stateMutability: 'nonpayable', 40 | }, 41 | { 42 | type: 'function', 43 | name: 'guardian', 44 | inputs: [], 45 | outputs: [{ name: '', type: 'address', internalType: 'address' }], 46 | stateMutability: 'view', 47 | }, 48 | { 49 | type: 'function', 50 | name: 'nextGuardian', 51 | inputs: [], 52 | outputs: [{ name: '', type: 'address', internalType: 'address' }], 53 | stateMutability: 'view', 54 | }, 55 | { 56 | type: 'function', 57 | name: 'rescueETH', 58 | inputs: [ 59 | { name: 'to', type: 'address', internalType: 'address' }, 60 | { 61 | name: 'amount', 62 | type: 'uint256', 63 | internalType: 'uint256', 64 | }, 65 | ], 66 | outputs: [], 67 | stateMutability: 'nonpayable', 68 | }, 69 | { 70 | type: 'function', 71 | name: 'rescueToken', 72 | inputs: [ 73 | { name: 'token', type: 'address', internalType: 'address' }, 74 | { 75 | name: 'to', 76 | type: 'address', 77 | internalType: 'address', 78 | }, 79 | { name: 'amount', type: 'uint256', internalType: 'uint256' }, 80 | ], 81 | outputs: [], 82 | stateMutability: 'nonpayable', 83 | }, 84 | { 85 | type: 'function', 86 | name: 'transferEth', 87 | inputs: [ 88 | { name: 'to', type: 'address', internalType: 'address' }, 89 | { 90 | name: 'referrerAddr', 91 | type: 'address', 92 | internalType: 'address', 93 | }, 94 | { name: 'referrerBps', type: 'uint8', internalType: 'uint8' }, 95 | ], 96 | outputs: [], 97 | stateMutability: 'payable', 98 | }, 99 | { 100 | type: 'function', 101 | name: 'transferToken', 102 | inputs: [ 103 | { name: 'token', type: 'address', internalType: 'address' }, 104 | { 105 | name: 'amount', 106 | type: 'uint256', 107 | internalType: 'uint256', 108 | }, 109 | { name: 'to', type: 'address', internalType: 'address' }, 110 | { 111 | name: 'referrerAddr', 112 | type: 'address', 113 | internalType: 'address', 114 | }, 115 | { name: 'referrerBps', type: 'uint8', internalType: 'uint8' }, 116 | ], 117 | outputs: [], 118 | stateMutability: 'nonpayable', 119 | }, 120 | { 121 | type: 'event', 122 | name: 'EthTransferred', 123 | inputs: [ 124 | { 125 | name: 'to', 126 | type: 'address', 127 | indexed: false, 128 | internalType: 'address', 129 | }, 130 | { 131 | name: 'amount', 132 | type: 'uint256', 133 | indexed: false, 134 | internalType: 'uint256', 135 | }, 136 | ], 137 | anonymous: false, 138 | }, 139 | { 140 | type: 'event', 141 | name: 'TokenTransferred', 142 | inputs: [ 143 | { 144 | name: 'token', 145 | type: 'address', 146 | indexed: false, 147 | internalType: 'address', 148 | }, 149 | { 150 | name: 'to', 151 | type: 'address', 152 | indexed: false, 153 | internalType: 'address', 154 | }, 155 | { 156 | name: 'amount', 157 | type: 'uint256', 158 | indexed: false, 159 | internalType: 'uint256', 160 | }, 161 | ], 162 | anonymous: false, 163 | }, 164 | { type: 'error', name: 'InvalidReferrerBps', inputs: [] }, 165 | { 166 | type: 'error', 167 | name: 'Unauthorized', 168 | inputs: [], 169 | }, 170 | ], 171 | }; 172 | -------------------------------------------------------------------------------- /src/evm/MayanSwapArtifact.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "_format": "hh-sol-artifact-1", 3 | "contractName": "MayanSwap", 4 | "sourceName": "src/MayanSwap.sol", 5 | "abi": [ 6 | { 7 | "inputs": [ 8 | { 9 | "internalType": "address", 10 | "name": "_tokenBridge", 11 | "type": "address" 12 | }, 13 | { 14 | "internalType": "address", 15 | "name": "_weth", 16 | "type": "address" 17 | } 18 | ], 19 | "stateMutability": "nonpayable", 20 | "type": "constructor" 21 | }, 22 | { 23 | "anonymous": false, 24 | "inputs": [ 25 | { 26 | "indexed": true, 27 | "internalType": "uint16", 28 | "name": "emitterChainId", 29 | "type": "uint16" 30 | }, 31 | { 32 | "indexed": true, 33 | "internalType": "bytes32", 34 | "name": "emitterAddress", 35 | "type": "bytes32" 36 | }, 37 | { 38 | "indexed": true, 39 | "internalType": "uint64", 40 | "name": "sequence", 41 | "type": "uint64" 42 | } 43 | ], 44 | "name": "Redeemed", 45 | "type": "event" 46 | }, 47 | { 48 | "inputs": [ 49 | { 50 | "internalType": "address", 51 | "name": "newGuardian", 52 | "type": "address" 53 | } 54 | ], 55 | "name": "changeGuardian", 56 | "outputs": [], 57 | "stateMutability": "nonpayable", 58 | "type": "function" 59 | }, 60 | { 61 | "inputs": [], 62 | "name": "claimGuardian", 63 | "outputs": [], 64 | "stateMutability": "nonpayable", 65 | "type": "function" 66 | }, 67 | { 68 | "inputs": [ 69 | { 70 | "components": [ 71 | { 72 | "internalType": "uint8", 73 | "name": "payloadId", 74 | "type": "uint8" 75 | }, 76 | { 77 | "internalType": "bytes32", 78 | "name": "tokenAddr", 79 | "type": "bytes32" 80 | }, 81 | { 82 | "internalType": "uint16", 83 | "name": "tokenChainId", 84 | "type": "uint16" 85 | }, 86 | { 87 | "internalType": "bytes32", 88 | "name": "destAddr", 89 | "type": "bytes32" 90 | }, 91 | { 92 | "internalType": "uint16", 93 | "name": "destChainId", 94 | "type": "uint16" 95 | }, 96 | { 97 | "internalType": "bytes32", 98 | "name": "sourceAddr", 99 | "type": "bytes32" 100 | }, 101 | { 102 | "internalType": "uint16", 103 | "name": "sourceChainId", 104 | "type": "uint16" 105 | }, 106 | { 107 | "internalType": "uint64", 108 | "name": "sequence", 109 | "type": "uint64" 110 | }, 111 | { 112 | "internalType": "uint64", 113 | "name": "amountOutMin", 114 | "type": "uint64" 115 | }, 116 | { 117 | "internalType": "uint64", 118 | "name": "deadline", 119 | "type": "uint64" 120 | }, 121 | { 122 | "internalType": "uint64", 123 | "name": "swapFee", 124 | "type": "uint64" 125 | }, 126 | { 127 | "internalType": "uint64", 128 | "name": "redeemFee", 129 | "type": "uint64" 130 | }, 131 | { 132 | "internalType": "uint64", 133 | "name": "refundFee", 134 | "type": "uint64" 135 | }, 136 | { 137 | "internalType": "bytes32", 138 | "name": "auctionAddr", 139 | "type": "bytes32" 140 | }, 141 | { 142 | "internalType": "bool", 143 | "name": "unwrapRedeem", 144 | "type": "bool" 145 | }, 146 | { 147 | "internalType": "bool", 148 | "name": "unwrapRefund", 149 | "type": "bool" 150 | } 151 | ], 152 | "internalType": "struct MayanStructs.Swap", 153 | "name": "s", 154 | "type": "tuple" 155 | } 156 | ], 157 | "name": "encodeSwap", 158 | "outputs": [ 159 | { 160 | "internalType": "bytes", 161 | "name": "encoded", 162 | "type": "bytes" 163 | } 164 | ], 165 | "stateMutability": "pure", 166 | "type": "function" 167 | }, 168 | { 169 | "inputs": [], 170 | "name": "getWeth", 171 | "outputs": [ 172 | { 173 | "internalType": "address", 174 | "name": "", 175 | "type": "address" 176 | } 177 | ], 178 | "stateMutability": "view", 179 | "type": "function" 180 | }, 181 | { 182 | "inputs": [], 183 | "name": "isPaused", 184 | "outputs": [ 185 | { 186 | "internalType": "bool", 187 | "name": "", 188 | "type": "bool" 189 | } 190 | ], 191 | "stateMutability": "view", 192 | "type": "function" 193 | }, 194 | { 195 | "inputs": [ 196 | { 197 | "internalType": "bytes", 198 | "name": "encoded", 199 | "type": "bytes" 200 | } 201 | ], 202 | "name": "parseRedeemPayload", 203 | "outputs": [ 204 | { 205 | "components": [ 206 | { 207 | "internalType": "uint8", 208 | "name": "payloadId", 209 | "type": "uint8" 210 | }, 211 | { 212 | "internalType": "bytes32", 213 | "name": "recipient", 214 | "type": "bytes32" 215 | }, 216 | { 217 | "internalType": "uint64", 218 | "name": "relayerFee", 219 | "type": "uint64" 220 | }, 221 | { 222 | "internalType": "bool", 223 | "name": "unwrap", 224 | "type": "bool" 225 | }, 226 | { 227 | "internalType": "uint64", 228 | "name": "gasDrop", 229 | "type": "uint64" 230 | }, 231 | { 232 | "internalType": "bytes", 233 | "name": "customPayload", 234 | "type": "bytes" 235 | } 236 | ], 237 | "internalType": "struct MayanStructs.Redeem", 238 | "name": "r", 239 | "type": "tuple" 240 | } 241 | ], 242 | "stateMutability": "pure", 243 | "type": "function" 244 | }, 245 | { 246 | "inputs": [ 247 | { 248 | "internalType": "bytes", 249 | "name": "encodedVm", 250 | "type": "bytes" 251 | } 252 | ], 253 | "name": "redeem", 254 | "outputs": [], 255 | "stateMutability": "payable", 256 | "type": "function" 257 | }, 258 | { 259 | "inputs": [ 260 | { 261 | "internalType": "bytes", 262 | "name": "encodedVm", 263 | "type": "bytes" 264 | } 265 | ], 266 | "name": "redeemAndUnwrap", 267 | "outputs": [], 268 | "stateMutability": "nonpayable", 269 | "type": "function" 270 | }, 271 | { 272 | "inputs": [ 273 | { 274 | "internalType": "bool", 275 | "name": "_pause", 276 | "type": "bool" 277 | } 278 | ], 279 | "name": "setPause", 280 | "outputs": [], 281 | "stateMutability": "nonpayable", 282 | "type": "function" 283 | }, 284 | { 285 | "inputs": [ 286 | { 287 | "components": [ 288 | { 289 | "internalType": "uint64", 290 | "name": "swapFee", 291 | "type": "uint64" 292 | }, 293 | { 294 | "internalType": "uint64", 295 | "name": "redeemFee", 296 | "type": "uint64" 297 | }, 298 | { 299 | "internalType": "uint64", 300 | "name": "refundFee", 301 | "type": "uint64" 302 | } 303 | ], 304 | "internalType": "struct MayanSwap.RelayerFees", 305 | "name": "relayerFees", 306 | "type": "tuple" 307 | }, 308 | { 309 | "components": [ 310 | { 311 | "internalType": "bytes32", 312 | "name": "mayanAddr", 313 | "type": "bytes32" 314 | }, 315 | { 316 | "internalType": "uint16", 317 | "name": "mayanChainId", 318 | "type": "uint16" 319 | }, 320 | { 321 | "internalType": "bytes32", 322 | "name": "auctionAddr", 323 | "type": "bytes32" 324 | }, 325 | { 326 | "internalType": "bytes32", 327 | "name": "destAddr", 328 | "type": "bytes32" 329 | }, 330 | { 331 | "internalType": "uint16", 332 | "name": "destChainId", 333 | "type": "uint16" 334 | }, 335 | { 336 | "internalType": "bytes32", 337 | "name": "referrer", 338 | "type": "bytes32" 339 | }, 340 | { 341 | "internalType": "bytes32", 342 | "name": "refundAddr", 343 | "type": "bytes32" 344 | } 345 | ], 346 | "internalType": "struct MayanSwap.Recepient", 347 | "name": "recipient", 348 | "type": "tuple" 349 | }, 350 | { 351 | "internalType": "bytes32", 352 | "name": "tokenOutAddr", 353 | "type": "bytes32" 354 | }, 355 | { 356 | "internalType": "uint16", 357 | "name": "tokenOutChainId", 358 | "type": "uint16" 359 | }, 360 | { 361 | "components": [ 362 | { 363 | "internalType": "uint256", 364 | "name": "transferDeadline", 365 | "type": "uint256" 366 | }, 367 | { 368 | "internalType": "uint64", 369 | "name": "swapDeadline", 370 | "type": "uint64" 371 | }, 372 | { 373 | "internalType": "uint64", 374 | "name": "amountOutMin", 375 | "type": "uint64" 376 | }, 377 | { 378 | "internalType": "bool", 379 | "name": "unwrap", 380 | "type": "bool" 381 | }, 382 | { 383 | "internalType": "uint64", 384 | "name": "gasDrop", 385 | "type": "uint64" 386 | }, 387 | { 388 | "internalType": "bytes", 389 | "name": "customPayload", 390 | "type": "bytes" 391 | } 392 | ], 393 | "internalType": "struct MayanSwap.Criteria", 394 | "name": "criteria", 395 | "type": "tuple" 396 | }, 397 | { 398 | "internalType": "address", 399 | "name": "tokenIn", 400 | "type": "address" 401 | }, 402 | { 403 | "internalType": "uint256", 404 | "name": "amountIn", 405 | "type": "uint256" 406 | } 407 | ], 408 | "name": "swap", 409 | "outputs": [ 410 | { 411 | "internalType": "uint64", 412 | "name": "sequence", 413 | "type": "uint64" 414 | } 415 | ], 416 | "stateMutability": "payable", 417 | "type": "function" 418 | }, 419 | { 420 | "inputs": [ 421 | { 422 | "internalType": "uint256", 423 | "name": "amount", 424 | "type": "uint256" 425 | }, 426 | { 427 | "internalType": "address payable", 428 | "name": "to", 429 | "type": "address" 430 | } 431 | ], 432 | "name": "sweepEth", 433 | "outputs": [], 434 | "stateMutability": "nonpayable", 435 | "type": "function" 436 | }, 437 | { 438 | "inputs": [ 439 | { 440 | "internalType": "address", 441 | "name": "token", 442 | "type": "address" 443 | }, 444 | { 445 | "internalType": "uint256", 446 | "name": "amount", 447 | "type": "uint256" 448 | }, 449 | { 450 | "internalType": "address", 451 | "name": "to", 452 | "type": "address" 453 | } 454 | ], 455 | "name": "sweepToken", 456 | "outputs": [], 457 | "stateMutability": "nonpayable", 458 | "type": "function" 459 | }, 460 | { 461 | "inputs": [ 462 | { 463 | "components": [ 464 | { 465 | "internalType": "uint64", 466 | "name": "swapFee", 467 | "type": "uint64" 468 | }, 469 | { 470 | "internalType": "uint64", 471 | "name": "redeemFee", 472 | "type": "uint64" 473 | }, 474 | { 475 | "internalType": "uint64", 476 | "name": "refundFee", 477 | "type": "uint64" 478 | } 479 | ], 480 | "internalType": "struct MayanSwap.RelayerFees", 481 | "name": "relayerFees", 482 | "type": "tuple" 483 | }, 484 | { 485 | "components": [ 486 | { 487 | "internalType": "bytes32", 488 | "name": "mayanAddr", 489 | "type": "bytes32" 490 | }, 491 | { 492 | "internalType": "uint16", 493 | "name": "mayanChainId", 494 | "type": "uint16" 495 | }, 496 | { 497 | "internalType": "bytes32", 498 | "name": "auctionAddr", 499 | "type": "bytes32" 500 | }, 501 | { 502 | "internalType": "bytes32", 503 | "name": "destAddr", 504 | "type": "bytes32" 505 | }, 506 | { 507 | "internalType": "uint16", 508 | "name": "destChainId", 509 | "type": "uint16" 510 | }, 511 | { 512 | "internalType": "bytes32", 513 | "name": "referrer", 514 | "type": "bytes32" 515 | }, 516 | { 517 | "internalType": "bytes32", 518 | "name": "refundAddr", 519 | "type": "bytes32" 520 | } 521 | ], 522 | "internalType": "struct MayanSwap.Recepient", 523 | "name": "recipient", 524 | "type": "tuple" 525 | }, 526 | { 527 | "internalType": "bytes32", 528 | "name": "tokenOutAddr", 529 | "type": "bytes32" 530 | }, 531 | { 532 | "internalType": "uint16", 533 | "name": "tokenOutChainId", 534 | "type": "uint16" 535 | }, 536 | { 537 | "components": [ 538 | { 539 | "internalType": "uint256", 540 | "name": "transferDeadline", 541 | "type": "uint256" 542 | }, 543 | { 544 | "internalType": "uint64", 545 | "name": "swapDeadline", 546 | "type": "uint64" 547 | }, 548 | { 549 | "internalType": "uint64", 550 | "name": "amountOutMin", 551 | "type": "uint64" 552 | }, 553 | { 554 | "internalType": "bool", 555 | "name": "unwrap", 556 | "type": "bool" 557 | }, 558 | { 559 | "internalType": "uint64", 560 | "name": "gasDrop", 561 | "type": "uint64" 562 | }, 563 | { 564 | "internalType": "bytes", 565 | "name": "customPayload", 566 | "type": "bytes" 567 | } 568 | ], 569 | "internalType": "struct MayanSwap.Criteria", 570 | "name": "criteria", 571 | "type": "tuple" 572 | } 573 | ], 574 | "name": "wrapAndSwapETH", 575 | "outputs": [ 576 | { 577 | "internalType": "uint64", 578 | "name": "sequence", 579 | "type": "uint64" 580 | } 581 | ], 582 | "stateMutability": "payable", 583 | "type": "function" 584 | }, 585 | { 586 | "stateMutability": "payable", 587 | "type": "receive" 588 | } 589 | ] 590 | } 591 | -------------------------------------------------------------------------------- /src/evm/ShuttleArtifact.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | abi: [ 3 | { 4 | type: 'function', 5 | name: 'batchMaxApprove', 6 | inputs: [{ name: 'approvals', type: 'bytes', internalType: 'bytes' }], 7 | outputs: [], 8 | stateMutability: 'nonpayable', 9 | }, 10 | { 11 | type: 'function', 12 | name: 'initiate', 13 | inputs: [ 14 | { name: 'recipient', type: 'bytes32', internalType: 'bytes32' }, 15 | { 16 | name: 'overrideAmountIn', 17 | type: 'uint256', 18 | internalType: 'uint256', 19 | }, 20 | { name: 'targetChain', type: 'uint16', internalType: 'uint16' }, 21 | { 22 | name: 'params', 23 | type: 'bytes', 24 | internalType: 'bytes', 25 | }, 26 | ], 27 | outputs: [{ name: '', type: 'bytes', internalType: 'bytes' }], 28 | stateMutability: 'payable', 29 | }, 30 | { 31 | type: 'error', 32 | name: 'ChainNotSupported', 33 | inputs: [{ name: 'chain', type: 'uint16', internalType: 'uint16' }], 34 | }, 35 | { 36 | type: 'error', 37 | name: 'DeadlineExpired', 38 | inputs: [ 39 | { name: 'blocktime', type: 'uint256', internalType: 'uint256' }, 40 | { 41 | name: 'deadline', 42 | type: 'uint256', 43 | internalType: 'uint256', 44 | }, 45 | ], 46 | }, 47 | { type: 'error', name: 'EthTransferFailed', inputs: [] }, 48 | { 49 | type: 'error', 50 | name: 'ExceedsMaxGasDropoff', 51 | inputs: [ 52 | { name: 'requested', type: 'uint256', internalType: 'uint256' }, 53 | { 54 | name: 'maximum', 55 | type: 'uint256', 56 | internalType: 'uint256', 57 | }, 58 | ], 59 | }, 60 | { 61 | type: 'error', 62 | name: 'ExceedsMaxRelayingFee', 63 | inputs: [ 64 | { name: 'fee', type: 'uint256', internalType: 'uint256' }, 65 | { 66 | name: 'maximum', 67 | type: 'uint256', 68 | internalType: 'uint256', 69 | }, 70 | ], 71 | }, 72 | { 73 | type: 'error', 74 | name: 'InsufficientInputAmount', 75 | inputs: [ 76 | { name: 'input', type: 'uint256', internalType: 'uint256' }, 77 | { 78 | name: 'minimum', 79 | type: 'uint256', 80 | internalType: 'uint256', 81 | }, 82 | ], 83 | }, 84 | { 85 | type: 'error', 86 | name: 'InvalidAddress', 87 | inputs: [{ name: 'addr', type: 'bytes32', internalType: 'bytes32' }], 88 | }, 89 | { 90 | type: 'error', 91 | name: 'InvalidBoolVal', 92 | inputs: [{ name: 'val', type: 'uint8', internalType: 'uint8' }], 93 | }, 94 | { 95 | type: 'error', 96 | name: 'InvalidOverrideAmount', 97 | inputs: [ 98 | { name: 'received', type: 'uint256', internalType: 'uint256' }, 99 | { 100 | name: 'maximum', 101 | type: 'uint256', 102 | internalType: 'uint256', 103 | }, 104 | ], 105 | }, 106 | { 107 | type: 'error', 108 | name: 'InvalidSwapType', 109 | inputs: [{ name: 'swapType', type: 'uint256', internalType: 'uint256' }], 110 | }, 111 | { 112 | type: 'error', 113 | name: 'InvalidSwapTypeForChain', 114 | inputs: [ 115 | { name: 'chain', type: 'uint16', internalType: 'uint16' }, 116 | { 117 | name: 'swapType', 118 | type: 'uint256', 119 | internalType: 'uint256', 120 | }, 121 | ], 122 | }, 123 | { 124 | type: 'error', 125 | name: 'LengthMismatch', 126 | inputs: [ 127 | { name: 'encodedLength', type: 'uint256', internalType: 'uint256' }, 128 | { 129 | name: 'expectedLength', 130 | type: 'uint256', 131 | internalType: 'uint256', 132 | }, 133 | ], 134 | }, 135 | { type: 'error', name: 'RelayingDisabledForChain', inputs: [] }, 136 | ], 137 | }; 138 | 139 | -------------------------------------------------------------------------------- /src/evm/evmFastMctp.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Contract, 3 | toBeHex, 4 | ZeroAddress, 5 | TransactionRequest, 6 | } from 'ethers'; 7 | import { SystemProgram } from '@solana/web3.js'; 8 | import type { EvmForwarderParams, Quote } from '../types'; 9 | import { 10 | nativeAddressToHexString, 11 | getAmountOfFractionalAmount, 12 | getWormholeChainIdByName, 13 | getWormholeChainIdById, 14 | getGasDecimal, 15 | ZeroPermit, 16 | FAST_MCTP_PAYLOAD_TYPE_CUSTOM_PAYLOAD, FAST_MCTP_PAYLOAD_TYPE_DEFAULT, FAST_MCTP_PAYLOAD_TYPE_ORDER 17 | } from '../utils'; 18 | 19 | import MayanFastMctpArtifact from './MayanFastMctpArtifact'; 20 | import MayanForwarderArtifact from './MayanForwarderArtifact'; 21 | import addresses from '../addresses'; 22 | import { Buffer } from 'buffer'; 23 | import { getCCTPDomain, CCTP_TOKEN_DECIMALS } from '../cctp'; 24 | import { Erc20Permit } from '../types'; 25 | 26 | type EvmFastMctpBridgeParams = { 27 | tokenIn: string, 28 | amountIn: bigint, 29 | redeemFee: bigint, 30 | gasDrop: bigint, 31 | destAddr: string, 32 | referrerAddr: string, 33 | referrerBps: number, 34 | customPayload: string, 35 | payloadType: number, 36 | destDomain: number, 37 | contractAddress: string, 38 | circleMaxFee: bigint, 39 | minFinalityThreshold: number, 40 | } 41 | function getEvmFastMctpBridgeParams( 42 | quote: Quote, destinationAddress: string, 43 | referrerAddress: string | null | undefined, signerChainId: number | string, 44 | customPayload?: Uint8Array | Buffer | null 45 | ): EvmFastMctpBridgeParams { 46 | const signerWormholeChainId = getWormholeChainIdById(Number(signerChainId)); 47 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 48 | const destChainId = getWormholeChainIdByName(quote.toChain); 49 | if (sourceChainId !== signerWormholeChainId) { 50 | throw new Error(`Signer chain id(${Number(signerChainId)}) and quote from chain are not same! ${sourceChainId} !== ${signerWormholeChainId}`); 51 | } 52 | const destinationAddressHex = nativeAddressToHexString(destinationAddress, destChainId); 53 | const redeemFee = getAmountOfFractionalAmount(quote.redeemRelayerFee, CCTP_TOKEN_DECIMALS); 54 | const gasDrop = getAmountOfFractionalAmount(quote.gasDrop, Math.min(getGasDecimal(quote.toChain), 8)); 55 | const circleMaxFee = BigInt(quote.circleMaxFee64); 56 | const amountIn = BigInt(quote.effectiveAmountIn64); 57 | const destDomain = getCCTPDomain(quote.toChain); 58 | let referrerHex: string; 59 | if (referrerAddress) { 60 | referrerHex = nativeAddressToHexString( 61 | referrerAddress, destChainId 62 | ); 63 | } else { 64 | referrerHex = nativeAddressToHexString( 65 | SystemProgram.programId.toString(), getWormholeChainIdByName('solana') 66 | ); 67 | } 68 | 69 | if (!quote.fastMctpMayanContract) { 70 | throw new Error('FastMctp contract address is missing'); 71 | } 72 | const contractAddress = quote.fastMctpMayanContract; 73 | 74 | return { 75 | tokenIn: quote.fastMctpInputContract, 76 | amountIn, 77 | redeemFee, 78 | gasDrop, 79 | destAddr: destinationAddressHex, 80 | destDomain, 81 | referrerAddr: referrerHex, 82 | referrerBps: quote.referrerBps, 83 | payloadType: customPayload ? FAST_MCTP_PAYLOAD_TYPE_CUSTOM_PAYLOAD : FAST_MCTP_PAYLOAD_TYPE_DEFAULT, 84 | customPayload: customPayload ? `0x${Buffer.from(customPayload).toString('hex')}` : '0x', 85 | minFinalityThreshold: Number(quote.fastMctpMinFinality), 86 | circleMaxFee, 87 | contractAddress, 88 | }; 89 | } 90 | 91 | function getEvmFastMctpBridgeTxPayload( 92 | quote: Quote, destinationAddress: string, 93 | referrerAddress: string | null | undefined, signerChainId: number | string, 94 | payload: Uint8Array | Buffer | null | undefined 95 | ): TransactionRequest & { _params: EvmFastMctpBridgeParams } { 96 | const params = getEvmFastMctpBridgeParams( 97 | quote, destinationAddress, referrerAddress, signerChainId, payload 98 | ); 99 | const { 100 | contractAddress, tokenIn, amountIn, destAddr, 101 | redeemFee, gasDrop, circleMaxFee, referrerAddr, referrerBps, 102 | destDomain, customPayload, minFinalityThreshold, payloadType, 103 | } = params; 104 | 105 | const fastMctpContract = new Contract(contractAddress, MayanFastMctpArtifact.abi); 106 | let data: string; 107 | let value: string | null; 108 | data = fastMctpContract.interface.encodeFunctionData( 109 | 'bridge', 110 | [ 111 | tokenIn, amountIn, redeemFee, circleMaxFee, gasDrop, destAddr, 112 | destDomain, referrerAddr, referrerBps, payloadType, minFinalityThreshold, customPayload 113 | ] 114 | ); 115 | value = toBeHex(0); 116 | 117 | return { 118 | to: contractAddress, 119 | data, 120 | value, 121 | _params: params 122 | }; 123 | } 124 | 125 | 126 | type EvmFastMctpCreateOrderParams = { 127 | tokenIn: string, 128 | amountIn: bigint, 129 | circleMaxFee: bigint, 130 | destDomain: number, 131 | minFinalityThreshold: number, 132 | orderPayload: { 133 | payloadType: number, 134 | destAddr: string, 135 | tokenOut: string, 136 | amountOutMin: bigint, 137 | gasDrop: bigint, 138 | redeemFee: bigint, 139 | refundFee: bigint, 140 | deadline: bigint, 141 | referrerAddr: string, 142 | referrerBps: number, 143 | }, 144 | contractAddress: string, 145 | } 146 | 147 | function getEvmFastMctpCreateOrderParams( 148 | quote: Quote, destinationAddress: string, 149 | referrerAddress: string | null | undefined, signerChainId: string | number 150 | ): EvmFastMctpCreateOrderParams { 151 | const signerWormholeChainId = getWormholeChainIdById(Number(signerChainId)); 152 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 153 | const destChainId = getWormholeChainIdByName(quote.toChain); 154 | if (sourceChainId !== signerWormholeChainId) { 155 | throw new Error(`Signer chain id(${Number(signerChainId)}) and quote from chain are not same! ${sourceChainId} !== ${signerWormholeChainId}`); 156 | } 157 | if (!quote.fastMctpMayanContract) { 158 | throw new Error('MCTP contract address is missing'); 159 | } 160 | const contractAddress = quote.fastMctpMayanContract; 161 | 162 | const destinationAddressHex = nativeAddressToHexString(destinationAddress, destChainId); 163 | let referrerHex: string; 164 | if (referrerAddress) { 165 | referrerHex = nativeAddressToHexString( 166 | referrerAddress, destChainId 167 | ); 168 | } else { 169 | referrerHex = nativeAddressToHexString( 170 | SystemProgram.programId.toString(), getWormholeChainIdByName('solana') 171 | ); 172 | } 173 | 174 | const redeemFee = getAmountOfFractionalAmount(quote.redeemRelayerFee, CCTP_TOKEN_DECIMALS); 175 | const refundFee = BigInt(quote.refundRelayerFee64); 176 | const circleMaxFee = BigInt(quote.circleMaxFee64); 177 | const gasDrop = getAmountOfFractionalAmount(quote.gasDrop, Math.min(getGasDecimal(quote.toChain), 8)); 178 | const destDomain = getCCTPDomain(quote.toChain); 179 | 180 | const amountIn = BigInt(quote.effectiveAmountIn64); 181 | const amountOutMin = getAmountOfFractionalAmount( 182 | quote.minAmountOut, Math.min(8, quote.toToken.decimals) 183 | ); 184 | 185 | const deadline = BigInt(quote.deadline64); 186 | 187 | const tokenOut = 188 | quote.toToken.contract === ZeroAddress ? 189 | nativeAddressToHexString(SystemProgram.programId.toString(), getWormholeChainIdByName('solana')) : 190 | nativeAddressToHexString( 191 | quote.toChain === 'sui' ? quote.toToken.verifiedAddress : quote.toToken.contract, 192 | quote.toToken.wChainId, 193 | ); 194 | 195 | return { 196 | tokenIn: quote.fastMctpInputContract, 197 | amountIn, 198 | circleMaxFee, 199 | destDomain, 200 | minFinalityThreshold: Number(quote.fastMctpMinFinality), 201 | orderPayload: { 202 | payloadType: FAST_MCTP_PAYLOAD_TYPE_ORDER, 203 | destAddr: destinationAddressHex, 204 | tokenOut, 205 | amountOutMin, 206 | gasDrop, 207 | redeemFee, 208 | refundFee, 209 | deadline, 210 | referrerAddr: referrerHex, 211 | referrerBps: quote.referrerBps || 0, 212 | }, 213 | contractAddress, 214 | }; 215 | } 216 | 217 | function getEvmFastMctpCreateOrderTxPayload( 218 | quote: Quote, destinationAddress: string, 219 | referrerAddress: string | null | undefined, signerChainId: string | number 220 | ): TransactionRequest & { _params: EvmFastMctpCreateOrderParams } { 221 | const orderParams = getEvmFastMctpCreateOrderParams( 222 | quote, destinationAddress, referrerAddress, signerChainId 223 | ); 224 | const { 225 | contractAddress, orderPayload, tokenIn, amountIn, circleMaxFee, destDomain, minFinalityThreshold 226 | } = orderParams; 227 | const fastMctpContract = new Contract(contractAddress, MayanFastMctpArtifact.abi); 228 | const data = fastMctpContract.interface.encodeFunctionData( 229 | 'createOrder', 230 | [tokenIn, amountIn, circleMaxFee, destDomain, minFinalityThreshold, orderPayload] 231 | ); 232 | const value = toBeHex(0); 233 | 234 | return { 235 | to: contractAddress, 236 | data, 237 | value, 238 | _params: orderParams, 239 | }; 240 | } 241 | 242 | export function getFastMctpFromEvmTxPayload( 243 | quote: Quote, destinationAddress: string, referrerAddress: string | null | undefined, 244 | signerChainId: number | string, permit: Erc20Permit | null, payload: Uint8Array | Buffer | null | undefined, 245 | ): TransactionRequest & { _forwarder: EvmForwarderParams } { 246 | 247 | if (quote.type !== 'FAST_MCTP') { 248 | throw new Error('Quote type is not FAST_MCTP'); 249 | } 250 | 251 | if (!Number.isFinite(Number(signerChainId))) { 252 | throw new Error('Invalid signer chain id'); 253 | } 254 | 255 | signerChainId = Number(signerChainId); 256 | 257 | const _permit = permit || ZeroPermit; 258 | const forwarder = new Contract(addresses.MAYAN_FORWARDER_CONTRACT, MayanForwarderArtifact.abi); 259 | 260 | if (quote.fromToken.contract === quote.fastMctpInputContract) { 261 | if (quote.hasAuction) { 262 | if (!Number(quote.deadline64)) { 263 | throw new Error('Fast Mctp order requires timeout'); 264 | } 265 | const fastMctpPayloadIx = getEvmFastMctpCreateOrderTxPayload( 266 | quote, destinationAddress, referrerAddress, signerChainId 267 | ); 268 | 269 | const forwarderMethod = 'forwardERC20'; 270 | const forwarderParams = [ 271 | quote.fromToken.contract, 272 | fastMctpPayloadIx._params.amountIn, 273 | _permit, 274 | fastMctpPayloadIx._params.contractAddress, 275 | fastMctpPayloadIx.data, 276 | ]; 277 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 278 | return { 279 | data, 280 | to: addresses.MAYAN_FORWARDER_CONTRACT, 281 | value: toBeHex(0), 282 | chainId: signerChainId, 283 | _forwarder: { 284 | method: forwarderMethod, 285 | params: forwarderParams, 286 | } 287 | } 288 | } else { 289 | const fastMctpPayloadIx = getEvmFastMctpBridgeTxPayload( 290 | quote, destinationAddress, referrerAddress, signerChainId, payload 291 | ); 292 | const forwarderMethod = 'forwardERC20'; 293 | const forwarderParams = [ 294 | quote.fromToken.contract, 295 | fastMctpPayloadIx._params.amountIn, 296 | _permit, 297 | fastMctpPayloadIx._params.contractAddress, 298 | fastMctpPayloadIx.data, 299 | ]; 300 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 301 | return { 302 | data, 303 | to: addresses.MAYAN_FORWARDER_CONTRACT, 304 | value: toBeHex(0), 305 | chainId: signerChainId, 306 | _forwarder: { 307 | method: forwarderMethod, 308 | params: forwarderParams, 309 | } 310 | } 311 | } 312 | } else { 313 | const { minMiddleAmount, evmSwapRouterAddress, evmSwapRouterCalldata } = quote; 314 | if (!minMiddleAmount || !evmSwapRouterAddress || !evmSwapRouterCalldata) { 315 | throw new Error('Fast Mctp swap requires middle amount, router address and calldata'); 316 | } 317 | if (quote.hasAuction) { 318 | if (!Number(quote.deadline64)) { 319 | throw new Error('Fast Mctp order requires timeout'); 320 | } 321 | const fastMctpPayloadIx = getEvmFastMctpCreateOrderTxPayload( 322 | quote, destinationAddress, referrerAddress, signerChainId 323 | ); 324 | const minMiddleAmount = getAmountOfFractionalAmount(quote.minMiddleAmount, CCTP_TOKEN_DECIMALS); 325 | 326 | if (quote.fromToken.contract === ZeroAddress) { 327 | const forwarderMethod = 'swapAndForwardEth'; 328 | const forwarderParams = [ 329 | fastMctpPayloadIx._params.amountIn, 330 | evmSwapRouterAddress, 331 | evmSwapRouterCalldata, 332 | quote.fastMctpInputContract, 333 | minMiddleAmount, 334 | fastMctpPayloadIx._params.contractAddress, 335 | fastMctpPayloadIx.data, 336 | ]; 337 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 338 | return { 339 | data, 340 | to: addresses.MAYAN_FORWARDER_CONTRACT, 341 | value: toBeHex(fastMctpPayloadIx._params.amountIn), 342 | chainId: signerChainId, 343 | _forwarder: { 344 | method: forwarderMethod, 345 | params: forwarderParams, 346 | } 347 | } 348 | } else { 349 | const forwarderMethod = 'swapAndForwardERC20'; 350 | const forwarderParams = [ 351 | quote.fromToken.contract, 352 | fastMctpPayloadIx._params.amountIn, 353 | _permit, 354 | evmSwapRouterAddress, 355 | evmSwapRouterCalldata, 356 | quote.fastMctpInputContract, 357 | minMiddleAmount, 358 | fastMctpPayloadIx._params.contractAddress, 359 | fastMctpPayloadIx.data, 360 | ]; 361 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 362 | return { 363 | data, 364 | to: addresses.MAYAN_FORWARDER_CONTRACT, 365 | value: toBeHex(0), 366 | chainId: signerChainId, 367 | _forwarder: { 368 | method: forwarderMethod, 369 | params: forwarderParams, 370 | } 371 | } 372 | } 373 | } else { 374 | const fastMctpPayloadIx = getEvmFastMctpBridgeTxPayload( 375 | quote, destinationAddress, referrerAddress, signerChainId, payload 376 | ); 377 | const minMiddleAmount = getAmountOfFractionalAmount(quote.minMiddleAmount, CCTP_TOKEN_DECIMALS); 378 | 379 | if (quote.fromToken.contract === ZeroAddress) { 380 | const forwarderMethod = 'swapAndForwardEth'; 381 | const forwarderParams = [ 382 | fastMctpPayloadIx._params.amountIn, 383 | evmSwapRouterAddress, 384 | evmSwapRouterCalldata, 385 | quote.fastMctpInputContract, 386 | minMiddleAmount, 387 | fastMctpPayloadIx._params.contractAddress, 388 | fastMctpPayloadIx.data, 389 | ]; 390 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 391 | return { 392 | data, 393 | to: addresses.MAYAN_FORWARDER_CONTRACT, 394 | value: toBeHex(fastMctpPayloadIx._params.amountIn), 395 | chainId: signerChainId, 396 | _forwarder: { 397 | method: forwarderMethod, 398 | params: forwarderParams, 399 | } 400 | } 401 | } else { 402 | const forwarderMethod = 'swapAndForwardERC20'; 403 | const forwarderParams = [ 404 | quote.fromToken.contract, 405 | fastMctpPayloadIx._params.amountIn, 406 | _permit, 407 | evmSwapRouterAddress, 408 | evmSwapRouterCalldata, 409 | quote.fastMctpInputContract, 410 | minMiddleAmount, 411 | fastMctpPayloadIx._params.contractAddress, 412 | fastMctpPayloadIx.data, 413 | ] 414 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 415 | return { 416 | data, 417 | to: addresses.MAYAN_FORWARDER_CONTRACT, 418 | value: toBeHex(0), 419 | chainId: signerChainId, 420 | _forwarder: { 421 | method: forwarderMethod, 422 | params: forwarderParams, 423 | } 424 | } 425 | } 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/evm/evmHyperCore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Contract, 3 | toBeHex, 4 | ZeroAddress, 5 | TransactionRequest, 6 | } from 'ethers'; 7 | import { SystemProgram } from '@solana/web3.js'; 8 | import type { EvmForwarderParams, Quote } from '../types'; 9 | import { 10 | nativeAddressToHexString, 11 | getAmountOfFractionalAmount, 12 | getWormholeChainIdByName, 13 | getWormholeChainIdById, 14 | getGasDecimal, 15 | ZeroPermit, 16 | hexToUint8Array 17 | } from '../utils'; 18 | 19 | import HCDepositInitiatorArtifact from './HCDepositInitiatorArtifact'; 20 | import MayanForwarderArtifact from './MayanForwarderArtifact'; 21 | import addresses from '../addresses'; 22 | import { Buffer } from 'buffer'; 23 | import { CCTP_TOKEN_DECIMALS } from '../cctp'; 24 | import { Erc20Permit } from '../types'; 25 | 26 | function getUsdcDepositInitiatorMctpTxPayload( 27 | quote: Quote, 28 | swapperAddress: string, 29 | destinationAddress: string, 30 | usdcPermitSignature: string, 31 | ): TransactionRequest & { _params: { amountIn: bigint, contractAddress: string } } { 32 | if (!quote.hyperCoreParams.initiateContractAddress) { 33 | throw new Error('HyperCore initiate contract address is missing'); 34 | } 35 | if (quote.type !== 'MCTP') { 36 | throw new Error('Unsupported quote type for HyperCore deposit: ' + quote.type); 37 | } 38 | 39 | const initiatorContract = new Contract( 40 | quote.hyperCoreParams.initiateContractAddress, 41 | HCDepositInitiatorArtifact.abi 42 | ); 43 | 44 | const signatureBuf = Buffer.from(hexToUint8Array(usdcPermitSignature)); 45 | if (signatureBuf.length !== 65) { 46 | throw new Error('Invalid USDC permit signature length'); 47 | } 48 | const r = '0x' + signatureBuf.subarray(0, 32).toString('hex'); 49 | const s = '0x' + signatureBuf.subarray(32, 64).toString('hex'); 50 | const v = signatureBuf[64]; 51 | 52 | let data: string; 53 | let value: string | null; 54 | data = initiatorContract.interface.encodeFunctionData('deposit', [ 55 | quote.hyperCoreParams.initiateTokenContract, 56 | BigInt(quote.hyperCoreParams.bridgeAmountUSDC64), 57 | swapperAddress, 58 | getAmountOfFractionalAmount( 59 | quote.hyperCoreParams.failureGasDrop, 60 | Math.min(getGasDecimal('arbitrum'), 8) 61 | ), 62 | BigInt(quote.hyperCoreParams.bridgeAmountUSDC64), 63 | { 64 | relayerFee: getAmountOfFractionalAmount(quote.redeemRelayerFee, CCTP_TOKEN_DECIMALS), 65 | permit: { 66 | user: destinationAddress, 67 | usd: BigInt(quote.hyperCoreParams.depositAmountUSDC64), 68 | deadline: BigInt(quote.deadline64), 69 | signature: { 70 | r, 71 | s, 72 | v, 73 | }, 74 | } 75 | }, 76 | ]); 77 | value = toBeHex(0); 78 | 79 | return { 80 | to: quote.hyperCoreParams.initiateContractAddress, 81 | data, 82 | value, 83 | _params: { 84 | amountIn: BigInt(quote.hyperCoreParams.bridgeAmountUSDC64), 85 | contractAddress: quote.hyperCoreParams.initiateContractAddress, 86 | }, 87 | }; 88 | } 89 | 90 | function getUsdcDepositInitiatorFastMctpTxPayload( 91 | quote: Quote, 92 | swapperAddress: string, 93 | destinationAddress: string, 94 | referrerAddress: string | null | undefined, 95 | usdcPermitSignature: string, 96 | ): TransactionRequest & { _params: { amountIn: bigint, contractAddress: string } } { 97 | const destChainId = getWormholeChainIdByName('arbitrum'); 98 | if (!quote.hyperCoreParams.initiateContractAddress) { 99 | throw new Error('HyperCore initiate contract address is missing'); 100 | } 101 | if (quote.type !== 'FAST_MCTP') { 102 | throw new Error('Unsupported quote type for HyperCore deposit: ' + quote.type); 103 | } 104 | 105 | const initiatorContract = new Contract( 106 | quote.hyperCoreParams.initiateContractAddress, 107 | HCDepositInitiatorArtifact.abi 108 | ); 109 | 110 | const signatureBuf = Buffer.from(hexToUint8Array(usdcPermitSignature)); 111 | if (signatureBuf.length !== 65) { 112 | throw new Error('Invalid USDC permit signature length'); 113 | } 114 | const r = '0x' + signatureBuf.subarray(0, 32).toString('hex'); 115 | const s = '0x' + signatureBuf.subarray(32, 64).toString('hex'); 116 | const v = signatureBuf[64]; 117 | 118 | let referrerHex: string; 119 | if (referrerAddress) { 120 | referrerHex = nativeAddressToHexString( 121 | referrerAddress, destChainId 122 | ); 123 | } else { 124 | referrerHex = nativeAddressToHexString( 125 | SystemProgram.programId.toString(), getWormholeChainIdByName('solana') 126 | ); 127 | } 128 | 129 | let data: string; 130 | let value: string | null; 131 | data = initiatorContract.interface.encodeFunctionData('fastDeposit', [ 132 | quote.hyperCoreParams.initiateTokenContract, 133 | BigInt(quote.hyperCoreParams.bridgeAmountUSDC64), 134 | swapperAddress, 135 | BigInt(quote.circleMaxFee64), 136 | getAmountOfFractionalAmount( 137 | quote.hyperCoreParams.failureGasDrop, 138 | Math.min(getGasDecimal('arbitrum'), 8) 139 | ), 140 | referrerHex, 141 | quote.referrerBps, 142 | Number(quote.fastMctpMinFinality), 143 | BigInt(quote.hyperCoreParams.bridgeAmountUSDC64), 144 | { 145 | relayerFee: getAmountOfFractionalAmount(quote.redeemRelayerFee, CCTP_TOKEN_DECIMALS), 146 | permit: { 147 | user: destinationAddress, 148 | usd: BigInt(quote.hyperCoreParams.depositAmountUSDC64), 149 | deadline: BigInt(quote.deadline64), 150 | signature: { 151 | r, 152 | s, 153 | v, 154 | }, 155 | } 156 | }, 157 | ]); 158 | value = toBeHex(0); 159 | 160 | return { 161 | to: quote.hyperCoreParams.initiateContractAddress, 162 | data, 163 | value, 164 | _params: { 165 | amountIn: BigInt(quote.hyperCoreParams.bridgeAmountUSDC64), 166 | contractAddress: quote.hyperCoreParams.initiateContractAddress, 167 | }, 168 | }; 169 | } 170 | 171 | export function getHyperCoreDepositFromEvmTxPayload( 172 | quote: Quote, swapperAddress: string, destinationAddress: string, referrerAddress: string | null | undefined, 173 | signerChainId: number | string, permit: Erc20Permit | null, payload: Uint8Array | Buffer | null | undefined, 174 | options: { 175 | usdcPermitSignature?: string; 176 | } = {} 177 | ): TransactionRequest & { _forwarder: EvmForwarderParams } { 178 | 179 | if ( 180 | quote.toToken.contract.toLowerCase() !== addresses.ARBITRUM_USDC_CONTRACT.toLowerCase() || 181 | (quote.type !== 'MCTP' && quote.type !== 'FAST_MCTP') 182 | ) { 183 | throw new Error('Unsupported quote type for USDC deposit: ' + quote.type); 184 | } 185 | if (!options?.usdcPermitSignature) { 186 | throw new Error('USDC permit signature is required for this quote'); 187 | } 188 | if (!quote.hyperCoreParams) { 189 | throw new Error('HyperCore parameters are required for this quote'); 190 | } 191 | if (payload) { 192 | throw new Error('HyperCore deposit does not support payload'); 193 | } 194 | 195 | if (!Number.isFinite(Number(signerChainId))) { 196 | throw new Error('Invalid signer chain id'); 197 | } 198 | 199 | const signerWormholeChainId = getWormholeChainIdById(Number(signerChainId)); 200 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 201 | if (sourceChainId !== signerWormholeChainId) { 202 | throw new Error(`Signer chain id(${Number(signerChainId)}) and quote from chain are not same! ${sourceChainId} !== ${signerWormholeChainId}`); 203 | } 204 | const _permit = permit || ZeroPermit; 205 | const forwarder = new Contract(addresses.MAYAN_FORWARDER_CONTRACT, MayanForwarderArtifact.abi); 206 | 207 | let initiatorPayloadIx: TransactionRequest & { _params: { amountIn: bigint, contractAddress: string } }; 208 | if (!Number(quote.deadline64)) { 209 | throw new Error('HyperCore deposit requires timeout'); 210 | } 211 | if (quote.type === 'MCTP') { 212 | initiatorPayloadIx = getUsdcDepositInitiatorMctpTxPayload( 213 | quote, swapperAddress, destinationAddress, options.usdcPermitSignature 214 | ); 215 | } else if (quote.type === 'FAST_MCTP') { 216 | initiatorPayloadIx = getUsdcDepositInitiatorFastMctpTxPayload( 217 | quote, swapperAddress, destinationAddress, referrerAddress, options.usdcPermitSignature 218 | ); 219 | } else { 220 | throw new Error('Unsupported quote type for HyperCore deposit: ' + quote.type); 221 | } 222 | 223 | if (quote.fromToken.contract.toLowerCase() === quote.hyperCoreParams.initiateTokenContract.toLowerCase()) { 224 | const forwarderMethod = 'forwardERC20'; 225 | const forwarderParams = [ 226 | quote.fromToken.contract, 227 | BigInt(quote.effectiveAmountIn64), 228 | _permit, 229 | initiatorPayloadIx._params.contractAddress, 230 | initiatorPayloadIx.data, 231 | ]; 232 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 233 | return { 234 | data, 235 | to: addresses.MAYAN_FORWARDER_CONTRACT, 236 | value: toBeHex(0), 237 | chainId: signerChainId, 238 | _forwarder: { 239 | method: forwarderMethod, 240 | params: forwarderParams, 241 | } 242 | 243 | } 244 | } else { 245 | const { evmSwapRouterAddress, evmSwapRouterCalldata } = quote; 246 | if (!quote.minMiddleAmount || !evmSwapRouterAddress || !evmSwapRouterCalldata) { 247 | throw new Error('Fast Mctp swap requires middle amount, router address and calldata'); 248 | } 249 | const minMiddleAmount = getAmountOfFractionalAmount(quote.minMiddleAmount, CCTP_TOKEN_DECIMALS); 250 | 251 | if (quote.fromToken.contract === ZeroAddress) { 252 | const forwarderMethod = 'swapAndForwardEth'; 253 | const forwarderParams = [ 254 | BigInt(quote.effectiveAmountIn64), 255 | evmSwapRouterAddress, 256 | evmSwapRouterCalldata, 257 | quote.hyperCoreParams.initiateTokenContract, 258 | minMiddleAmount, 259 | initiatorPayloadIx._params.contractAddress, 260 | initiatorPayloadIx.data, 261 | ]; 262 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 263 | return { 264 | data, 265 | to: addresses.MAYAN_FORWARDER_CONTRACT, 266 | value: toBeHex(BigInt(quote.effectiveAmountIn64)), 267 | chainId: signerChainId, 268 | _forwarder: { 269 | method: forwarderMethod, 270 | params: forwarderParams, 271 | } 272 | } 273 | } else { 274 | const forwarderMethod = 'swapAndForwardERC20'; 275 | const forwarderParams = [ 276 | quote.fromToken.contract, 277 | BigInt(quote.effectiveAmountIn64), 278 | _permit, 279 | evmSwapRouterAddress, 280 | evmSwapRouterCalldata, 281 | quote.hyperCoreParams.initiateTokenContract, 282 | minMiddleAmount, 283 | initiatorPayloadIx._params.contractAddress, 284 | initiatorPayloadIx.data, 285 | ]; 286 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 287 | return { 288 | data, 289 | to: addresses.MAYAN_FORWARDER_CONTRACT, 290 | value: toBeHex(0), 291 | chainId: signerChainId, 292 | _forwarder: { 293 | method: forwarderMethod, 294 | params: forwarderParams, 295 | } 296 | } 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/evm/evmMctp.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Contract, 3 | toBeHex, 4 | ZeroAddress, 5 | TransactionRequest, 6 | } from 'ethers'; 7 | import { SystemProgram } from '@solana/web3.js'; 8 | import type { EvmForwarderParams, Quote } from '../types'; 9 | import { 10 | nativeAddressToHexString, 11 | getAmountOfFractionalAmount, getWormholeChainIdByName, 12 | getWormholeChainIdById, getGasDecimal, ZeroPermit, MCTP_PAYLOAD_TYPE_CUSTOM_PAYLOAD, MCTP_PAYLOAD_TYPE_DEFAULT 13 | } from '../utils'; 14 | 15 | import MayanCircleArtifact from './MayanCircleArtifact'; 16 | import MayanForwarderArtifact from './MayanForwarderArtifact'; 17 | import addresses from '../addresses'; 18 | import { Buffer } from 'buffer'; 19 | import { getCCTPDomain, CCTP_TOKEN_DECIMALS } from '../cctp'; 20 | import { Erc20Permit } from '../types'; 21 | 22 | type EvmMctpBridgeParams = { 23 | lockFee: boolean, 24 | tokenIn: string, 25 | amountIn: bigint, 26 | redeemFee: bigint, 27 | gasDrop: bigint, 28 | destAddr: string, 29 | customPayload: string, 30 | payloadType: number, 31 | destDomain: number, 32 | bridgeFee: bigint, 33 | contractAddress: string, 34 | } 35 | function getEvmMctpBridgeParams( 36 | quote: Quote, destinationAddress: string, signerChainId: number | string, customPayload?: Uint8Array | Buffer | null 37 | ): EvmMctpBridgeParams { 38 | const signerWormholeChainId = getWormholeChainIdById(Number(signerChainId)); 39 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 40 | const destChainId = getWormholeChainIdByName(quote.toChain); 41 | if (sourceChainId !== signerWormholeChainId) { 42 | throw new Error(`Signer chain id(${Number(signerChainId)}) and quote from chain are not same! ${sourceChainId} !== ${signerWormholeChainId}`); 43 | } 44 | const lockFee: boolean = quote.cheaperChain === quote.fromChain; 45 | if (lockFee && !!customPayload) { 46 | throw new Error('Bridge lock fee cannot have custom payload'); 47 | } 48 | const destinationAddressHex = nativeAddressToHexString(destinationAddress, destChainId); 49 | const redeemFee = getAmountOfFractionalAmount(quote.redeemRelayerFee, CCTP_TOKEN_DECIMALS); 50 | const gasDrop = getAmountOfFractionalAmount(quote.gasDrop, Math.min(getGasDecimal(quote.toChain), 8)); 51 | const amountIn = BigInt(quote.effectiveAmountIn64); 52 | const destDomain = getCCTPDomain(quote.toChain); 53 | 54 | if (!quote.mctpMayanContract) { 55 | throw new Error('MCTP contract address is missing'); 56 | } 57 | const contractAddress = quote.mctpMayanContract; 58 | 59 | if (quote.toChain === 'solana' && lockFee) { 60 | throw new Error('Cannot lock fee for transfer to solana'); 61 | } 62 | 63 | let bridgeFee = getAmountOfFractionalAmount( 64 | quote.bridgeFee, getGasDecimal(quote.fromChain) 65 | ); 66 | if (lockFee) { 67 | bridgeFee = BigInt(0); 68 | } 69 | 70 | 71 | 72 | return { 73 | lockFee, 74 | tokenIn: quote.mctpInputContract, 75 | amountIn, 76 | redeemFee, 77 | gasDrop, 78 | destAddr: destinationAddressHex, 79 | destDomain, 80 | payloadType: customPayload ? MCTP_PAYLOAD_TYPE_CUSTOM_PAYLOAD : MCTP_PAYLOAD_TYPE_DEFAULT, 81 | customPayload: customPayload ? `0x${Buffer.from(customPayload).toString('hex')}` : '0x', 82 | bridgeFee, 83 | contractAddress, 84 | }; 85 | } 86 | 87 | function getEvmMctpBridgeTxPayload( 88 | quote: Quote, destinationAddress: string, signerChainId: number | string, 89 | payload: Uint8Array | Buffer | null | undefined 90 | ): TransactionRequest & { _params: EvmMctpBridgeParams } { 91 | const params = getEvmMctpBridgeParams( 92 | quote, destinationAddress, signerChainId, payload 93 | ); 94 | const { 95 | contractAddress, tokenIn, amountIn, destAddr, 96 | lockFee, redeemFee, gasDrop, 97 | destDomain, customPayload, payloadType, bridgeFee 98 | } = params; 99 | 100 | const mctpContract = new Contract(contractAddress, MayanCircleArtifact.abi); 101 | let data: string; 102 | let value: string | null; 103 | if (lockFee) { 104 | data = mctpContract.interface.encodeFunctionData( 105 | 'bridgeWithLockedFee', 106 | [tokenIn, amountIn, gasDrop, redeemFee, destDomain, destAddr] 107 | ); 108 | } else { 109 | data = mctpContract.interface.encodeFunctionData( 110 | 'bridgeWithFee', 111 | [tokenIn, amountIn, redeemFee, gasDrop, destAddr, destDomain, payloadType, customPayload] 112 | ); 113 | } 114 | value = toBeHex(bridgeFee); 115 | 116 | return { 117 | to: contractAddress, 118 | data, 119 | value, 120 | _params: params 121 | }; 122 | } 123 | 124 | 125 | type EvmMctpCreateOrderParams = { 126 | params: { 127 | tokenIn: string, 128 | amountIn: bigint, 129 | gasDrop: bigint, 130 | destAddr: string, 131 | destChain: number, 132 | tokenOut: string, 133 | minAmountOut: bigint, 134 | deadline: bigint, 135 | redeemFee: bigint, 136 | referrerAddr: string, 137 | referrerBps: number, 138 | }, 139 | bridgeFee: bigint, 140 | contractAddress: string, 141 | } 142 | 143 | function getEvmMctpCreateOrderParams( 144 | quote: Quote, destinationAddress: string, 145 | referrerAddress: string | null | undefined, signerChainId: string | number 146 | ): EvmMctpCreateOrderParams { 147 | const signerWormholeChainId = getWormholeChainIdById(Number(signerChainId)); 148 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 149 | const destChainId = getWormholeChainIdByName(quote.toChain); 150 | if (sourceChainId !== signerWormholeChainId) { 151 | throw new Error(`Signer chain id(${Number(signerChainId)}) and quote from chain are not same! ${sourceChainId} !== ${signerWormholeChainId}`); 152 | } 153 | if (!quote.mctpMayanContract) { 154 | throw new Error('MCTP contract address is missing'); 155 | } 156 | const contractAddress = quote.mctpMayanContract; 157 | 158 | const destinationAddressHex = nativeAddressToHexString(destinationAddress, destChainId); 159 | let referrerHex: string; 160 | if (referrerAddress) { 161 | referrerHex = nativeAddressToHexString( 162 | referrerAddress, destChainId 163 | ); 164 | } else { 165 | referrerHex = nativeAddressToHexString( 166 | SystemProgram.programId.toString(), getWormholeChainIdByName('solana') 167 | ); 168 | } 169 | 170 | const redeemFee = getAmountOfFractionalAmount(quote.redeemRelayerFee, CCTP_TOKEN_DECIMALS); 171 | const gasDrop = getAmountOfFractionalAmount(quote.gasDrop, Math.min(getGasDecimal(quote.toChain), 8)); 172 | 173 | let amountIn = BigInt(quote.effectiveAmountIn64); 174 | const minAmountOut = getAmountOfFractionalAmount( 175 | quote.minAmountOut, Math.min(8, quote.toToken.decimals) 176 | ); 177 | 178 | const deadline = BigInt(quote.deadline64); 179 | 180 | const tokenOut = 181 | quote.toToken.contract === ZeroAddress ? 182 | nativeAddressToHexString(SystemProgram.programId.toString(), getWormholeChainIdByName('solana')) : 183 | nativeAddressToHexString( 184 | quote.toChain === 'sui' ? quote.toToken.verifiedAddress : quote.toToken.contract, 185 | quote.toToken.wChainId, 186 | ); 187 | 188 | return { 189 | params: { 190 | tokenIn: quote.mctpInputContract, 191 | amountIn, 192 | gasDrop, 193 | destAddr: destinationAddressHex, 194 | destChain: destChainId, 195 | tokenOut, 196 | minAmountOut, 197 | deadline, 198 | redeemFee, 199 | referrerAddr: referrerHex, 200 | referrerBps: quote.referrerBps || 0 201 | }, 202 | bridgeFee: getAmountOfFractionalAmount(quote.bridgeFee, getGasDecimal(quote.fromChain)), 203 | contractAddress, 204 | }; 205 | } 206 | 207 | function getEvmMctpCreateOrderTxPayload( 208 | quote: Quote, destinationAddress: string, 209 | referrerAddress: string | null | undefined, signerChainId: string | number 210 | ): TransactionRequest & { _params: EvmMctpCreateOrderParams } { 211 | const orderParams = getEvmMctpCreateOrderParams( 212 | quote, destinationAddress, referrerAddress, signerChainId 213 | ); 214 | const { 215 | contractAddress, params, bridgeFee 216 | } = orderParams; 217 | const mctpContract = new Contract(contractAddress, MayanCircleArtifact.abi); 218 | const data = mctpContract.interface.encodeFunctionData( 219 | 'createOrder', 220 | [params] 221 | ); 222 | const value = toBeHex(bridgeFee); 223 | 224 | return { 225 | to: contractAddress, 226 | data, 227 | value, 228 | _params: orderParams, 229 | }; 230 | } 231 | 232 | export function getMctpFromEvmTxPayload( 233 | quote: Quote, destinationAddress: string, referrerAddress: string | null | undefined, 234 | signerChainId: number | string, permit: Erc20Permit | null, payload: Uint8Array | Buffer | null | undefined, 235 | ): TransactionRequest & { _forwarder: EvmForwarderParams } { 236 | 237 | if (quote.type !== 'MCTP') { 238 | throw new Error('Quote type is not MCTP'); 239 | } 240 | 241 | if (!Number.isFinite(Number(signerChainId))) { 242 | throw new Error('Invalid signer chain id'); 243 | } 244 | 245 | signerChainId = Number(signerChainId); 246 | 247 | const _permit = permit || ZeroPermit; 248 | const forwarder = new Contract(addresses.MAYAN_FORWARDER_CONTRACT, MayanForwarderArtifact.abi); 249 | 250 | const bridgeFee = getAmountOfFractionalAmount( 251 | quote.bridgeFee, getGasDecimal(quote.fromChain) 252 | ); 253 | 254 | let value = toBeHex(bridgeFee); 255 | 256 | if (quote.fromToken.contract === quote.mctpInputContract) { 257 | if (quote.hasAuction) { 258 | if (!Number(quote.deadline64)) { 259 | throw new Error('MCTP order requires timeout'); 260 | } 261 | const mctpPayloadIx = getEvmMctpCreateOrderTxPayload( 262 | quote, destinationAddress, referrerAddress, signerChainId 263 | ); 264 | 265 | const forwarderMethod = 'forwardERC20'; 266 | const forwarderParams = [ 267 | quote.fromToken.contract, 268 | mctpPayloadIx._params.params.amountIn, 269 | _permit, 270 | mctpPayloadIx._params.contractAddress, 271 | mctpPayloadIx.data, 272 | ]; 273 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 274 | return { 275 | data, 276 | to: addresses.MAYAN_FORWARDER_CONTRACT, 277 | value: toBeHex(value), 278 | chainId: signerChainId, 279 | _forwarder: { 280 | method: forwarderMethod, 281 | params: forwarderParams, 282 | } 283 | } 284 | } else { 285 | const mctpPayloadIx = getEvmMctpBridgeTxPayload( 286 | quote, destinationAddress, signerChainId, payload 287 | ); 288 | const forwarderMethod = 'forwardERC20'; 289 | const forwarderParams = [ 290 | quote.fromToken.contract, 291 | mctpPayloadIx._params.amountIn, 292 | _permit, 293 | mctpPayloadIx._params.contractAddress, 294 | mctpPayloadIx.data, 295 | ]; 296 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 297 | return { 298 | data, 299 | to: addresses.MAYAN_FORWARDER_CONTRACT, 300 | value: toBeHex(value), 301 | chainId: signerChainId, 302 | _forwarder: { 303 | method: forwarderMethod, 304 | params: forwarderParams, 305 | } 306 | } 307 | } 308 | } else { 309 | const { minMiddleAmount, evmSwapRouterAddress, evmSwapRouterCalldata } = quote; 310 | if (!minMiddleAmount || !evmSwapRouterAddress || !evmSwapRouterCalldata) { 311 | throw new Error('MCTP swap requires middle amount, router address and calldata'); 312 | } 313 | if (quote.hasAuction) { 314 | if (!Number(quote.deadline64)) { 315 | throw new Error('MCTP order requires timeout'); 316 | } 317 | const mctpPayloadIx = getEvmMctpCreateOrderTxPayload( 318 | quote, destinationAddress, referrerAddress, signerChainId 319 | ); 320 | const minMiddleAmount = getAmountOfFractionalAmount(quote.minMiddleAmount, CCTP_TOKEN_DECIMALS); 321 | 322 | if (quote.fromToken.contract === ZeroAddress) { 323 | 324 | let amountIn = mctpPayloadIx._params.params.amountIn; 325 | if (amountIn <= bridgeFee) { 326 | throw new Error('Amount in is less than bridge fee'); 327 | } 328 | if (bridgeFee !== BigInt(0)) { 329 | amountIn -= bridgeFee; 330 | } 331 | 332 | value = toBeHex(mctpPayloadIx._params.params.amountIn); 333 | 334 | const forwarderMethod = 'swapAndForwardEth'; 335 | const forwarderParams = [ 336 | amountIn, 337 | evmSwapRouterAddress, 338 | evmSwapRouterCalldata, 339 | quote.mctpInputContract, 340 | minMiddleAmount, 341 | mctpPayloadIx._params.contractAddress, 342 | mctpPayloadIx.data, 343 | ]; 344 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 345 | return { 346 | data, 347 | to: addresses.MAYAN_FORWARDER_CONTRACT, 348 | value: toBeHex(value), 349 | chainId: signerChainId, 350 | _forwarder: { 351 | method: forwarderMethod, 352 | params: forwarderParams, 353 | } 354 | } 355 | } else { 356 | const forwarderMethod = 'swapAndForwardERC20'; 357 | const forwarderParams = [ 358 | quote.fromToken.contract, 359 | mctpPayloadIx._params.params.amountIn, 360 | _permit, 361 | evmSwapRouterAddress, 362 | evmSwapRouterCalldata, 363 | quote.mctpInputContract, 364 | minMiddleAmount, 365 | mctpPayloadIx._params.contractAddress, 366 | mctpPayloadIx.data, 367 | ]; 368 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 369 | return { 370 | data, 371 | to: addresses.MAYAN_FORWARDER_CONTRACT, 372 | value: toBeHex(value), 373 | chainId: signerChainId, 374 | _forwarder: { 375 | method: forwarderMethod, 376 | params: forwarderParams, 377 | } 378 | } 379 | } 380 | } else { 381 | const mctpPayloadIx = getEvmMctpBridgeTxPayload( 382 | quote, destinationAddress, signerChainId, payload 383 | ); 384 | const minMiddleAmount = getAmountOfFractionalAmount(quote.minMiddleAmount, CCTP_TOKEN_DECIMALS); 385 | 386 | if (quote.fromToken.contract === ZeroAddress) { 387 | let amountIn = mctpPayloadIx._params.amountIn; 388 | if (amountIn <= bridgeFee) { 389 | throw new Error('Amount in is less than bridge fee'); 390 | } 391 | if (bridgeFee !== BigInt(0)) { 392 | amountIn -= bridgeFee; 393 | } 394 | 395 | value = toBeHex(mctpPayloadIx._params.amountIn); 396 | 397 | const forwarderMethod = 'swapAndForwardEth'; 398 | const forwarderParams = [ 399 | amountIn, 400 | evmSwapRouterAddress, 401 | evmSwapRouterCalldata, 402 | quote.mctpInputContract, 403 | minMiddleAmount, 404 | mctpPayloadIx._params.contractAddress, 405 | mctpPayloadIx.data, 406 | ]; 407 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 408 | return { 409 | data, 410 | to: addresses.MAYAN_FORWARDER_CONTRACT, 411 | value: toBeHex(value), 412 | chainId: signerChainId, 413 | _forwarder: { 414 | method: forwarderMethod, 415 | params: forwarderParams, 416 | } 417 | } 418 | } else { 419 | const forwarderMethod = 'swapAndForwardERC20'; 420 | const forwarderParams = [ 421 | quote.fromToken.contract, 422 | mctpPayloadIx._params.amountIn, 423 | _permit, 424 | evmSwapRouterAddress, 425 | evmSwapRouterCalldata, 426 | quote.mctpInputContract, 427 | minMiddleAmount, 428 | mctpPayloadIx._params.contractAddress, 429 | mctpPayloadIx.data, 430 | ] 431 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 432 | return { 433 | data, 434 | to: addresses.MAYAN_FORWARDER_CONTRACT, 435 | value: toBeHex(value), 436 | chainId: signerChainId, 437 | _forwarder: { 438 | method: forwarderMethod, 439 | params: forwarderParams, 440 | } 441 | } 442 | } 443 | } 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/evm/evmMonoChain.ts: -------------------------------------------------------------------------------- 1 | import { Contract, toBeHex, ZeroAddress, TransactionRequest } from 'ethers'; 2 | import { SystemProgram } from '@solana/web3.js'; 3 | import type { EvmForwarderParams, Quote } from '../types'; 4 | import { 5 | nativeAddressToHexString, 6 | getAmountOfFractionalAmount, 7 | getWormholeChainIdByName, 8 | getWormholeChainIdById, 9 | ZeroPermit, 10 | } from '../utils'; 11 | 12 | import MayanMonoChainArtifact from './MayanMonoChainArtifact'; 13 | import MayanForwarderArtifact from './MayanForwarderArtifact'; 14 | import addresses from '../addresses'; 15 | import { Erc20Permit } from '../types'; 16 | 17 | function getEvmMonoChainTxPayload( 18 | quote: Quote, 19 | destinationAddress: string, 20 | referrerAddress: string | null | undefined, 21 | ): TransactionRequest & { _params: { amountIn: bigint; tokenIn: string } } { 22 | const amountOut = getAmountOfFractionalAmount( 23 | quote.expectedAmountOut, 24 | quote.toToken.decimals 25 | ); 26 | const monoChainContract = new Contract( 27 | quote.monoChainMayanContract, 28 | MayanMonoChainArtifact.abi 29 | ); 30 | const referrerBps = referrerAddress ? quote.referrerBps || 0 : 0; 31 | 32 | let data: string; 33 | let value: string | null; 34 | if (quote.toToken.contract === ZeroAddress) { 35 | data = monoChainContract.interface.encodeFunctionData('transferEth', [ 36 | destinationAddress, 37 | referrerAddress || ZeroAddress, 38 | referrerBps, 39 | ]); 40 | } else { 41 | data = monoChainContract.interface.encodeFunctionData('transferToken', [ 42 | quote.toToken.contract, 43 | amountOut, 44 | destinationAddress, 45 | referrerAddress || ZeroAddress, 46 | referrerBps, 47 | ]); 48 | } 49 | if (quote.fromToken.contract === ZeroAddress) { 50 | value = toBeHex(quote.effectiveAmountIn64); 51 | } else { 52 | value = toBeHex(0); 53 | } 54 | 55 | return { 56 | to: quote.monoChainMayanContract, 57 | data, 58 | value, 59 | _params: { 60 | amountIn: BigInt(quote.effectiveAmountIn64), 61 | tokenIn: quote.fromToken.contract, 62 | }, 63 | }; 64 | } 65 | 66 | export function getMonoChainFromEvmTxPayload( 67 | quote: Quote, 68 | destinationAddress: string, 69 | referrerAddress: string | null | undefined, 70 | signerChainId: number | string, 71 | permit: Erc20Permit | null, 72 | ): TransactionRequest & { _forwarder: EvmForwarderParams } { 73 | if (quote.type !== 'MONO_CHAIN') { 74 | throw new Error('Quote type is not MONO_CHAIN'); 75 | } 76 | if (quote.fromChain !== quote.toChain) { 77 | throw new Error('Quote chains are not equal'); 78 | } 79 | if (quote.fromToken.contract.toLowerCase() === quote.toToken.contract.toLowerCase()) { 80 | throw new Error( 81 | `From token and to token are the same: ${quote.fromToken.contract}` 82 | ); 83 | } 84 | 85 | if (!Number.isFinite(Number(signerChainId))) { 86 | throw new Error('Invalid signer chain id'); 87 | } 88 | 89 | const signerWormholeChainId = getWormholeChainIdById(Number(signerChainId)); 90 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 91 | if (sourceChainId !== signerWormholeChainId) { 92 | throw new Error( 93 | `Signer chain id(${Number( 94 | signerChainId 95 | )}) and quote from chain are not same! ${sourceChainId} !== ${signerWormholeChainId}` 96 | ); 97 | } 98 | signerChainId = Number(signerChainId); 99 | 100 | const _permit = permit || ZeroPermit; 101 | const forwarder = new Contract( 102 | addresses.MAYAN_FORWARDER_CONTRACT, 103 | MayanForwarderArtifact.abi 104 | ); 105 | 106 | const { evmSwapRouterAddress, evmSwapRouterCalldata } = quote; 107 | if (!evmSwapRouterAddress || !evmSwapRouterCalldata) { 108 | throw new Error( 109 | 'Mono chain swap requires router address and calldata' 110 | ); 111 | } 112 | 113 | const monoChainPayloadIx = getEvmMonoChainTxPayload( 114 | quote, 115 | destinationAddress, 116 | referrerAddress, 117 | ); 118 | const minMiddleAmount = getAmountOfFractionalAmount( 119 | quote.minAmountOut, 120 | quote.toToken.decimals 121 | ); 122 | 123 | if (quote.fromToken.contract === ZeroAddress) { 124 | const forwarderMethod = 'swapAndForwardEth'; 125 | const forwarderParams = [ 126 | monoChainPayloadIx._params.amountIn, 127 | evmSwapRouterAddress, 128 | evmSwapRouterCalldata, 129 | quote.toToken.contract, 130 | minMiddleAmount, 131 | quote.monoChainMayanContract, 132 | monoChainPayloadIx.data, 133 | ]; 134 | const data = forwarder.interface.encodeFunctionData( 135 | forwarderMethod, 136 | forwarderParams 137 | ); 138 | return { 139 | data, 140 | to: addresses.MAYAN_FORWARDER_CONTRACT, 141 | value: toBeHex(monoChainPayloadIx._params.amountIn), 142 | chainId: signerChainId, 143 | _forwarder: { 144 | method: forwarderMethod, 145 | params: forwarderParams, 146 | }, 147 | }; 148 | } else { 149 | const forwarderMethod = 'swapAndForwardERC20'; 150 | const forwarderParams = [ 151 | quote.fromToken.contract, 152 | monoChainPayloadIx._params.amountIn, 153 | _permit, 154 | evmSwapRouterAddress, 155 | evmSwapRouterCalldata, 156 | quote.toToken.contract, 157 | minMiddleAmount, 158 | quote.monoChainMayanContract, 159 | monoChainPayloadIx.data, 160 | ]; 161 | const data = forwarder.interface.encodeFunctionData( 162 | forwarderMethod, 163 | forwarderParams 164 | ); 165 | return { 166 | data, 167 | to: addresses.MAYAN_FORWARDER_CONTRACT, 168 | value: toBeHex(0), 169 | chainId: signerChainId, 170 | _forwarder: { 171 | method: forwarderMethod, 172 | params: forwarderParams, 173 | }, 174 | }; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/evm/evmShuttle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Contract, 3 | toBeHex, 4 | ZeroAddress, 5 | TransactionRequest 6 | } from 'ethers'; 7 | import { Erc20Permit, EvmForwarderParams, Quote } from '../types'; 8 | import { 9 | nativeAddressToHexString, 10 | getAmountOfFractionalAmount, getWormholeChainIdByName, 11 | getWormholeChainIdById, getGasDecimal, ZeroPermit 12 | } from '../utils'; 13 | import addresses from '../addresses'; 14 | import MayanForwarderArtifact from './MayanForwarderArtifact'; 15 | import { Buffer } from 'buffer'; 16 | import ShuttleArtifact from './ShuttleArtifact'; 17 | 18 | 19 | const shuttleConstants = { 20 | FAST_MODE_FLAG: 1, 21 | RELAY_REDEEM_MODE: 2, 22 | EXACT_IN_FLAG: 1, 23 | USDC_INPUT_TOKEN_TYPE: 0, 24 | PRE_APPROVED_ACQUIRE_MODE: 0, 25 | OUTPUT_USDC_MODE: 0, 26 | OUTPUT_NATIVE_MODE: 1, 27 | OUTPUT_OTHER_MODE: 2, 28 | } 29 | 30 | function writeBigIntTo16BytesBuffer(value: bigint): Buffer { 31 | // Validate the range of the BigInt 32 | const maxUint128 = (1n << 128n) - 1n; // 2^128 - 1 33 | if (value < 0n || value > maxUint128) { 34 | throw new RangeError("Value must fit in an unsigned 128-bit integer (0 <= value < 2^128)"); 35 | } 36 | 37 | const buffer = Buffer.alloc(16); 38 | 39 | for (let i = 15; i >= 0; i--) { 40 | buffer[i] = Number(value & 0xFFn); 41 | value >>= 8n; 42 | } 43 | 44 | return buffer; 45 | } 46 | 47 | export function getShuttleParams( 48 | quote: Quote, destinationAddress: string, signerChainId: string | number 49 | ): { 50 | destAddr: string; 51 | destChainId: number; 52 | serializedParams: string; 53 | contractAddress: string; 54 | amountIn: bigint; 55 | bridgeFee: bigint; 56 | } { 57 | const { shuttleParams } = quote; 58 | if (!shuttleParams) { 59 | throw new Error('Swap layer params are missing in quote response'); 60 | } 61 | const signerWormholeChainId = getWormholeChainIdById(Number(signerChainId)); 62 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 63 | const destChainId = getWormholeChainIdByName(quote.toChain); 64 | if (sourceChainId !== signerWormholeChainId) { 65 | throw new Error(`Signer chain id(${Number(signerChainId)}) and quote from chain are not same! ${sourceChainId} !== ${signerWormholeChainId}`); 66 | } 67 | 68 | let bytes = []; 69 | 70 | bytes.push(shuttleConstants.FAST_MODE_FLAG); // [0] 71 | 72 | const maxLLFeeBuffer8Bytes = Buffer.alloc(8); 73 | maxLLFeeBuffer8Bytes.writeBigUInt64BE(BigInt(shuttleParams.maxLLFee)); 74 | const maxLLFeeBytes = maxLLFeeBuffer8Bytes.subarray(2); 75 | bytes.push(...maxLLFeeBytes); // [1..6] 76 | 77 | const deadLineBuffer = Buffer.alloc(4); 78 | deadLineBuffer.writeUInt32BE(shuttleParams.fastTransferDeadline); 79 | bytes.push(...deadLineBuffer); // [7..10] 80 | 81 | bytes.push(shuttleConstants.RELAY_REDEEM_MODE); // [11] 82 | 83 | const gasDrop = getAmountOfFractionalAmount( 84 | quote.gasDrop, 85 | Math.min(6, getGasDecimal(quote.toChain)) 86 | ); 87 | const gasDropBuffer8Bytes = Buffer.alloc(8); 88 | gasDropBuffer8Bytes.writeBigUInt64BE(gasDrop); 89 | const gasDropBytes = gasDropBuffer8Bytes.subarray(4); 90 | bytes.push(...gasDropBytes); // [12..15] 91 | 92 | const maxRelayerFeeBuffer8Bytes = Buffer.alloc(8); 93 | maxRelayerFeeBuffer8Bytes.writeBigUInt64BE(BigInt(shuttleParams.maxRelayingFee)); 94 | const maxRelayerFeeBytes = maxRelayerFeeBuffer8Bytes.subarray(2); 95 | bytes.push(...maxRelayerFeeBytes); // [16..21] 96 | 97 | bytes.push(shuttleConstants.EXACT_IN_FLAG); // [22] 98 | bytes.push(shuttleConstants.USDC_INPUT_TOKEN_TYPE); // [23] 99 | 100 | const amountIn = BigInt(quote.effectiveAmountIn64); 101 | // 102 | if (quote.fromToken.contract === quote.shuttleInputContract) { 103 | // we are sure because of the input token is USDC the amount in will be fit in 8 bytes 104 | bytes.push(...Array(8).fill(0)); // offset of amount_in (8 bytes) 105 | const amountInBuffer = Buffer.alloc(8); 106 | amountInBuffer.writeBigUInt64BE(amountIn); 107 | bytes.push(...amountInBuffer); 108 | 109 | } else { 110 | // as forwarder contract overrides the amount in, we can set the u128 to 0 111 | bytes.push(...Array(16).fill(0)); 112 | } 113 | // 114 | 115 | bytes.push(shuttleConstants.PRE_APPROVED_ACQUIRE_MODE); 116 | 117 | if (shuttleParams.hasDestSwap) { 118 | if (quote.toToken.contract === ZeroAddress) { 119 | bytes.push(shuttleConstants.OUTPUT_NATIVE_MODE); 120 | } else { 121 | bytes.push(shuttleConstants.OUTPUT_OTHER_MODE); 122 | const tokenOut = Buffer.from(nativeAddressToHexString(quote.toToken.contract, destChainId).slice(2), 'hex'); 123 | bytes.push(...tokenOut); 124 | } 125 | const swapDeadlineBuffer = Buffer.alloc(4); 126 | swapDeadlineBuffer.writeUInt32BE(Number(BigInt(quote.deadline64))); 127 | bytes.push(...swapDeadlineBuffer); 128 | 129 | const minAmountOut = getAmountOfFractionalAmount(quote.minAmountOut, quote.toToken.decimals); 130 | if (quote.toChain === 'solana') { // limit_amount should be 8 bytes (u64) 131 | bytes.push(...Array(8).fill(0)); 132 | const minAmountOutBuffer = Buffer.alloc(8); 133 | minAmountOutBuffer.writeBigUInt64BE(minAmountOut); 134 | bytes.push(...minAmountOutBuffer); 135 | } else { 136 | const minAmountOutBuffer = writeBigIntTo16BytesBuffer(minAmountOut); 137 | bytes.push(...minAmountOutBuffer); 138 | } 139 | const swapPath = Buffer.from(shuttleParams.path.slice(2), 'hex'); 140 | bytes.push(...swapPath); 141 | 142 | } else { 143 | bytes.push(shuttleConstants.OUTPUT_USDC_MODE); 144 | } 145 | 146 | const destinationAddressHex = nativeAddressToHexString(destinationAddress, destChainId); 147 | 148 | return { 149 | destAddr: destinationAddressHex, 150 | destChainId, 151 | serializedParams: `0x${Buffer.from(bytes).toString('hex')}`, 152 | contractAddress: quote.shuttleContract, 153 | amountIn, 154 | bridgeFee: getAmountOfFractionalAmount(quote.bridgeFee, getGasDecimal(quote.fromChain)), 155 | } 156 | } 157 | 158 | export function getShuttleFromEvmTxPayload( 159 | quote: Quote, destinationAddress: string, 160 | signerChainId: number | string, permit: Erc20Permit | null 161 | ): TransactionRequest & { _forwarder: EvmForwarderParams } { 162 | if (quote.type !== 'SHUTTLE') { 163 | throw new Error('Quote type is not SHUTTLE'); 164 | } 165 | 166 | if (!Number.isFinite(Number(signerChainId))) { 167 | throw new Error('Invalid signer chain id'); 168 | } 169 | 170 | signerChainId = Number(signerChainId); 171 | 172 | const _permit = permit || ZeroPermit; 173 | const forwarder = new Contract(addresses.MAYAN_FORWARDER_CONTRACT, MayanForwarderArtifact.abi); 174 | 175 | const { 176 | destAddr, 177 | destChainId, 178 | serializedParams, 179 | contractAddress: shuttleContractAddress, 180 | amountIn, 181 | bridgeFee, 182 | } = getShuttleParams(quote, destinationAddress, signerChainId); 183 | 184 | let shuttleCallData: string; 185 | const shuttleContract = new Contract(shuttleContractAddress, ShuttleArtifact.abi); 186 | 187 | shuttleCallData = shuttleContract.interface.encodeFunctionData( 188 | 'initiate', 189 | [destAddr, amountIn, destChainId, serializedParams] 190 | ); 191 | 192 | let forwarderMethod: string; 193 | let forwarderParams: any[]; 194 | let value: string | null; 195 | 196 | if (quote.fromToken.contract === quote.shuttleInputContract) { 197 | forwarderMethod = 'forwardERC20'; 198 | forwarderParams = [quote.shuttleInputContract, amountIn, _permit, shuttleContractAddress, shuttleCallData]; 199 | value = toBeHex(bridgeFee); 200 | } else { 201 | const { evmSwapRouterAddress, evmSwapRouterCalldata } = quote; 202 | if (!quote.minMiddleAmount || !evmSwapRouterAddress || !evmSwapRouterCalldata) { 203 | throw new Error('Shuttle source chain swap requires middle amount, router address and calldata'); 204 | } 205 | const tokenIn = quote.fromToken.contract; 206 | 207 | const minMiddleAmount = getAmountOfFractionalAmount(quote.minMiddleAmount, quote.shuttleInputDecimals); 208 | 209 | if (quote.fromToken.contract === ZeroAddress) { 210 | forwarderMethod = 'swapAndForwardEth'; 211 | forwarderParams = [ 212 | amountIn, 213 | evmSwapRouterAddress, 214 | evmSwapRouterCalldata, 215 | quote.shuttleInputContract, 216 | minMiddleAmount, 217 | shuttleContractAddress, 218 | shuttleCallData, 219 | ]; 220 | value = toBeHex(amountIn); 221 | } else { 222 | forwarderMethod = 'swapAndForwardERC20'; 223 | forwarderParams = [ 224 | tokenIn, 225 | amountIn, 226 | _permit, 227 | evmSwapRouterAddress, 228 | evmSwapRouterCalldata, 229 | quote.shuttleInputContract, 230 | minMiddleAmount, 231 | shuttleContractAddress, 232 | shuttleCallData, 233 | ]; 234 | value = toBeHex(bridgeFee); 235 | } 236 | } 237 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 238 | 239 | return { 240 | data, 241 | to: addresses.MAYAN_FORWARDER_CONTRACT, 242 | value, 243 | chainId: signerChainId, 244 | _forwarder: { 245 | method: forwarderMethod, 246 | params: forwarderParams 247 | } 248 | }; 249 | } 250 | -------------------------------------------------------------------------------- /src/evm/evmSwift.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Contract, 3 | toBeHex, 4 | ZeroAddress, 5 | TransactionRequest 6 | } from 'ethers'; 7 | import { Keypair, SystemProgram } from '@solana/web3.js'; 8 | import { Erc20Permit, EvmForwarderParams, Quote, SwiftEvmOrderTypedData } from '../types'; 9 | import { 10 | nativeAddressToHexString, 11 | getAmountOfFractionalAmount, getWormholeChainIdByName, 12 | getWormholeChainIdById, getGasDecimal, ZeroPermit 13 | } from '../utils'; 14 | import MayanSwiftArtifact from './MayanSwiftArtifact'; 15 | import addresses from '../addresses'; 16 | import MayanForwarderArtifact from './MayanForwarderArtifact'; 17 | import { createSwiftOrderHash } from '../solana'; 18 | 19 | 20 | export type SwiftOrderParams = { 21 | trader: string; 22 | tokenOut: string; 23 | minAmountOut: bigint; 24 | gasDrop: bigint; 25 | cancelFee: bigint; 26 | refundFee: bigint; 27 | deadline: bigint; 28 | destAddr: string; 29 | destChainId: number; 30 | referrerAddr: string; 31 | referrerBps: number; 32 | auctionMode: number; 33 | random: string; 34 | }; 35 | 36 | export type EvmSwiftParams = { 37 | contractAddress: string; 38 | tokenIn: string; 39 | amountIn: bigint; 40 | order: SwiftOrderParams; 41 | }; 42 | 43 | export function getEvmSwiftParams( 44 | quote: Quote, swapperAddress: string, destinationAddress: string, 45 | referrerAddress: string | null | undefined, signerChainId: string | number 46 | ): EvmSwiftParams { 47 | const signerWormholeChainId = getWormholeChainIdById(Number(signerChainId)); 48 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 49 | const destChainId = getWormholeChainIdByName(quote.toChain); 50 | if (sourceChainId !== signerWormholeChainId) { 51 | throw new Error(`Signer chain id(${Number(signerChainId)}) and quote from chain are not same! ${sourceChainId} !== ${signerWormholeChainId}`); 52 | } 53 | if (!quote.swiftMayanContract) { 54 | throw new Error('SWIFT contract address is missing'); 55 | } 56 | 57 | if (quote.toToken.wChainId !== destChainId) { 58 | throw new Error(`Destination chain ID mismatch: ${destChainId} != ${quote.toToken.wChainId}`); 59 | } 60 | const contractAddress = quote.swiftMayanContract; 61 | 62 | if (!Number(quote.deadline64)) { 63 | throw new Error('Swift order requires timeout'); 64 | } 65 | 66 | const deadline = BigInt(quote.deadline64); 67 | 68 | const tokenIn = quote.swiftInputContract; 69 | const amountIn = BigInt(quote.effectiveAmountIn64); 70 | let referrerHex: string; 71 | if (referrerAddress) { 72 | referrerHex = nativeAddressToHexString( 73 | referrerAddress, 74 | destChainId 75 | ); 76 | } else { 77 | referrerHex = nativeAddressToHexString( 78 | SystemProgram.programId.toString(), 79 | 1 80 | ); 81 | } 82 | 83 | const random = nativeAddressToHexString(Keypair.generate().publicKey.toString(), 1); 84 | 85 | const tokenOut = quote.toToken.contract === ZeroAddress ? 86 | nativeAddressToHexString(SystemProgram.programId.toString(), 1) : 87 | nativeAddressToHexString(quote.toToken.contract, destChainId); 88 | 89 | const minAmountOut = getAmountOfFractionalAmount( 90 | quote.minAmountOut, Math.min(8, quote.toToken.decimals) 91 | ); 92 | 93 | const gasDrop = getAmountOfFractionalAmount( 94 | quote.gasDrop, 95 | Math.min(8, getGasDecimal(quote.toChain)) 96 | ); 97 | 98 | const destinationAddressHex = nativeAddressToHexString(destinationAddress, destChainId); 99 | const orderParams: SwiftOrderParams = { 100 | trader: nativeAddressToHexString(swapperAddress, sourceChainId), 101 | tokenOut, 102 | minAmountOut, 103 | gasDrop, 104 | cancelFee: BigInt(quote.cancelRelayerFee64), 105 | refundFee: BigInt(quote.refundRelayerFee64), 106 | deadline, 107 | destAddr: destinationAddressHex, 108 | destChainId, 109 | referrerAddr: referrerHex, 110 | referrerBps: quote.referrerBps || 0, 111 | auctionMode: quote.swiftAuctionMode, 112 | random, 113 | }; 114 | 115 | return { 116 | contractAddress, 117 | tokenIn, 118 | amountIn, 119 | order: orderParams 120 | }; 121 | } 122 | 123 | export function getSwiftFromEvmTxPayload( 124 | quote: Quote, swapperAddress: string, destinationAddress: string, referrerAddress: string | null | undefined, 125 | signerChainId: number | string, permit: Erc20Permit | null 126 | ): TransactionRequest & { _forwarder: EvmForwarderParams } { 127 | if (quote.type !== 'SWIFT') { 128 | throw new Error('Quote type is not SWIFT'); 129 | } 130 | 131 | if (!Number.isFinite(Number(signerChainId))) { 132 | throw new Error('Invalid signer chain id'); 133 | } 134 | 135 | if (!Number(quote.deadline64)) { 136 | throw new Error('Swift order requires timeout'); 137 | } 138 | 139 | signerChainId = Number(signerChainId); 140 | 141 | const _permit = permit || ZeroPermit; 142 | const forwarder = new Contract(addresses.MAYAN_FORWARDER_CONTRACT, MayanForwarderArtifact.abi); 143 | 144 | const { 145 | tokenIn: swiftTokenIn, 146 | amountIn, 147 | order, 148 | contractAddress: swiftContractAddress 149 | } = getEvmSwiftParams(quote, swapperAddress, destinationAddress, referrerAddress, signerChainId); 150 | 151 | let swiftCallData: string; 152 | const swiftContract = new Contract(swiftContractAddress, MayanSwiftArtifact.abi); 153 | 154 | if (quote.swiftInputContract === ZeroAddress) { 155 | swiftCallData = swiftContract.interface.encodeFunctionData( 156 | 'createOrderWithEth', 157 | [order] 158 | ); 159 | } else { 160 | swiftCallData = swiftContract.interface.encodeFunctionData( 161 | 'createOrderWithToken', 162 | [swiftTokenIn, amountIn, order] 163 | ); 164 | } 165 | 166 | let forwarderMethod: string; 167 | let forwarderParams: any[]; 168 | let value: string | null; 169 | 170 | if (quote.fromToken.contract === quote.swiftInputContract) { 171 | if (quote.fromToken.contract === ZeroAddress) { 172 | forwarderMethod = 'forwardEth'; 173 | forwarderParams = [swiftContractAddress, swiftCallData]; 174 | value = toBeHex(amountIn); 175 | } else { 176 | forwarderMethod = 'forwardERC20'; 177 | forwarderParams = [swiftTokenIn, amountIn, _permit, swiftContractAddress, swiftCallData]; 178 | value = toBeHex(0); 179 | } 180 | } else { 181 | const { evmSwapRouterAddress, evmSwapRouterCalldata } = quote; 182 | if (!quote.minMiddleAmount || !evmSwapRouterAddress || !evmSwapRouterCalldata) { 183 | throw new Error('Swift swap requires middle amount, router address and calldata'); 184 | } 185 | const tokenIn = quote.fromToken.contract; 186 | 187 | const minMiddleAmount = getAmountOfFractionalAmount(quote.minMiddleAmount, quote.swiftInputDecimals); 188 | 189 | if (quote.fromToken.contract === ZeroAddress) { 190 | forwarderMethod = 'swapAndForwardEth'; 191 | forwarderParams = [ 192 | amountIn, 193 | evmSwapRouterAddress, 194 | evmSwapRouterCalldata, 195 | quote.swiftInputContract, 196 | minMiddleAmount, 197 | swiftContractAddress, 198 | swiftCallData 199 | ]; 200 | value = toBeHex(amountIn); 201 | } else { 202 | forwarderMethod = 'swapAndForwardERC20'; 203 | forwarderParams = [ 204 | tokenIn, 205 | amountIn, 206 | _permit, 207 | evmSwapRouterAddress, 208 | evmSwapRouterCalldata, 209 | quote.swiftInputContract, 210 | minMiddleAmount, 211 | swiftContractAddress, 212 | swiftCallData 213 | ]; 214 | value = toBeHex(0); 215 | } 216 | } 217 | const data = forwarder.interface.encodeFunctionData(forwarderMethod, forwarderParams); 218 | 219 | return { 220 | data, 221 | to: addresses.MAYAN_FORWARDER_CONTRACT, 222 | value, 223 | chainId: signerChainId, 224 | _forwarder: { 225 | method: forwarderMethod, 226 | params: forwarderParams 227 | } 228 | }; 229 | } 230 | 231 | 232 | export function getSwiftOrderTypeData( 233 | quote: Quote, orderHash: string, signerChainId: number | string 234 | ): SwiftEvmOrderTypedData { 235 | if (!Number.isFinite(Number(signerChainId))) { 236 | throw new Error('Invalid signer chain id'); 237 | } 238 | 239 | const totalAmountIn = BigInt(quote.effectiveAmountIn64); 240 | const submitFee = BigInt(quote.submitRelayerFee64); 241 | return { 242 | domain: { 243 | name: 'Mayan Swift', 244 | chainId: Number(signerChainId), 245 | verifyingContract: quote.swiftMayanContract, 246 | }, 247 | types: { 248 | CreateOrder: [ 249 | { name: 'OrderId', type: 'bytes32' }, 250 | { name: 'InputAmount', type: 'uint256' }, 251 | { name: 'SubmissionFee', type: 'uint256' }, 252 | ], 253 | }, 254 | value: { 255 | OrderId: orderHash, 256 | InputAmount: totalAmountIn - submitFee, 257 | SubmissionFee: submitFee, 258 | } 259 | } 260 | } 261 | 262 | export type SwiftEvmGasLessParams = { 263 | permitParams: Erc20Permit; 264 | orderHash: string; 265 | orderParams: { 266 | trader: string; 267 | sourceChainId: number; 268 | tokenIn: string; 269 | amountIn: bigint; 270 | destAddr: string; 271 | destChainId: number; 272 | tokenOut: string; 273 | minAmountOut: bigint; 274 | gasDrop: bigint; 275 | cancelFee: bigint; 276 | refundFee: bigint; 277 | deadline: bigint; 278 | referrerAddr: string; 279 | referrerBps: number; 280 | auctionMode: number; 281 | random: string; 282 | submissionFee: bigint; 283 | }; 284 | orderTypedData: SwiftEvmOrderTypedData; 285 | } 286 | 287 | export function getSwiftFromEvmGasLessParams( 288 | quote: Quote, swapperAddress: string, destinationAddress: string, referrerAddress: string | null | undefined, 289 | signerChainId: number | string, permit: Erc20Permit | null 290 | ): SwiftEvmGasLessParams { 291 | if (quote.type !== 'SWIFT') { 292 | throw new Error('Quote type is not SWIFT'); 293 | } 294 | 295 | if (!quote.gasless) { 296 | throw new Error('Quote does not support gasless'); 297 | } 298 | 299 | if (!Number.isFinite(Number(signerChainId))) { 300 | throw new Error('Invalid signer chain id'); 301 | } 302 | 303 | if (!Number(quote.deadline64)) { 304 | throw new Error('Swift order requires timeout'); 305 | } 306 | 307 | if (quote.fromToken.contract !== quote.swiftInputContract) { 308 | throw new Error('Swift gasless order creation does not support source swap'); 309 | } 310 | 311 | const { 312 | tokenIn, 313 | amountIn, 314 | order, 315 | } = getEvmSwiftParams( 316 | quote, swapperAddress, destinationAddress, 317 | referrerAddress, Number(signerChainId) 318 | ); 319 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 320 | 321 | const orderHashBuf = createSwiftOrderHash(quote, swapperAddress, destinationAddress, referrerAddress, order.random); 322 | const orderHash = `0x${orderHashBuf.toString('hex')}` 323 | const orderTypedData = getSwiftOrderTypeData(quote, orderHash, signerChainId); 324 | 325 | return { 326 | permitParams: permit, 327 | orderParams: { 328 | ...order, 329 | sourceChainId, 330 | amountIn, 331 | tokenIn, 332 | submissionFee: BigInt(quote.submitRelayerFee64), 333 | }, 334 | orderHash, 335 | orderTypedData 336 | }; 337 | } 338 | -------------------------------------------------------------------------------- /src/evm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './evmSwap'; 2 | export * from './evmMctp'; 3 | export * from './evmHyperCore'; 4 | export * from './evmMonoChain'; 5 | export * from './evmSwift'; 6 | export * from './evmFastMctp'; 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './evm'; 3 | export * from './solana'; 4 | export * from './sui'; 5 | export * from './types'; 6 | export * from './utils'; 7 | export { default as addresses } from './addresses'; 8 | -------------------------------------------------------------------------------- /src/solana/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './solanaSwap'; 3 | export * from './solanaMctp'; 4 | export * from './solanaSwift'; 5 | export * from './solanaHyperCore'; 6 | export * from './solanaMonoChain'; 7 | -------------------------------------------------------------------------------- /src/solana/solanaHyperCore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | PublicKey, 4 | Keypair, 5 | TransactionInstruction, 6 | ComputeBudgetProgram, 7 | AddressLookupTableAccount, 8 | } from '@solana/web3.js'; 9 | import { Quote, ChainName, SwapMessageV0Params, SolanaBridgeOptions } from '../types'; 10 | import { 11 | getAssociatedTokenAddress, 12 | getHyperCoreUSDCDepositCustomPayload, 13 | } from '../utils'; 14 | import { Buffer } from 'buffer'; 15 | import addresses from '../addresses'; 16 | import { getSwapSolana } from '../api'; 17 | import { 18 | createAssociatedTokenAccountInstruction, 19 | createInitializeRandomTokenAccountInstructions, createPayloadWriterCloseInstruction, 20 | createPayloadWriterCreateInstruction, 21 | createSplTransferInstruction, 22 | createTransferAllAndCloseInstruction, 23 | decentralizeClientSwapInstructions, 24 | getAddressLookupTableAccounts, 25 | sandwichInstructionInCpiProxy, 26 | validateJupSwap, 27 | } from './utils'; 28 | import { createMctpBridgeLedgerInstruction, createMctpBridgeWithFeeInstruction } from './solanaMctp'; 29 | 30 | export async function createHyperCoreDepositFromSolanaInstructions( 31 | quote: Quote, 32 | swapperAddress: string, 33 | destinationAddress: string, 34 | referrerAddress: string | null | undefined, 35 | connection: Connection, 36 | options: SolanaBridgeOptions = {} 37 | ): Promise<{ 38 | instructions: TransactionInstruction[]; 39 | signers: Keypair[]; 40 | lookupTables: AddressLookupTableAccount[]; 41 | swapMessageV0Params: SwapMessageV0Params | null; 42 | }> { 43 | if ( 44 | quote.toToken.contract.toLowerCase() !== addresses.ARBITRUM_USDC_CONTRACT.toLowerCase() || 45 | quote.type !== 'MCTP' 46 | ) { 47 | throw new Error('Unsupported quote type for USDC deposit: ' + quote.type); 48 | } 49 | if (!options?.usdcPermitSignature) { 50 | throw new Error('USDC permit signature is required for this quote'); 51 | } 52 | if (!quote.hyperCoreParams) { 53 | throw new Error('HyperCore parameters are required for this quote'); 54 | } 55 | if (!Number(quote.deadline64)) { 56 | throw new Error('HyperCore deposit requires timeout'); 57 | } 58 | 59 | const allowSwapperOffCurve = options.allowSwapperOffCurve || false; 60 | 61 | let instructions: TransactionInstruction[] = []; 62 | let signers: Keypair[] = []; 63 | let lookupTables: AddressLookupTableAccount[] = []; 64 | 65 | let _lookupTablesAddress: string[] = []; 66 | 67 | _lookupTablesAddress.push(addresses.LOOKUP_TABLE); 68 | 69 | // using for the swap via Jito Bundle 70 | let _swapAddressLookupTables: string[] = []; 71 | let swapInstructions: TransactionInstruction[] = []; 72 | let createSwapTpmTokenAccountInstructions: TransactionInstruction[] = []; 73 | const tmpSwapTokenAccount: Keypair = Keypair.generate(); 74 | let swapMessageV0Params: SwapMessageV0Params | null = null; 75 | 76 | const trader = new PublicKey(swapperAddress); 77 | const relayerAddress = quote.relayer || swapperAddress; 78 | 79 | const inputMint = new PublicKey(quote.hyperCoreParams.initiateTokenContract); 80 | 81 | const payloadNonce = Math.floor(Math.random() * 65000); // Random nonce for the payload 82 | const [payloadAccount] = PublicKey.findProgramAddressSync( 83 | [ 84 | Buffer.from('PAYLOAD'), 85 | trader.toBuffer(), 86 | (() => { 87 | const buf = Buffer.alloc(2); 88 | buf.writeUInt16LE(payloadNonce, 0); 89 | return buf; 90 | })(), 91 | ], 92 | new PublicKey(addresses.PAYLOAD_WRITER_PROGRAM_ID) 93 | ); 94 | const payload = getHyperCoreUSDCDepositCustomPayload(quote, destinationAddress, options.usdcPermitSignature); 95 | 96 | const mctpRandomKey = Keypair.generate(); 97 | 98 | const mctpProgram = new PublicKey(addresses.MCTP_PROGRAM_ID); 99 | 100 | const [ledger] = PublicKey.findProgramAddressSync( 101 | [ 102 | Buffer.from('LEDGER_BRIDGE'), 103 | trader.toBytes(), 104 | mctpRandomKey.publicKey.toBytes(), 105 | ], 106 | mctpProgram 107 | ); 108 | const ledgerAccount = getAssociatedTokenAddress( 109 | inputMint, ledger, true 110 | ); 111 | 112 | if ( 113 | quote.fromToken.contract === quote.hyperCoreParams.initiateTokenContract 114 | ) { 115 | if (quote.suggestedPriorityFee > 0) { 116 | instructions.push( 117 | ComputeBudgetProgram.setComputeUnitPrice({ 118 | microLamports: quote.suggestedPriorityFee, 119 | }) 120 | ); 121 | } 122 | instructions.push( 123 | sandwichInstructionInCpiProxy(createAssociatedTokenAccountInstruction(trader, ledgerAccount, ledger, inputMint)) 124 | ); 125 | instructions.push( 126 | sandwichInstructionInCpiProxy(createSplTransferInstruction( 127 | getAssociatedTokenAddress( 128 | inputMint, trader, allowSwapperOffCurve 129 | ), 130 | ledgerAccount, 131 | trader, 132 | BigInt(quote.hyperCoreParams.bridgeAmountUSDC64), 133 | )) 134 | ); 135 | instructions.push( 136 | sandwichInstructionInCpiProxy(createPayloadWriterCreateInstruction( 137 | trader, 138 | payloadAccount, 139 | payload, 140 | payloadNonce 141 | )) 142 | ); 143 | instructions.push( 144 | sandwichInstructionInCpiProxy(createMctpBridgeLedgerInstruction({ 145 | ledger, 146 | randomKey: mctpRandomKey.publicKey, 147 | swapperAddress: trader.toString(), 148 | mintAddress: inputMint.toString(), 149 | mode: 'WITH_FEE', 150 | feeSolana: BigInt(0), 151 | amountInMin64: BigInt(quote.hyperCoreParams.bridgeAmountUSDC64), 152 | customPayload: payloadAccount, 153 | destinationAddress: addresses.HC_ARBITRUM_DEPOSIT_PROCESSOR, 154 | referrerAddress, 155 | feeRedeem: 0, 156 | gasDrop: quote.hyperCoreParams.failureGasDrop, 157 | toChain: 'arbitrum', 158 | relayerAddress, 159 | }), options.skipProxyMayanInstructions) 160 | ); 161 | const { 162 | instruction: _instruction, 163 | signers: _signers 164 | } = createMctpBridgeWithFeeInstruction( 165 | ledger, 166 | 'arbitrum', 167 | quote.hyperCoreParams.initiateTokenContract, 168 | relayerAddress, 169 | BigInt(0), 170 | ); 171 | instructions.push(sandwichInstructionInCpiProxy(_instruction, options.skipProxyMayanInstructions)); 172 | signers.push(..._signers); 173 | instructions.push(sandwichInstructionInCpiProxy(createPayloadWriterCloseInstruction( 174 | trader, 175 | payloadAccount, 176 | payloadNonce, 177 | ))); 178 | } else { 179 | const clientSwapRaw = await getSwapSolana({ 180 | minMiddleAmount: quote.minMiddleAmount, 181 | middleToken: quote.hyperCoreParams.initiateTokenContract, 182 | userWallet: trader.toString(), 183 | slippageBps: quote.slippageBps, 184 | fromToken: quote.fromToken.contract, 185 | amountIn64: quote.effectiveAmountIn64, 186 | depositMode: 'HC_USDC', 187 | fillMaxAccounts: options?.separateSwapTx || false, 188 | tpmTokenAccount: tmpSwapTokenAccount.publicKey.toString() 189 | }); 190 | const clientSwap = decentralizeClientSwapInstructions(clientSwapRaw, connection); 191 | 192 | if (options?.separateSwapTx && clientSwapRaw.maxAccountsFilled) { 193 | validateJupSwap(clientSwap, tmpSwapTokenAccount.publicKey, trader); 194 | createSwapTpmTokenAccountInstructions = await createInitializeRandomTokenAccountInstructions( 195 | connection, 196 | trader, 197 | inputMint, 198 | trader, 199 | tmpSwapTokenAccount, 200 | ); 201 | swapInstructions.push(...clientSwap.computeBudgetInstructions); 202 | if (clientSwap.setupInstructions) { 203 | swapInstructions.push(...clientSwap.setupInstructions); 204 | } 205 | swapInstructions.push(clientSwap.swapInstruction); 206 | if (clientSwap.cleanupInstruction) { 207 | swapInstructions.push(clientSwap.cleanupInstruction); 208 | } 209 | _swapAddressLookupTables.push(...clientSwap.addressLookupTableAddresses); 210 | } else { 211 | validateJupSwap(clientSwap, tmpSwapTokenAccount.publicKey, trader); 212 | instructions.push(...clientSwap.computeBudgetInstructions); 213 | const _createSwapTpmTokenAccountInstructions = await createInitializeRandomTokenAccountInstructions( 214 | connection, 215 | trader, 216 | inputMint, 217 | trader, 218 | tmpSwapTokenAccount, 219 | ); 220 | instructions.push(...(_createSwapTpmTokenAccountInstructions).map(ins => sandwichInstructionInCpiProxy(ins))); 221 | signers.push(tmpSwapTokenAccount); 222 | if (clientSwap.setupInstructions) { 223 | instructions.push(...(clientSwap.setupInstructions.map(ins => sandwichInstructionInCpiProxy(ins)))); 224 | } 225 | instructions.push(sandwichInstructionInCpiProxy(clientSwap.swapInstruction)); 226 | if (clientSwap.cleanupInstruction) { 227 | instructions.push(sandwichInstructionInCpiProxy(clientSwap.cleanupInstruction)); 228 | } 229 | _lookupTablesAddress.push(...clientSwap.addressLookupTableAddresses); 230 | } 231 | 232 | const feeSolana: bigint = swapInstructions.length > 0 ? BigInt(0) : BigInt(quote.solanaRelayerFee64); 233 | let initiateAmountUSDC64 = BigInt(quote.hyperCoreParams.bridgeAmountUSDC64); 234 | if (swapInstructions.length > 0) { 235 | initiateAmountUSDC64 = initiateAmountUSDC64 - BigInt(quote.solanaRelayerFee64); 236 | } 237 | 238 | instructions.push(sandwichInstructionInCpiProxy(createAssociatedTokenAccountInstruction( 239 | trader, ledgerAccount, ledger, new PublicKey(quote.mctpInputContract) 240 | ))); 241 | 242 | instructions.push( 243 | sandwichInstructionInCpiProxy(createSplTransferInstruction( 244 | tmpSwapTokenAccount.publicKey, 245 | ledgerAccount, 246 | trader, 247 | initiateAmountUSDC64, 248 | )) 249 | ); 250 | 251 | const traderInputMintAccount = getAssociatedTokenAddress( 252 | inputMint, trader, allowSwapperOffCurve 253 | ); 254 | const traderInputMintAccountInfo = await connection.getAccountInfo(traderInputMintAccount); 255 | if (!traderInputMintAccountInfo || !traderInputMintAccountInfo.data) { 256 | instructions.push(sandwichInstructionInCpiProxy(createAssociatedTokenAccountInstruction( 257 | trader, 258 | traderInputMintAccount, 259 | trader, 260 | inputMint 261 | ))); 262 | } 263 | instructions.push(sandwichInstructionInCpiProxy(createTransferAllAndCloseInstruction( 264 | trader, 265 | inputMint, 266 | tmpSwapTokenAccount.publicKey, 267 | traderInputMintAccount, 268 | trader, 269 | ))); 270 | 271 | instructions.push( 272 | sandwichInstructionInCpiProxy(createPayloadWriterCreateInstruction( 273 | trader, 274 | payloadAccount, 275 | payload, 276 | payloadNonce 277 | )) 278 | ); 279 | 280 | instructions.push(sandwichInstructionInCpiProxy(createMctpBridgeLedgerInstruction({ 281 | ledger, 282 | swapperAddress: trader.toString(), 283 | mintAddress: inputMint.toString(), 284 | randomKey: mctpRandomKey.publicKey, 285 | mode: 'WITH_FEE', 286 | feeSolana, 287 | amountInMin64: initiateAmountUSDC64, 288 | customPayload: payloadAccount, 289 | destinationAddress: addresses.HC_ARBITRUM_DEPOSIT_PROCESSOR, 290 | referrerAddress, 291 | feeRedeem: 0, 292 | gasDrop: quote.hyperCoreParams.failureGasDrop, 293 | toChain: 'arbitrum', 294 | relayerAddress, 295 | }), options.skipProxyMayanInstructions)); 296 | instructions.push(sandwichInstructionInCpiProxy(createPayloadWriterCloseInstruction( 297 | trader, 298 | payloadAccount, 299 | payloadNonce, 300 | ))); 301 | if (swapInstructions.length > 0) { 302 | const { 303 | instruction: _instruction, 304 | signers: _signers 305 | } = createMctpBridgeWithFeeInstruction( 306 | ledger, 307 | 'arbitrum', 308 | quote.hyperCoreParams.initiateTokenContract, 309 | relayerAddress, 310 | BigInt(0), 311 | ); 312 | instructions.push(sandwichInstructionInCpiProxy(_instruction, options.skipProxyMayanInstructions)); 313 | signers.push(..._signers); 314 | } 315 | } 316 | 317 | const totalLookupTables = await getAddressLookupTableAccounts( 318 | _lookupTablesAddress.concat(_swapAddressLookupTables), connection 319 | ); 320 | lookupTables = totalLookupTables.slice(0, _lookupTablesAddress.length); 321 | if (swapInstructions.length > 0) { 322 | const swapLookupTables = totalLookupTables.slice(_lookupTablesAddress.length); 323 | swapMessageV0Params = { 324 | messageV0: { 325 | payerKey: trader, 326 | instructions: swapInstructions, 327 | addressLookupTableAccounts: swapLookupTables, 328 | }, 329 | createTmpTokenAccountIxs: createSwapTpmTokenAccountInstructions, 330 | tmpTokenAccount: tmpSwapTokenAccount, 331 | }; 332 | } 333 | 334 | return { instructions, signers, lookupTables, swapMessageV0Params }; 335 | } 336 | -------------------------------------------------------------------------------- /src/solana/solanaMonoChain.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountMeta, 3 | Connection, 4 | PublicKey, 5 | Keypair, 6 | SystemProgram, 7 | TransactionInstruction, 8 | AddressLookupTableAccount, 9 | ComputeBudgetProgram, 10 | } from '@solana/web3.js'; 11 | import { blob, struct, u16, u8 } from '@solana/buffer-layout'; 12 | import { Quote, SwapMessageV0Params } from '../types'; 13 | import { 14 | hexToUint8Array, 15 | nativeAddressToHexString, 16 | getSafeU64Blob, 17 | getAmountOfFractionalAmount, 18 | getAssociatedTokenAddress, 19 | getWormholeChainIdByName, 20 | getGasDecimal, 21 | } from '../utils'; 22 | import { Buffer } from 'buffer'; 23 | import addresses from '../addresses'; 24 | import { ethers, ZeroAddress } from 'ethers'; 25 | import { getSwapSolana } from '../api'; 26 | import { 27 | createAssociatedTokenAccountInstruction, 28 | createInitializeRandomTokenAccountInstructions, 29 | createSplTransferInstruction, 30 | createSyncNativeInstruction, 31 | createTransferAllAndCloseInstruction, 32 | decentralizeClientSwapInstructions, 33 | getAddressLookupTableAccounts, 34 | getAnchorInstructionData, sandwichInstructionInCpiProxy, 35 | solMint, 36 | validateJupSwap, validateJupSwapInstructionData 37 | } from './utils'; 38 | import { swapFromSolana } from './solanaSwap'; 39 | 40 | export async function createMonoChainFromSolanaInstructions( 41 | quote: Quote, 42 | swapperAddress: string, 43 | destinationAddress: string, 44 | referrerAddress: string | null | undefined, 45 | connection: Connection, 46 | options: { 47 | allowSwapperOffCurve?: boolean; 48 | separateSwapTx?: boolean; 49 | skipProxyMayanInstructions?: boolean, 50 | } = {} 51 | ): Promise<{ 52 | instructions: TransactionInstruction[]; 53 | signers: Keypair[]; 54 | lookupTables: AddressLookupTableAccount[]; 55 | swapMessageV0Params: SwapMessageV0Params | null; 56 | }> { 57 | if (quote.type !== 'MONO_CHAIN') { 58 | throw new Error('Unsupported quote type for mono chain: ' + quote.type); 59 | } 60 | if (quote.fromChain !== 'solana') { 61 | throw new Error( 62 | 'Unsupported destination chain for mono chain: ' + quote.fromChain 63 | ); 64 | } 65 | if (quote.toChain !== 'solana') { 66 | throw new Error( 67 | 'Unsupported destination chain for mono chain: ' + quote.toChain 68 | ); 69 | } 70 | if (quote.fromToken.contract === quote.toToken.contract) { 71 | throw new Error( 72 | 'From token and to token are the same: ' + quote.fromToken.contract 73 | ); 74 | } 75 | if (destinationAddress.startsWith('0x')) { 76 | throw new Error('Destination address should not be EVM address'); 77 | } 78 | try { 79 | new PublicKey(destinationAddress); 80 | } catch (e) { 81 | throw new Error('Invalid destination address: ' + destinationAddress); 82 | } 83 | 84 | let instructions: TransactionInstruction[] = []; 85 | let lookupTables: AddressLookupTableAccount[] = []; 86 | 87 | let _lookupTablesAddress: string[] = []; 88 | 89 | const swapper = new PublicKey(swapperAddress); 90 | const destination = new PublicKey(destinationAddress); 91 | 92 | const toMint = quote.toToken.contract === ZeroAddress ? solMint : new PublicKey(quote.toToken.contract); 93 | const destAcc = getAssociatedTokenAddress( 94 | toMint, 95 | destination, 96 | true, 97 | quote.toToken.standard === 'spl2022' ? new PublicKey(addresses.TOKEN_2022_PROGRAM_ID) : new PublicKey(addresses.TOKEN_PROGRAM_ID) 98 | ); 99 | 100 | const expectedAmountOut64 = getAmountOfFractionalAmount( 101 | quote.expectedAmountOut, quote.toToken.decimals 102 | ); 103 | const clientSwapRaw = await getSwapSolana({ 104 | userWallet: swapperAddress, 105 | destinationWallet: destinationAddress, 106 | slippageBps: quote.slippageBps, 107 | fromToken: quote.fromToken.contract, 108 | middleToken: quote.toToken.contract, 109 | amountIn64: quote.effectiveAmountIn64, 110 | expectedAmountOut64: String(expectedAmountOut64), 111 | depositMode: 'MONO_CHAIN', 112 | referrerAddress: referrerAddress, 113 | referrerBps: quote.referrerBps, 114 | }); 115 | const clientSwap = decentralizeClientSwapInstructions( 116 | clientSwapRaw, 117 | connection 118 | ); 119 | validateJupSwap(clientSwap, destAcc, swapper, destination, swapper.equals(destination)); 120 | validateJupSwapInstructionData(clientSwap.swapInstruction, quote); 121 | instructions.push(...clientSwap.computeBudgetInstructions); 122 | if (clientSwap.setupInstructions) { 123 | instructions.push(...(clientSwap.setupInstructions.map(ins => sandwichInstructionInCpiProxy(ins)))); 124 | } 125 | instructions.push(sandwichInstructionInCpiProxy(clientSwap.swapInstruction, options?.skipProxyMayanInstructions)); 126 | if (clientSwap.cleanupInstruction) { 127 | instructions.push(sandwichInstructionInCpiProxy(clientSwap.cleanupInstruction)); 128 | } 129 | _lookupTablesAddress.push(...clientSwap.addressLookupTableAddresses); 130 | _lookupTablesAddress.push(addresses.LOOKUP_TABLE); 131 | 132 | lookupTables = await getAddressLookupTableAccounts(_lookupTablesAddress, connection); 133 | 134 | 135 | 136 | return { instructions, signers: [], lookupTables, swapMessageV0Params: null }; 137 | } 138 | -------------------------------------------------------------------------------- /src/solana/solanaSwift.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountMeta, 3 | Connection, 4 | PublicKey, 5 | Keypair, 6 | SystemProgram, 7 | TransactionInstruction, AddressLookupTableAccount, ComputeBudgetProgram 8 | } from '@solana/web3.js'; 9 | import { blob, struct, u16, u8 } from '@solana/buffer-layout'; 10 | import { Quote, SwapMessageV0Params } from '../types'; 11 | import { 12 | hexToUint8Array, 13 | nativeAddressToHexString, 14 | getSafeU64Blob, getAmountOfFractionalAmount, getAssociatedTokenAddress, getWormholeChainIdByName, getGasDecimal 15 | } from '../utils'; 16 | import {Buffer} from 'buffer'; 17 | import addresses from '../addresses' 18 | import { ethers, ZeroAddress } from 'ethers'; 19 | import { getSwapSolana } from '../api'; 20 | import { 21 | createAssociatedTokenAccountInstruction, 22 | createInitializeRandomTokenAccountInstructions, 23 | createSplTransferInstruction, 24 | createSyncNativeInstruction, 25 | createTransferAllAndCloseInstruction, 26 | decentralizeClientSwapInstructions, 27 | getAddressLookupTableAccounts, 28 | getAnchorInstructionData, 29 | sandwichInstructionInCpiProxy, 30 | solMint, 31 | validateJupSwap 32 | } from './utils'; 33 | 34 | export function createSwiftOrderHash( 35 | quote: Quote, swapperAddress: string, destinationAddress: string, 36 | referrerAddress: string | null | undefined, randomKeyHex: string 37 | ): Buffer { 38 | const orderDataSize = 239; 39 | const data = Buffer.alloc(orderDataSize); 40 | let offset = 0; 41 | 42 | const sourceChainId = getWormholeChainIdByName(quote.fromChain); 43 | const trader = Buffer.from(hexToUint8Array(nativeAddressToHexString(swapperAddress, sourceChainId))); 44 | data.set(trader, 0); 45 | offset += 32; 46 | 47 | 48 | data.writeUInt16BE(sourceChainId, offset); 49 | offset += 2; 50 | 51 | const _tokenIn = quote.swiftInputContract === ZeroAddress ? 52 | nativeAddressToHexString(SystemProgram.programId.toString(), getWormholeChainIdByName('solana')) : 53 | nativeAddressToHexString(quote.swiftInputContract, sourceChainId); 54 | const tokenIn = Buffer.from(hexToUint8Array(_tokenIn)); 55 | data.set(tokenIn, offset); 56 | offset += 32; 57 | 58 | const destinationChainId = getWormholeChainIdByName(quote.toChain); 59 | const destAddress = Buffer.from(hexToUint8Array(nativeAddressToHexString(destinationAddress, destinationChainId))); 60 | data.set(destAddress, offset); 61 | offset += 32; 62 | 63 | data.writeUInt16BE(destinationChainId, offset); 64 | offset += 2; 65 | 66 | const _tokenOut = 67 | quote.toToken.contract === ZeroAddress ? 68 | nativeAddressToHexString(SystemProgram.programId.toString(), getWormholeChainIdByName('solana')) : 69 | nativeAddressToHexString(quote.toToken.contract, destinationChainId); 70 | const tokenOut = Buffer.from(hexToUint8Array(_tokenOut)); 71 | data.set(tokenOut, offset); 72 | offset += 32; 73 | 74 | data.writeBigUInt64BE(getAmountOfFractionalAmount(quote.minAmountOut, Math.min(quote.toToken.decimals, 8)), offset); 75 | offset += 8; 76 | 77 | data.writeBigUInt64BE(getAmountOfFractionalAmount(quote.gasDrop, Math.min(getGasDecimal(quote.toChain), 8)), offset); 78 | offset += 8; 79 | 80 | data.writeBigUInt64BE(BigInt(quote.cancelRelayerFee64), offset); 81 | offset += 8; 82 | 83 | data.writeBigUInt64BE(BigInt(quote.refundRelayerFee64), offset); 84 | offset += 8; 85 | 86 | data.writeBigUInt64BE(BigInt(quote.deadline64), offset); 87 | offset += 8; 88 | 89 | const refAddress = referrerAddress ? 90 | Buffer.from(hexToUint8Array(nativeAddressToHexString(referrerAddress, destinationChainId))) : 91 | SystemProgram.programId.toBuffer(); 92 | data.set(refAddress, offset); 93 | offset += 32; 94 | 95 | data.writeUInt8(quote.referrerBps, offset); 96 | offset += 1; 97 | 98 | const feeRateMayan = quote.protocolBps; 99 | data.writeUInt8(feeRateMayan, offset); 100 | offset += 1; 101 | 102 | data.writeUInt8(quote.swiftAuctionMode, offset); 103 | offset += 1; 104 | 105 | const _randomKey = Buffer.from(hexToUint8Array(randomKeyHex)); 106 | data.set(_randomKey, offset); 107 | offset += 32; 108 | 109 | if (offset !== orderDataSize) { 110 | throw new Error(`Invalid order data size: ${offset}`); 111 | } 112 | 113 | const hash = ethers.keccak256(data); 114 | return Buffer.from(hexToUint8Array(hash)); 115 | } 116 | 117 | type CreateInitSwiftInstructionParams = { 118 | quote: Quote, 119 | state: PublicKey, 120 | trader: PublicKey, 121 | relayer: PublicKey, 122 | stateAccount: PublicKey, 123 | relayerAccount: PublicKey, 124 | randomKey: PublicKey, 125 | destinationAddress: string, 126 | deadline: bigint, 127 | referrerAddress: string | null | undefined, 128 | } 129 | 130 | const InitSwiftLayout = struct([ 131 | blob(8, 'instruction'), 132 | blob(8, 'amountInMin'), 133 | u8('nativeInput'), 134 | blob(8, 'feeSubmit'), 135 | blob(32, 'destAddress'), 136 | u16('destinationChain'), 137 | blob(32, 'tokenOut'), 138 | blob(8, 'amountOutMin'), 139 | blob(8, 'gasDrop'), 140 | blob(8, 'feeCancel'), 141 | blob(8, 'feeRefund'), 142 | blob(8, 'deadline'), 143 | blob(32, 'refAddress'), 144 | u8('feeRateRef'), 145 | u8('feeRateMayan'), 146 | u8('auctionMode'), 147 | blob(32, 'randomKey'), 148 | ]); 149 | 150 | function createSwiftInitInstruction( 151 | params: CreateInitSwiftInstructionParams, 152 | ): TransactionInstruction { 153 | const { quote } = params; 154 | const mint = quote.swiftInputContract === ZeroAddress ? 155 | solMint : new PublicKey(quote.swiftInputContract); 156 | const destinationChainId = getWormholeChainIdByName(quote.toChain); 157 | 158 | if (destinationChainId !== quote.toToken.wChainId) { 159 | throw new Error(`Destination chain ID mismatch: ${destinationChainId} != ${quote.toToken.wChainId}`); 160 | } 161 | const accounts: AccountMeta[] = [ 162 | { pubkey: params.trader, isWritable: false, isSigner: true }, 163 | { pubkey: params.relayer, isWritable: true, isSigner: true }, 164 | { pubkey: params.state, isWritable: true, isSigner: false }, 165 | { pubkey: params.stateAccount, isWritable: true, isSigner: false }, 166 | { pubkey: params.relayerAccount, isWritable: true, isSigner: false }, 167 | { pubkey: mint, isWritable: false, isSigner: false }, 168 | { pubkey: new PublicKey(addresses.FEE_MANAGER_PROGRAM_ID), isWritable: false, isSigner: false }, 169 | { pubkey: new PublicKey(addresses.TOKEN_PROGRAM_ID), isWritable: false, isSigner: false }, 170 | { pubkey: SystemProgram.programId, isWritable: false, isSigner: false }, 171 | ]; 172 | 173 | const data = Buffer.alloc(InitSwiftLayout.span); 174 | 175 | const refAddress = params.referrerAddress ? 176 | Buffer.from(hexToUint8Array(nativeAddressToHexString(params.referrerAddress, destinationChainId))) : 177 | SystemProgram.programId.toBuffer(); 178 | 179 | const minMiddleAmount: bigint = 180 | quote.fromToken.contract === quote.swiftInputContract ? 181 | BigInt(quote.effectiveAmountIn64) : 182 | getAmountOfFractionalAmount(quote.minMiddleAmount, quote.swiftInputDecimals); 183 | 184 | InitSwiftLayout.encode({ 185 | instruction: getAnchorInstructionData('init_order'), 186 | amountInMin: getSafeU64Blob(minMiddleAmount), 187 | nativeInput: quote.swiftInputContract === ZeroAddress ? 1 : 0, 188 | feeSubmit: getSafeU64Blob(BigInt(quote.submitRelayerFee64)), 189 | destAddress: Buffer.from(hexToUint8Array(nativeAddressToHexString(params.destinationAddress, destinationChainId))), 190 | destinationChain: destinationChainId, 191 | tokenOut: Buffer.from(hexToUint8Array(nativeAddressToHexString(quote.toToken.contract, destinationChainId))), 192 | amountOutMin: getSafeU64Blob(getAmountOfFractionalAmount(quote.minAmountOut, Math.min(quote.toToken.decimals, 8))), 193 | gasDrop: getSafeU64Blob(getAmountOfFractionalAmount(quote.gasDrop, Math.min(getGasDecimal(quote.toChain), 8))), 194 | feeCancel: getSafeU64Blob(BigInt(quote.cancelRelayerFee64)), 195 | feeRefund: getSafeU64Blob(BigInt(quote.refundRelayerFee64)), 196 | deadline: getSafeU64Blob(params.deadline), 197 | refAddress: refAddress, 198 | feeRateRef: quote.referrerBps, 199 | feeRateMayan: quote.protocolBps, 200 | auctionMode: quote.swiftAuctionMode, 201 | randomKey: params.randomKey.toBuffer(), 202 | }, data); 203 | 204 | return new TransactionInstruction({ 205 | keys: accounts, 206 | data, 207 | programId: new PublicKey(addresses.SWIFT_PROGRAM_ID) 208 | }); 209 | } 210 | 211 | export async function createSwiftFromSolanaInstructions( 212 | quote: Quote, swapperAddress: string, destinationAddress: string, 213 | referrerAddress: string | null | undefined, 214 | connection: Connection, options: { 215 | allowSwapperOffCurve?: boolean, 216 | separateSwapTx?: boolean, 217 | skipProxyMayanInstructions?: boolean, 218 | } = {} 219 | ): Promise<{ 220 | instructions: TransactionInstruction[], 221 | signers: Keypair[], 222 | lookupTables: AddressLookupTableAccount[], 223 | swapMessageV0Params: SwapMessageV0Params | null, 224 | }> { 225 | 226 | if (quote.type !== 'SWIFT') { 227 | throw new Error('Unsupported quote type for Swift: ' + quote.type); 228 | } 229 | if (quote.toChain === 'solana') { 230 | throw new Error('Unsupported destination chain: ' + quote.toChain); 231 | } 232 | 233 | const allowSwapperOffCurve = options.allowSwapperOffCurve || false; 234 | 235 | let instructions: TransactionInstruction[] = []; 236 | let lookupTables: AddressLookupTableAccount[] = []; 237 | 238 | let _lookupTablesAddress: string[] = []; 239 | 240 | _lookupTablesAddress.push(addresses.LOOKUP_TABLE); 241 | 242 | // using for the swap via Jito Bundle 243 | let _swapAddressLookupTables: string[] = []; 244 | let swapInstructions: TransactionInstruction[] = []; 245 | let createSwapTpmTokenAccountInstructions: TransactionInstruction[] = []; 246 | const tmpSwapTokenAccount: Keypair = Keypair.generate(); 247 | let swapMessageV0Params: SwapMessageV0Params | null = null; 248 | 249 | const swiftProgram = new PublicKey(addresses.SWIFT_PROGRAM_ID); 250 | const trader = new PublicKey(swapperAddress); 251 | 252 | const randomKey = Keypair.generate(); 253 | 254 | if (!Number(quote.deadline64)) { 255 | throw new Error('Swift mode requires a timeout'); 256 | } 257 | const deadline = BigInt(quote.deadline64); 258 | 259 | const hash = createSwiftOrderHash( 260 | quote, swapperAddress, destinationAddress, 261 | referrerAddress, randomKey.publicKey.toBuffer().toString('hex') 262 | ); 263 | const [state] = PublicKey.findProgramAddressSync( 264 | [Buffer.from('STATE_SOURCE'), hash], 265 | swiftProgram, 266 | ); 267 | 268 | const swiftInputMint = quote.swiftInputContract === ZeroAddress ? solMint : new PublicKey(quote.swiftInputContract); 269 | 270 | const stateAccount = getAssociatedTokenAddress( 271 | swiftInputMint, state, true 272 | ); 273 | 274 | const relayer = quote.gasless ? new PublicKey(quote.relayer) : trader; 275 | const relayerAccount = getAssociatedTokenAddress(swiftInputMint, relayer, false); 276 | if (quote.fromToken.contract === quote.swiftInputContract) { 277 | if (quote.suggestedPriorityFee > 0) { 278 | instructions.push(ComputeBudgetProgram.setComputeUnitPrice({ 279 | microLamports: quote.suggestedPriorityFee, 280 | })) 281 | } 282 | instructions.push( 283 | sandwichInstructionInCpiProxy(createAssociatedTokenAccountInstruction(relayer, stateAccount, state, swiftInputMint)) 284 | ); 285 | if (quote.swiftInputContract === ZeroAddress) { 286 | instructions.push( 287 | sandwichInstructionInCpiProxy(SystemProgram.transfer({ 288 | fromPubkey: trader, 289 | toPubkey: stateAccount, 290 | lamports: BigInt(quote.effectiveAmountIn64), 291 | })), 292 | sandwichInstructionInCpiProxy(createSyncNativeInstruction(stateAccount)), 293 | ); 294 | } else { 295 | instructions.push( 296 | sandwichInstructionInCpiProxy(createSplTransferInstruction( 297 | getAssociatedTokenAddress( 298 | swiftInputMint, trader, allowSwapperOffCurve 299 | ), 300 | stateAccount, 301 | trader, 302 | BigInt(quote.effectiveAmountIn64), 303 | )) 304 | ); 305 | } 306 | } else { 307 | const clientSwapRaw = await getSwapSolana({ 308 | minMiddleAmount: quote.minMiddleAmount, 309 | middleToken: quote.swiftInputContract, 310 | userWallet: swapperAddress, 311 | slippageBps: quote.slippageBps, 312 | fromToken: quote.fromToken.contract, 313 | amountIn64: quote.effectiveAmountIn64, 314 | depositMode: quote.gasless ? 'SWIFT_GASLESS' : 'SWIFT', 315 | orderHash: `0x${hash.toString('hex')}`, 316 | fillMaxAccounts: options?.separateSwapTx || false, 317 | tpmTokenAccount: options?.separateSwapTx ? tmpSwapTokenAccount.publicKey.toString() : null, 318 | }); 319 | const clientSwap = decentralizeClientSwapInstructions(clientSwapRaw, connection, relayer); 320 | if (options?.separateSwapTx && clientSwapRaw.maxAccountsFilled) { 321 | validateJupSwap(clientSwap, tmpSwapTokenAccount.publicKey, trader); 322 | createSwapTpmTokenAccountInstructions = await createInitializeRandomTokenAccountInstructions( 323 | connection, 324 | relayer, 325 | swiftInputMint, 326 | trader, 327 | tmpSwapTokenAccount, 328 | ); 329 | swapInstructions.push(...clientSwap.computeBudgetInstructions); 330 | if (clientSwap.setupInstructions) { 331 | swapInstructions.push(...clientSwap.setupInstructions); 332 | } 333 | swapInstructions.push(clientSwap.swapInstruction); 334 | if (clientSwap.cleanupInstruction) { 335 | swapInstructions.push(clientSwap.cleanupInstruction); 336 | } 337 | _swapAddressLookupTables.push(...clientSwap.addressLookupTableAddresses); 338 | instructions.push(sandwichInstructionInCpiProxy( 339 | createAssociatedTokenAccountInstruction(relayer, stateAccount, state, swiftInputMint) 340 | )); 341 | instructions.push(sandwichInstructionInCpiProxy(createTransferAllAndCloseInstruction( 342 | trader, 343 | swiftInputMint, 344 | tmpSwapTokenAccount.publicKey, 345 | stateAccount, 346 | relayer, 347 | ))); 348 | } else { 349 | validateJupSwap(clientSwap, stateAccount, trader); 350 | instructions.push(...clientSwap.computeBudgetInstructions); 351 | if (clientSwap.setupInstructions) { 352 | instructions.push(...(clientSwap.setupInstructions.map(ins => sandwichInstructionInCpiProxy(ins)))); 353 | } 354 | instructions.push(sandwichInstructionInCpiProxy(clientSwap.swapInstruction)); 355 | if (clientSwap.cleanupInstruction) { 356 | instructions.push(sandwichInstructionInCpiProxy(clientSwap.cleanupInstruction)); 357 | } 358 | _lookupTablesAddress.push(...clientSwap.addressLookupTableAddresses); 359 | } 360 | } 361 | 362 | instructions.push(sandwichInstructionInCpiProxy(createSwiftInitInstruction({ 363 | quote, 364 | state, 365 | trader, 366 | stateAccount, 367 | randomKey: randomKey.publicKey, 368 | relayerAccount, 369 | relayer, 370 | destinationAddress, 371 | deadline, 372 | referrerAddress, 373 | }), options.skipProxyMayanInstructions)); 374 | 375 | const totalLookupTables = await getAddressLookupTableAccounts(_lookupTablesAddress.concat(_swapAddressLookupTables), connection); 376 | lookupTables = totalLookupTables.slice(0, _lookupTablesAddress.length); 377 | 378 | if (swapInstructions.length > 0) { 379 | const swapLookupTables = totalLookupTables.slice(_lookupTablesAddress.length); 380 | swapMessageV0Params = { 381 | messageV0: { 382 | payerKey: relayer, 383 | instructions: swapInstructions, 384 | addressLookupTableAccounts: swapLookupTables, 385 | }, 386 | createTmpTokenAccountIxs: createSwapTpmTokenAccountInstructions, 387 | tmpTokenAccount: tmpSwapTokenAccount, 388 | }; 389 | } 390 | 391 | return { instructions, signers: [], lookupTables, swapMessageV0Params }; 392 | } 393 | 394 | 395 | 396 | 397 | 398 | 399 | -------------------------------------------------------------------------------- /src/sui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './suiMctp'; 3 | export * from './suiSwap'; 4 | 5 | -------------------------------------------------------------------------------- /src/sui/suiHyperCore.ts: -------------------------------------------------------------------------------- 1 | import { Transaction, TransactionResult } from '@mysten/sui/transactions'; 2 | import { SuiClient } from '@mysten/sui/client'; 3 | import { 4 | Quote, 5 | SuiFunctionParameter, 6 | SuiFunctionNestedResult, 7 | ComposableSuiMoveCallsOptions 8 | } from '../types'; 9 | import { 10 | fetchMayanSuiPackageId, 11 | resolveInputCoin, 12 | } from './utils'; 13 | import { 14 | getAmountOfFractionalAmount, getDisplayAmount, 15 | getHyperCoreUSDCDepositCustomPayload, 16 | getWormholeChainIdByName, 17 | nativeAddressToHexString 18 | } from '../utils'; 19 | import addresses from '../addresses'; 20 | import { CCTP_TOKEN_DECIMALS } from '../cctp'; 21 | import { SystemProgram } from '@solana/web3.js'; 22 | import { getSwapSui } from '../api'; 23 | import { addBridgeWithFeeMoveCalls2 } from './suiMctp'; 24 | 25 | export async function createHyperCoreDepositFromSuiMoveCalls( 26 | quote: Quote, 27 | swapperAddress: string, 28 | destinationAddress: string, 29 | referrerAddress: string | null | undefined, 30 | suiClient: SuiClient, 31 | options?: ComposableSuiMoveCallsOptions 32 | ): Promise { 33 | if ( 34 | quote.toToken.contract.toLowerCase() !== addresses.ARBITRUM_USDC_CONTRACT.toLowerCase() || 35 | quote.type !== 'MCTP' 36 | ) { 37 | throw new Error('Unsupported quote type for USDC deposit: ' + quote.type); 38 | } 39 | if (!quote.hyperCoreParams) { 40 | throw new Error('HyperCore parameters are required for this quote'); 41 | } 42 | if (!options?.usdcPermitSignature) { 43 | throw new Error('USDC permit signature is required for this quote'); 44 | } 45 | if (!Number(quote.deadline64)) { 46 | throw new Error('HyperCore deposit requires timeout'); 47 | } 48 | 49 | const [mctpPackageId] = await Promise.all([ 50 | fetchMayanSuiPackageId(addresses.SUI_MCTP_STATE, suiClient), 51 | ]); 52 | 53 | const amountInMin = getAmountOfFractionalAmount( 54 | quote.minMiddleAmount, 55 | CCTP_TOKEN_DECIMALS 56 | ); 57 | let tx: Transaction; 58 | let inputCoin: 59 | | TransactionResult 60 | | SuiFunctionNestedResult 61 | | { $kind: 'Input'; Input: number; type?: 'object' }; 62 | let whFeeCoin: SuiFunctionParameter; 63 | 64 | // Setup tx based on we should have client swap or not 65 | if (quote.fromToken.contract === quote.hyperCoreParams.initiateTokenContract) { 66 | tx = options?.builtTransaction ?? new Transaction(); 67 | inputCoin = await resolveInputCoin( 68 | BigInt(quote.hyperCoreParams.bridgeAmountUSDC64), 69 | swapperAddress, 70 | quote.mctpInputContract, 71 | suiClient, 72 | tx, 73 | options?.inputCoin 74 | ); 75 | } else { 76 | const { 77 | tx: serializedTx, 78 | outCoin, 79 | whFeeCoin: suiSplitViaSwap, 80 | } = await getSwapSui({ 81 | amountIn64: quote.effectiveAmountIn64, 82 | inputCoinType: quote.fromToken.contract, 83 | middleCoinType: quote.hyperCoreParams.initiateTokenContract, 84 | userWallet: swapperAddress, 85 | withWhFee: true, 86 | referrerAddress, 87 | inputCoin: options?.inputCoin, 88 | transaction: options?.builtTransaction ? (await options.builtTransaction.toJSON()) : undefined, 89 | }); 90 | tx = Transaction.from(serializedTx); 91 | const [initiateCoin] = tx.splitCoins( 92 | outCoin, 93 | [ BigInt(quote.hyperCoreParams.bridgeAmountUSDC64) ] 94 | ); 95 | 96 | tx.transferObjects( 97 | [ outCoin ], 98 | tx.pure.address(swapperAddress) 99 | ); 100 | 101 | inputCoin = initiateCoin; 102 | whFeeCoin = suiSplitViaSwap ? { result: suiSplitViaSwap } : null; 103 | } 104 | 105 | const payload = getHyperCoreUSDCDepositCustomPayload(quote, destinationAddress, options.usdcPermitSignature); 106 | await addBridgeWithFeeMoveCalls2({ 107 | swapperAddress, 108 | destinationAddress: addresses.HC_ARBITRUM_DEPOSIT_PROCESSOR, 109 | mctpPackageId, 110 | toChain: 'arbitrum', 111 | minMiddleAmount: getDisplayAmount(quote.hyperCoreParams.bridgeAmountUSDC64, CCTP_TOKEN_DECIMALS), 112 | bridgeFee: quote.bridgeFee, 113 | gasDrop: quote.hyperCoreParams.failureGasDrop, 114 | mctpInputContract: quote.mctpInputContract, 115 | mctpInputTreasury: quote.mctpInputTreasury, 116 | mctpVerifiedInputAddress: quote.mctpVerifiedInputAddress, 117 | redeemRelayerFee: 0, 118 | payload, 119 | suiClient, 120 | options: { 121 | inputCoin: { result: inputCoin }, 122 | whFeeCoin, 123 | builtTransaction: tx, 124 | }, 125 | }); 126 | 127 | 128 | // Log initial coin and amount 129 | const amountIn = BigInt(quote.effectiveAmountIn64); 130 | const _payload = Uint8Array.from(payload); 131 | tx.moveCall({ 132 | package: mctpPackageId, 133 | module: 'init_order', 134 | function: 'log_initialize_mctp', 135 | typeArguments: [quote.fromToken.contract], 136 | arguments: [ 137 | tx.pure.u64(amountIn), 138 | tx.object(quote.fromToken.verifiedAddress), 139 | tx.pure.vector('u8', _payload), 140 | ] 141 | }); 142 | 143 | try { 144 | // Log referrer 145 | let referrerHex: string; 146 | if (referrerAddress) { 147 | referrerHex = nativeAddressToHexString(referrerAddress, getWormholeChainIdByName(quote.toChain)); 148 | } else { 149 | referrerHex = nativeAddressToHexString( 150 | SystemProgram.programId.toString(), 151 | getWormholeChainIdByName('solana') 152 | ); 153 | } 154 | tx.moveCall({ 155 | package: addresses.SUI_LOGGER_PACKAGE_ID, 156 | module: 'referrer_logger', 157 | function: 'log_referrer', 158 | arguments: [ 159 | tx.pure.address(referrerHex), 160 | tx.pure.u8(quote.referrerBps || 0), 161 | ] 162 | }) 163 | } catch (err) { 164 | console.error('Failed to log referrer', err); 165 | } 166 | return tx; 167 | } 168 | -------------------------------------------------------------------------------- /src/sui/suiSwap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Quote, 3 | ReferrerAddresses, 4 | ComposableSuiMoveCallsOptions, 5 | } from '../types'; 6 | import { SuiClient } from '@mysten/sui/client'; 7 | import { Transaction } from '@mysten/sui/transactions'; 8 | import { getQuoteSuitableReferrerAddress } from '../utils'; 9 | import { createMctpFromSuiMoveCalls } from './suiMctp'; 10 | import { Buffer } from 'buffer'; 11 | import { createHyperCoreDepositFromSuiMoveCalls } from './suiHyperCore'; 12 | 13 | export async function createSwapFromSuiMoveCalls( 14 | quote: Quote, 15 | swapperWalletAddress: string, 16 | destinationAddress: string, 17 | referrerAddresses: ReferrerAddresses | null | undefined, 18 | payload: Uint8Array | Buffer | null | undefined, 19 | suiClient: SuiClient, 20 | options?: ComposableSuiMoveCallsOptions 21 | ): Promise { 22 | const referrerAddress = getQuoteSuitableReferrerAddress( 23 | quote, 24 | referrerAddresses 25 | ); 26 | 27 | if (quote.toChain === 'hypercore') { 28 | if (!quote.hyperCoreParams) { 29 | throw new Error('HyperCore parameters are required for this quote'); 30 | } 31 | if (!options?.usdcPermitSignature) { 32 | throw new Error('USDC permit signature is required for this quote'); 33 | } 34 | if (quote.type !== 'MCTP') { 35 | throw new Error('Unsupported quote type for HyperCore deposit: ' + quote.type); 36 | } 37 | if (payload) { 38 | throw new Error('Payload is not supported for HyperCore deposit quotes'); 39 | } 40 | return createHyperCoreDepositFromSuiMoveCalls( 41 | quote, 42 | swapperWalletAddress, 43 | destinationAddress, 44 | referrerAddress, 45 | suiClient, 46 | options, 47 | ); 48 | } 49 | 50 | if (quote.type === 'MCTP') { 51 | return createMctpFromSuiMoveCalls( 52 | quote, 53 | swapperWalletAddress, 54 | destinationAddress, 55 | referrerAddress, 56 | payload, 57 | suiClient, 58 | options 59 | ); 60 | } else { 61 | throw new Error('Unsupported quote type from Sui chain'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/sui/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SuiClient, 3 | SuiMoveFunctionArgType, 4 | CoinStruct, 5 | PaginatedCoins, 6 | SuiObjectResponse, 7 | } from '@mysten/sui/client'; 8 | import { SuiFunctionNestedResult, SuiFunctionParameter } from '../types'; 9 | import { Transaction, TransactionResult } from '@mysten/sui/transactions'; 10 | 11 | /** 12 | * assertArgumentIsImmutable 13 | * 14 | * Validates whether a specific argument of a given Move function in a Sui module is immutable. 15 | * This includes checking if the argument type is either `Pure` or an object passed by immutable reference. 16 | * 17 | * @param params - An object containing the following properties: 18 | * - package: The Sui package containing the Move module. 19 | * - module: The name of the Move module. 20 | * - function: The name of the Move function to validate. 21 | * - argumentIndex: The index of the argument to check for immutability. 22 | * @param suiClient - An instance of `SuiClient` used to interact with the Sui blockchain. 23 | * 24 | * @throws Will throw an error if: 25 | * - The argument types cannot be retrieved from the Sui client. 26 | * - The specified argument is not immutable (i.e., not `Pure` or passed as an immutable reference). 27 | * 28 | * @returns A Promise that resolves if the specified argument is immutable, or rejects with an error otherwise. 29 | */ 30 | export async function assertArgumentIsImmutable( 31 | params: { 32 | package: string; 33 | module: string; 34 | function: string; 35 | argumentIndex: number; 36 | }, 37 | suiClient: SuiClient 38 | ): Promise { 39 | let argTypes: SuiMoveFunctionArgType[]; 40 | try { 41 | argTypes = await suiClient.getMoveFunctionArgTypes({ 42 | package: params.package, 43 | module: params.module, 44 | function: params.function, 45 | }); 46 | } catch (error) { 47 | throw new Error( 48 | `Failed to fetch ${params.package}::${params.module}::${params.function} ArgTypes` 49 | ); 50 | } 51 | if (argTypes) { 52 | if ( 53 | argTypes[params.argumentIndex] !== 'Pure' && 54 | //@ts-ignore 55 | argTypes[params.argumentIndex]?.Object !== 'ByImmutableReference' 56 | ) { 57 | throw new Error( 58 | `Argument ${params.argumentIndex} of ${params.module}::${params.function} is not immutable` 59 | ); 60 | } 61 | } else { 62 | throw new Error( 63 | `Failed to fetch package::${params.module}::${params.function} ArgTypes` 64 | ); 65 | } 66 | } 67 | 68 | /** 69 | * fetchCoinsUntilAmountReachedOrEnd 70 | * 71 | * This function is inspired by the `fetchAllCoins` implementation from the 72 | * Aftermath Finance SDK (reference: https://github.com/AftermathFinance/aftermath-ts-sdk/blob/74087402caf5ebf06f6c639cc5e23445d40a039f/src/packages/coin/api/coinApi.ts#L85). 73 | * 74 | * It retrieves a list of coins associated with a specified wallet address and coin type until 75 | * either the target coin amount is reached or no more coins are available. The function returns 76 | * an object containing the collected coins and the cumulative sum of their values. 77 | * 78 | * @param inputs - An object containing the following properties: 79 | * - walletAddress: The address of the wallet to fetch coins from. 80 | * - coinType: The type of coin to filter for during retrieval. 81 | * - coinAmount: The target coin amount to reach. 82 | * 83 | * @param suiClient - An instance of `SuiClient` used to interact with the Sui blockchain. 84 | * 85 | * @returns A Promise that resolves to an object containing: 86 | * - coins: An array of CoinStruct objects representing the retrieved coins. 87 | * - sum: The cumulative value of the retrieved coins. 88 | */ 89 | export async function fetchAllCoins( 90 | inputs: { 91 | walletAddress: string; 92 | coinType: string; 93 | coinAmount: bigint; 94 | }, 95 | suiClient: SuiClient 96 | ): Promise<{ 97 | coins: CoinStruct[]; 98 | sum: bigint; 99 | }> { 100 | let allCoinData: CoinStruct[] = []; 101 | let currentSum = BigInt(0); 102 | let cursor: string | undefined = undefined; 103 | do { 104 | const paginatedCoins: PaginatedCoins = await suiClient.getCoins({ 105 | ...inputs, 106 | owner: inputs.walletAddress, 107 | cursor, 108 | }); 109 | 110 | const coinData = paginatedCoins.data.filter( 111 | (data) => BigInt(data.balance) > BigInt(0) 112 | ); 113 | allCoinData = [...allCoinData, ...coinData]; 114 | 115 | coinData.forEach((coin) => { 116 | currentSum += BigInt(coin.balance); 117 | }); 118 | 119 | if ( 120 | paginatedCoins.data.length === 0 || 121 | !paginatedCoins.hasNextPage || 122 | !paginatedCoins.nextCursor || 123 | currentSum >= inputs.coinAmount 124 | ) 125 | return { 126 | coins: allCoinData.sort((b, a) => 127 | Number(BigInt(b.coinObjectId) - BigInt(a.coinObjectId)) 128 | ), 129 | sum: currentSum, 130 | }; 131 | 132 | cursor = paginatedCoins.nextCursor; 133 | } while (true); 134 | } 135 | 136 | 137 | /** 138 | * Fetches the latest Mayan Sui package ID from a shared state object on the Sui blockchain. 139 | * 140 | * By Mayan standards: 141 | * - The latest package ID is stored in a shared state object to avoid forced SDK updates during package upgrades. 142 | * - The shared state object ID is hardcoded in the SDK to ensure security. 143 | * - Upgrades to Mayan packages require multiple signatures to perform, enhancing governance and security. 144 | * 145 | * @param {string} stateObjectId - The ID of the shared state object containing the latest package ID. 146 | * @param {SuiClient} suiClient - An instance of the SuiClient used to interact with the Sui blockchain. 147 | * @returns {Promise} - A promise that resolves to the latest Mayan Sui package ID. 148 | * @throws {Error} - Throws an error if the state object cannot be fetched or if the `latest_package_id` field is not found. 149 | */ 150 | export async function fetchMayanSuiPackageId( 151 | stateObjectId: string, 152 | suiClient: SuiClient 153 | ): Promise { 154 | let object: SuiObjectResponse; 155 | try { 156 | object = await suiClient.getObject({ 157 | id: stateObjectId, 158 | options: { 159 | showContent: true, 160 | }, 161 | }); 162 | } catch (err) { 163 | throw new Error(`Failed to fetch Mayan Sui package ID: \n\n ${err}`); 164 | } 165 | // @ts-ignore 166 | if (object.data?.content?.fields?.latest_package_id) { 167 | // @ts-ignore 168 | return object.data.content.fields.latest_package_id; 169 | } 170 | throw new Error('latest_package_id not found in Mayan Sui state object'); 171 | } 172 | 173 | export async function resolveInputCoin( 174 | amount: bigint, 175 | owner: string, 176 | coinType: string, 177 | suiClient: SuiClient, 178 | tx: Transaction, 179 | preparedCoin: SuiFunctionParameter 180 | ) { 181 | let inputCoin: 182 | | TransactionResult 183 | | SuiFunctionNestedResult 184 | | { $kind: 'Input'; Input: number; type?: 'object' }; 185 | if (preparedCoin?.result) { 186 | inputCoin = preparedCoin.result; 187 | } else if (preparedCoin?.objectId) { 188 | inputCoin = tx.object(preparedCoin.objectId); 189 | } else { 190 | const { coins, sum } = await fetchAllCoins( 191 | { 192 | walletAddress: owner, 193 | coinType: coinType, 194 | coinAmount: amount, 195 | }, 196 | suiClient 197 | ); 198 | if (sum < amount) { 199 | throw new Error( 200 | `Insufficient funds to create Coin ${coinType} with amount ${amount}` 201 | ); 202 | } 203 | if (coins.length > 1) { 204 | tx.mergeCoins( 205 | coins[0].coinObjectId, 206 | coins.slice(1).map((c) => c.coinObjectId) 207 | ); 208 | } 209 | const [spitedCoin] = tx.splitCoins(coins[0].coinObjectId, [amount]); 210 | inputCoin = spitedCoin; 211 | } 212 | return inputCoin; 213 | } 214 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompileV0Args, 3 | Transaction, 4 | VersionedTransaction, 5 | TransactionInstruction as SolanaTransactionInstruction, 6 | Keypair as SolanaKeypair, 7 | } from '@solana/web3.js'; 8 | import { 9 | Transaction as SuiTransaction, 10 | TransactionResult as SuiTransactionResult, 11 | } from '@mysten/sui/transactions'; 12 | import { Buffer } from 'buffer'; 13 | 14 | export type ChainName = 'solana' 15 | | 'ethereum' | 'bsc' | 'polygon' | 'avalanche' | 'arbitrum' | 'optimism' | 16 | 'base' | 'aptos' | 'sui' | 'unichain' | 'linea' | 'hypercore' | 'sonic' | 'hyperevm'; 17 | 18 | export type TokenStandard = 'native' | 'erc20' | 'spl' | 'spl2022' | 'suicoin' | 'hypertoken'; 19 | 20 | export type Token = { 21 | name: string, 22 | symbol: string, 23 | mint: string, 24 | contract: string, 25 | chainId: number, 26 | wChainId?: number, 27 | decimals: number, 28 | logoURI: string, 29 | coingeckoId: string, 30 | realOriginChainId?: number, 31 | realOriginContractAddress?: string, 32 | supportsPermit: boolean, 33 | verified: boolean; 34 | standard: TokenStandard, 35 | verifiedAddress: string, 36 | }; 37 | 38 | export type QuoteParams = { 39 | /** 40 | * @deprecated to avoid precision issues, use {@link amountIn64} instead 41 | */ 42 | amount?: number; 43 | amountIn64?: string; 44 | fromToken: string; 45 | fromChain: ChainName; 46 | toToken: string; 47 | toChain: ChainName; 48 | /** 49 | * @deprecated Use the new property {@link slippageBps} instead 50 | */ 51 | slippage?: number; 52 | /** 53 | * Slippage in basis points. 54 | * One basis point (bps) = 0.01%. 55 | * 56 | * - A value of `50` means a slippage of 0.5%. 57 | * - A value of `100` means a slippage of 1%. 58 | * - If set to `'auto'`, the system will automatically determine slippage. 59 | * 60 | * @example 61 | * slippageBps: 50 // 0.5% slippage 62 | */ 63 | slippageBps: 'auto' | number; 64 | gasDrop?: number; 65 | referrer?: string; 66 | referrerBps?: number; 67 | }; 68 | 69 | export type QuoteError = { 70 | message: string, 71 | code: number, 72 | data: any, 73 | } 74 | 75 | export type Quote = { 76 | type: 'WH' | 'SWIFT' | 'MCTP' | 'SHUTTLE' | 'FAST_MCTP' | 'MONO_CHAIN'; 77 | /** 78 | * @deprecated Use the new property {@link effectiveAmountIn64} instead 79 | */ 80 | effectiveAmountIn: number; 81 | effectiveAmountIn64: string; 82 | expectedAmountOut: number; 83 | priceImpact: number; 84 | minAmountOut: number; 85 | minReceived: number; 86 | gasDrop: number; 87 | price: number; 88 | swapRelayerFee: number; 89 | redeemRelayerFee: number; 90 | refundRelayerFee: number; 91 | solanaRelayerFee: number; 92 | redeemRelayerFee64: string; 93 | refundRelayerFee64: string; 94 | cancelRelayerFee64: string; 95 | submitRelayerFee64: string; 96 | solanaRelayerFee64: string; 97 | clientRelayerFeeSuccess: number | null; 98 | clientRelayerFeeRefund: number | null; 99 | eta: number; 100 | etaSeconds: number; 101 | clientEta: string; 102 | fromToken: Token; 103 | toToken: Token; 104 | fromChain: ChainName; 105 | toChain: ChainName; 106 | slippageBps: number; 107 | priceStat: { 108 | ratio: number; 109 | status: 'GOOD' | 'NORMAL' | 'BAD'; 110 | } 111 | mintDecimals: { 112 | from: number; 113 | to: number; 114 | }; 115 | bridgeFee: number; 116 | suggestedPriorityFee: number; 117 | meta: { 118 | icon: string; 119 | title: string; 120 | advertisedTitle: string; 121 | advertisedDescription: string; 122 | switchText: string; 123 | }; 124 | onlyBridging: boolean; 125 | deadline64: string; 126 | referrerBps?: number; 127 | protocolBps?: number; 128 | whMayanContract: string; 129 | cheaperChain: ChainName; 130 | mctpInputContract: string; 131 | mctpOutputContract: string; 132 | hasAuction: boolean; 133 | minMiddleAmount?: number; 134 | evmSwapRouterAddress?: string; 135 | evmSwapRouterCalldata?: string; 136 | mctpMayanContract?: string; 137 | swiftMayanContract?: string; 138 | shuttleContract?: string; 139 | swiftAuctionMode?: number; 140 | swiftInputContract: string; 141 | swiftInputDecimals: number; 142 | gasless: boolean; 143 | relayer: string; 144 | sendTransactionCost: number; 145 | maxUserGasDrop: number; 146 | rentCost?: bigint; 147 | shuttleParams : { 148 | maxLLFee: string; 149 | maxRelayingFee: string; 150 | fastTransferDeadline: number; 151 | hasDestSwap: boolean 152 | path: string; 153 | } 154 | shuttleInputContract: string; 155 | shuttleInputDecimals: number; 156 | mctpVerifiedInputAddress: string; 157 | mctpInputTreasury: string; 158 | circleMaxFee64: string; 159 | fastMctpMayanContract: string; 160 | fastMctpInputContract: string; 161 | fastMctpMinFinality: number; 162 | hyperCoreParams?: { 163 | depositAmountUSDC64: string; 164 | bridgeAmountUSDC64: string; 165 | initiateTokenContract: string; 166 | initiateContractAddress?: string; 167 | failureGasDrop: number; 168 | }; 169 | monoChainMayanContract: string; 170 | }; 171 | 172 | export type QuoteOptions = { 173 | wormhole?: boolean; 174 | swift?: boolean; 175 | mctp?: boolean; 176 | shuttle?: boolean; 177 | fastMctp?: boolean; 178 | gasless?: boolean; 179 | onlyDirect?: boolean; 180 | fullList?: boolean; 181 | payload?: string; 182 | monoChain?: boolean; 183 | }; 184 | 185 | export type SolanaTransactionSigner = { 186 | (trx: Transaction): Promise; 187 | (trx: VersionedTransaction): Promise; 188 | }; 189 | 190 | export type Erc20Permit = { 191 | value: bigint, 192 | deadline: number, 193 | v: number, 194 | r: string, 195 | s: string, 196 | } 197 | 198 | type BaseGetSolanaSwapParams = { 199 | amountIn64: string, 200 | fromToken: string, 201 | minMiddleAmount: number, 202 | middleToken: string, 203 | userWallet: string, 204 | slippageBps: number, 205 | referrerAddress?: string, 206 | fillMaxAccounts?: boolean, 207 | tpmTokenAccount?: string, 208 | } 209 | 210 | type MctpGetSolanaSwapParams = BaseGetSolanaSwapParams & { 211 | userLedger: string, 212 | depositMode: 'WITH_FEE' | 'LOCK_FEE' | 'SWAP', 213 | } 214 | 215 | type SwiftGetSolanaSwapParams = BaseGetSolanaSwapParams & { 216 | orderHash: string, 217 | depositMode: 'SWIFT' | 'SWIFT_GASLESS', 218 | } 219 | 220 | type HCDepositUSDCGetSolanaSwapParams = BaseGetSolanaSwapParams & { 221 | tpmTokenAccount: string, 222 | depositMode: 'HC_USDC', 223 | } 224 | 225 | type MonoChainGetSolanaSwapParams = Omit< 226 | BaseGetSolanaSwapParams, 227 | 'minMiddleAmount' | 'fillMaxAccounts' | 'tpmTokenAccount' 228 | > & { 229 | destinationWallet: string; 230 | expectedAmountOut64: string; 231 | depositMode: 'MONO_CHAIN'; 232 | referrerBps: number; 233 | }; 234 | 235 | 236 | export type GetSolanaSwapParams = 237 | MctpGetSolanaSwapParams | SwiftGetSolanaSwapParams | HCDepositUSDCGetSolanaSwapParams | MonoChainGetSolanaSwapParams; 238 | 239 | type BaseGetSuiSwapParams = { 240 | amountIn64: string, 241 | inputCoinType: string, 242 | middleCoinType: string, 243 | userWallet: string, 244 | referrerAddress?: string, 245 | inputCoin: SuiFunctionParameter, 246 | transaction: string, 247 | } 248 | 249 | type MctpGetSuiSwapParams = BaseGetSuiSwapParams & { 250 | withWhFee: boolean 251 | } 252 | 253 | export type GetSuiSwapParams = MctpGetSuiSwapParams; 254 | 255 | export type SolanaKeyInfo = { 256 | pubkey: string, 257 | isWritable: boolean, 258 | isSigner: boolean, 259 | } 260 | export type InstructionInfo = { 261 | accounts: SolanaKeyInfo[], 262 | data: string, 263 | programId: string, 264 | } 265 | 266 | export type SolanaClientSwap = { 267 | computeBudgetInstructions?: InstructionInfo[], 268 | setupInstructions?: InstructionInfo[], 269 | swapInstruction: InstructionInfo, 270 | cleanupInstruction: InstructionInfo, 271 | addressLookupTableAddresses: string[], 272 | maxAccountsFilled: boolean, 273 | } 274 | 275 | export type SuiFunctionNestedResult = { 276 | $kind: 'NestedResult'; 277 | NestedResult: [number, number]; 278 | }; 279 | 280 | export type SuiFunctionParameter = 281 | { 282 | result: 283 | | SuiTransactionResult 284 | | SuiFunctionNestedResult 285 | | { $kind: 'Input'; Input: number; type?: 'object' }; 286 | objectId?: undefined | null; 287 | } 288 | | { 289 | result?: undefined | null; 290 | objectId: string; 291 | }; 292 | 293 | export type SuiClientSwap = { 294 | tx: string, 295 | outCoin: SuiTransactionResult, 296 | whFeeCoin?: SuiTransactionResult | SuiFunctionNestedResult, 297 | } 298 | 299 | export type ReferrerAddresses = { 300 | solana?: string | null, 301 | evm?: string | null, 302 | sui?: string | null, 303 | } 304 | 305 | export type SwiftEvmOrderTypedData = { 306 | domain: { 307 | name: "Mayan Swift", 308 | chainId: number, 309 | verifyingContract: string, 310 | }, 311 | types: { 312 | CreateOrder: [ 313 | { name: 'OrderId', type: 'bytes32' }, 314 | { name: 'InputAmount', type: 'uint256' }, 315 | { name: 'SubmissionFee', type: 'uint256' }, 316 | ], 317 | }, 318 | value: { 319 | OrderId: string, 320 | InputAmount: bigint, 321 | SubmissionFee: bigint, 322 | } 323 | } 324 | 325 | export type EvmForwarderParams = { 326 | method: string, 327 | params: any[], 328 | } 329 | 330 | export type JitoBundleOptions = { 331 | tipLamports: number, 332 | signAllTransactions: (transactions: T[]) => Promise 333 | jitoAccount?: string, 334 | jitoSendUrl?: string, 335 | separateSwapTx?: boolean, 336 | } 337 | 338 | export type ComposableSuiMoveCallsOptions = { 339 | builtTransaction?: SuiTransaction; 340 | inputCoin?: SuiFunctionParameter; 341 | whFeeCoin?: SuiFunctionParameter; 342 | usdcPermitSignature?: string; 343 | }; 344 | 345 | export type SwapMessageV0Params = { 346 | messageV0: Omit, 347 | createTmpTokenAccountIxs: SolanaTransactionInstruction[], 348 | tmpTokenAccount: SolanaKeypair, 349 | } 350 | 351 | export type PermitDomain = { 352 | name: string; 353 | version: string; 354 | chainId: number; 355 | verifyingContract: string; 356 | }; 357 | 358 | export type PermitValue = { 359 | owner: string; 360 | spender: string; 361 | value: string; 362 | nonce: string; 363 | deadline: string; 364 | } 365 | 366 | export type SolanaBridgeOptions = { 367 | allowSwapperOffCurve?: boolean, 368 | forceSkipCctpInstructions?: boolean, 369 | separateSwapTx?: boolean, 370 | usdcPermitSignature?: string; 371 | skipProxyMayanInstructions?: boolean; 372 | customPayload?: Buffer | Uint8Array | null; 373 | } 374 | 375 | export type EstimateGasEvmParams = { 376 | from: string; 377 | tokenIn: string; 378 | value: string; 379 | to: string; 380 | data: string; 381 | chainId: number; 382 | } 383 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers, zeroPadValue, parseUnits, formatUnits, TypedDataEncoder, JsonRpcProvider } from 'ethers'; 2 | import {PublicKey, SystemProgram} from '@solana/web3.js'; 3 | import { Buffer } from 'buffer'; 4 | import addresses from './addresses'; 5 | import { ChainName, Erc20Permit, Quote, ReferrerAddresses, Token, PermitDomain, PermitValue } from './types'; 6 | import ERC20Artifact from './evm/ERC20Artifact'; 7 | import * as sha3 from 'js-sha3'; 8 | import { CCTP_TOKEN_DECIMALS } from './cctp'; 9 | import { checkHyperCoreDeposit } from './api'; 10 | const sha3_256 = sha3.sha3_256; 11 | 12 | export const isValidAptosType = (str: string): boolean => 13 | /^(0x)?[0-9a-fA-F]+::\w+::\w+$/.test(str); 14 | 15 | export function nativeAddressToHexString( 16 | address: string, wChainId: number) : string { 17 | if (wChainId === 1) { 18 | return zeroPadValue(new PublicKey(address).toBytes(), 32); 19 | } else if ( 20 | wChainId === chains.ethereum || wChainId === chains.bsc || wChainId === chains.polygon || 21 | wChainId === chains.avalanche || wChainId === chains.arbitrum || wChainId === chains.optimism || 22 | wChainId === chains.base || wChainId === chains.unichain || wChainId === chains.linea || 23 | wChainId === chains.sonic || wChainId === chains.hyperevm 24 | ) { 25 | return zeroPadValue(address, 32); 26 | } else if (wChainId === chains.aptos && isValidAptosType(address)) { 27 | return `0x${sha3_256(address)}` 28 | } else if (wChainId === chains.sui) { 29 | let addressStr = address.startsWith('0x') ? address.substring(2) : address; 30 | if (Buffer.from(addressStr, 'hex').length !== 32) { 31 | throw new Error('Invalid sui address: ' + address); 32 | } 33 | return zeroPadValue(address, 32); 34 | } else { 35 | console.log(`Unsupported chain id: ${wChainId}`, address); 36 | throw new Error('Unsupported token chain'); 37 | } 38 | } 39 | 40 | export function hexToUint8Array(input: string): Uint8Array { 41 | return new Uint8Array( 42 | Buffer.from( 43 | input.startsWith('0x') ? input.substring(2) : input, 44 | "hex" 45 | ) 46 | ); 47 | } 48 | 49 | export function getAssociatedTokenAddress( 50 | mint: PublicKey, 51 | owner: PublicKey, 52 | allowOwnerOffCurve = false, 53 | programId = new PublicKey(addresses.TOKEN_PROGRAM_ID), 54 | associatedTokenProgramId = new PublicKey(addresses.ASSOCIATED_TOKEN_PROGRAM_ID) 55 | ): PublicKey { 56 | if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer())) { 57 | throw new Error('TokenOwnerOffCurveError'); 58 | } 59 | 60 | const [address] = PublicKey.findProgramAddressSync( 61 | [owner.toBuffer(), programId.toBuffer(), mint.toBuffer()], 62 | associatedTokenProgramId 63 | ); 64 | 65 | return address; 66 | } 67 | 68 | function isValidNumericInput(value: any): boolean { 69 | return ( 70 | (typeof value === 'string' || typeof value === 'number') && 71 | value !== '' && 72 | value !== null && 73 | !isNaN(Number(value)) && 74 | Number.isFinite(Number(value)) 75 | ); 76 | } 77 | 78 | export function getAmountOfFractionalAmount( 79 | amount: string | number, decimals: string | number) : bigint { 80 | if (amount === null || amount === undefined) { 81 | throw new Error('getAmountOfFractionalAmount: Amount is null or undefined'); 82 | } 83 | if (typeof amount !== 'string' && typeof amount !== 'number') { 84 | throw new Error('getAmountOfFractionalAmount: Amount is not a string or number'); 85 | } 86 | if (typeof amount === 'string' && amount.length === 0) { 87 | throw new Error('getAmountOfFractionalAmount: Amount is empty'); 88 | } 89 | if (!Number.isFinite(Number(amount))) { 90 | throw new Error('getAmountOfFractionalAmount: Amount is not a number'); 91 | } 92 | if (!isValidNumericInput(decimals)) { 93 | throw new Error('getAmountOfFractionalAmount: decimals is not a number'); 94 | } 95 | const cutFactor = Math.min(8, Number(decimals)); 96 | const numStr = Number(amount).toFixed(cutFactor + 1); 97 | const reg = new RegExp(`^-?\\d+(?:\\.\\d{0,${cutFactor}})?`); 98 | const matchResult = numStr.match(reg); 99 | if (!matchResult) { 100 | throw new Error('getAmountOfFractionalAmount: fixedAmount is null'); 101 | } 102 | const fixedAmount = matchResult[0]; 103 | return parseUnits(fixedAmount, Number(decimals)) 104 | } 105 | 106 | export function getDisplayAmount( 107 | inputAmount: ethers.BigNumberish, decimals: string | ethers.BigNumberish) : number { 108 | return Number(formatUnits(inputAmount, decimals)) 109 | } 110 | 111 | const chains: { [index in ChainName]: number } = { 112 | solana: 1, 113 | ethereum: 2, 114 | bsc: 4, 115 | polygon: 5, 116 | avalanche: 6, 117 | arbitrum: 23, 118 | optimism: 24, 119 | base: 30, 120 | aptos: 22, 121 | sui: 21, 122 | unichain: 44, 123 | linea: 38, 124 | hypercore: 65000, 125 | sonic: 52, 126 | hyperevm: 47, 127 | }; 128 | 129 | export function getWormholeChainIdByName(chain: string) : number | null { 130 | return chains[chain]; 131 | } 132 | 133 | const evmChainIdMap: { [index: string]: number } = { 134 | [1]: 2, 135 | [56]: 4, 136 | [137]: 5, 137 | [43114]: 6, 138 | [42161]: 23, 139 | [10]: 24, 140 | [8453]: 30, 141 | [130]: 44, 142 | [59144]: 38, 143 | [146]: 52, 144 | [999]: 47, 145 | }; 146 | 147 | export function getEvmChainIdByName(chain: ChainName) { 148 | const wormholeChainId = chains[chain]; 149 | const evmIds = Object.keys(evmChainIdMap); 150 | for (const evmId of evmIds) { 151 | if (evmChainIdMap[evmId] === wormholeChainId) { 152 | return Number(evmId); 153 | } 154 | } 155 | throw new Error(`Unsupported chain: ${chain}`); 156 | } 157 | 158 | 159 | 160 | export function getWormholeChainIdById(chainId: number) : number | null { 161 | return evmChainIdMap[chainId]; 162 | } 163 | 164 | const sdkVersion = [11, 4, 0]; 165 | 166 | export function getSdkVersion(): string { 167 | return sdkVersion.join('_'); 168 | } 169 | 170 | export function checkSdkVersionSupport(minimumVersion: [number, number, number]): boolean { 171 | //major 172 | if (sdkVersion[0] < minimumVersion[0]) { 173 | return false; 174 | } 175 | if (sdkVersion[0] > minimumVersion[0]) { 176 | return true; 177 | } 178 | //minor 179 | if (sdkVersion[1] < minimumVersion[1]) { 180 | return false; 181 | } 182 | if (sdkVersion[1] > minimumVersion[1]) { 183 | return true; 184 | } 185 | //patch 186 | if (sdkVersion[2] >= minimumVersion[2]) { 187 | return true; 188 | } 189 | return false; 190 | } 191 | 192 | export function getGasDecimal(chain: ChainName): number { 193 | if (chain === 'solana') { 194 | return 9; 195 | } 196 | return 18; 197 | } 198 | 199 | export function getGasDecimalsInSolana(chain: ChainName): number { 200 | if (chain === 'solana') { 201 | return 9; 202 | } 203 | return 8; 204 | } 205 | 206 | const MAX_U64 = BigInt(2) ** BigInt(64) - BigInt(1); 207 | export function getSafeU64Blob(value: bigint): Buffer { 208 | if (value < BigInt(0) || value > MAX_U64) { 209 | throw new Error(`Invalid u64: ${value}`); 210 | } 211 | const buf = Buffer.alloc(8); 212 | buf.writeBigUInt64LE(value); 213 | return buf; 214 | } 215 | 216 | export const ZeroPermit: Erc20Permit = { 217 | value: BigInt(0), 218 | deadline: 0, 219 | v: 0, 220 | r: `0x${SystemProgram.programId.toBuffer().toString('hex')}`, 221 | s: `0x${SystemProgram.programId.toBuffer().toString('hex')}`, 222 | } 223 | 224 | export function wait(time: number): Promise { 225 | return new Promise((resolve) => { 226 | setTimeout(() => { 227 | resolve(); 228 | }, time); 229 | }); 230 | } 231 | 232 | export function getQuoteSuitableReferrerAddress( 233 | quote: Quote, 234 | referrerAddresses?: ReferrerAddresses, 235 | ): string | null { 236 | if (!quote || !referrerAddresses) { 237 | return null; 238 | } 239 | if (quote.type === 'WH') { 240 | return referrerAddresses?.solana || null; 241 | } 242 | if (quote.type === 'MCTP' || quote.type === 'SWIFT') { 243 | if (quote.toChain === 'solana') { 244 | return referrerAddresses?.solana || null; 245 | } 246 | if (quote.toChain === 'sui') { 247 | return referrerAddresses?.sui || null; 248 | } 249 | return referrerAddresses?.evm || null; 250 | } 251 | if (quote.type === 'FAST_MCTP') { 252 | if (quote.toChain !== 'solana' && quote.toChain !== 'sui') { 253 | return referrerAddresses?.evm || null; 254 | } 255 | } 256 | if (quote.type === 'MONO_CHAIN') { 257 | if (quote.fromChain === 'solana') { 258 | return referrerAddresses?.solana || null; 259 | } else if (quote.fromChain === 'sui') { 260 | return referrerAddresses?.sui || null; 261 | } 262 | return referrerAddresses?.evm || null; 263 | } 264 | return null; 265 | } 266 | 267 | export const MCTP_PAYLOAD_TYPE_DEFAULT = 1; 268 | export const MCTP_PAYLOAD_TYPE_CUSTOM_PAYLOAD = 2; 269 | export const MCTP_INIT_ORDER_PAYLOAD_ID = 1; 270 | export const FAST_MCTP_PAYLOAD_TYPE_DEFAULT = 1; 271 | export const FAST_MCTP_PAYLOAD_TYPE_CUSTOM_PAYLOAD = 2; 272 | export const FAST_MCTP_PAYLOAD_TYPE_ORDER = 3; 273 | 274 | export async function getPermitDomain(token: Token, provider: JsonRpcProvider): Promise { 275 | const contract = new ethers.Contract(token.contract, ERC20Artifact.abi, provider); 276 | let domainSeparator: string; 277 | let name: string; 278 | try { 279 | let [_domainSeparator, _name] = await Promise.all([contract.DOMAIN_SEPARATOR(), contract.name()]); 280 | domainSeparator = _domainSeparator; 281 | name = _name; 282 | } catch (err) { 283 | throw { 284 | mayanError: { 285 | permitIssue: true, 286 | }, 287 | }; 288 | } 289 | const domain: PermitDomain = { 290 | name: name, 291 | version: '1', 292 | chainId: token.chainId, 293 | verifyingContract: token.contract, 294 | }; 295 | for (let i = 1; i < 11; i++) { 296 | domain.version = String(i); 297 | const hash = TypedDataEncoder.hashDomain(domain); 298 | if (hash.toLowerCase() === domainSeparator.toLowerCase()) { 299 | return domain; 300 | } 301 | } 302 | throw { 303 | mayanError: { 304 | permitIssue: true, 305 | }, 306 | }; 307 | } 308 | 309 | export const PermitTypes = { 310 | Permit: [ 311 | { 312 | name: 'owner', 313 | type: 'address', 314 | }, 315 | { 316 | name: 'spender', 317 | type: 'address', 318 | }, 319 | { 320 | name: 'value', 321 | type: 'uint256', 322 | }, 323 | { 324 | name: 'nonce', 325 | type: 'uint256', 326 | }, 327 | { 328 | name: 'deadline', 329 | type: 'uint256', 330 | }, 331 | ], 332 | }; 333 | 334 | export async function getPermitParams( 335 | token: Token, 336 | walletAddress: string, 337 | spender: string, 338 | amount: bigint, 339 | provider: JsonRpcProvider, 340 | deadline: BigInt 341 | ): Promise<{ 342 | domain: PermitDomain; 343 | types: typeof PermitTypes; 344 | value: PermitValue; 345 | }> { 346 | if (token.standard !== 'erc20' && token.standard !== 'hypertoken') { 347 | throw new Error('Token is not ERC20'); 348 | } 349 | if (!token.supportsPermit) { 350 | throw new Error('Token does not support permit'); 351 | } 352 | const contract = new ethers.Contract(token.contract, ERC20Artifact.abi, provider); 353 | const [domain, nonce] = await Promise.all([getPermitDomain(token, provider), contract.nonces(walletAddress)]); 354 | return { 355 | domain, 356 | types: PermitTypes, 357 | value: { 358 | owner: walletAddress, 359 | spender: spender, 360 | nonce: String(nonce), 361 | value: String(amount), 362 | deadline: String(deadline), 363 | } 364 | } 365 | } 366 | 367 | export async function getHyperCoreUSDCDepositPermitParams( 368 | quote: Quote, 369 | userArbitrumAddress: string, 370 | arbProvider: JsonRpcProvider, 371 | ): Promise<{ 372 | domain: PermitDomain; 373 | types: typeof PermitTypes; 374 | value: PermitValue; 375 | }> { 376 | if (!quote.hyperCoreParams) { 377 | throw new Error('Quote does not have hyperCoreParams'); 378 | } 379 | if (quote.toChain !== 'hypercore') { 380 | throw new Error('Quote toChain is not hypercore'); 381 | } 382 | if (quote.toToken.contract.toLowerCase() !== addresses.ARBITRUM_USDC_CONTRACT.toLowerCase()) { 383 | throw new Error('Quote toToken is not USDC on Arbitrum'); 384 | } 385 | 386 | const USDC_ARB_TOKEN: Token = { 387 | name: "USDC", 388 | standard: "erc20", 389 | symbol: "USDC", 390 | mint: "CR4xnGrhsu1fWNPoX4KbTUUtqGMF3mzRLfj4S6YEs1Yo", 391 | verified: true, 392 | contract: "0xaf88d065e77c8cc2239327c5edb3a432268e5831", 393 | chainId: 42161, 394 | wChainId: 23, 395 | decimals: 6, 396 | logoURI: "http://assets.coingecko.com/coins/images/6319/small/usdc.png?1696506694", 397 | coingeckoId: "usd-coin", 398 | realOriginContractAddress: "0xaf88d065e77c8cc2239327c5edb3a432268e5831", 399 | realOriginChainId: 23, 400 | supportsPermit: true, 401 | verifiedAddress: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', 402 | } 403 | const [permitParams, isAllowed] = await Promise.all([ 404 | getPermitParams( 405 | USDC_ARB_TOKEN, 406 | userArbitrumAddress, 407 | addresses.HC_ARBITRUM_BRIDGE, 408 | BigInt(quote.hyperCoreParams.depositAmountUSDC64), 409 | arbProvider, 410 | BigInt(quote.deadline64) 411 | ), 412 | checkHyperCoreDeposit(userArbitrumAddress, quote.toToken.contract) 413 | ]); 414 | if (!isAllowed) { 415 | throw new Error('Because of concurrency, deposit is not possible at the moment, please try again later'); 416 | } 417 | return permitParams; 418 | } 419 | 420 | export function getHyperCoreUSDCDepositCustomPayload( 421 | quote: Quote, 422 | destinationAddress: string, 423 | usdcPermitSignature: string, 424 | ): Buffer { 425 | const payload = Buffer.alloc(109); 426 | const destAddressBuf = Buffer.from(hexToUint8Array(destinationAddress)); 427 | if (destAddressBuf.length !== 20) { 428 | throw new Error('Invalid destination address length, expected 20 bytes'); 429 | } 430 | const permitSignatureBuf = Buffer.from( 431 | hexToUint8Array(usdcPermitSignature) 432 | ); 433 | if (permitSignatureBuf.length !== 65) { 434 | throw new Error('Invalid USDC permit signature length, expected 65 bytes'); 435 | } 436 | payload.writeBigUInt64BE(BigInt(quote.redeemRelayerFee64), 0) 437 | payload.set(destAddressBuf, 8); 438 | payload.writeBigUInt64BE(BigInt(quote.hyperCoreParams.depositAmountUSDC64), 28); 439 | payload.writeBigUInt64BE(BigInt(quote.deadline64), 36); 440 | payload.set(permitSignatureBuf, 44); 441 | 442 | return payload 443 | } 444 | -------------------------------------------------------------------------------- /src/wormhole.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import addresses from './addresses'; 3 | import { Buffer } from 'buffer'; 4 | 5 | export function getWormholePDAs(supplierProgram: string): { 6 | bridgeConfig: PublicKey, 7 | sequenceKey: PublicKey, 8 | feeCollector: PublicKey, 9 | emitter: PublicKey, 10 | shimEventAuth: PublicKey, 11 | shimMessage: PublicKey, 12 | } { 13 | const wormholeProgramId = new PublicKey(addresses.WORMHOLE_PROGRAM_ID); 14 | const wormholeShimProgramId = new PublicKey(addresses.WORMHOLE_SHIM_POST_MESSAGE_PROGRAM_ID); 15 | const programId = new PublicKey(supplierProgram); 16 | const [ bridgeConfig ] = PublicKey.findProgramAddressSync( 17 | [Buffer.from('Bridge')], 18 | wormholeProgramId, 19 | ); 20 | const [emitter] = PublicKey.findProgramAddressSync( 21 | [Buffer.from('emitter')], 22 | programId, 23 | ); 24 | const [sequenceKey] = PublicKey.findProgramAddressSync( 25 | [Buffer.from('Sequence'), emitter.toBuffer()], 26 | wormholeProgramId, 27 | ); 28 | const [feeCollector] = PublicKey.findProgramAddressSync( 29 | [Buffer.from('fee_collector')], 30 | wormholeProgramId, 31 | ); 32 | const [shimMessage] = PublicKey.findProgramAddressSync( 33 | [emitter.toBuffer()], 34 | wormholeShimProgramId, 35 | ); 36 | const [shimEventAuth] = PublicKey.findProgramAddressSync( 37 | [Buffer.from('__event_authority')], 38 | wormholeShimProgramId, 39 | ); 40 | return { 41 | bridgeConfig, 42 | sequenceKey, 43 | feeCollector, 44 | emitter, 45 | shimEventAuth, 46 | shimMessage, 47 | }; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2021" 6 | ], 7 | "target": "es2021", 8 | // TODO: Add this back 9 | "strict": false, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "sourceMap": false, 14 | "moduleResolution": "node", 15 | "baseUrl": ".", 16 | "declaration": true, 17 | "outDir": "./lib" 18 | }, 19 | "include": [ 20 | "src" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "**/__tests__/*" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { Format, defineConfig } from 'tsup'; 2 | 3 | // const format = 4 | 5 | export default defineConfig((options) => { 6 | const format: Format[] = ['cjs', 'esm'] 7 | if (options.minify) format.push('iife') 8 | 9 | return { 10 | entry: ['src/index.ts'], 11 | splitting: false, 12 | sourcemap: false, 13 | clean: false, 14 | outDir: 'dist', 15 | dts: options.minify, 16 | format, 17 | outExtension: options.minify ? ({ format }) => { 18 | return { 19 | js: `.${format}.min.js`, 20 | }; 21 | } : undefined, 22 | ...options, 23 | }; 24 | }); 25 | --------------------------------------------------------------------------------