>

75 stars
18 forks
C++
18 views

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.

Security: Mainnet use is not yet security-audited. Crypto can be stolen. Use with care.


Installation

Via Godot Asset Library (recommended)

  1. Search for "Solana" in the Godot Asset Library and install.
  2. Install must go into the addons/ folder at the project root.
  3. Go to Project → Project Settings → Plugins and enable SolanaSDK.
  4. Verify SolanaService appears 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 WalletService child 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_pda finds the canonical valid PDA (searches for bump). Pubkey.new_program_address does not search — it constructs from exact seeds. Use new_pda for 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: SolanaClient must 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_payeradd_instructionupdate_latest_blockhashsign_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), not transfer.

# 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

AssociatedTokenAccountProgram does not have get_associated_token_address. Use Pubkey.new_associated_token_address to 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 = 800000 and unit_price = 8000 built in. Use ComputeBudget instructions 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 AnchorProgram node to your scene.
  • Set pid to the program's base58 address.
  • Assign the IDL JSON resource to json_file and set try_from_json_file = true, or fetch it from the chain with try_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

  • SolanaClient must be in the scene tree. Call add_child(client) before making requests; remove and queue_free() when done.
  • TokenProgram.transfer_checked is the correct method, not transfer. It requires the mint address and decimals to prevent cross-mint transfers.
  • Pubkey.new_associated_token_address is how you derive ATAs — not a method on AssociatedTokenAccountProgram.
  • Signers are not stored in serialized transaction bytes. After Transaction.new_from_bytes, call set_signers([...]) again before signing.
  • AnchorProgram IDL loading: Set pid and try_from_json_file/try_from_pid in the Godot inspector, not from GDScript.
  • Pubkey.new_pda returns null if it cannot find a valid PDA. Always null-check.
  • WalletAdapter is primarily for Web exports. Use Keypair for desktop/server builds.