Skip to content

Commit

Permalink
plugin: spl token (#32)
Browse files Browse the repository at this point in the history
* plugin: spl token

* lint

* get_solana_token_balance_by_mint_address

* better
  • Loading branch information
mPaella authored Dec 9, 2024
1 parent a27bcdb commit 12bd01f
Show file tree
Hide file tree
Showing 16 changed files with 445 additions and 5 deletions.
4 changes: 1 addition & 3 deletions typescript/packages/plugins/solana-nfts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@
"@metaplex-foundation/umi-bundle-defaults": "0.9.2",
"@metaplex-foundation/umi-web3js-adapters": "0.8.10",
"@solana/web3.js": "catalog:",
"viem": "catalog:",
"zod": "catalog:"
},
"peerDependencies": {
"@goat-sdk/core": "workspace:*",
"viem": "catalog:"
"@goat-sdk/core": "workspace:*"
},
"homepage": "https://ohmygoat.dev",
"repository": {
Expand Down
33 changes: 33 additions & 0 deletions typescript/packages/plugins/spl-token/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@goat-sdk/plugin-spl-token",
"version": "0.0.1",
"files": ["dist/**/*", "README.md", "package.json"],
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "vitest run --passWithNoTests"
},
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"dependencies": {
"@goat-sdk/core": "workspace:*",
"@solana/web3.js": "catalog:",
"@solana/spl-token": "0.4.9",
"zod": "catalog:"
},
"peerDependencies": {
"@goat-sdk/core": "workspace:*"
},
"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"]
}
2 changes: 2 additions & 0 deletions typescript/packages/plugins/spl-token/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./plugin";
export * from "./tokens";
8 changes: 8 additions & 0 deletions typescript/packages/plugins/spl-token/src/methods/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { type Connection, PublicKey } from "@solana/web3.js";

export async function balanceOf(connection: Connection, walletAddress: string, tokenAddress: string) {
const tokenAccount = getAssociatedTokenAddressSync(new PublicKey(tokenAddress), new PublicKey(walletAddress));
const balance = await connection.getTokenAccountBalance(tokenAccount);
return balance;
}
58 changes: 58 additions & 0 deletions typescript/packages/plugins/spl-token/src/methods/transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { SolanaWalletClient } from "@goat-sdk/core";
import {
createAssociatedTokenAccountInstruction,
createTransferCheckedInstruction,
getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import { type Connection, PublicKey, type TransactionInstruction } from "@solana/web3.js";
import type { SolanaNetwork } from "../tokens";
import { doesAccountExist } from "../utils/doesAccountExist";
import { getTokenByMintAddress } from "../utils/getTokenByMintAddress";

export async function transfer(
connection: Connection,
network: SolanaNetwork,
walletClient: SolanaWalletClient,
to: string,
tokenMintAddress: string,
amount: string,
) {
const token = getTokenByMintAddress(tokenMintAddress, network);
if (!token) {
throw new Error(`Token with mint address ${tokenMintAddress} not found`);
}

const tokenMintPublicKey = new PublicKey(tokenMintAddress);
const fromPublicKey = new PublicKey(walletClient.getAddress());
const toPublicKey = new PublicKey(to);

const fromTokenAccount = getAssociatedTokenAddressSync(tokenMintPublicKey, fromPublicKey);
const toTokenAccount = getAssociatedTokenAddressSync(tokenMintPublicKey, toPublicKey);

const fromAccountExists = await doesAccountExist(connection, fromTokenAccount);
const toAccountExists = await doesAccountExist(connection, toTokenAccount);

if (!fromAccountExists) {
throw new Error(`From account ${fromTokenAccount.toBase58()} does not exist`);
}

const instructions: TransactionInstruction[] = [];

if (!toAccountExists) {
instructions.push(
createAssociatedTokenAccountInstruction(fromPublicKey, toTokenAccount, toPublicKey, tokenMintPublicKey),
);
}
instructions.push(
createTransferCheckedInstruction(
fromTokenAccount,
tokenMintPublicKey,
toTokenAccount,
fromPublicKey,
BigInt(amount) * BigInt(10) ** BigInt(token.decimals),
token.decimals,
),
);

return await walletClient.sendTransaction({ instructions });
}
17 changes: 17 additions & 0 deletions typescript/packages/plugins/spl-token/src/parameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from "zod";
import { splTokenSymbolSchema } from "./tokens";

export const getTokenMintAddressBySymbolParametersSchema = z.object({
symbol: splTokenSymbolSchema.describe("The symbol of the token to get the mint address of"),
});

export const getTokenBalanceByMintAddressParametersSchema = z.object({
walletAddress: z.string().describe("The address to get the balance of"),
mintAddress: z.string().describe("The mint address of the token to get the balance of"),
});

export const transferTokenByMintAddressParametersSchema = z.object({
mintAddress: z.string().describe("The mint address of the token to transfer"),
to: z.string().describe("The address to transfer the token to"),
amount: z.string().describe("The amount of tokens to transfer"),
});
17 changes: 17 additions & 0 deletions typescript/packages/plugins/spl-token/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Plugin, SolanaWalletClient } from "@goat-sdk/core";
import type { Connection } from "@solana/web3.js";
import type { SolanaNetwork, Token } from "./tokens";
import { getTokensForNetwork } from "./utils/getTokensForNetwork";
import { getTools } from "./utils/getTools";

export function splToken({
connection,
network,
}: { connection: Connection; network: SolanaNetwork }): Plugin<SolanaWalletClient> {
return {
name: "splToken",
supportsSmartWallets: () => false,
supportsChain: (chain) => chain.type === "solana",
getTools: async () => getTools(connection, network),
};
}
30 changes: 30 additions & 0 deletions typescript/packages/plugins/spl-token/src/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { z } from "zod";

export type SolanaNetwork = "devnet" | "mainnet";

export const splTokenSymbolSchema = z.enum(["USDC"]);
export type SplTokenSymbol = z.infer<typeof splTokenSymbolSchema>;

export type Token = {
decimals: number;
symbol: SplTokenSymbol;
name: string;
mintAddresses: Record<SolanaNetwork, string | null>;
};

export type NetworkSpecificToken = Omit<Token, "mintAddresses"> & {
network: SolanaNetwork;
mintAddress: string;
};

export const USDC: Token = {
decimals: 6,
symbol: splTokenSymbolSchema.Enum.USDC,
name: "USDC",
mintAddresses: {
devnet: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
mainnet: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
},
};

export const SPL_TOKENS: Token[] = [USDC];
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Connection, PublicKey } from "@solana/web3.js";

export async function doesAccountExist(connection: Connection, address: PublicKey) {
const account = await connection.getAccountInfo(address);
return account != null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SPL_TOKENS, type SolanaNetwork } from "../tokens";
import { getTokensForNetwork } from "./getTokensForNetwork";

export function getTokenByMintAddress(mintAddress: string, network: SolanaNetwork) {
const tokensForNetwork = getTokensForNetwork(network, SPL_TOKENS);
const token = tokensForNetwork.find((token) => token.mintAddress === mintAddress);
return token || null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SPL_TOKENS, type SolanaNetwork, type SplTokenSymbol } from "../tokens";
import { getTokensForNetwork } from "./getTokensForNetwork";

export function getTokenMintAddressBySymbol(symbol: SplTokenSymbol, network: SolanaNetwork) {
const tokensForNetwork = getTokensForNetwork(network, SPL_TOKENS);
const token = tokensForNetwork.find((token) => [token.symbol, token.symbol.toLowerCase()].includes(symbol));
return token?.mintAddress || null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { NetworkSpecificToken, SolanaNetwork, Token } from "../tokens";

export function getTokensForNetwork(network: SolanaNetwork, tokens: Token[]) {
const result: NetworkSpecificToken[] = [];

for (const token of tokens) {
const mintAddress = token.mintAddresses[network];
if (mintAddress) {
result.push({
decimals: token.decimals,
symbol: token.symbol,
name: token.name,
network,
mintAddress,
});
}
}

return result;
}
50 changes: 50 additions & 0 deletions typescript/packages/plugins/spl-token/src/utils/getTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { DeferredTool, SolanaWalletClient } from "@goat-sdk/core";
import type { Connection } from "@solana/web3.js";
import type { z } from "zod";
import { balanceOf } from "../methods/balance";
import { transfer } from "../methods/transfer";
import {
getTokenBalanceByMintAddressParametersSchema,
getTokenMintAddressBySymbolParametersSchema,
transferTokenByMintAddressParametersSchema,
} from "../parameters";
import type { SolanaNetwork } from "../tokens";
import { getTokenMintAddressBySymbol } from "./getTokenMintAddressBySymbol";

export function getTools(connection: Connection, network: SolanaNetwork): DeferredTool<SolanaWalletClient>[] {
const tools: DeferredTool<SolanaWalletClient>[] = [];

tools.push({
name: "get_token_mint_address_by_symbol",
description: "This {{tool}} gets the mint address of an SPL token by its symbol",
parameters: getTokenMintAddressBySymbolParametersSchema,
method: async (
walletClient: SolanaWalletClient,
parameters: z.infer<typeof getTokenMintAddressBySymbolParametersSchema>,
) => getTokenMintAddressBySymbol(parameters.symbol, network),
});

tools.push({
name: "get_token_balance_by_mint_address",
description:
"This {{tool}} gets the balance of an SPL token by its mint address. Use get_token_mint_address_by_symbol to get the mint address first.",
parameters: getTokenBalanceByMintAddressParametersSchema,
method: async (
walletClient: SolanaWalletClient,
parameters: z.infer<typeof getTokenBalanceByMintAddressParametersSchema>,
) => balanceOf(connection, parameters.walletAddress, parameters.mintAddress),
});

tools.push({
name: "transfer_token_by_mint_address",
description:
"This {{tool}} transfers an SPL token by its mint address. Use get_token_mint_address_by_symbol to get the mint address first.",
parameters: transferTokenByMintAddressParametersSchema,
method: async (
walletClient: SolanaWalletClient,
parameters: z.infer<typeof transferTokenByMintAddressParametersSchema>,
) => transfer(connection, network, walletClient, parameters.to, parameters.mintAddress, parameters.amount),
});

return tools;
}
6 changes: 6 additions & 0 deletions typescript/packages/plugins/spl-token/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
6 changes: 6 additions & 0 deletions typescript/packages/plugins/spl-token/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from "tsup";
import { treeShakableConfig } from "../../../tsup.config.base";

export default defineConfig({
...treeShakableConfig,
});
Loading

0 comments on commit 12bd01f

Please sign in to comment.