Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: integrate Mayan SDK #322

Merged
merged 12 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ If you would like to see your wallet provider supported, please open an issue or
| Jupiter | Swap tokens on Jupiter | [@goat-sdk/plugin-jupiter](https://www.npmjs.com/package/@goat-sdk/plugin-jupiter) |
| KIM | Swap tokens on KIM | [@goat-sdk/plugin-kim](https://www.npmjs.com/package/@goat-sdk/plugin-kim) |
| Lulo | Deposit USDC on Lulo | [@goat-sdk/plugin-lulo](https://www.npmjs.com/package/@goat-sdk/plugin-lulo) |
| Mayan | Cross-chain token swap using Mayan SDK (Solana, EVM, SUI) | [@goat-sdk/plugin-mayan](https://www.npmjs.com/package/@goat-sdk/plugin-mayan) |
| Meteora | Create liquidity pools on Meteora | [@goat-sdk/plugin-meteora](https://www.npmjs.com/package/@goat-sdk/plugin-meteora) |
| Mode Governance | Create a governance proposal on Mode | [@goat-sdk/plugin-mode-governance](https://www.npmjs.com/package/@goat-sdk/plugin-mode-governance) |
| Mode Voting | Vote on a governance proposal on Mode | [@goat-sdk/plugin-mode-voting](https://www.npmjs.com/package/@goat-sdk/plugin-mode-voting) |
Expand Down
24 changes: 24 additions & 0 deletions typescript/packages/plugins/mayan/README.md
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
39 changes: 39 additions & 0 deletions typescript/packages/plugins/mayan/package.json
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:"
}
}
2 changes: 2 additions & 0 deletions typescript/packages/plugins/mayan/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./mayan.plugin";
export * from "./parameters";
14 changes: 14 additions & 0 deletions typescript/packages/plugins/mayan/src/mayan.plugin.ts
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();
}
298 changes: 298 additions & 0 deletions typescript/packages/plugins/mayan/src/mayan.service.ts
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;
}
Loading
Loading