-
Notifications
You must be signed in to change notification settings - Fork 152
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: integrate Mayan SDK * docs: add readme * fix: use workspace in package.json * build: update the lock file * fix: use solana/web3 catalog version * fix: lint and build errors * fix: no need to wait on callERC20 result * style: lint fix * Minor readme fixes * Add changeset --------- Co-authored-by: Agustin Armellini Fischer <[email protected]>
- Loading branch information
1 parent
274fe57
commit f68960d
Showing
12 changed files
with
621 additions
and
351 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@goat-sdk/plugin-mayan": patch | ||
--- | ||
|
||
Release package |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Mayan GOAT Plugin | ||
|
||
Cross-chain token swap using Mayan SDK (Solana, EVM, SUI) | ||
|
||
## Installation | ||
```bash | ||
npm install @goat-sdk/plugin-mayan | ||
``` | ||
|
||
## Usage | ||
```typescript | ||
import { mayan } from '@goat-sdk/plugin-mayan'; | ||
|
||
const tools = await getOnChainTools({ | ||
wallet: // ... | ||
plugins: [ | ||
mayan() | ||
] | ||
}); | ||
``` | ||
|
||
## Tools | ||
- Swap from Solana to Solana, EVM, SUI | ||
- Swap from EVM to EVM, Solana, SUI |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
{ | ||
"name": "@goat-sdk/plugin-mayan", | ||
"version": "0.1.0", | ||
"files": ["dist/**/*", "README.md", "package.json"], | ||
"scripts": { | ||
"build": "tsup", | ||
"clean": "rm -rf dist", | ||
"test": "vitest run --passWithNoTests" | ||
}, | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.mjs", | ||
"types": "./dist/index.d.ts", | ||
"sideEffects": false, | ||
"homepage": "https://ohmygoat.dev", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/goat-sdk/goat.git" | ||
}, | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/goat-sdk/goat/issues" | ||
}, | ||
"keywords": ["ai", "agents", "web3"], | ||
"dependencies": { | ||
"@goat-sdk/core": "workspace:*", | ||
"@goat-sdk/wallet-evm": "workspace:*", | ||
"@goat-sdk/wallet-solana": "workspace:*", | ||
"@goat-sdk/wallet-sui": "workspace:*", | ||
"@goat-sdk/wallet-viem": "workspace:*", | ||
"@mayanfinance/swap-sdk": "10.2.0", | ||
"@mysten/sui": "^1.18.0", | ||
"@solana/web3.js": "catalog:", | ||
"abitype": "1.0.8", | ||
"bs58": "^6.0.0", | ||
"ethers": "6.13.5", | ||
"viem": "catalog:", | ||
"zod": "catalog:" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./mayan.plugin"; | ||
export * from "./parameters"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Chain, PluginBase } from "@goat-sdk/core"; | ||
import { MayanService } from "./mayan.service"; | ||
|
||
export class MayanPlugin extends PluginBase { | ||
constructor() { | ||
super("mayan", [new MayanService()]); | ||
} | ||
|
||
supportsChain = (chain: Chain) => ["sui", "evm", "solana"].includes(chain.type); | ||
} | ||
|
||
export function mayan() { | ||
return new MayanPlugin(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,298 @@ | ||
import { Tool } from "@goat-sdk/core"; | ||
import { EVMWalletClient } from "@goat-sdk/wallet-evm"; | ||
import { SolanaWalletClient } from "@goat-sdk/wallet-solana"; | ||
import { | ||
ChainName, | ||
Erc20Permit, | ||
Quote, | ||
addresses, | ||
createSwapFromSolanaInstructions, | ||
fetchQuote, | ||
fetchTokenList, | ||
getSwapFromEvmTxPayload, | ||
} from "@mayanfinance/swap-sdk"; | ||
import { TypedDataDomain } from "abitype"; | ||
import { Signature, TypedDataEncoder } from "ethers"; | ||
import { parseAbi } from "viem"; | ||
import { EVMSwapParameters, SwapParameters } from "./parameters"; | ||
|
||
const ERC20_ABI = parseAbi([ | ||
"function allowance(address owner, address spender) external view returns (uint256)", | ||
"function approve(address spender, uint256 amount) external returns (bool)", | ||
"function nonces(address owner) external returns (uint256)", | ||
"function name() external returns (string)", | ||
"function DOMAIN_SEPARATOR() external returns (bytes32)", | ||
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)", | ||
]); | ||
|
||
export class MayanService { | ||
@Tool({ | ||
name: "mayan_swap_from_solana", | ||
description: "Swap from solana to solana, EVM, sui chain", | ||
}) | ||
async swapSolanaTool(walletClient: SolanaWalletClient, params: SwapParameters): Promise<string> { | ||
if (params.fromToken.length < 32) { | ||
params.fromToken = await this.findTokenContract(params.fromToken, "solana"); | ||
} | ||
if (params.toToken.length < 32) { | ||
params.toToken = await this.findTokenContract(params.toToken, params.toChain); | ||
} | ||
|
||
const quotes = await fetchQuote({ | ||
amount: +params.amount, | ||
fromChain: "solana", | ||
toChain: params.toChain as ChainName, | ||
fromToken: params.fromToken, | ||
toToken: params.toToken, | ||
slippageBps: params.slippageBps ?? "auto", | ||
}); | ||
if (quotes.length === 0) { | ||
throw new Error("There is no quote available for the tokens you requested."); | ||
} | ||
|
||
const { instructions, signers, lookupTables } = await createSwapFromSolanaInstructions( | ||
quotes[0], | ||
walletClient.getAddress(), | ||
params.dstAddr, | ||
null, | ||
walletClient.getConnection(), | ||
); | ||
let hash: string; | ||
try { | ||
hash = ( | ||
await walletClient.sendTransaction({ | ||
instructions, | ||
addressLookupTableAddresses: lookupTables.map((a) => a.key.toString()), | ||
accountsToSign: signers, | ||
}) | ||
).hash; | ||
} catch (error) { | ||
if (!hasSignatureProperty(error) || !error.signature) { | ||
throw error; | ||
} | ||
|
||
await new Promise((f) => setTimeout(f, 3000)); | ||
hash = error.signature; | ||
const res = await fetch(`https://explorer-api.mayan.finance/v3/swap/trx/${hash}`); | ||
if (res.status !== 200) { | ||
throw error; | ||
} | ||
} | ||
|
||
return `https://explorer.mayan.finance/swap/${hash}`; | ||
} | ||
|
||
@Tool({ | ||
name: "mayan_swap_from_evm", | ||
description: "Swap from EVM to solana, EVM, sui chain", | ||
}) | ||
async swapEVMTool(walletClient: EVMWalletClient, params: EVMSwapParameters): Promise<string> { | ||
if (params.fromToken.length < 32) { | ||
params.fromToken = await this.findTokenContract(params.fromToken, params.fromChain); | ||
} | ||
if (params.toToken.length < 32) { | ||
params.toToken = await this.findTokenContract(params.toToken, params.toChain); | ||
} | ||
|
||
const quotes = await fetchQuote({ | ||
amount: +params.amount, | ||
fromChain: params.fromChain as ChainName, | ||
toChain: params.toChain as ChainName, | ||
fromToken: params.fromToken, | ||
toToken: params.toToken, | ||
slippageBps: params.slippageBps ?? "auto", | ||
}); | ||
if (quotes.length === 0) { | ||
throw new Error("There is no quote available for the tokens you requested."); | ||
} | ||
|
||
const amountIn = BigInt(quotes[0].effectiveAmountIn64); | ||
const allowance: bigint = (await this.callERC20(walletClient, params.fromToken, "allowance", [ | ||
walletClient.getAddress(), | ||
addresses.MAYAN_FORWARDER_CONTRACT, | ||
])) as bigint; | ||
if (allowance < amountIn) { | ||
// Approve the spender to spend the tokens | ||
const approveTx = (await this.callERC20(walletClient, params.fromToken, "approve", [ | ||
addresses.MAYAN_FORWARDER_CONTRACT, | ||
amountIn, | ||
])) as boolean; | ||
if (!approveTx) { | ||
throw new Error("couldn't get approve for spending allowance"); | ||
} | ||
} | ||
|
||
let permit: Erc20Permit | undefined; | ||
if (quotes[0].fromToken.supportsPermit) { | ||
permit = await this.getERC20Permit(walletClient, quotes[0], amountIn); | ||
} | ||
|
||
const transactionReq = getSwapFromEvmTxPayload( | ||
quotes[0], | ||
walletClient.getAddress(), | ||
params.dstAddr, | ||
null, | ||
walletClient.getAddress(), | ||
walletClient.getChain().id, | ||
null, | ||
permit, | ||
); | ||
const { hash } = await walletClient.sendTransaction({ | ||
to: transactionReq.to as string, | ||
value: transactionReq.value ? BigInt(transactionReq.value) : undefined, | ||
data: transactionReq.data ? (transactionReq.data as `0x${string}`) : undefined, | ||
}); | ||
|
||
return `https://explorer.mayan.finance/swap/${hash}`; | ||
} | ||
|
||
//SuiKeyPairWalletClien isn't exported from @goat-sdk/wallet-sui | ||
//so I couldn't test this. | ||
//@Tool({ | ||
// name: "mayan_swap_from_sui", | ||
// description: "Swap from sui to solana, EVM, sui chain", | ||
//}) | ||
//async swapSUITool( | ||
// walletClient: SuiWalletClient, | ||
// params: SwapParameters | ||
//): Promise<string> { | ||
// if (params.fromToken.length < 32) { | ||
// params.fromToken = await this.findTokenContract( | ||
// params.fromToken, | ||
// "sui" | ||
// ); | ||
// } | ||
// if (params.toToken.length < 32) { | ||
// params.toToken = await this.findTokenContract( | ||
// params.toToken, | ||
// params.toChain | ||
// ); | ||
// } | ||
// | ||
// const quotes = await fetchQuote({ | ||
// amount: +params.amount, | ||
// fromChain: "sui", | ||
// toChain: params.toChain as ChainName, | ||
// fromToken: params.fromToken, | ||
// toToken: params.toToken, | ||
// slippageBps: params.slippageBps ?? "auto", | ||
// }); | ||
// if (quotes.length === 0) { | ||
// throw new Error( | ||
// "There is no quote available for the tokens you requested." | ||
// ); | ||
// } | ||
// | ||
// const transaction = await createSwapFromSuiMoveCalls( | ||
// quotes[0], | ||
// walletClient.getAddress(), | ||
// params.dstAddr, | ||
// null, | ||
// null, | ||
// walletClient.getClient() | ||
// ); | ||
// const { hash } = await walletClient.sendTransaction({ transaction }); | ||
// | ||
// return `https://explorer.mayan.finance/swap/${hash}`; | ||
//} | ||
|
||
private async findTokenContract(symbol: string, chain: string): Promise<string> { | ||
const tokens = await fetchTokenList(chain as ChainName, true); | ||
const token = tokens.find((t) => t.symbol.toLowerCase() === symbol.toLowerCase()); | ||
if (!token) { | ||
throw new Error(`Couldn't find token with ${symbol} symbol`); | ||
} | ||
|
||
return token.contract; | ||
} | ||
|
||
private async callERC20( | ||
walletClient: EVMWalletClient, | ||
contract: string, | ||
functionName: string, | ||
args?: unknown[], | ||
): Promise<unknown> { | ||
const ret = await walletClient.read({ | ||
address: contract, | ||
abi: ERC20_ABI, | ||
functionName, | ||
args, | ||
}); | ||
return ret.value; | ||
} | ||
|
||
private async getERC20Permit(walletClient: EVMWalletClient, quote: Quote, amountIn: bigint): Promise<Erc20Permit> { | ||
const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now | ||
const spender = addresses.MAYAN_FORWARDER_CONTRACT; | ||
const walletSrcAddr = walletClient.getAddress(); | ||
const nonce = (await this.callERC20(walletClient, quote.fromToken.contract, "nonces", [ | ||
walletSrcAddr, | ||
])) as bigint; | ||
const name = (await this.callERC20(walletClient, quote.fromToken.contract, "name")) as string; | ||
|
||
const domain: TypedDataDomain = { | ||
name, | ||
version: "1", | ||
chainId: quote.fromToken.chainId, | ||
verifyingContract: quote.fromToken.contract as `0x${string}`, | ||
}; | ||
const domainSeparator = (await this.callERC20( | ||
walletClient, | ||
quote.fromToken.contract, | ||
"DOMAIN_SEPARATOR", | ||
)) as string; | ||
for (let i = 1; i < 11; i++) { | ||
domain.version = String(i); | ||
const hash = TypedDataEncoder.hashDomain(domain); | ||
if (hash.toLowerCase() === domainSeparator.toLowerCase()) { | ||
break; | ||
} | ||
} | ||
|
||
const types = { | ||
Permit: [ | ||
{ name: "owner", type: "address" }, | ||
{ name: "spender", type: "address" }, | ||
{ name: "value", type: "uint256" }, | ||
{ name: "nonce", type: "uint256" }, | ||
{ name: "deadline", type: "uint256" }, | ||
], | ||
}; | ||
const value = { | ||
owner: walletSrcAddr, | ||
spender, | ||
value: amountIn, | ||
nonce, | ||
deadline, | ||
}; | ||
|
||
const { signature } = await walletClient.signTypedData({ | ||
domain, | ||
types, | ||
primaryType: "Permit", | ||
message: value, | ||
}); | ||
const { v, r, s } = Signature.from(signature); | ||
await this.callERC20(walletClient, quote.fromToken.contract, "permit", [ | ||
walletSrcAddr, | ||
spender, | ||
amountIn, | ||
deadline, | ||
v, | ||
r, | ||
s, | ||
]); | ||
|
||
return { | ||
value: amountIn, | ||
deadline, | ||
v, | ||
r, | ||
s, | ||
}; | ||
} | ||
} | ||
|
||
function hasSignatureProperty(error: unknown): error is { signature?: string } { | ||
return typeof error === "object" && error !== null && "signature" in error; | ||
} |
Oops, something went wrong.