SKILL.md
name: godot-solana-sdk description: > Use this skill when building Solana-enabled games or apps in Godot 4 with the Godot Solana SDK. Triggers include: Solana transactions in GDScript, SPL tokens or NFTs in a Godot project, connecting browser or mobile wallets (Phantom, Solflare, Backpack) in a Godot game, calling Anchor smart contracts from GDScript, working with Candy Machine in Godot, or any mention of SolanaClient, Transaction, Pubkey, Keypair, WalletAdapter, or AnchorProgram in a Godot context. Do NOT use for non-Godot Solana SDKs (solana-py, solana-go, solana-web3.js).
Godot Solana SDK
A GDExtension (C++) plugin for Godot 4 that brings full Solana blockchain support to game development. Write GDScript to send transactions, manage wallets, mint NFTs, and interact with Anchor programs — on Windows, Linux, macOS, Web, Android, and iOS.
- Repo: https://github.com/Virus-Axel/godot-solana-sdk
- Docs: https://zenwiki.gitbook.io/solana-godot-sdk-docs
- Demo: https://zenrepublic.github.io/GodotSolanaSDKDemos/
- Asset Library: https://godotengine.org/asset-library/asset/3232
- Discord: https://discord.gg/9aFDCvqPgt
Security: Mainnet use is not yet security-audited. Crypto can be stolen. Use with care.
Installation
Via Godot Asset Library (recommended)
- Search for "Solana" in the Godot Asset Library and install.
- Install must go into the
addons/folder at the project root. - Go to Project → Project Settings → Plugins and enable SolanaSDK.
- Verify
SolanaServiceappears under Project Settings → Autoload.
Via GitHub Releases (manual)
Download binaries from the Releases page,
place them under res://bin/, and reload the project.
Configuration
After enabling the plugin, SolanaService is automatically added as an autoload singleton.
- RPC cluster: Select Mainnet or Devnet in the SolanaService inspector.
Provide a custom RPC URL (e.g. from Helius) for better performance.
The URL is also read from
Project Settings → solana_sdk/client/default_url. - Wallet: The
WalletServicechild node handles auth.- Use Generated — deterministic wallet seeded from your machine ID (testing only).
- Custom Wallet — path to a JSON file with the private key as a 64-byte array.
Core Nodes
| Node | Inherits | Purpose |
|---|---|---|
SolanaClient |
Node |
Low-level RPC calls to the Solana network |
Transaction |
SolanaClient |
Build, sign, and send transactions |
WalletAdapter |
Node |
Browser / mobile wallet integration |
Account |
Node |
Mirror of an on-chain account, auto-syncs |
AccountFetcher |
SolanaClient |
Bulk-fetch a list of Account nodes |
SystemProgram |
Node |
SOL transfers and account creation |
TokenProgram |
Node |
SPL Token instructions |
AssociatedTokenAccountProgram |
Node |
Create Associated Token Accounts |
ComputeBudget |
Node |
Set compute unit limits and priority fees |
MplCandyMachine |
Node |
Candy Machine v3 minting |
MplCandyGuard |
Node |
Candy Guard configuration |
MplTokenMetadata |
Node |
Metaplex token metadata |
AnchorProgram |
Node |
Generic interface for any Anchor program |
SolanaUtils |
Node |
Base58/Base64 encoding, hashing utilities |
Core Resources
| Resource | Purpose |
|---|---|
Pubkey |
32-byte Solana public key / address |
Keypair |
ed25519 keypair for signing |
Instruction |
A single transaction instruction |
AccountMeta |
Account metadata entry in an instruction |
CandyMachineData |
Candy Machine configuration |
Pubkey
# Create from base58 string
var pk: Pubkey = Pubkey.new_from_string("78GVwUb8ojcJVrEVkwCU5tfUKTfJuiazRrysGwgjqsif")
# Create from bytes
var pk: Pubkey = Pubkey.new_from_bytes(some_packed_byte_array)
# Derive a PDA (searches for valid bump, returns null on failure)
var program_key := Pubkey.new_from_string("CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR")
var pda: Pubkey = Pubkey.new_pda(["Level1"], program_key)
# Derive a PDA from raw byte seeds
var pda: Pubkey = Pubkey.new_pda_bytes([some_pubkey.to_bytes()], program_key)
# Derive Associated Token Address
var ata: Pubkey = Pubkey.new_associated_token_address(owner, mint, TokenProgram.get_pid())
# Random pubkey (for testing)
var pk: Pubkey = Pubkey.new_random()
# Convert
pk.to_string() # base58 String
pk.to_bytes() # PackedByteArray (32 bytes)
Pubkey.new_pdafinds the canonical valid PDA (searches for bump).Pubkey.new_program_addressdoes not search — it constructs from exact seeds. Usenew_pdafor normal PDA derivation.
Keypair
# Generate random keypair
var kp: Keypair = Keypair.new_random()
# From a deterministic seed
var seed = PackedByteArray(); seed.resize(32)
var kp: Keypair = Keypair.new_from_seed(seed)
# From Phantom private key (base58 string of 64 bytes)
var kp: Keypair = Keypair.new_from_bytes("3wUbDHMtMVQ...")
# From CLI-style JSON file
var kp: Keypair = Keypair.new_from_file("res://payer.json")
# Save to JSON file
kp.save_to_file("keypair.json")
# Inspect keys
kp.get_public_string() # base58 String
kp.get_public_bytes() # PackedByteArray (32 bytes)
kp.get_private_bytes() # PackedByteArray (64 bytes)
kp.to_pubkey() # Pubkey resource
# Sign and verify
var sig: PackedByteArray = kp.sign_message("hello".to_ascii_buffer())
var ok: bool = kp.verify_signature(sig, "hello".to_ascii_buffer())
SolanaClient (RPC)
Important:
SolanaClientmust be added as a child node (add_child(client)) for async HTTP requests to work. Remove and free it when done.
Signals:
http_request_completed(error: Error, response: Dictionary)
socket_response_received
var client: SolanaClient = SolanaClient.new()
add_child(client)
# Configure (optional — reads from Project Settings by default)
client.set_url_override("https://api.devnet.solana.com")
client.set_commitment("confirmed") # "confirmed" | "finalized" | "processed"
# Make an RPC call, then await the signal
client.get_account_info("4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZAMdL4VZHirAn")
var response: Dictionary = (await client.http_request_completed)[1]
if response.has("result"):
print(response["result"])
remove_child(client)
client.queue_free()
RPC methods (all void — listen to http_request_completed):
get_account_info(account)
get_balance(account)
get_latest_blockhash()
get_minimum_balance_for_rent_extemption(data_size)
get_transaction(signature)
get_signature_statuses(signatures, search_history)
get_signature_for_address(address, before, until)
get_token_account_balance(token_account)
get_token_accounts_by_owner(owner, mint, program_id)
get_token_supply(token_mint)
get_program_accounts(program_address, filters, with_context)
get_multiple_accounts(accounts)
request_airdrop(address, lamports)
send_transaction(encoded_transaction, max_retries, skip_preflight)
simulate_transaction(encoded_transaction, sig_verify, replace_blockhash, ...)
DAS (Digital Asset Standard) methods:
get_asset(asset_id)
get_asset_proof(asset_id)
get_assets_by_owner(owner, page, limit, show_fungible)
get_assets_by_authority(authority, page, limit)
get_assets_by_creator_address(creator, only_verified, page, limit)
get_assets_by_group(group_key, group_value, page, limit)
WebSocket subscriptions:
client.account_subscribe(account_key, Callable(self, "_on_account_changed"))
client.signature_subscribe(signature, Callable(self, "_on_confirmed"), "finalized")
client.program_subscribe(program_id, Callable(self, "_on_program_change"))
client.unsubscribe_all(callback) # call when done; keep client in tree while subscribed
Transaction
Workflow: set_payer → add_instruction → update_latest_blockhash → sign_and_send
Signals:
transaction_response_received(response: Dictionary)
transaction_simulation_failed(message_and_data: Array)
processed
confirmed
finalized
fully_signed
blockhash_updated
var payer: Keypair = Keypair.new_from_file("res://payer.json")
var receiver: Pubkey = Pubkey.new_from_string("78GVwUb8ojcJVrEVkwCU5tfUKTfJuiazRrysGwgjqsif")
var tx = Transaction.new()
add_child(tx) # must be in scene tree
tx.set_payer(payer)
tx.add_instruction(SystemProgram.transfer(payer, receiver, 500_000)) # 0.0005 SOL
tx.update_latest_blockhash()
tx.sign_and_send()
var response = await tx.transaction_response_received
if response.has("result"):
print("Signature:", response["result"])
# Await confirmation stages
await tx.confirmed
await tx.finalized
Other useful methods:
tx.set_instructions(instructions_array)
tx.set_signers([payer, another_keypair])
tx.sign() # signs without sending
tx.send() # sends without signing
tx.serialize() -> PackedByteArray
tx.get_message_hash() -> String
# Reconstruct from bytes (e.g. after passing serialized tx to wallet)
var new_tx = Transaction.new_from_bytes(tx.serialize())
new_tx.set_signers([payer]) # signers are not stored in bytes
new_tx.update_latest_blockhash()
new_tx.sign()
new_tx.send()
Handle simulation failures:
var data = await tx.transaction_simulation_failed
push_error("Simulation failed: " + data[0])
print("Logs:", data[1].get("logs", []))
SystemProgram
# Transfer SOL
SystemProgram.transfer(sender, receiver, lamports) -> Instruction
# Create a new on-chain account
SystemProgram.create_account(from_keypair, new_account_keypair, lamports, space_bytes, owner_program) -> Instruction
# Create account with seed
SystemProgram.create_account_with_seed(from, base, seed, lamports, space, owner) -> Instruction
# Well-known pubkeys
SystemProgram.get_pid()
SystemProgram.get_sysvar_clock_pubkey()
SystemProgram.get_sysvar_rent_pubkey()
# ... and all other sysvar pubkeys
TokenProgram (SPL Token)
Use
transfer_checked(requires mint + decimals), nottransfer.
# Initialize a new mint (MINT_SIZE = 82 bytes)
TokenProgram.initialize_mint(mint_keypair, mint_authority, freeze_authority, decimals) -> Instruction
# Initialize a token account (MINT_ACCOUNT_SIZE = 165 bytes)
TokenProgram.initialize_account(account_keypair, mint_keypair, owner) -> Instruction
# Mint tokens
TokenProgram.mint_to(mint, account, owner, mint_authority, amount) -> Instruction
# Transfer with decimals verification
TokenProgram.transfer_checked(source, mint, destination, source_authority, amount, decimals) -> Instruction
# Freeze a token account
TokenProgram.freeze_account(account, mint, freeze_authority) -> Instruction
# Change authority
TokenProgram.set_authority(mint, current_authority, new_authority, authority_type) -> Instruction
# authority_type: 1=MintTokens, 2=FreezeAccount, 3=AccountOwner, 4=CloseAccount
TokenProgram.get_pid() -> Pubkey
Create Mint + Initialize (full example)
const MINT_SIZE := 82
func create_token_mint(payer: Keypair) -> Keypair:
var client = SolanaClient.new()
add_child(client)
client.get_minimum_balance_for_rent_extemption(MINT_SIZE)
var lamports = (await client.http_request_completed)[1]["result"]
remove_child(client); client.queue_free()
var mint_kp: Keypair = Keypair.new_random()
var tx := Transaction.new()
add_child(tx)
tx.add_instruction(SystemProgram.create_account(payer, mint_kp, lamports, MINT_SIZE, TokenProgram.get_pid()))
tx.add_instruction(TokenProgram.initialize_mint(mint_kp, payer, payer, 0))
tx.set_payer(payer)
tx.update_latest_blockhash()
tx.sign_and_send()
await tx.transaction_response_received
await tx.confirmed
return mint_kp
AssociatedTokenAccountProgram
# Derive the ATA address (does not create it)
var ata: Pubkey = Pubkey.new_associated_token_address(owner, mint, TokenProgram.get_pid())
# Create the ATA (instruction — add to transaction)
AssociatedTokenAccountProgram.create_associated_token_account(payer, owner, mint, TokenProgram.get_pid()) -> Instruction
AssociatedTokenAccountProgram.get_pid() -> Pubkey
AssociatedTokenAccountProgramdoes not haveget_associated_token_address. UsePubkey.new_associated_token_addressto derive the address.
ComputeBudget
# Add to transaction before other instructions
tx.add_instruction(ComputeBudget.set_compute_unit_price(50_000)) # micro-lamports
tx.add_instruction(ComputeBudget.set_compute_unit_limit(200_000))
Transactions already have default
unit_limit = 800000andunit_price = 8000built in. UseComputeBudgetinstructions when you need explicit control over priority fees.
WalletAdapter (Browser / Mobile)
Primarily intended for web/browser platform exports. Desktop and mobile require tweaking.
# WalletAdapter node must be in the scene tree.
$WalletAdapter.set_wallet_type(WalletAdapter.PHANTOM) # PHANTOM, SOLFLARE, BACKPACK, LEDGER, ...
$WalletAdapter.connect_wallet()
await $WalletAdapter.connection_established
var pubkey: Pubkey = $WalletAdapter.get_connected_key()
# Use WalletAdapter as payer — triggers wallet popup on sign
$Transaction.set_payer($WalletAdapter)
$Transaction.add_instruction(SystemProgram.transfer($WalletAdapter, Pubkey.new_random(), 1_000_000))
$Transaction.update_latest_blockhash()
$Transaction.sign()
await $Transaction.fully_signed
$Transaction.send()
var response = await $Transaction.transaction_response_received
Sign a text message:
$WalletAdapter.sign_text_message("Hello Godot")
var signature: PackedByteArray = await $WalletAdapter.message_signed # 64 bytes
Supported wallets (enum values):
PHANTOM, SOLFLARE, BACKPACK, LEDGER, TREZOR, TRUST, WALLETCONNECT, COINBASE, NIGHTLY,
COIN98, ALPHA, AVANA, BITKEEP, BITPIE, CLOVER, COINHUB, FRACTAL, HUOBI, HYPERPAY,
KEYSTONE, KRYSTAL, MATH, NEKO, NUFI, ONTO, PARTICLE, SAFEPAL, SAIFU, SALMON, SKY,
SOLONG, SPOT, TOKENARY, TOKENPOCKET, TORUS, UNSAFEBURNER, XDEFI
AnchorProgram
Interact with any Anchor smart contract by loading its IDL.
Setup (prefer inspector over code):
- Add an
AnchorProgramnode to your scene. - Set
pidto the program's base58 address. - Assign the IDL JSON resource to
json_fileand settry_from_json_file = true, or fetch it from the chain withtry_from_pid = true.
Signals:
idl_fetched
account_fetched(account_data: Dictionary)
all_account_fetched
Call an instruction:
# Inspect anchor_program.idl in the Godot inspector to verify account order and arg types.
var program_key := Pubkey.new_from_string(PROGRAM_ID)
var level_pda := Pubkey.new_pda(["Level1"], program_key)
var vault_pda := Pubkey.new_pda(["Vault1"], program_key)
var ix: Instruction = $AnchorProgram.build_instruction(
"restartLevel", # exact instruction name from IDL
[
level_pda, # accounts in IDL order
vault_pda,
SolanaService.wallet.get_kp(), # signer
SystemProgram.get_pid() # system program
],
AnchorProgram.u64(int(prize_sol * pow(10, 9)))
)
var tx := Transaction.new()
add_child(tx)
tx.set_payer(SolanaService.wallet.get_kp())
tx.add_instruction(ix)
tx.update_latest_blockhash()
tx.sign_and_send()
var result = await tx.transaction_response_received
Typed argument helpers:
AnchorProgram.u8(val) AnchorProgram.u16(val) AnchorProgram.u32(val) AnchorProgram.u64(val)
AnchorProgram.s8(val) AnchorProgram.s16(val) AnchorProgram.s32(val) AnchorProgram.s64(val)
AnchorProgram.f32(val) AnchorProgram.f64(val) AnchorProgram.option(val)
Fetch an account:
$AnchorProgram.fetch_account("CandyMachine", "2kJKEGhqGXJJtoWPfxnKq1Y2bN4eF9GQ39SAcGu8TDZn")
var data: Dictionary = await $AnchorProgram.account_fetched
MplCandyMachine
# Initialize a Candy Machine account (InitializeV2 in Metaplex program)
MplCandyMachine.initialize(authority, candy_machine_account, collection_mint, candy_machine_data, pnft) -> Instruction
# Mint an NFT (MintV2 in Metaplex program)
MplCandyMachine.mint(payer, receiver, mint, collection_mint, collection_update_authority, candy_machine_key) -> Instruction
# Add config lines to hidden data section
MplCandyMachine.add_config_lines(candy_machine_key, authority, config_lines, index) -> Instruction
# Derive the candy machine authority PDA
MplCandyMachine.new_candy_machine_authority_pda(candy_machine_key) -> Pubkey
# Fetch candy machine account data (listens to http_request_completed)
candy_machine_node.get_candy_machine_info(candy_machine_key)
MplCandyMachine.get_pid() -> Pubkey
SolanaUtils
SolanaUtils.bs58_decode(str: String) -> PackedByteArray
SolanaUtils.bs58_encode(bytes: PackedByteArray) -> String
SolanaUtils.bs64_encode(bytes: PackedByteArray) -> String
SolanaUtils.bs64_decode(str: String) -> PackedByteArray
SolanaUtils.sha256_hash_array(contents: PackedStringArray) -> PackedByteArray
SolanaService (Helper Layer)
SolanaService is the autoload singleton added by the plugin. Its wallet child node is a
WalletService that abstracts keypair vs. wallet adapter login.
# Get the connected wallet's pubkey (works for both keypair and adapter)
var pubkey: Pubkey = SolanaService.wallet.get_pubkey()
# Get the keypair or WalletAdapter as a signer
var signer = SolanaService.wallet.get_kp() # Keypair or WalletAdapter
# Check login state
var logged_in: bool = SolanaService.wallet.is_logged_in()
# Request an airdrop via SolanaService helper
var balance = await SolanaService.get_balance(pubkey.to_string())
Error Handling
# HTTP request pattern
var params = await client.http_request_completed
var error: Error = params[0]
var response: Dictionary = params[1]
if error != OK:
push_error("Connection error: %d" % error)
return
if response.is_empty() or response.has("error"):
push_error("RPC error: " + str(response.get("error", {}).get("message", "unknown")))
return
var result = response["result"]
Common Pitfalls
SolanaClientmust be in the scene tree. Calladd_child(client)before making requests; remove andqueue_free()when done.TokenProgram.transfer_checkedis the correct method, nottransfer. It requires the mint address and decimals to prevent cross-mint transfers.Pubkey.new_associated_token_addressis how you derive ATAs — not a method onAssociatedTokenAccountProgram.- Signers are not stored in serialized transaction bytes. After
Transaction.new_from_bytes, callset_signers([...])again before signing. - AnchorProgram IDL loading: Set
pidandtry_from_json_file/try_from_pidin the Godot inspector, not from GDScript. Pubkey.new_pdareturnsnullif it cannot find a valid PDA. Always null-check.- WalletAdapter is primarily for Web exports. Use
Keypairfor desktop/server builds.