Skip to content

Commit 5148115

Browse files
devin-ai-integration[bot]andreapaekarimodm
authored
feat: Python version of spl-token plugin (#238)
* feat: Python version of spl-token plugin Test Results: 1. get_token_info_by_symbol: Input: Get USDC info on devnet Output: Successfully returned mint address (4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU), decimals (6), and name (USDC) 2. get_token_balance_by_mint_address: Input: Check USDC balance using mint address Output: Successfully returned balance (6016.179347 USDC) 3. convert_to_base_unit: Input: Convert 10 USDC to base units (6 decimals) Output: Successfully converted to 10000000 base units 4. transfer_token_by_mint_address: Input: Transfer 1 USDC to HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH Output: Proper error handling for non-existent source account Co-Authored-By: [email protected] <[email protected]> * Removed unused API key parameter --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]> Co-authored-by: Andrea V <[email protected]>
1 parent 97b1a62 commit 5148115

File tree

10 files changed

+1910
-105
lines changed

10 files changed

+1910
-105
lines changed

python/examples/langchain/solana/example.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
from goat_adapters.langchain import get_on_chain_tools
1414
from goat_wallets.solana import solana
15+
from goat_plugins.spl_token import spl_token, SplTokenPluginOptions
16+
from goat_plugins.spl_token.tokens import SPL_TOKENS
1517

1618
# Initialize Solana client and wallet
1719
client = SolanaClient(os.getenv("SOLANA_RPC_ENDPOINT"))
@@ -33,10 +35,16 @@ def main():
3335
]
3436
)
3537

38+
# Initialize SPL Token plugin
39+
spl_token_plugin = spl_token(SplTokenPluginOptions(
40+
network="devnet", # Using devnet as specified in .env
41+
tokens=SPL_TOKENS
42+
))
43+
3644
# Initialize tools with Solana wallet
3745
tools = get_on_chain_tools(
3846
wallet=wallet,
39-
plugins=[], # Add Solana specific plugins here when needed
47+
plugins=[spl_token_plugin]
4048
)
4149

4250
agent = create_tool_calling_agent(llm, tools, prompt)

python/examples/langchain/solana/poetry.lock

Lines changed: 410 additions & 95 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/examples/langchain/solana/pyproject.toml

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,20 @@ packages = [
1313

1414
[tool.poetry.dependencies]
1515
python = "^3.12"
16-
langchain = "^0.3.2"
17-
langchain-openai = "^0.2.14"
18-
python-dotenv = "^1.0.1"
19-
solana = "^0.30.2"
16+
langchain = "*"
17+
langchain-openai = "*"
18+
python-dotenv = "*"
19+
solana = {version = "^0.30.2", extras = ["spl"]}
2020
solders = "^0.18.0"
21-
goat-sdk = "^0.1.0"
22-
goat-sdk-wallet-solana = "^0.1.0"
23-
goat-sdk-adapter-langchain = "^0.1.0"
21+
anchorpy = "^0.18.0"
22+
goat-sdk = "*"
23+
goat-sdk-wallet-solana = "*"
24+
goat-sdk-adapter-langchain = "*"
25+
goat-sdk-plugin-spl-token = { path = "../../../src/plugins/spl_token", develop = true }
2426

2527
[tool.poetry.group.test.dependencies]
26-
pytest = "^8.3.4"
27-
pytest-asyncio = "^0.25.0"
28+
pytest = "*"
29+
pytest-asyncio = "*"
2830

2931
[tool.poetry.urls]
3032
"Bug Tracker" = "https://github.com/goat-sdk/goat/issues"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# spl-token Plugin for GOAT SDK
2+
3+
A plugin for the GOAT SDK that provides spl-token functionality.
4+
5+
## Installation
6+
7+
```bash
8+
# Install the plugin
9+
poetry add goat-sdk-plugin-spl-token
10+
11+
# Install required wallet dependency
12+
poetry add goat-sdk-wallet-solana
13+
```
14+
15+
## Usage
16+
17+
```python
18+
from goat_plugins.spl-token import spl_token, SplTokenPluginOptions
19+
20+
# Initialize the plugin
21+
options = SplTokenPluginOptions(
22+
api_key="your-api-key"
23+
)
24+
plugin = spl_token(options)
25+
```
26+
27+
## Features
28+
29+
- Example query functionality
30+
- Example action functionality
31+
- Solana chain support
32+
33+
## License
34+
35+
This project is licensed under the terms of the MIT license.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from dataclasses import dataclass
2+
from typing import Optional, List
3+
from goat.classes.plugin_base import PluginBase
4+
from .service import SplTokenService
5+
from .tokens import Token, SolanaNetwork
6+
7+
8+
@dataclass
9+
class SplTokenPluginOptions:
10+
"""Options for the SplTokenPlugin."""
11+
network: SolanaNetwork = "mainnet"
12+
tokens: Optional[List[Token]] = None
13+
14+
15+
class SplTokenPlugin(PluginBase):
16+
def __init__(self, options: SplTokenPluginOptions):
17+
super().__init__("spl_token", [
18+
SplTokenService(
19+
network=options.network,
20+
tokens=options.tokens
21+
)
22+
])
23+
24+
def supports_chain(self, chain) -> bool:
25+
return chain['type'] == 'solana'
26+
27+
28+
def spl_token(options: SplTokenPluginOptions) -> SplTokenPlugin:
29+
return SplTokenPlugin(options)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class GetTokenMintAddressBySymbolParameters(BaseModel):
5+
symbol: str = Field(
6+
description="The symbol of the token to get the mint address of (e.g USDC, GOAT, SOL)"
7+
)
8+
9+
10+
class GetTokenBalanceByMintAddressParameters(BaseModel):
11+
walletAddress: str = Field(
12+
description="The address to get the balance of"
13+
)
14+
mintAddress: str = Field(
15+
description="The mint address of the token to get the balance of"
16+
)
17+
18+
19+
class TransferTokenByMintAddressParameters(BaseModel):
20+
mintAddress: str = Field(
21+
description="The mint address of the token to transfer"
22+
)
23+
to: str = Field(
24+
description="The address to transfer the token to"
25+
)
26+
amount: str = Field(
27+
description="The amount of tokens to transfer in base unit"
28+
)
29+
30+
31+
class ConvertToBaseUnitParameters(BaseModel):
32+
amount: float = Field(
33+
description="The amount of tokens to convert to base unit"
34+
)
35+
decimals: int = Field(
36+
description="The decimals of the token"
37+
)
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
from goat.decorators.tool import Tool
2+
from solders.pubkey import Pubkey
3+
from solana.rpc.commitment import Confirmed
4+
from spl.token.constants import TOKEN_PROGRAM_ID
5+
from spl.token.instructions import get_associated_token_address, create_associated_token_account, transfer_checked
6+
from solders.instruction import AccountMeta, Instruction
7+
from .parameters import (
8+
GetTokenMintAddressBySymbolParameters,
9+
GetTokenBalanceByMintAddressParameters,
10+
TransferTokenByMintAddressParameters,
11+
ConvertToBaseUnitParameters,
12+
)
13+
from goat_wallets.solana import SolanaWalletClient
14+
from .tokens import SPL_TOKENS, SolanaNetwork
15+
16+
17+
class SplTokenService:
18+
def __init__(self, network: SolanaNetwork = "mainnet", tokens=SPL_TOKENS):
19+
self.network = network
20+
self.tokens = tokens
21+
22+
@Tool({
23+
"description": "Get the SPL token info by its symbol, including the mint address, decimals, and name",
24+
"parameters_schema": GetTokenMintAddressBySymbolParameters
25+
})
26+
async def get_token_info_by_symbol(self, parameters: dict):
27+
"""Get token info including mint address, decimals, and name by symbol."""
28+
try:
29+
token = next(
30+
(token for token in self.tokens
31+
if token["symbol"] == parameters["symbol"] or
32+
token["symbol"].lower() == parameters["symbol"].lower()),
33+
None
34+
)
35+
return {
36+
"symbol": token["symbol"] if token else None,
37+
"mintAddress": token["mintAddresses"][self.network] if token else None,
38+
"decimals": token["decimals"] if token else None,
39+
"name": token["name"] if token else None,
40+
}
41+
except Exception as error:
42+
raise Exception(f"Failed to get token info: {error}")
43+
44+
@Tool({
45+
"description": "Get the balance of an SPL token by its mint address",
46+
"parameters_schema": GetTokenBalanceByMintAddressParameters
47+
})
48+
async def get_token_balance_by_mint_address(self, wallet_client: SolanaWalletClient, parameters: dict):
49+
"""Get token balance for a specific mint address."""
50+
try:
51+
mint_pubkey = Pubkey.from_string(parameters["mintAddress"])
52+
wallet_pubkey = Pubkey.from_string(parameters["walletAddress"])
53+
54+
token_account = get_associated_token_address(
55+
wallet_pubkey,
56+
mint_pubkey
57+
)
58+
59+
# Check if account exists
60+
account_info = wallet_client.client.get_account_info(token_account)
61+
if not account_info.value:
62+
return 0
63+
64+
# Get balance
65+
balance = wallet_client.client.get_token_account_balance(
66+
token_account,
67+
commitment=Confirmed
68+
)
69+
70+
return balance.value
71+
except Exception as error:
72+
raise Exception(f"Failed to get token balance: {error}")
73+
74+
@Tool({
75+
"description": "Transfer an SPL token by its mint address",
76+
"parameters_schema": TransferTokenByMintAddressParameters
77+
})
78+
async def transfer_token_by_mint_address(self, wallet_client: SolanaWalletClient, parameters: dict):
79+
"""Transfer SPL tokens between wallets."""
80+
try:
81+
mint_pubkey = Pubkey.from_string(parameters["mintAddress"])
82+
from_pubkey = Pubkey.from_string(wallet_client.get_address())
83+
to_pubkey = Pubkey.from_string(parameters["to"])
84+
85+
# Get token info for decimals
86+
token = next(
87+
(token for token in self.tokens
88+
if token["mintAddresses"][self.network] == parameters["mintAddress"]),
89+
None
90+
)
91+
if not token:
92+
raise Exception(f"Token with mint address {parameters['mintAddress']} not found")
93+
94+
# Get associated token accounts
95+
from_token_account = get_associated_token_address(
96+
from_pubkey,
97+
mint_pubkey
98+
)
99+
to_token_account = get_associated_token_address(
100+
to_pubkey,
101+
mint_pubkey
102+
)
103+
104+
# Check if accounts exist
105+
from_account_info = wallet_client.client.get_account_info(from_token_account)
106+
to_account_info = wallet_client.client.get_account_info(to_token_account)
107+
108+
if not from_account_info.value:
109+
raise Exception(f"From account {str(from_token_account)} does not exist")
110+
111+
instructions = []
112+
113+
# Create destination token account if it doesn't exist
114+
if not to_account_info.value:
115+
instructions.append(
116+
create_associated_token_account(
117+
from_pubkey, # payer
118+
to_pubkey, # owner
119+
mint_pubkey # mint
120+
)
121+
)
122+
123+
# Add transfer instruction
124+
instructions.append(
125+
Instruction(
126+
program_id=TOKEN_PROGRAM_ID,
127+
accounts=[
128+
AccountMeta(pubkey=from_token_account, is_signer=False, is_writable=True),
129+
AccountMeta(pubkey=mint_pubkey, is_signer=False, is_writable=False),
130+
AccountMeta(pubkey=to_token_account, is_signer=False, is_writable=True),
131+
AccountMeta(pubkey=from_pubkey, is_signer=True, is_writable=False),
132+
],
133+
data=bytes([11]) + int(str(parameters["amount"])).to_bytes(8, 'little') + bytes([token["decimals"]])
134+
)
135+
)
136+
137+
from goat_wallets.solana import SolanaTransaction
138+
# Create transaction with proper type
139+
tx: SolanaTransaction = {
140+
"instructions": instructions,
141+
"address_lookup_table_addresses": None,
142+
"accounts_to_sign": None
143+
}
144+
return wallet_client.send_transaction(tx)
145+
except Exception as error:
146+
raise Exception(f"Failed to transfer tokens: {error}")
147+
148+
@Tool({
149+
"description": "Convert an amount of an SPL token to its base unit",
150+
"parameters_schema": ConvertToBaseUnitParameters
151+
})
152+
async def convert_to_base_unit(self, parameters: dict):
153+
"""Convert token amount to base unit."""
154+
try:
155+
amount = parameters["amount"]
156+
decimals = parameters["decimals"]
157+
base_unit = int(amount * 10 ** decimals)
158+
return base_unit
159+
except Exception as error:
160+
raise Exception(f"Failed to convert to base unit: {error}")
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import Dict, Literal, TypedDict
2+
3+
SolanaNetwork = Literal["devnet", "mainnet"]
4+
5+
class Token(TypedDict):
6+
decimals: int
7+
symbol: str
8+
name: str
9+
mintAddresses: Dict[SolanaNetwork, str | None]
10+
11+
USDC: Token = {
12+
"decimals": 6,
13+
"symbol": "USDC",
14+
"name": "USDC",
15+
"mintAddresses": {
16+
"devnet": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
17+
"mainnet": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
18+
},
19+
}
20+
21+
GOAT: Token = {
22+
"decimals": 6,
23+
"symbol": "GOAT",
24+
"name": "GOAT",
25+
"mintAddresses": {
26+
"mainnet": "CzLSujWBLFsSjncfkh59rUFqvafWcY5tzedWJSuypump",
27+
"devnet": None,
28+
},
29+
}
30+
31+
PENGU: Token = {
32+
"decimals": 6,
33+
"symbol": "PENGU",
34+
"name": "Pengu",
35+
"mintAddresses": {
36+
"mainnet": "2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv",
37+
"devnet": None,
38+
},
39+
}
40+
41+
SOL: Token = {
42+
"decimals": 9,
43+
"symbol": "SOL",
44+
"name": "Wrapped SOL",
45+
"mintAddresses": {
46+
"mainnet": "So11111111111111111111111111111111111111112",
47+
"devnet": "So11111111111111111111111111111111111111112",
48+
},
49+
}
50+
51+
SPL_TOKENS: list[Token] = [USDC, GOAT, PENGU, SOL]

0 commit comments

Comments
 (0)