From daecb6a3a226664caf08b6b513b5146e85dd91d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Garc=C3=ADa=20Planes?= Date: Thu, 20 Mar 2025 17:01:39 +0100 Subject: [PATCH 1/2] SSW: improvements --- .../solana_smart_wallet_creation_examples.py | 88 +++++ ...a_smart_wallet_delegated_signer_example.py | 70 ++-- ..._smart_wallet_fireblocks_signer_example.py | 56 ++-- ...art_wallet_keypair_admin_signer_example.py | 64 ++-- .../solana_smart_wallet_transfer_example.py | 113 +++++++ .../goat_wallets/crossmint/__init__.py | 10 +- .../goat_wallets/crossmint/api_client.py | 309 +++++++++++------- .../goat_wallets/crossmint/parameters.py | 68 +++- .../crossmint/solana_smart_wallet.py | 149 +++++---- .../crossmint/solana_smart_wallet_factory.py | 194 +++++++---- .../crossmint/goat_wallets/crossmint/types.py | 32 +- 11 files changed, 787 insertions(+), 366 deletions(-) create mode 100644 python/examples/by-wallet/crossmint/solana_smart_wallet_creation_examples.py create mode 100644 python/examples/by-wallet/crossmint/solana_smart_wallet_transfer_example.py diff --git a/python/examples/by-wallet/crossmint/solana_smart_wallet_creation_examples.py b/python/examples/by-wallet/crossmint/solana_smart_wallet_creation_examples.py new file mode 100644 index 000000000..819088d3f --- /dev/null +++ b/python/examples/by-wallet/crossmint/solana_smart_wallet_creation_examples.py @@ -0,0 +1,88 @@ +""" +This file contains examples of how to create a Solana Smart Wallet with different configurations and linked users. + +To run these examples, you need to set the following environment variables: +- CROSSMINT_API_KEY +- SOLANA_RPC_ENDPOINT +- CROSSMINT_BASE_URL +""" + +from goat_wallets.crossmint.solana_smart_wallet_factory import SolanaSmartWalletFactory +from goat_wallets.crossmint.types import SolanaKeypairSigner, SolanaFireblocksSigner +from goat_wallets.crossmint.solana_smart_wallet import SolanaSmartWalletConfig +from goat_wallets.crossmint.solana_smart_wallet import SolanaSmartWalletClient +from goat_wallets.crossmint.api_client import CrossmintWalletsAPI +from solders.keypair import Keypair +from goat_wallets.crossmint.parameters import CoreSignerType +from typing import Literal, Optional +from solana.rpc.api import Client as SolanaClient +import os +from dotenv import load_dotenv +import uuid + +load_dotenv() + + +def create_wallet(factory: SolanaSmartWalletFactory, signer_type: Literal["solana-keypair", "solana-fireblocks-custodial"], linked_user: Optional[str] = None, idempotency_key: Optional[str] = None) -> SolanaSmartWalletClient: + print("=" * 50) + print( + f"\nšŸ”‘ Creating Solana Smart Wallet {"idempotently" if idempotency_key or linked_user else ""} with {signer_type} admin signer and linked user {linked_user}...") + print(f"Idempotency key: {idempotency_key}") if idempotency_key else None + + config = SolanaSmartWalletConfig( + adminSigner=SolanaKeypairSigner( + type=CoreSignerType.SOLANA_KEYPAIR, + keyPair=Keypair() + ) if signer_type == "solana-keypair" else SolanaFireblocksSigner( + type=CoreSignerType.SOLANA_FIREBLOCKS_CUSTODIAL, + ) + ) + params = {"config": config} + if linked_user: + params["linkedUser"] = linked_user + wallet = factory.get_or_create(params, idempotency_key=idempotency_key) + + print(f"āœ… Wallet created successfully!") + print(f"šŸ“ Wallet Address: {wallet.get_address()}") + print( + f"šŸ‘¤ Admin Signer: {wallet.get_admin_signer_address()}. Type: {"MPC Custodial" if signer_type == "solana-fireblocks-custodial" else "Non-custodial"}") + return wallet + + +def main(): + print("šŸš€ Starting Solana Smart Wallet Creation Examples") + 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) + + print("\nšŸ”§ Initializing factory...") + factory = SolanaSmartWalletFactory(api_client, connection) + + # Signer configurations + # create_wallet(factory, "solana-keypair") + # create_wallet(factory, "solana-fireblocks-custodial") + + # Idempotency key configurations (both requests will return the same wallet) + idempotency_key = str(uuid.uuid4()) + create_wallet(factory, "solana-fireblocks-custodial", + idempotency_key=idempotency_key) + create_wallet(factory, "solana-fireblocks-custodial", + idempotency_key=idempotency_key) + + # Linked user configurations. Creations with the same linked user will return the same wallet. + # create_wallet(factory, "solana-keypair", "email:example@example.com") + # create_wallet(factory, "solana-fireblocks-custodial", + # "phoneNumber:+1234567890") + # create_wallet(factory, "solana-keypair", "twitter:example") + # create_wallet(factory, "solana-fireblocks-custodial", "userId:1234567890") + # create_wallet(factory, "solana-keypair", "x:example") + + +if __name__ == "__main__": + main() diff --git a/python/examples/by-wallet/crossmint/solana_smart_wallet_delegated_signer_example.py b/python/examples/by-wallet/crossmint/solana_smart_wallet_delegated_signer_example.py index 4f1b8b385..2b7781d3d 100644 --- a/python/examples/by-wallet/crossmint/solana_smart_wallet_delegated_signer_example.py +++ b/python/examples/by-wallet/crossmint/solana_smart_wallet_delegated_signer_example.py @@ -10,12 +10,12 @@ import os from goat_wallets.crossmint.solana_smart_wallet import SolanaSmartWalletClient -from goat_wallets.crossmint.parameters import WalletType from goat_wallets.crossmint.parameters import CoreSignerType -from goat_wallets.crossmint.parameters import AdminSigner +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 goat_wallets.crossmint.solana_smart_wallet_factory import solana_smart_wallet_factory from solders.keypair import Keypair from dotenv import load_dotenv from solders.pubkey import Pubkey @@ -24,9 +24,12 @@ load_dotenv() + def create_memo_instruction(fee_payer: Pubkey, memo: str) -> Instruction: - memo_program_id = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") - accounts = [AccountMeta(pubkey=fee_payer, is_signer=False, is_writable=False)] + memo_program_id = Pubkey.from_string( + "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") + accounts = [AccountMeta( + pubkey=fee_payer, is_signer=False, is_writable=False)] data = bytes(memo, "utf-8") return Instruction( memo_program_id, @@ -34,31 +37,22 @@ def create_memo_instruction(fee_payer: Pubkey, memo: str) -> Instruction: accounts, ) -def create_wallet(api_client: CrossmintWalletsAPI, connection: SolanaClient, signer: Keypair) -> SolanaSmartWalletClient: + +def create_wallet(factory: SolanaSmartWalletFactory, signer: Keypair) -> SolanaSmartWalletClient: print("\nšŸ”‘ Creating Solana Smart Wallet...") - wallet_creation_response = api_client.create_smart_wallet( - WalletType.SOLANA_SMART_WALLET, - AdminSigner( + wallet = factory.get_or_create({ + "config": SolanaSmartWalletConfig( + adminSigner=SolanaKeypairSigner( type=CoreSignerType.SOLANA_KEYPAIR, - address=str(signer.pubkey()) - ), + keyPair=signer + ) ) + }) print(f"āœ… Wallet created successfully!") - print(f"šŸ“ Wallet Address: {wallet_creation_response['address']}") + print(f"šŸ“ Wallet Address: {wallet.get_address()}") print(f"šŸ‘¤ Admin Signer: {signer.pubkey()}") - return SolanaSmartWalletClient( - wallet_creation_response["address"], - api_client, - { - "config": { - "adminSigner": { - "type": "solana-keypair", - "keyPair": signer - } - } - }, - connection=connection - ) + return wallet + def register_delegated_signer(wallet: SolanaSmartWalletClient, signer: Keypair) -> Pubkey: print("\nšŸ”‘ Registering delegated signer...") @@ -66,52 +60,58 @@ def register_delegated_signer(wallet: SolanaSmartWalletClient, signer: Keypair) str(signer.pubkey()) ) print(f"āœ… Delegated signer registered successfully!") - print(f"šŸ“ Delegated Signer Locator: {delegated_signer_response['locator']}") + print( + f"šŸ“ Delegated Signer Locator: {delegated_signer_response['locator']}") print(f"šŸ‘¤ Delegated Signer Address: {signer.pubkey()}") return delegated_signer_response["locator"] + def send_transaction(wallet: SolanaSmartWalletClient, signer: Keypair): print("\nšŸ’ø Preparing transaction...") - transaction = create_memo_instruction(Pubkey.from_string(wallet.get_address()), "My first Solana Smart Wallet transaction! šŸš€") + instruction = create_memo_instruction(Pubkey.from_string( + wallet.get_address()), "My first Solana Smart Wallet transaction! šŸš€") print(f"šŸ“ Transaction Details:") print(f" From: {wallet.get_address()}") print(f" Signer: {signer.pubkey()}") print(f" Message: My first Solana Smart Wallet transaction! šŸš€") - + print("\nšŸ“¤ Sending transaction to network...") transaction_response = wallet.send_transaction( { - "instructions": [transaction], + "instructions": [instruction], "signer": signer } ) print(f"āœ… Transaction sent successfully!") print(f"šŸ”— Transaction Hash: {transaction_response.get('hash')}") + def main(): print("šŸš€ Starting Solana Smart Wallet Delegated 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) - + print("\nšŸ”‘ Generating keypairs...") admin_signer = Keypair() delegated_signer = Keypair() - - wallet = create_wallet(api_client, connection, admin_signer) + + factory = SolanaSmartWalletFactory(api_client, connection) + wallet = create_wallet(factory, admin_signer) register_delegated_signer(wallet, delegated_signer) send_transaction(wallet, delegated_signer) - + print("\nāœØ Example completed successfully!") print("=" * 50) + if __name__ == "__main__": main() diff --git a/python/examples/by-wallet/crossmint/solana_smart_wallet_fireblocks_signer_example.py b/python/examples/by-wallet/crossmint/solana_smart_wallet_fireblocks_signer_example.py index efdc633c3..440883c61 100644 --- a/python/examples/by-wallet/crossmint/solana_smart_wallet_fireblocks_signer_example.py +++ b/python/examples/by-wallet/crossmint/solana_smart_wallet_fireblocks_signer_example.py @@ -10,9 +10,10 @@ import os from goat_wallets.crossmint.solana_smart_wallet import SolanaSmartWalletClient -from goat_wallets.crossmint.parameters import WalletType from goat_wallets.crossmint.parameters import CoreSignerType -from goat_wallets.crossmint.parameters import AdminSigner +from goat_wallets.crossmint.solana_smart_wallet_factory import SolanaSmartWalletFactory +from goat_wallets.crossmint.types import SolanaFireblocksSigner +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 @@ -22,9 +23,12 @@ load_dotenv() + def create_memo_instruction(fee_payer: Pubkey, memo: str) -> Instruction: - memo_program_id = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") - accounts = [AccountMeta(pubkey=fee_payer, is_signer=False, is_writable=False)] + memo_program_id = Pubkey.from_string( + "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") + accounts = [AccountMeta( + pubkey=fee_payer, is_signer=False, is_writable=False)] data = bytes(memo, "utf-8") return Instruction( memo_program_id, @@ -32,63 +36,61 @@ def create_memo_instruction(fee_payer: Pubkey, memo: str) -> Instruction: accounts, ) -def create_wallet(api_client: CrossmintWalletsAPI, connection: SolanaClient - ) -> SolanaSmartWalletClient: + +def create_wallet(factory: SolanaSmartWalletFactory) -> SolanaSmartWalletClient: print("\nšŸ”‘ Creating Solana Smart Wallet with Fireblocks custodial signer...") - wallet_creation_response = api_client.create_smart_wallet( - WalletType.SOLANA_SMART_WALLET, - AdminSigner( + wallet = factory.get_or_create({ + "config": SolanaSmartWalletConfig( + adminSigner=SolanaFireblocksSigner( type=CoreSignerType.SOLANA_FIREBLOCKS_CUSTODIAL, - ), + ) ) - address = wallet_creation_response["address"] + }) + print(f"āœ… Wallet created successfully!") - print(f"šŸ“ Wallet Address: {address}") + print(f"šŸ“ Wallet Address: {wallet.get_address()}") print(f"šŸ” Signer Type: Fireblocks Custodial") - return SolanaSmartWalletClient( - address, - api_client, - { - "config": {"adminSigner": {"type": "fireblocks"}}, - }, - connection=connection - ) + return wallet + def send_transaction(wallet: SolanaSmartWalletClient): print("\nšŸ’ø Preparing transaction...") - transaction = create_memo_instruction(Pubkey.from_string(wallet.get_address()), "My first Solana Smart Wallet transaction! šŸš€") + instruction = create_memo_instruction(Pubkey.from_string( + wallet.get_address()), "My first Solana Smart Wallet transaction! šŸš€") print(f"šŸ“ Transaction Details:") print(f" From: {wallet.get_address()}") print(f" Message: My first Solana Smart Wallet transaction! šŸš€") - + print("\nšŸ“¤ Sending transaction to network...") transaction_response = wallet.send_transaction( { - "instructions": [transaction], + "instructions": [instruction], } ) print(f"āœ… Transaction sent successfully!") print(f"šŸ”— Transaction Hash: {transaction_response.get('hash')}") + def main(): print("šŸš€ Starting Solana Smart Wallet Fireblocks 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) - wallet = create_wallet(api_client, connection) + wallet = create_wallet(SolanaSmartWalletFactory(api_client, connection)) send_transaction(wallet) - + print("\nāœØ Example completed successfully!") print("=" * 50) + if __name__ == "__main__": main() diff --git a/python/examples/by-wallet/crossmint/solana_smart_wallet_keypair_admin_signer_example.py b/python/examples/by-wallet/crossmint/solana_smart_wallet_keypair_admin_signer_example.py index adbcf1b0b..65cea6da2 100644 --- a/python/examples/by-wallet/crossmint/solana_smart_wallet_keypair_admin_signer_example.py +++ b/python/examples/by-wallet/crossmint/solana_smart_wallet_keypair_admin_signer_example.py @@ -10,12 +10,12 @@ import os from goat_wallets.crossmint.solana_smart_wallet import SolanaSmartWalletClient -from goat_wallets.crossmint.parameters import WalletType from goat_wallets.crossmint.parameters import CoreSignerType -from goat_wallets.crossmint.parameters import AdminSigner +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 goat_wallets.crossmint.solana_smart_wallet_factory import solana_smart_wallet_factory from solders.keypair import Keypair from dotenv import load_dotenv from solders.pubkey import Pubkey @@ -24,9 +24,12 @@ load_dotenv() + def create_memo_instruction(fee_payer: Pubkey, memo: str) -> Instruction: - memo_program_id = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") - accounts = [AccountMeta(pubkey=fee_payer, is_signer=False, is_writable=False)] + memo_program_id = Pubkey.from_string( + "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") + accounts = [AccountMeta( + pubkey=fee_payer, is_signer=False, is_writable=False)] data = bytes(memo, "utf-8") return Instruction( memo_program_id, @@ -34,70 +37,65 @@ def create_memo_instruction(fee_payer: Pubkey, memo: str) -> Instruction: accounts, ) -def create_wallet(api_client: CrossmintWalletsAPI, connection: SolanaClient, signer: Keypair) -> SolanaSmartWalletClient: + +def create_wallet(factory: SolanaSmartWalletFactory, signer: Keypair) -> SolanaSmartWalletClient: print("\nšŸ”‘ Creating Solana Smart Wallet with keypair admin signer...") - wallet_creation_response = api_client.create_smart_wallet( - WalletType.SOLANA_SMART_WALLET, - AdminSigner( + wallet = factory.get_or_create({ + "config": SolanaSmartWalletConfig( + adminSigner=SolanaKeypairSigner( type=CoreSignerType.SOLANA_KEYPAIR, - address=str(signer.pubkey()) - ), + keyPair=signer + ) ) + }) print(f"āœ… Wallet created successfully!") - print(f"šŸ“ Wallet Address: {wallet_creation_response['address']}") + print(f"šŸ“ Wallet Address: {wallet.get_address()}") print(f"šŸ‘¤ Admin Signer: {signer.pubkey()}") - return SolanaSmartWalletClient( - wallet_creation_response["address"], - api_client, - { - "config": { - "adminSigner": { - "type": "solana-keypair", - "keyPair": signer - } - } - }, - connection=connection - ) + return wallet + def send_transaction(wallet: SolanaSmartWalletClient): print("\nšŸ’ø Preparing transaction...") - transaction = create_memo_instruction(Pubkey.from_string(wallet.get_address()), "My first Solana Smart Wallet transaction! šŸš€") + instruction = create_memo_instruction(Pubkey.from_string( + wallet.get_address()), "My first Solana Smart Wallet transaction! šŸš€") print(f"šŸ“ Transaction Details:") print(f" From: {wallet.get_address()}") print(f" Message: My first Solana Smart Wallet transaction! šŸš€") - + print("\nšŸ“¤ Sending transaction to network...") transaction_response = wallet.send_transaction( { - "instructions": [transaction], + "instructions": [instruction], } ) print(f"āœ… Transaction sent successfully!") print(f"šŸ”— Transaction Hash: {transaction_response.get('hash')}") + 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) - + print("\nšŸ”‘ Generating admin keypair...") admin_signer = Keypair() - wallet = create_wallet(api_client, connection, admin_signer) + factory = SolanaSmartWalletFactory(api_client, connection) + wallet = create_wallet(factory, admin_signer) send_transaction(wallet) - + print("\nāœØ Example completed successfully!") print("=" * 50) + if __name__ == "__main__": main() diff --git a/python/examples/by-wallet/crossmint/solana_smart_wallet_transfer_example.py b/python/examples/by-wallet/crossmint/solana_smart_wallet_transfer_example.py new file mode 100644 index 000000000..6a43387a7 --- /dev/null +++ b/python/examples/by-wallet/crossmint/solana_smart_wallet_transfer_example.py @@ -0,0 +1,113 @@ +""" +This example shows how to create a Solana Smart Wallet and send a transaction to the network +using a fireblocks custodial admin signer. + +To run this example, you need to set the following environment variables: +- CROSSMINT_API_KEY +- SOLANA_RPC_ENDPOINT +- CROSSMINT_BASE_URL + +""" + +import os +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 solders.keypair import Keypair +from dotenv import load_dotenv +from solders.pubkey import Pubkey +from solders.instruction import Instruction +from solders.system_program import TransferParams, transfer +import json + +load_dotenv() + + +def create_send_sol_instruction(sender: str, recipient: str, amount_lamports: int) -> Instruction: + return transfer(TransferParams(from_pubkey=Pubkey.from_string(sender), to_pubkey=Pubkey.from_string(recipient), lamports=amount_lamports)) + + +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 + + +def send_transfer_transaction(wallet: SolanaSmartWalletClient): + print("\nšŸ’ø Preparing transaction...") + recipient = Keypair().pubkey() + instructions = [create_send_sol_instruction( + wallet.get_address(), wallet.get_address(), 1_000)] + print(f"šŸ“ Transaction Details:") + print(f" From: {wallet.get_address()}") + print(f" To: {recipient}") + print(f" Amount: 1e-6 SOL") + + print("\nšŸ“¤ Sending transaction to network...") + transaction_response = wallet.send_transaction( + { + "instructions": instructions, + } + ) + print(f"āœ… Transaction sent successfully!") + print(f"šŸ”— Transaction Hash: {transaction_response.get('hash')}") + + +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 = "sol" + balances = wallet.balance_of([token]) + print("šŸ’° Wallet balances:") + print(json.dumps(balances[0], indent=2)) + sol_balance = next((balance.get("balances", {}).get("total") + for balance in balances if balance.get("token") == token)) + if sol_balance is None: + raise ValueError("āŒ No SOL balance found") + if int(sol_balance) >= 1_000_000: # 1e-3 SOL + print("āœ… Balance is sufficient. Proceeding to send transaction...") + break + print("Your balance is less than 1e-3 SOL. Please fund your wallet using https://faucet.solana.com/ before proceeding.") + print("Mind that the balance may take a moment to be reflected on your wallet") + input("Press Enter to continue...") + + send_transfer_transaction(wallet) + + print("\nāœØ Example completed successfully!") + print("=" * 50) + + +if __name__ == "__main__": + main() diff --git a/python/src/wallets/crossmint/goat_wallets/crossmint/__init__.py b/python/src/wallets/crossmint/goat_wallets/crossmint/__init__.py index 43f83cb87..c29181692 100644 --- a/python/src/wallets/crossmint/goat_wallets/crossmint/__init__.py +++ b/python/src/wallets/crossmint/goat_wallets/crossmint/__init__.py @@ -9,14 +9,15 @@ from .evm_smart_wallet import EVMSmartWalletClient from .solana_smart_wallet import SolanaSmartWalletClient from .evm_smart_wallet import smart_wallet_factory as evm_smart_wallet_factory -from .solana_smart_wallet_factory import solana_smart_wallet_factory +from .solana_smart_wallet_factory import SolanaSmartWalletFactory + def crossmint(api_key: str) -> Dict[str, Any]: """Initialize CrossMint SDK with API key. - + Args: api_key: CrossMint API key - + Returns: Dict containing CrossMint wallet and plugin factories """ @@ -25,12 +26,13 @@ def crossmint(api_key: str) -> Dict[str, Any]: return { "custodial": custodial_factory(api_client), "evm_smartwallet": evm_smart_wallet_factory(api_client), - "solana_smartwallet": solana_smart_wallet_factory(api_client), + "solana_smartwallet": SolanaSmartWalletFactory(api_client), "faucet": faucet_plugin(api_client), "mint": mint_plugin(api_client), "wallets": wallets_plugin(api_client) } + __all__ = [ "crossmint", "EVMSmartWalletClient", diff --git a/python/src/wallets/crossmint/goat_wallets/crossmint/api_client.py b/python/src/wallets/crossmint/goat_wallets/crossmint/api_client.py index b85b8982b..26b080246 100644 --- a/python/src/wallets/crossmint/goat_wallets/crossmint/api_client.py +++ b/python/src/wallets/crossmint/goat_wallets/crossmint/api_client.py @@ -1,4 +1,6 @@ from typing import Any, Dict, List, Optional, Union, cast + +from goat_wallets.crossmint.types import SupportedToken from .parameters import ( SignTypedDataRequest, AdminSigner, Call, WalletType, SolanaSmartWalletTransactionParams, DelegatedSignerPermission @@ -12,17 +14,17 @@ class CrossmintWalletsAPI: """Python implementation of CrossmintWalletsAPI.""" - + def __init__(self, api_key: str, base_url: str = "https://staging.crossmint.com"): """Initialize the Crossmint Wallets API client. - + Args: api_key: API key for authentication base_url: Base URL for the Crossmint API """ self.api_key = api_key self.base_url = f"{base_url}/api/v1-alpha2" - + def _request( self, endpoint: str, @@ -31,16 +33,16 @@ def _request( **kwargs ) -> Dict[str, Any]: """Make an HTTP request to the Crossmint API. - + Args: endpoint: API endpoint (relative to base_url) method: HTTP method to use timeout: Optional request timeout in seconds **kwargs: Additional arguments to pass to requests - + Returns: Parsed JSON response - + Raises: Exception: If the response is not OK """ @@ -50,22 +52,22 @@ def _request( "Content-Type": "application/json", **(kwargs.pop("headers", {})) } - + try: kwargs["timeout"] = timeout if timeout is not None else 30 response = requests.request(method, url, headers=headers, **kwargs) response_body = response.json() - + if not response.ok: error_message = f"Error {response.status_code}: {response.reason}" if response_body: error_message += f"\n\n{json.dumps(response_body, indent=2)}" raise Exception(error_message) - + return response_body except Exception as e: raise Exception(f"Failed to {method.lower()} {endpoint}: {e}") - + def create_smart_wallet( self, wallet_type: WalletType, @@ -73,15 +75,15 @@ def create_smart_wallet( linked_user: Optional[str] = None, ) -> Dict[str, Any]: """Create a new smart wallet. - + Args: wallet_type: Type of smart wallet (EVM_SMART_WALLET or SOLANA_SMART_WALLET) admin_signer: Optional admin signer configuration linked_user: Linked user locator - + Returns: Wallet creation response - + Raises: ValueError: If no user locator is provided """ @@ -94,18 +96,18 @@ def create_smart_wallet( } if linked_user: payload["linkedUser"] = linked_user - + if admin_signer: payload["config"]["adminSigner"] = admin_signer.model_dump() - + return self._request("/wallets", method="POST", json=payload) - + def create_custodial_wallet(self, linked_user: str) -> Dict[str, Any]: """Create a new Solana custodial wallet. - + Args: linked_user: User identifier (email, phone, or userId) - + Returns: Wallet creation response """ @@ -116,35 +118,35 @@ def create_custodial_wallet(self, linked_user: str) -> Dict[str, Any]: linked_user = f"phoneNumber:{linked_user}" else: linked_user = f"userId:{linked_user}" - + payload = { "type": "solana-mpc-wallet", "linkedUser": linked_user } - + return self._request("/wallets", method="POST", json=payload) - + def get_wallet(self, locator: str) -> Dict[str, Any]: """Get wallet details by locator. - + Args: locator: Wallet locator string - + Returns: Wallet details """ endpoint = f"/wallets/{quote(locator)}" return self._request(endpoint) - + def sign_message_for_custodial_wallet( self, locator: str, message: str ) -> Dict[str, Any]: """Sign a message using a Solana custodial wallet. - + Args: locator: Wallet locator string message: Message to sign - + Returns: Signature response """ @@ -153,9 +155,9 @@ def sign_message_for_custodial_wallet( "type": "solana-message", "params": {"message": message} } - + return self._request(endpoint, method="POST", json=payload) - + def sign_message_for_smart_wallet( self, wallet_address: str, @@ -165,19 +167,19 @@ def sign_message_for_smart_wallet( required_signers: Optional[List[str]] = None ) -> Dict[str, Any]: """Sign a message using a smart wallet. - + Args: wallet_address: Wallet address message: Message to sign chain: Chain identifier signer: Optional signer address required_signers: Optional list of additional required signers - + Returns: Signature response """ endpoint = f"/wallets/{quote(wallet_address)}/signatures" - + if chain == "solana": payload = { "type": "solana-message", @@ -196,10 +198,11 @@ def sign_message_for_smart_wallet( "chain": chain } } - - payload["params"] = {k: v for k, v in payload["params"].items() if v is not None} + + payload["params"] = {k: v for k, + v in payload["params"].items() if v is not None} return self._request(endpoint, method="POST", json=payload) - + def sign_typed_data_for_smart_wallet( self, wallet_address: str, @@ -208,13 +211,13 @@ def sign_typed_data_for_smart_wallet( signer: str ) -> Dict[str, Any]: """Sign typed data using an EVM smart wallet. - + Args: wallet_address: Wallet address typed_data: EVM typed data to sign chain: Chain identifier signer: Signer address - + Returns: Signature response """ @@ -227,24 +230,24 @@ def sign_typed_data_for_smart_wallet( "signer": signer } ).model_dump() - + return self._request(endpoint, method="POST", json=payload) def check_signature_status( self, signature_id: str, wallet_address: str ) -> Dict[str, Any]: """Check the status of a signature request. - + Args: signature_id: ID of the signature request wallet_address: Address of the wallet - + Returns: Signature status response """ endpoint = f"/wallets/{quote(wallet_address)}/signatures/{quote(signature_id)}" return self._request(endpoint) - + def approve_signature_for_smart_wallet( self, signature_id: str, @@ -254,20 +257,20 @@ def approve_signature_for_smart_wallet( approvals: Optional[List[Dict[str, str]]] = None ) -> Dict[str, Any]: """Approve a signature request for a smart wallet. - + Args: signature_id: ID of the signature request locator: Wallet locator string signer: Optional signer identifier signature: Optional signature value approvals: Optional list of approval objects with signer and signature - + Returns: Approval response """ endpoint = f"/wallets/{quote(locator)}/signatures/{quote(signature_id)}/approve" payload = {} - + if approvals: endpoint = f"/wallets/{quote(locator)}/signatures/{quote(signature_id)}/approvals" payload = {"approvals": approvals} @@ -276,18 +279,18 @@ def approve_signature_for_smart_wallet( payload["signature"] = signature if signer: payload["signer"] = signer - + return self._request(endpoint, method="POST", json=payload) - + def create_transaction_for_custodial_wallet( self, locator: str, transaction: str ) -> Dict[str, Any]: """Create a transaction using a Solana custodial wallet. - + Args: locator: Wallet locator string (email:address, phoneNumber:address, or userId:address) transaction: Encoded transaction data - + Returns: Transaction creation response """ @@ -298,9 +301,9 @@ def create_transaction_for_custodial_wallet( "transaction": transaction } } - + return self._request(endpoint, method="POST", json=payload) - + def create_transaction( self, wallet_locator: str, @@ -309,13 +312,13 @@ def create_transaction( required_signers: Optional[List[str]] = None, ) -> Dict[str, Any]: """Create a new Solana transaction. - + Args: wallet_locator: Wallet identifier transaction: Base58 encoded serialized Solana transaction required_signers: Optional list of additional required signers signer: Optional signer locator (defaults to admin signer) - + Returns: Transaction creation response """ @@ -327,8 +330,9 @@ def create_transaction( "signer": signer } } - payload["params"] = {k: v for k, v in payload["params"].items() if v is not None} - + payload["params"] = {k: v for k, + v in payload["params"].items() if v is not None} + return self._request(endpoint, method="POST", json=payload) def create_transaction_for_smart_wallet( @@ -339,19 +343,20 @@ def create_transaction_for_smart_wallet( signer: Optional[str] = None ) -> Dict[str, Any]: """Create a transaction using a smart wallet. - + Args: wallet_address: Wallet address params: Transaction parameters (List[Call] for EVM, SolanaSmartWalletTransactionParams for Solana) chain: Chain identifier (required for EVM) signer: Optional signer address (for EVM only) - + Returns: Transaction creation response """ if isinstance(params, list): if not chain: - raise ValueError("Chain identifier is required for EVM transactions") + raise ValueError( + "Chain identifier is required for EVM transactions") return self.create_transaction_for_evm_smart_wallet(wallet_address, cast(List[Call], params), chain, signer) else: return self.create_transaction( @@ -360,7 +365,7 @@ def create_transaction_for_smart_wallet( params.signer, params.required_signers, ) - + def create_transaction_for_evm_smart_wallet( self, wallet_address: str, @@ -369,18 +374,18 @@ def create_transaction_for_evm_smart_wallet( signer: Optional[str] = None ) -> Dict[str, Any]: """Create a transaction using an EVM smart wallet. - + Args: wallet_address: Wallet address calls: List of contract calls or dictionaries chain: Chain identifier signer: Optional signer address - + Returns: Transaction creation response """ endpoint = f"/wallets/{quote(wallet_address)}/transactions" - + # Convert dictionaries to Call models if needed formatted_calls = [] for call in calls: @@ -388,7 +393,7 @@ def create_transaction_for_evm_smart_wallet( formatted_calls.append(Call(**call).model_dump()) else: formatted_calls.append(call.model_dump()) - + payload = { "params": { "chain": chain, @@ -398,9 +403,9 @@ def create_transaction_for_evm_smart_wallet( } if signer: payload["params"]["signer"] = f"evm-keypair:{signer}" - + return self._request(endpoint, method="POST", json=payload) - + def approve_transaction( self, locator: str, @@ -408,30 +413,30 @@ def approve_transaction( approvals: List[Dict[str, str]] ) -> Dict[str, Any]: """Approve a transaction. - + Args: locator: Wallet locator string transaction_id: ID of the transaction approvals: List of approval objects or AdminSigner instances - + Returns: Approval response """ endpoint = f"/wallets/{quote(locator)}/transactions/{quote(transaction_id)}/approvals" - + payload = {"approvals": approvals} - + return self._request(endpoint, method="POST", json=payload) - + def check_transaction_status( self, locator: str, transaction_id: str ) -> Dict[str, Any]: """Check the status of a transaction. - + Args: locator: Wallet locator string transaction_id: ID of the transaction - + Returns: Transaction status response """ @@ -440,11 +445,11 @@ def check_transaction_status( def create_collection(self, parameters: Dict[str, Any], chain: str) -> Dict[str, Any]: """Create a new NFT collection. - + Args: parameters: Collection creation parameters chain: Chain identifier - + Returns: Collection creation response """ @@ -454,7 +459,7 @@ def create_collection(self, parameters: Dict[str, Any], chain: str) -> Dict[str, def get_all_collections(self) -> Dict[str, Any]: """Get all collections created by the user. - + Returns: List of collections """ @@ -463,12 +468,12 @@ def get_all_collections(self) -> Dict[str, Any]: def mint_nft(self, collection_id: str, recipient: str, metadata: Dict[str, Any]) -> Dict[str, Any]: """Mint a new NFT in a collection. - + Args: collection_id: ID of the collection recipient: Recipient identifier (email:address:chain or chain:address) metadata: NFT metadata - + Returns: Minted NFT details """ @@ -481,11 +486,11 @@ def mint_nft(self, collection_id: str, recipient: str, metadata: Dict[str, Any]) def create_wallet_for_twitter(self, username: str, chain: str) -> Dict[str, Any]: """Create a wallet for a Twitter user. - + Args: username: Twitter username chain: Chain identifier - + Returns: Created wallet details """ @@ -498,11 +503,11 @@ def create_wallet_for_twitter(self, username: str, chain: str) -> Dict[str, Any] def create_wallet_for_email(self, email: str, chain: str) -> Dict[str, Any]: """Create a wallet for an email user. - + Args: email: Email address chain: Chain identifier - + Returns: Created wallet details """ @@ -515,11 +520,11 @@ def create_wallet_for_email(self, email: str, chain: str) -> Dict[str, Any]: def get_wallet_by_twitter_username(self, username: str, chain: str) -> Dict[str, Any]: """Get wallet details by Twitter username. - + Args: username: Twitter username chain: Chain identifier - + Returns: Wallet details """ @@ -528,12 +533,12 @@ def get_wallet_by_twitter_username(self, username: str, chain: str) -> Dict[str, def get_wallet_by_email(self, email: str, chain: str, timeout: Optional[float] = None) -> Dict[str, Any]: """Get wallet details by email. - + Args: email: Email address chain: Chain identifier timeout: Optional request timeout in seconds - + Returns: Wallet details """ @@ -541,13 +546,35 @@ def get_wallet_by_email(self, email: str, chain: str, timeout: Optional[float] = endpoint = f"/wallets/{quote(locator)}" return self._request(endpoint, timeout=timeout) + def fund_wallet(self, wallet_locator: str, token: SupportedToken, amount: int, chain: Optional[str] = None) -> Dict[str, any]: + """Fund a wallet with a specified amount of this token. + + Args: + wallet_address: Wallet address + token: Token to fund + amount: Amount of token to fund + chain: Chain to fund the wallet on + + Returns: + Fund request response + """ + endpoint = f"/wallets/{quote(wallet_locator)}/balances" + payload = { + "amount": amount, + "token": token, + } + if chain: + payload["chain"] = chain + print(f"Payload: {payload}") + return self._request(endpoint, method="POST", json=payload) + def request_faucet_tokens(self, wallet_address: str, chain_id: str) -> Dict[str, Any]: """Request tokens from faucet. - + Args: wallet_address: Wallet address chain_id: Chain identifier - + Returns: Faucet request response """ @@ -563,19 +590,20 @@ def register_delegated_signer( self, wallet_locator: str, signer: str, - chain: Optional[str] = None, # Only for EVM - expires_at: Optional[int] = None, # Only for EVM - permissions: Optional[List[DelegatedSignerPermission]] = None # Only for EVM + chain: Optional[str] = None, # Only for EVM + expires_at: Optional[int] = None, # Only for EVM + # Only for EVM + permissions: Optional[List[DelegatedSignerPermission]] = None ) -> Dict[str, Any]: """Register a delegated signer for a smart wallet. - + Args: wallet_locator: Wallet identifier signer: The locator of the delegated signer chain: Optional chain identifier expires_at: Optional expiry date in milliseconds since UNIX epoch permissions: Optional list of ERC-7715 permission objects - + Returns: Delegated signer registration response """ @@ -589,38 +617,69 @@ def register_delegated_signer( if expires_at: payload["expiresAt"] = str(expires_at) if permissions: - payload["permissions"] = [{"type": p.type, "value": p.value} for p in permissions] - + payload["permissions"] = [ + {"type": p.type, "value": p.value} for p in permissions] + return self._request(endpoint, method="POST", json=payload) - + + def get_balance(self, wallet_locator: str, tokens: List[SupportedToken], chains: List[str] = None) -> Dict[str, Any]: + """Get the balance of a wallet for a specific token. + + Args: + wallet_locator: Wallet identifier + tokens: List of token identifiers + chains: List of chain identifiers + + Returns: + + List[{ + "token": token, + "decimals": int, + "balances": { + : int, + : int, + ... + "total": int + } + }] + """ + query_params = { + "tokens": ",".join(tokens), + } + if chains: + query_params["chains"] = ",".join(chains) + + endpoint = f"/wallets/{quote(wallet_locator)}/balances" + return self._request(endpoint, method="GET", params=query_params) + def get_delegated_signer( self, wallet_locator: str, signer_locator: str ) -> Dict[str, Any]: """Get information about a delegated signer. - + Args: wallet_locator: Wallet identifier signer_locator: Signer locator string - + Returns: Delegated signer information """ endpoint = f"/wallets/{quote(wallet_locator)}/signers/{quote(signer_locator)}" return self._request(endpoint, method="GET") - + def wait_for_action(self, action_id: str, interval: float = 1.0, max_attempts: int = 60) -> Dict[str, Any]: """Wait for an action to complete. - + Args: action_id: Action ID to wait for interval: Time to wait between attempts in seconds max_attempts: Maximum number of attempts to check status - + Returns: Action response when completed - + Raises: Exception: If action times out or fails """ @@ -629,26 +688,26 @@ def wait_for_action(self, action_id: str, interval: float = 1.0, max_attempts: i attempts += 1 endpoint = f"/actions/{quote(action_id)}" response = self._request(endpoint) - + if response.get("status") == "succeeded": return response - + time.sleep(interval) - + raise Exception("Timed out waiting for action") def wait_for_transaction(self, locator: str, transaction_id: str, interval: float = 1.0, max_attempts: int = 60) -> Dict[str, Any]: """Wait for a transaction to complete. - + Args: locator: Wallet locator string transaction_id: Transaction ID to wait for interval: Time to wait between attempts in seconds max_attempts: Maximum number of attempts to check status - + Returns: Transaction response when completed - + Raises: Exception: If transaction times out or fails """ @@ -656,26 +715,26 @@ def wait_for_transaction(self, locator: str, transaction_id: str, interval: floa while attempts < max_attempts: attempts += 1 response = self.check_transaction_status(locator, transaction_id) - + if response["status"] in ["success", "completed", "failed"]: return response - + time.sleep(interval) - + raise Exception("Timed out waiting for transaction") def wait_for_signature(self, locator: str, signature_id: str, interval: float = 1.0, max_attempts: int = 60) -> Dict[str, Any]: """Wait for a signature request to complete. - + Args: locator: Wallet locator string signature_id: Signature ID to wait for interval: Time to wait between attempts in seconds max_attempts: Maximum number of attempts to check status - + Returns: Signature response when completed - + Raises: Exception: If signature request times out or fails """ @@ -683,40 +742,44 @@ def wait_for_signature(self, locator: str, signature_id: str, interval: float = while attempts < max_attempts: attempts += 1 response = self.check_signature_status(signature_id, locator) - + if response["status"] in ["success", "completed", "failed"]: return response - + time.sleep(interval) - + raise Exception("Timed out waiting for signature") - def create_wallet(self, wallet_type: str, linked_user: Optional[str] = None, config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + def create_wallet(self, wallet_type: str, linked_user: Optional[str] = None, config: Optional[Dict[str, Any]] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]: """Create a new wallet. - + Args: wallet_type: Type of wallet to create linked_user: Optional user identifier to link the wallet to config: Optional wallet configuration - + Returns: Created wallet details """ payload = { "type": wallet_type, - "linkedUser": linked_user } + headers = {} + if linked_user: + payload["linkedUser"] = linked_user if config: payload["config"] = config - return self._request("/wallets", method="POST", json=payload) + if idempotency_key: + headers["x-idempotency-key"] = idempotency_key + return self._request("/wallets", method="POST", json=payload, headers=headers) def create_wallet_for_phone(self, phone: str, chain: str) -> Dict[str, Any]: """Create a wallet for a phone number. - + Args: phone: Phone number chain: Chain identifier - + Returns: Created wallet details """ @@ -727,11 +790,11 @@ def create_wallet_for_phone(self, phone: str, chain: str) -> Dict[str, Any]: def create_wallet_for_user_id(self, user_id: str, chain: str) -> Dict[str, Any]: """Create a wallet for a user ID. - + Args: user_id: User identifier chain: Chain identifier - + Returns: Created wallet details """ diff --git a/python/src/wallets/crossmint/goat_wallets/crossmint/parameters.py b/python/src/wallets/crossmint/goat_wallets/crossmint/parameters.py index e0676dcbe..fdcc6998a 100644 --- a/python/src/wallets/crossmint/goat_wallets/crossmint/parameters.py +++ b/python/src/wallets/crossmint/goat_wallets/crossmint/parameters.py @@ -5,6 +5,7 @@ class BaseModelWithoutNone(BaseModel): """Base model that excludes None values from model_dump output.""" + def model_dump(self) -> Dict[str, Any]: # type: ignore """Convert model to dictionary, filtering out None values.""" data = super().model_dump() @@ -37,6 +38,21 @@ class AdminSigner(BaseModelWithoutNone): chain: Optional[str] = None +class SolanaFireblocksSigner(BaseModelWithoutNone): + """Configuration for Solana Fireblocks custodial signer.""" + type: Literal[CoreSignerType.SOLANA_FIREBLOCKS_CUSTODIAL] + address: str + + +class SolanaKeypairSigner(BaseModelWithoutNone): + """Configuration for Solana keypair signer.""" + type: Literal[CoreSignerType.SOLANA_KEYPAIR] + address: str + + +SolanaSmartWalletSigner = Union[SolanaFireblocksSigner, SolanaKeypairSigner] + + class CreateSmartWalletParameters(BaseModelWithoutNone): """Parameters for creating a smart wallet.""" admin_signer: Optional[AdminSigner] = Field( @@ -87,7 +103,8 @@ class Call(BaseModelWithoutNone): class SolanaSmartWalletTransactionParams(BaseModelWithoutNone): """Parameters for creating a Solana Smart Wallet transaction.""" - transaction: str = Field(description="Base58 encoded serialized Solana transaction") + transaction: str = Field( + description="Base58 encoded serialized Solana transaction") required_signers: Optional[List[str]] = Field( None, description="Optional array of additional signers required for the transaction" @@ -105,10 +122,12 @@ class EVMTypedData(BaseModel): domain: Dict[str, Any] message: Dict[str, Any] + class TransactionParams(BaseModelWithoutNone): """Parameters for transaction creation.""" calls: Optional[List[Call]] = None - chain: Optional[Literal["ethereum", "polygon", "avalanche", "arbitrum", "optimism", "base", "sepolia"]] = None + chain: Optional[Literal["ethereum", "polygon", "avalanche", + "arbitrum", "optimism", "base", "sepolia"]] = None signer: Optional[str] = None transaction: Optional[str] = None signers: Optional[List[str]] = None @@ -132,14 +151,17 @@ class TransactionApprovals(BaseModelWithoutNone): class SignMessageRequest(BaseModelWithoutNone): """Request parameters for message signing.""" - type: str = Field(description="Message type (evm-message or solana-message)") - params: Dict[str, Any] = Field(description="Message parameters including the message to sign") + type: str = Field( + description="Message type (evm-message or solana-message)") + params: Dict[str, Any] = Field( + description="Message parameters including the message to sign") class SignTypedDataRequest(BaseModelWithoutNone): """Request parameters for typed data signing.""" type: Literal["evm-typed-data"] - params: Dict[str, Any] = Field(description="Parameters including typed data and chain") + params: Dict[str, Any] = Field( + description="Parameters including typed data and chain") class SignatureResponse(BaseModelWithoutNone): @@ -166,8 +188,10 @@ class TransactionResponse(BaseModelWithoutNone): class CollectionMetadata(BaseModelWithoutNone): name: str = Field(description="The name of the collection") description: str = Field(description="A description of the NFT collection") - image: Optional[str] = Field(None, description="URL pointing to an image that represents the collection") - symbol: Optional[str] = Field(None, description="Shorthand identifier for the NFT collection (Max length: 10)") + image: Optional[str] = Field( + None, description="URL pointing to an image that represents the collection") + symbol: Optional[str] = Field( + None, description="Shorthand identifier for the NFT collection (Max length: 10)") class CollectionParameters(BaseModelWithoutNone): @@ -179,7 +203,8 @@ class CollectionParameters(BaseModelWithoutNone): symbol=None ) ) - fungibility: Literal["semi-fungible", "non-fungible"] = Field(default="non-fungible") + fungibility: Literal["semi-fungible", + "non-fungible"] = Field(default="non-fungible") transferable: bool = Field(default=True) @@ -192,12 +217,15 @@ class NFTMetadata(BaseModelWithoutNone): name: str = Field(description="The name of the NFT") description: str = Field(description="The description of the NFT") image: str = Field(description="URL pointing to the NFT image") - animation_url: Optional[str] = Field(None, description="URL pointing to the NFT animation") - attributes: Optional[List[NFTAttribute]] = Field(None, description="The attributes of the NFT") + animation_url: Optional[str] = Field( + None, description="URL pointing to the NFT animation") + attributes: Optional[List[NFTAttribute]] = Field( + None, description="The attributes of the NFT") class MintNFTParameters(BaseModelWithoutNone): - collection_id: str = Field(description="The ID of the collection to mint the NFT in") + collection_id: str = Field( + description="The ID of the collection to mint the NFT in") recipient: str = Field(description="The recipient of the NFT") recipient_type: Literal["wallet", "email"] = Field( default="email", @@ -236,7 +264,8 @@ class GetWalletByEmailParameters(BaseModelWithoutNone): class RequestFaucetTokensParameters(BaseModelWithoutNone): """Parameters for requesting tokens from faucet.""" - wallet_address: str = Field(description="The wallet address to receive tokens") + wallet_address: str = Field( + description="The wallet address to receive tokens") chain_id: str = Field(description="The chain ID for the faucet request") @@ -282,9 +311,11 @@ class CreateTransactionCustodialParameters(BaseModelWithoutNone): class CreateTransactionSmartParameters(BaseModelWithoutNone): """Parameters for creating a transaction with a smart wallet.""" wallet_address: str = Field(description="The wallet address") - calls: Optional[List[Call]] = Field(None, description="The transaction calls for EVM") + calls: Optional[List[Call]] = Field( + None, description="The transaction calls for EVM") chain: str = Field(description="The chain of the wallet") - transaction: Optional[str] = Field(None, description="Base58 encoded serialized Solana transaction") + transaction: Optional[str] = Field( + None, description="Base58 encoded serialized Solana transaction") signer: Optional[str] = Field(None, description="Optional signer address") @@ -298,7 +329,8 @@ class ApproveTransactionParameters(BaseModelWithoutNone): """Parameters for approving a transaction.""" locator: str = Field(description="The wallet locator") transaction_id: str = Field(description="The transaction ID") - approvals: List[ApprovalItem] = Field(description="List of transaction approvals") + approvals: List[ApprovalItem] = Field( + description="List of transaction approvals") class CheckTransactionStatusParameters(BaseModelWithoutNone): @@ -317,8 +349,10 @@ class RegisterDelegatedSignerParameters(BaseModelWithoutNone): """Parameters for registering a delegated signer.""" signer: str = Field(description="The locator of the delegated signer") chain: str = Field(description="Chain identifier") - expires_at: Optional[int] = Field(None, description="Optional expiry date in milliseconds since UNIX epoch") - permissions: Optional[List[DelegatedSignerPermission]] = Field(None, description="Optional list of ERC-7715 permission objects") + expires_at: Optional[int] = Field( + None, description="Optional expiry date in milliseconds since UNIX epoch") + permissions: Optional[List[DelegatedSignerPermission]] = Field( + None, description="Optional list of ERC-7715 permission objects") class GetDelegatedSignerParameters(BaseModelWithoutNone): 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 cea0032ad..875018bcc 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 @@ -10,50 +10,57 @@ from solders.message import Message from solana.rpc.api import Client as SolanaClient from solders.transaction import VersionedTransaction -from goat.classes.wallet_client_base import Balance, Signature +from goat.classes.wallet_client_base import Signature from goat_wallets.solana import SolanaWalletClient, SolanaTransaction from .api_client import CrossmintWalletsAPI from .parameters import SolanaSmartWalletTransactionParams from .base_wallet import BaseWalletClient, get_locator -from .types import LinkedUser, SolanaFireblocksSigner, SolanaKeypairSigner +from .types import LinkedUser, SolanaFireblocksSigner, SolanaKeypairSigner, SupportedToken, TokenBalance -# Type aliases for Solana-specific signers class SolanaSmartWalletConfig(TypedDict): """Configuration specific to Solana smart wallets.""" adminSigner: Union[SolanaKeypairSigner, SolanaFireblocksSigner] + class SolanaSmartWalletOptions(TypedDict): """Options specific to Solana smart wallets.""" config: SolanaSmartWalletConfig linkedUser: Optional[LinkedUser] + class SolanaSmartWalletOptionsWithLinkedUser(TypedDict): connection: SolanaClient config: SolanaSmartWalletConfig linkedUser: LinkedUser + class SolanaSmartWalletOptionsWithAddress(TypedDict): connection: SolanaClient config: SolanaSmartWalletConfig address: str + class TransactionApproval(TypedDict): signer: str signature: Optional[str] + class SolanaSmartWalletClient(SolanaWalletClient, BaseWalletClient): def __init__( self, address: str, api_client: CrossmintWalletsAPI, options: SolanaSmartWalletOptions, - connection: SolanaClient = SolanaClient("https://api.devnet.solana.com") + connection: SolanaClient = SolanaClient( + "https://api.devnet.solana.com") ): SolanaWalletClient.__init__(self, connection) BaseWalletClient.__init__(self, address, api_client, "solana") - self._locator = get_locator(address, options.get("linkedUser", None), "solana-smart-wallet") + self._locator = get_locator(address, options.get( + "linkedUser", None), "solana-smart-wallet") self._admin_signer = options["config"]["adminSigner"] + self._admin_signer["address"] = self._retrieve_admin_signer_address() def get_address(self) -> str: return self._address @@ -64,7 +71,8 @@ def sign_message( required_signers: Optional[List[str]] = None, signer: Optional[str] = None ) -> Signature: - raise UnsupportedOperationException("Sign message is not supported for Solana smart wallets") + raise UnsupportedOperationException( + "Sign message is not supported for Solana smart wallets") def send_transaction( self, @@ -79,31 +87,22 @@ def send_transaction( data=instruction.data ) instructions.append(instruction) - + message = Message( instructions=instructions, payer=Pubkey.from_string(self._address) ) versioned_transaction = VersionedTransaction( message, - [NullSigner(Pubkey.from_string(self._address))]+[NullSigner(signer.pubkey()) for signer in additional_signers] + [NullSigner(Pubkey.from_string(self._address))] + + [NullSigner(signer.pubkey()) for signer in additional_signers] ) - - serialized = base58.b58encode(bytes(versioned_transaction)).decode() + serialized = base58.b58encode(bytes(versioned_transaction)).decode() return self.send_raw_transaction(serialized, additional_signers, transaction.get("signer", None)) - def balance_of(self, address: str) -> Balance: - pubkey = Pubkey.from_string(address) - balance = self.client.get_balance(pubkey) - - return Balance( - value=str(balance.value / 10**9), - in_base_units=str(balance.value), - decimals=9, - symbol="SOL", - name="Solana" - ) + def balance_of(self, tokens: List[SupportedToken]) -> List[TokenBalance]: + return self._client.get_balance(self._address, tokens) def handle_approvals( self, @@ -112,12 +111,12 @@ def handle_approvals( signers: List[Keypair] ) -> None: """Send approval signatures for a pending transaction. - + Args: transaction_id: The ID of the transaction to approve pending_approvals: The pending approvals signers: The signers to approve the transaction - + Raises: ValueError: If signature generation or approval submission fails """ @@ -125,18 +124,22 @@ def handle_approvals( approvals = [] for pending_approval in pending_approvals: signer = next( - (s for s in signers if str(s.pubkey()) in pending_approval["signer"]), + (s for s in signers if str(s.pubkey()) + in pending_approval["signer"]), None ) if not signer: - raise ValueError(f"Signer not found for approval: {pending_approval['signer']}. Available signers: {[str(s.pubkey()) for s in signers]}") - signature = signer.sign_message(base58.b58decode(pending_approval["message"])) - encoded_signature = base58.b58encode(signature.to_bytes()).decode() + raise ValueError( + f"Signer not found for approval: {pending_approval['signer']}. Available signers: {[str(s.pubkey()) for s in signers]}") + signature = signer.sign_message( + base58.b58decode(pending_approval["message"])) + encoded_signature = base58.b58encode( + signature.to_bytes()).decode() approvals.append({ "signer": "solana-keypair:" + base58.b58encode(bytes(signer.pubkey())).decode(), "signature": encoded_signature }) - + self._client.approve_transaction( self._locator, transaction_id, @@ -152,15 +155,15 @@ def handle_transaction_flow( error_prefix: str = "Transaction" ) -> Dict[str, Any]: """Handle the transaction approval flow and monitor transaction status until completion. - + Args: transaction_id: The ID of the transaction to monitor signers: Array of keypairs that can be used for signing approvals error_prefix: Prefix for error messages - + Returns: The successful transaction data - + Raises: ValueError: If the transaction fails or remains in awaiting-approval state """ @@ -169,7 +172,7 @@ def handle_transaction_flow( self._locator, transaction_id ) - + # Handle approvals if needed if status["status"] == "awaiting-approval": pending_approvals = status["approvals"]["pending"] @@ -179,26 +182,27 @@ def handle_transaction_flow( pending_approvals, signers ) - + # Wait for transaction success while status["status"] != "success": status = self._client.check_transaction_status( self._locator, transaction_id ) - + if status["status"] == "failed": error = status.get("error", {}) raise ValueError(f"{error_prefix} failed: {error}") - + if status["status"] == "awaiting-approval": - raise ValueError(f"{error_prefix} still awaiting approval after submission") - + raise ValueError( + f"{error_prefix} still awaiting approval after submission") + if status["status"] == "success": break - + sleep(1) - + return status def send_raw_transaction( @@ -218,7 +222,7 @@ def send_raw_transaction( self._address, params, ) - + # Prepare signers array signers = additional_signers if self._admin_signer["type"] == "solana-keypair": @@ -226,30 +230,31 @@ def send_raw_transaction( if signer: signers.append(signer) signers.extend(additional_signers) - + # Handle transaction flow completed_transaction = self.handle_transaction_flow( response["id"], signers ) - + return { "hash": completed_transaction.get("onChain", {}).get("txId", "") } - + except Exception as e: - raise ValueError(f"Failed to create or process transaction: {str(e)}") + raise ValueError( + f"Failed to create or process transaction: {str(e)}") def register_delegated_signer( self, signer: str, ) -> Dict[str, Any]: """Register a delegated signer for this wallet. - + Args: signer: The locator of the delegated signer expires_at: Optional expiry date in milliseconds since UNIX epoch - + Returns: Delegated signer registration response """ @@ -258,16 +263,16 @@ def register_delegated_signer( self._locator, signer, ) - + # For Solana non-custodial delegated signers, we need to handle the transaction approval if 'transaction' in response and response['transaction']: transaction_id = response['transaction']['id'] - + # For delegated signer registration, only the admin signer is needed signers = [] if self._admin_signer["type"] == "solana-keypair": signers.append(self._admin_signer["keyPair"]) - + # Handle transaction flow self.handle_transaction_flow( transaction_id, @@ -278,41 +283,69 @@ def register_delegated_signer( return response except Exception as e: raise ValueError(f"Failed to register delegated signer: {str(e)}") - + def get_delegated_signer(self, signer_locator: str) -> Dict[str, Any]: """Get information about a delegated signer. - + Args: signer_locator: Signer locator string - + Returns: Delegated signer information """ return self._client.get_delegated_signer(self._locator, signer_locator) + def get_admin_signer_address(self) -> str: + return self._admin_signer["address"] + + def fund_wallet(self, token: SupportedToken, amount: int): + """Fund the wallet with a specified amount of this token. + + Args: + amount: The amount of SOL to fund the wallet with + """ + return self._client.fund_wallet(self._address, token, amount) + + def request_airdrop(self, amount_lamports: int): + """Request an airdrop for the wallet. + + Args: + amount: The amount of SOL to fund the wallet with + """ + return self.connection.request_airdrop(Pubkey.from_string(self._address), amount_lamports) + + def _retrieve_admin_signer_address(self) -> str: + wallet = self._client.get_wallet(self._address) + address = wallet.get("config", {}).get( + "adminSigner", {}).get("address", None) + if not address: + raise ValueError( + f"Admin signer address not found for wallet {self._address}") + return address + @staticmethod def derive_address_from_secret_key(secret_key: str) -> str: """Derive a Solana address from a base58-encoded secret key. - + Args: secret_key: Base58-encoded secret key string - + Returns: Base58-encoded public key (address) string - + Raises: ValueError: If the secret key is invalid """ try: # Decode the base58 secret key decoded_key = base58.b58decode(secret_key) - + # Create signing key from bytes signing_key = nacl.signing.SigningKey(decoded_key) - + # Get verify key (public key) verify_key = signing_key.verify_key - + # Convert to bytes and encode as base58 public_key_bytes = bytes(verify_key) return base58.b58encode(public_key_bytes).decode() diff --git a/python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet_factory.py b/python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet_factory.py index f71b40803..97db2f935 100644 --- a/python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet_factory.py +++ b/python/src/wallets/crossmint/goat_wallets/crossmint/solana_smart_wallet_factory.py @@ -1,72 +1,132 @@ -from typing import Dict, Optional, cast -from solders.pubkey import Pubkey +from typing import Optional, TypedDict, get_type_hints from solana.rpc.api import Client as SolanaClient from .api_client import CrossmintWalletsAPI -from .parameters import WalletType, AdminSigner -from .solana_smart_wallet import SolanaSmartWalletClient, LinkedUser, SolanaSmartWalletOptions -from .base_wallet import get_locator - -# This should be enforced by the type, not by a successive if-else -def generate_user_locator(linked_user: LinkedUser) -> str: - if "email" in linked_user: - return f"email:{linked_user['email']}" - elif "phone" in linked_user: - return f"phone:{linked_user['phone']}" - elif "userId" in linked_user: - return f"userId:{linked_user['userId']}" - elif "twitter" in linked_user: - return f"x:{linked_user['twitter']}" - else: - raise ValueError("Invalid linked user") - -def create_wallet(api_client: CrossmintWalletsAPI, config: Optional[Dict] = None) -> Dict: - admin_signer = None - user_locator = generate_user_locator(config["linkedUser"]) - - try: - wallet = api_client.create_smart_wallet( - WalletType.SOLANA_SMART_WALLET, - admin_signer, - user_locator - ) - return wallet - except Exception as e: - raise ValueError(f"Failed to create Solana Smart Wallet: {str(e)}") - -def solana_smart_wallet_factory(api_client: CrossmintWalletsAPI): - def create_smart_wallet(options: Dict) -> SolanaSmartWalletClient: - print(f"Creating smart wallet with options: {options}") - linked_user: Optional[LinkedUser] = None - if "linkedUser" in options: - linked_user = cast(LinkedUser, options["linkedUser"]) - elif any(key in options for key in ["email", "phone", "userId"]): - linked_user = {} - if "email" in options: - linked_user["email"] = options["email"] - elif "phone" in options: - linked_user["phone"] = options["phone"] - else: - linked_user["userId"] = options["userId"] - - locator = get_locator(options.get("address"), linked_user, "solana-smart-wallet") - +from .parameters import AdminSigner, CoreSignerType +from .solana_smart_wallet import SolanaSmartWalletClient, SolanaSmartWalletConfig, SolanaSmartWalletOptions +from .types import SolanaFireblocksSigner, SolanaKeypairSigner +import sys +import os + + +class UserLocatorParams(TypedDict, total=False): + linkedUser: str + email: str + phone: str + userId: str + twitter: str + x: str + + +user_locator_identifier_fields = get_type_hints(UserLocatorParams) + + +class SolanaSmartWalletCreationParams(TypedDict, UserLocatorParams, total=False): + config: SolanaSmartWalletConfig + + +class SolanaSmartWalletFactory: + def __init__(self, api_client: CrossmintWalletsAPI, connection: Optional[SolanaClient] = None): + self.api_client = api_client + self.connection = connection + if not self.connection: + connection_url = os.getenv("SOLANA_RPC_ENDPOINT", None) + default_connection_url = "https://api.devnet.solana.com" + if (connection_url is None): + print( + f"Environment variable SOLANA_RPC_ENDPOINT is not set, using default endpoint: {default_connection_url}", file=sys.stderr) + self.connection = SolanaClient(connection_url) + + def get_or_create(self, config: SolanaSmartWalletCreationParams, idempotency_key: Optional[str] = None) -> SolanaSmartWalletClient: + if self.connection is None: + raise ValueError( + f"Connection is not set, call {self.__class__.__name__}.set_connection() first") + + validated_params = self._validate_creation_params(config) + wallet_locator = self._get_wallet_locator( + validated_params["linkedUser"]) + try: + wallet = self._get_wallet(wallet_locator) + if wallet: + print("Wallet found! Returning existing wallet", file=sys.stderr) + return self._instantiate_wallet(wallet["address"], validated_params["config"]["adminSigner"]) + except Exception as e: + print("Wallet not found, creating new wallet", file=sys.stderr) + try: - print(f"Getting wallet with locator: {locator}") - wallet = api_client.get_wallet(locator) - except Exception: - print(f"Creating wallet with options: {options}") - wallet = create_wallet(api_client, { - "adminSigner": options["adminSigner"], - "linkedUser": locator - }) - - print(f"Returning wallet: {wallet}") + wallet = self.api_client.create_wallet( + "solana-smart-wallet", validated_params["linkedUser"], self._project_config_to_api_params(validated_params["config"]), idempotency_key) + return self._instantiate_wallet(wallet["address"], validated_params["config"]["adminSigner"]) + except Exception as e: + raise Exception( + f"Failed to create wallet: {e}") from e + + def set_connection(self, connection: SolanaClient): + self.connection = connection + + def _get_wallet(self, wallet_locator: str): + """Internal method to get wallet.""" + return self.api_client.get_wallet(wallet_locator) + + def _get_linked_user_from_config(self, config: SolanaSmartWalletCreationParams) -> str | None: + """Internal method to extract linked user from config.""" + present_fields = [ + field for field in user_locator_identifier_fields if field in config] + if len(present_fields) > 1: + raise ValueError( + f"Exactly one identifier field among {user_locator_identifier_fields} must be present. Found: {present_fields}" + ) + if len(present_fields) == 1: + present_field = present_fields[0] + if present_field == "linkedUser": + return config["linkedUser"] + return f"{present_fields[0]}:{config[present_fields[0]]}" + return None + + def _instantiate_wallet(self, address: str, admin_signer: AdminSigner) -> SolanaSmartWalletClient: + """Internal method to create wallet instance.""" return SolanaSmartWalletClient( - wallet["address"], - api_client, - { - "adminSigner": options["adminSigner"], - } + address, + self.api_client, + {"config": {"adminSigner": admin_signer}}, + self.connection ) - - return create_smart_wallet + + def _get_wallet_locator(self, linked_user: str) -> str: + """Internal method to generate wallet locator.""" + return f"{linked_user}:solana-smart-wallet" + + def _validate_creation_params(self, params: SolanaSmartWalletCreationParams) -> SolanaSmartWalletOptions: + """Internal method to validate parameters.""" + present_fields = [ + field for field in user_locator_identifier_fields if field in params] + + if len(present_fields) > 1: + raise ValueError( + f"At most one identifier among {user_locator_identifier_fields} must be present. Found: {present_fields}" + ) + + return SolanaSmartWalletOptions( + config=params["config"], + linkedUser=self._get_linked_user_from_config(params) + ) + + def _project_config_to_api_params(self, config: SolanaSmartWalletConfig): + admin_signer = config.get("adminSigner") + if not admin_signer: + return {} + type = admin_signer.get("type") + if type == CoreSignerType.SOLANA_FIREBLOCKS_CUSTODIAL: + return { + "adminSigner": { + "type": "solana-fireblocks-custodial", + } + } + if type == CoreSignerType.SOLANA_KEYPAIR: + return { + "adminSigner": { + "type": "solana-keypair", + "address": str(config["adminSigner"]["keyPair"].pubkey()), + } + } + raise ValueError( + f"Invalid admin signer type: {type}") diff --git a/python/src/wallets/crossmint/goat_wallets/crossmint/types.py b/python/src/wallets/crossmint/goat_wallets/crossmint/types.py index 62a5ad1bb..25715130c 100644 --- a/python/src/wallets/crossmint/goat_wallets/crossmint/types.py +++ b/python/src/wallets/crossmint/goat_wallets/crossmint/types.py @@ -1,33 +1,61 @@ from typing import NotRequired, TypedDict, Optional, Dict, Union, Literal from solders.keypair import Keypair +SupportedToken = Literal[ + 'ape', 'eth', 'matic', 'pol', 'sei', 'chz', 'avax', 'xai', 'fuel', 'vic', + 'ip', 'zcx', 'usdc', 'usdce', 'busd', 'usdxm', 'weth', 'degen', 'brett', + 'toshi', 'eurc', 'superverse', 'pirate', 'bonk', 'trump', 'fartcoin', 'giga', + 'moodeng', 'jailstool', 'wen', 'mlg', 'duo', 'pep', 'harambe', 'usedcar', + 'vine', 'fartboy', 'pnut', 'stonks', 'mew', 'baby', 'michi', 'butthole', + 'anglerfish', 'usa', 'chillguy', 'sigma', 'maneki', 'purpe', 'lockin', 'y2k', + 'fafo', 'nub', 'fullsend', 'shoggoth', 'mini', 'llm', 'sc', 'fatgf', 'pwease', + 'popcat', 'spx', 'fwog', 'mother', 'wif', 'fric', 'etf', 'gyat', 'bigballs', + 'goat', 'stupid', 'duko', 'bitcoin', 'buttcoin', 'mcdull', 'skbdi', 'elon4afd', + 'mumu', 'gme', 'biao', 'fred', 'pengu', 'asscoin', 'bhad', 'habibi', 'quant', + 'hammy', 'boden', 'dolan', 'nap', 'scf', 'titcoin', 'sol', 'ada', 'bnb', 'sui', + 'apt', 'sfuel' +] +TokenBalance = TypedDict('TokenBalance', { + 'token': SupportedToken, + 'decimals': int, + 'balances': Dict[str, any], +}) + + class LinkedUser(TypedDict): """Type definition for a linked user.""" email: NotRequired[str] phone: NotRequired[str] userId: NotRequired[int] + class TransactionApproval(TypedDict): """Type definition for a transaction approval.""" signer: str signature: Optional[str] + class BaseFireblocksSigner(TypedDict): """Base type for Fireblocks-based signers.""" - type: Literal["solana-fireblocks-custodial", "evm-fireblocks-custodial"] # Add all possible values + type: Literal["solana-fireblocks-custodial", + "evm-fireblocks-custodial"] # Add all possible values + class SolanaKeypairSigner(TypedDict): type: Literal["solana-keypair"] keyPair: Keypair + class SolanaFireblocksSigner(TypedDict): type: Literal["solana-fireblocks-custodial"] + class BaseWalletConfig(TypedDict): """Base configuration for any wallet type.""" adminSigner: Union[SolanaKeypairSigner, SolanaFireblocksSigner] + class BaseWalletOptions(TypedDict): """Base options for any wallet type.""" config: BaseWalletConfig - linkedUser: Optional[LinkedUser] \ No newline at end of file + linkedUser: Optional[LinkedUser] From 14a2e7351f54fddad0ba869b86864690d03798c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Garc=C3=ADa=20Planes?= Date: Thu, 20 Mar 2025 17:54:20 +0100 Subject: [PATCH 2/2] oops --- .../solana_smart_wallet_creation_examples.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python/examples/by-wallet/crossmint/solana_smart_wallet_creation_examples.py b/python/examples/by-wallet/crossmint/solana_smart_wallet_creation_examples.py index 819088d3f..ffce44067 100644 --- a/python/examples/by-wallet/crossmint/solana_smart_wallet_creation_examples.py +++ b/python/examples/by-wallet/crossmint/solana_smart_wallet_creation_examples.py @@ -65,8 +65,8 @@ def main(): factory = SolanaSmartWalletFactory(api_client, connection) # Signer configurations - # create_wallet(factory, "solana-keypair") - # create_wallet(factory, "solana-fireblocks-custodial") + create_wallet(factory, "solana-keypair") + create_wallet(factory, "solana-fireblocks-custodial") # Idempotency key configurations (both requests will return the same wallet) idempotency_key = str(uuid.uuid4()) @@ -76,12 +76,12 @@ def main(): idempotency_key=idempotency_key) # Linked user configurations. Creations with the same linked user will return the same wallet. - # create_wallet(factory, "solana-keypair", "email:example@example.com") - # create_wallet(factory, "solana-fireblocks-custodial", - # "phoneNumber:+1234567890") - # create_wallet(factory, "solana-keypair", "twitter:example") - # create_wallet(factory, "solana-fireblocks-custodial", "userId:1234567890") - # create_wallet(factory, "solana-keypair", "x:example") + create_wallet(factory, "solana-keypair", "email:example@example.com") + create_wallet(factory, "solana-fireblocks-custodial", + "phoneNumber:+1234567890") + create_wallet(factory, "solana-keypair", "twitter:example") + create_wallet(factory, "solana-fireblocks-custodial", "userId:1234567890") + create_wallet(factory, "solana-keypair", "x:example") if __name__ == "__main__":