Repository: goat-sdk/goat
Pull request #461: Solana Smart Wallets: fix b64 tx
Author: login: alberto-crossmint
Created: 2025-04-02T16:41:49Z | Merged: 2025-04-02T19:54:27Z

## What does this PR do?

We're passing a base64 tx where the send_raw_transaction expects base58. This fixes that and adds a mechanism to prevent these situations in the future by, if the b58 decoding fails, it tries to decode it b64.



Diff:
diff --git a/python/examples/by-wallet/crossmint/solana_smart_wallet_swap_example.py b/python/examples/by-wallet/crossmint/solana_smart_wallet_swap_example.py
new file mode 100644
index 000000000..5ba5ac325
--- /dev/null
+++ b/python/examples/by-wallet/crossmint/solana_smart_wallet_swap_example.py
@@ -0,0 +1,112 @@
+"""
+This example shows how to create a Solana Smart Wallet and swap USDC for wSOL using the Jupiter DEX.
+
+To run this example, you need to set the following environment variables:
+- CROSSMINT_API_KEY
+- SOLANA_RPC_ENDPOINT
+- CROSSMINT_BASE_URL
+
+"""
+
+import os
+import asyncio
+from goat_wallets.crossmint.solana_smart_wallet import SolanaSmartWalletClient
+from goat_wallets.crossmint.parameters import CoreSignerType
+from goat_wallets.crossmint.solana_smart_wallet_factory import SolanaSmartWalletFactory
+from goat_wallets.crossmint.types import SolanaKeypairSigner
+from goat_wallets.crossmint.solana_smart_wallet import SolanaSmartWalletConfig
+from solana.rpc.api import Client as SolanaClient
+from goat_wallets.crossmint.api_client import CrossmintWalletsAPI
+from dotenv import load_dotenv
+from goat_plugins.jupiter.service import JupiterService
+import json
+
+load_dotenv()
+
+
+def create_wallet(factory: SolanaSmartWalletFactory) -> SolanaSmartWalletClient:
+    print("\n🔑 Creating Solana Smart Wallet with fireblocks custodial admin signer...")
+    wallet = factory.get_or_create({
+        "config": SolanaSmartWalletConfig(
+            adminSigner=SolanaKeypairSigner(
+                type=CoreSignerType.SOLANA_FIREBLOCKS_CUSTODIAL,
+            )
+        ),
+        "linkedUser": "email:test+1@crossmint.com"
+    })
+    print(f"✅ Wallet created successfully!")
+    print(f"📝 Wallet Address: {wallet.get_address()}")
+    print(f"👤 Admin Signer: {wallet.get_admin_signer_address()}")
+    return wallet
+
+
+async def send_swap_transaction(wallet: SolanaSmartWalletClient):
+    usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
+    wsol_mint = "So11111111111111111111111111111111111111112"
+    amount = 1e-3
+
+    print("\n💸 Preparing transaction...")
+    print(f"📝 Transaction Details:")
+    print(f"   Input Mint: {usdc_mint}")
+    print(f"   Output Mint: {wsol_mint}")
+    print(f"   Amount: {amount} USDC")
+
+    print("\n📤 Sending swap transaction to network...")
+    transaction_response = await JupiterService().swap_tokens(
+        wallet,
+        {
+            "inputMint": usdc_mint,
+            "outputMint": wsol_mint,
+            "amount": int(amount * 1e6),
+            "slippageBps": 100,
+        }
+    )
+
+    print(f"✅ Transaction sent successfully!")
+    print(f"🔗 Transaction Hash: {transaction_response.get('hash')}")
+
+
+async def main():
+    print("🚀 Starting Solana Smart Wallet Keypair Admin Signer Example")
+    print("=" * 50)
+
+    api_key = os.getenv("CROSSMINT_API_KEY")
+    base_url = os.getenv("CROSSMINT_BASE_URL",
+                         "https://staging.crossmint.com")
+    rpc_url = os.getenv("SOLANA_RPC_ENDPOINT",
+                        "https://api.devnet.solana.com")
+    if not api_key:
+        raise ValueError("❌ CROSSMINT_API_KEY is required")
+
+    print("\n🔧 Initializing API client and connection...")
+    api_client = CrossmintWalletsAPI(api_key, base_url=base_url)
+    connection = SolanaClient(rpc_url)
+
+    factory = SolanaSmartWalletFactory(api_client, connection)
+    wallet = create_wallet(factory)
+
+    while True:
+        print("🔄 Checking balance...")
+        token = "usdc"
+        balances = wallet.balance_of([token])
+        print("💰 Wallet balances:")
+        print(json.dumps(balances, indent=2))
+        usdc_balance = next((balance.get("balances", {}).get("total")
+                            for balance in balances if balance.get("token") == token))
+        if usdc_balance is None:
+            raise ValueError("❌ No USDC balance found")
+        if int(usdc_balance) >= 1_000:  # 1e-3 USDC
+            print("✅ Balance is sufficient. Proceeding to send transaction...")
+            break
+        print("Your balance is less than 1e-3 USDC. Please fund your wallet before proceeding.")
+        print("Mind that the balance may take a moment to be reflected on your wallet")
+        input("Press Enter to continue...")
+
+    await send_swap_transaction(wallet)
+
+    print("\n✨ Example completed successfully!")
+    print("=" * 50)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/python/src/plugins/jupiter/goat_plugins/jupiter/service.py b/python/src/plugins/jupiter/goat_plugins/jupiter/service.py
index 12ab881be..a4e0f1b34 100644
--- a/python/src/plugins/jupiter/goat_plugins/jupiter/service.py
+++ b/python/src/plugins/jupiter/goat_plugins/jupiter/service.py
@@ -1,4 +1,5 @@
 import base64
+import base58
 import aiohttp
 from goat.decorators.tool import Tool
 from goat_wallets.solana.wallet import SolanaTransaction
@@ -36,14 +37,16 @@ async def get_quote(self, parameters: dict) -> dict:
                 async with session.get(f"{self.base_url}/quote", params=request_params) as response:
                     response_text = await response.text()
                     print(f"Got response: {response_text}")
-                    
+
                     if response.status != 200:
                         try:
                             error_data = await response.json()
-                            raise Exception(f"Failed to get quote: {error_data.get('error', 'Unknown error')}")
+                            raise Exception(
+                                f"Failed to get quote: {error_data.get('error', 'Unknown error')}")
                         except:
-                            raise Exception(f"Failed to get quote: {response_text}")
-                    
+                            raise Exception(
+                                f"Failed to get quote: {response_text}")
+
                     response_data = await response.json()
                     QuoteResponse.model_validate(response_data)
 
@@ -68,7 +71,7 @@ async def swap_tokens(self, wallet_client: SolanaWalletClient, parameters: dict)
         try:
             # First get the quote
             quote_response = await self.get_quote(parameters)
-            
+
             # Transform quote response following the same structure as in TypeScript
             transformed_quote_response = {
                 "inputMint": quote_response.get("inputMint"),
@@ -98,7 +101,8 @@ async def swap_tokens(self, wallet_client: SolanaWalletClient, parameters: dict)
             }
 
             # Remove None values from the transformed response
-            transformed_quote_response = {k: v for k, v in transformed_quote_response.items() if v is not None}
+            transformed_quote_response = {
+                k: v for k, v in transformed_quote_response.items() if v is not None}
 
             # Add optional fields if they exist
             for field in ["computedAutoSlippage", "contextSlot", "timeTaken"]:
@@ -129,26 +133,31 @@ async def swap_tokens(self, wallet_client: SolanaWalletClient, parameters: dict)
                 "useTokenLedger": False,
                 "destinationTokenAccount": None  # Can be specified if needed
             }
-            
+
             # Get swap transaction
             async with aiohttp.ClientSession(timeout=self._timeout) as session:
                 async with session.post(f"{self.base_url}/swap", json=swap_request) as response:
                     if response.status != 200:
                         error_data = await response.json()
-                        raise Exception(f"Failed to create swap transaction: {error_data.get('error', 'Unknown error')}")
-                    
+                        raise Exception(
+                            f"Failed to create swap transaction: {error_data.get('error', 'Unknown error')}")
+
                     swap_response = await response.json()
                     swap_transaction = swap_response.get("swapTransaction")
-                    
+
                     if not swap_transaction:
                         raise Exception("No swap transaction returned")
-                    
+
+                    base58_tx = base58.b58encode(
+                        base64.b64decode(swap_transaction)).decode()
+
                     # Send the raw transaction directly
-                    result = wallet_client.send_raw_transaction(swap_transaction)
-                    
+                    result = wallet_client.send_raw_transaction(
+                        base58_tx)
+
                     return {
                         "hash": result["hash"]
                     }
-                    
+
         except Exception as error:
             raise Exception(f"Failed to swap tokens: {error}")
diff --git a/python/src/plugins/jupiter/pyproject.toml b/python/src/plugins/jupiter/pyproject.toml
index cc41cb123..7f09cdbde 100644
--- a/python/src/plugins/jupiter/pyproject.toml
+++ b/python/src/plugins/jupiter/pyproject.toml
@@ -23,6 +23,7 @@ packages = [
 python = "^3.10"
 goat-sdk = "^0.1.0"
 goat-sdk-wallet-solana = "^0.1.1"
+base58 = ">=2.1"
 
 [tool.poetry.group.test.dependencies]
 pytest = "^8.3.4"
diff --git a/python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet.py b/python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet.py
index 03225c479..7bf85e775 100644
--- a/python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet.py
+++ b/python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet.py
@@ -1,6 +1,7 @@
 from time import sleep
 from typing import Dict, List, Optional, Any, TypedDict, Union
 import base58
+import base64
 import nacl.signing
 from solders.instruction import Instruction
 from solders.keypair import Keypair
@@ -211,6 +212,8 @@ def send_raw_transaction(
         signer: Optional[Keypair] = None,
         required_signers: Optional[List[str]] = None,
     ) -> Dict[str, str]:
+        transaction = self.parse_serialized_transaction(transaction)
+
         params = SolanaSmartWalletTransactionParams(
             transaction=transaction,
             required_signers=required_signers,
@@ -322,6 +325,19 @@ def _retrieve_admin_signer_address(self) -> str:
                 f"Admin signer address not found for wallet {self._address}")
         return address
 
+    def parse_serialized_transaction(self, transaction: str) -> str:
+        try:
+            base58.b58decode(transaction)
+            return transaction
+        except Exception:
+            print("Transaction is not base58 encoded, trying to decode as base64")
+            try:
+                return base58.b58encode(
+                    base64.b64decode(transaction)).decode()
+            except Exception:
+                raise ValueError(
+                    "Transaction is not base58 or base64 encoded")
+
     @staticmethod
     def derive_address_from_secret_key(secret_key: str) -> str:
         """Derive a Solana address from a base58-encoded secret key.
diff --git a/python/src/wallets/crossmint/tests/conftest.py b/python/src/wallets/crossmint/tests/conftest.py
index 550fe5aed..434da664c 100644
--- a/python/src/wallets/crossmint/tests/conftest.py
+++ b/python/src/wallets/crossmint/tests/conftest.py
@@ -60,6 +60,12 @@ def test_solana_transaction():
     return "4hXTCkRzt9WyecNzV1XPgCDfGAZzQKNxLXgynz5QDuWWPSAZBZSHptvWRL3BjCvzUXRdKvHL2b7yGrRQcWyaqsaBCncVG7BFggS8w9snUts67BSh3EqKpXLUm5UMHfD7ZBe9GhARjbNQMLJ1QD3Spr6oMTBU6EhdB4RD8CP2xUxr2u3d6fos36PD98XS6oX8TQjLpsMwncs5DAMiD4nNnR8NBfyghGCWvCVifVwvA8B8TJxE1aiyiv2L429BCWfyzAme5sZW8rDb14NeCQHhZbtNqfXhcp2tAnaAT"
 
 
+@pytest.fixture
+def test_solana_transaction_base64():
+    """Fixture providing test Solana transaction encoded in base64."""
+    return "AVXo5X7UNzpuOmYzkZ+fqHDGiRLTSMlWlUCcZKzEV5CIKlrdvZa3/2GrJJfPrXgZqJbYDaGiOnP99tI/sRJfiwwBAAEDRQ/n5E5CLbMbHanUG3+iVvBAWZu0WFM6NoB5xfybQ7kNwwgfIhv6odn2qTUu/gOisDtaeCW1qlwW/gx3ccr/4wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvsInicc+E3IZzLqeA+iM5cn9kSaeFzOuClz1Z2kZQy0BAgIAAQwCAAAAAPIFKgEAAAA="
+
+
 @pytest.fixture
 def solana_connection():
     """Fixture providing Solana RPC client connection."""
diff --git a/python/src/wallets/crossmint/tests/test_solana_smart_wallet.py b/python/src/wallets/crossmint/tests/test_solana_smart_wallet.py
index 040a22e1d..0e595db7e 100644
--- a/python/src/wallets/crossmint/tests/test_solana_smart_wallet.py
+++ b/python/src/wallets/crossmint/tests/test_solana_smart_wallet.py
@@ -10,6 +10,7 @@
 
 class SolanaSmartWalletTransactionParams:
     """Parameters for Solana smart wallet transactions."""
+
     def __init__(self, transaction: str, required_signers: list = [], signer: str = ""):
         self.transaction = transaction
         self.requiredSigners = required_signers or []
@@ -23,7 +24,7 @@ def test_solana_smart_wallet_creation(smart_api):
         linked_user="email:test@example.com"
     )
     assert wallet["type"] == "solana-smart-wallet"
-    
+
     # Get wallet and verify only the address matches since other fields might differ
     retrieved = smart_api.get_wallet(wallet["address"])
     assert retrieved["address"] == wallet["address"]
@@ -36,7 +37,7 @@ def test_solana_smart_wallet_transaction(smart_api, test_solana_transaction):
         wallet_type=WalletType.SOLANA_SMART_WALLET,
         linked_user="email:test@example.com"
     )
-    
+
     # Test transaction submission
     try:
         # Create transaction parameters
@@ -44,7 +45,7 @@ def test_solana_smart_wallet_transaction(smart_api, test_solana_transaction):
             transaction=test_solana_transaction,
             required_signers=[]  # No required signers for basic test
         )
-        
+
         # Create transaction
         tx = smart_api.create_transaction_for_smart_wallet(
             wallet["address"],
@@ -77,13 +78,13 @@ def test_solana_smart_wallet_error_handling(smart_api):
             linked_user="email:test@example.com"
         )
     assert "error" in str(exc.value).lower()
-    
+
     # Test invalid transaction
     wallet = smart_api.create_wallet(
         wallet_type=WalletType.SOLANA_SMART_WALLET,
         linked_user="email:test@example.com"
     )
-    
+
     # Test with invalid transaction format
     invalid_tx = SolanaSmartWalletTransactionParams(
         transaction="invalid-transaction"
@@ -94,7 +95,8 @@ def test_solana_smart_wallet_error_handling(smart_api):
             invalid_tx,
             "solana"
         )
-    assert "error" in str(exc.value).lower() or "invalid" in str(exc.value).lower()
+    assert "error" in str(exc.value).lower(
+    ) or "invalid" in str(exc.value).lower()
 
 
 def test_solana_smart_wallet_with_email(smart_api, test_email, test_solana_wallet_options):
@@ -125,3 +127,24 @@ def test_solana_smart_wallet_with_user_id(smart_api, test_user_id, test_solana_w
     )
     assert wallet["type"] == "solana-smart-wallet"
     assert "linkedUser" in wallet
+
+
+def test_parse_serialized_transaction_base58(smart_api, test_solana_transaction):
+    """Test parsing of serialized transactions."""
+    parsed_tx = smart_api.parse_serialized_transaction(test_solana_transaction)
+    assert parsed_tx == test_solana_transaction
+
+
+def test_parse_serialized_transaction_base64(smart_api, test_solana_transaction_base64, test_solana_transaction):
+    """Test parsing of serialized transactions."""
+    parsed_tx = smart_api.parse_serialized_transaction(
+        test_solana_transaction_base64)
+    assert parsed_tx == test_solana_transaction
+
+
+def test_parse_serialized_transaction_invalid(smart_api):
+    """Test parsing of invalid serialized transactions."""
+    with pytest.raises(Exception) as exc:
+        smart_api.parse_serialized_transaction("invalid-transaction")
+    assert "transaction is not base58 encoded, trying to decode as base64" in str(
+        exc.value)


Reviews:
user: login: alfonso-paella
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T18:24:00Z
user: login: alberto-crossmint
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T18:56:28Z
user: login: alfonso-paella
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-02T19:48:11Z

Review comments:
diff_hunk: @@ -211,6 +212,17 @@ def send_raw_transaction(
         signer: Optional[Keypair] = None,
         required_signers: Optional[List[str]] = None,
     ) -> Dict[str, str]:
+        try:
path: python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet.py
user: login: alfonso-paella
body: should this code live in the wallet sdk side? (could be done as a follow up, we can still have this here)
created_at: 2025-04-02T18:23:59Z
author_association: COLLABORATOR
diff_hunk: @@ -211,6 +212,17 @@ def send_raw_transaction(
         signer: Optional[Keypair] = None,
         required_signers: Optional[List[str]] = None,
     ) -> Dict[str, str]:
+        try:
path: python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet.py
user: login: alberto-crossmint
body: We haven't integrated the SDK on goat yet, but yes in the future it should be there
created_at: 2025-04-02T18:56:28Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-02T16:41:52Z
body: ###  ⚠️  No Changeset found

Latest commit: 63d8101f85a3b238fba5b37d02ee92c252b97261

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**



[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/goat-sdk/goat/new/alb/fix-base64-tx-ssw?filename=.changeset/perfect-phones-notice.md&value=---%0A%22%40fake-scope%2Ffake-pkg%22%3A%20patch%0A---%0A%0ASolana%20Smart%20Wallets%3A%20fix%20b64%20tx%0A)


author_association: NONE
user: login: alfonso-paella
created_at: 2025-04-02T18:24:29Z
body: any automated tests we should add?
author_association: COLLABORATOR
user: login: alberto-crossmint
created_at: 2025-04-02T19:16:28Z
body: > any automated tests we should add?

@alfonso-paella Added UTs for the added functionality, but we don't have e2e testing capabilities here yet
author_association: COLLABORATOR