Go SDK for the Phantasma blockchain.

Overview
This project aims to be an easy to use SDK for the Phantasma blockchain.
Documentation
Installation
Requires Go 1.25 or newer. The module is developed with the Go 1.26 toolchain.
phantasma-sdk-go is distributed as a library that includes all SDK functionality.
go get -u github.com/phantasma-io/phantasma-sdk-go
Getting started
To start interacting with Phantasma blockchain you need to choose the network and create an RPC client. All typed RPC methods take context.Context as their first argument so callers can set deadlines and cancellation.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client := rpc.NewRPCTestnet()
// client := rpc.NewRPCMainnet()
To create a new key pair structure from private key in WIF format use following code:
keyPair, err := cryptography.FromWIF("put WIF here")
if err != nil {
log.Fatalf("creating key pair: %v", err)
}
To get detailed description of tokens deployed on the chain and read token characteristics you can use following code:
chainTokens, err := client.GetTokens(ctx, false)
if err != nil {
log.Fatal(err)
}
for _, token := range chainTokens {
if token.Symbol == "SOUL" && token.IsFungible() {
fmt.Println("Token SOUL is fungible")
break
}
}
Code samples in the following sections of this documentation use client and keyPair structures which should be initialized in advance.
Carbon transactions and token builders
The SDK includes pkg/carbon for Phantasma Phoenix Carbon transaction serialization and token-module calls. This package mirrors the C#/TS/C++ SDK model:
- fixed-width Carbon types:
Bytes16, Bytes32, Bytes64, SmallString, IntX;
TxMsg, SignedTxMsg, witnesses, call sections, and trade payloads;
- VM schema structures used by token metadata;
- token-module argument/result blobs for create token, create series, mint, transfer, burn and metadata update calls;
- high-level helpers for token metadata, NFT ROM/RAM, standard NFT schemas, transaction signing, and NFT address/instance-id conversion.
Example: build and sign a Carbon NFT mint transaction:
signer, err := cryptography.FromWIF("put WIF here")
if err != nil {
log.Fatal(err)
}
receiver, err := carbon.Bytes32FromPhantasmaAddressText("put receiver address here")
if err != nil {
log.Fatal(err)
}
schemas := carbon.PrepareStandardTokenSchemas(false)
rom, err := carbon.BuildNFTRom(schemas.ROM, big.NewInt(1), []carbon.MetadataField{
{Name: "name", Value: "Example NFT"},
{Name: "description", Value: "Minted with phantasma-sdk-go Carbon helpers"},
{Name: "imageURL", Value: "https://example.com/nft.png"},
{Name: "infoURL", Value: "https://example.com/nft"},
{Name: "royalties", Value: int32(0)},
})
if err != nil {
log.Fatal(err)
}
signedTx, err := carbon.BuildMintNonFungibleTxAndSignHex(
42, // Carbon token id
1, // Carbon series id
signer,
receiver,
rom,
nil, // RAM
carbon.DefaultMintNFTFeeOptions(),
100_000_000,
time.Now().UTC().Add(20*time.Minute).UnixMilli(),
)
if err != nil {
log.Fatal(err)
}
txHash, err := client.SendCarbonTransaction(ctx, signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Carbon tx hash:", txHash)
See docs/carbon.md for the package-level API map.
Carbon Build... helpers return validation errors for user input; MustBuild... variants are available only when panic-on-invalid-input is intentional.
Carbon RPC wrappers
The RPC client now exposes the Carbon endpoints used by the current C#/TS SDKs. Important additions include:
- chain/block lookup:
GetChains, GetChain, GetNexus, GetBlockByHash, GetLatestBlock, GetTransactionByBlockHashAndIndex;
- contract/organization lookup:
GetContracts, GetContractByName, GetContractByAddress, GetOrganization, GetOrganizationByName, GetOrganizations;
- Carbon token views:
GetTokensByOwner, GetTokenWithID, GetTokenSeries, GetTokenSeriesByID, GetTokenNFTs, GetAccountFungibleTokens, GetAccountNFTs, GetAccountOwnedTokens, GetAccountOwnedTokenSeries;
- archive and auction helpers:
GetArchive, ReadArchive, WriteArchive, GetAuctionsCount, GetAuctions, GetAuction;
- transaction broadcast:
SendCarbonTransaction, SignAndSendCarbonTransaction.
Cursor-paginated endpoints return response.CursorPaginatedResult[T].
Carbon token id filters use uint64, Carbon series id filters use uint32, and 0 means no Carbon id filter. GetTokenNFTsWithSeriesID accepts the Phantasma Series ID string filter exposed by current RPC nodes.
Use client.Call(ctx, method, params...) for low-level calls that need request cancellation or deadlines.
Public package surface
Prefer the high-level packages for application code:
pkg/rpc and pkg/rpc/response for node calls and typed RPC results;
pkg/carbon for Carbon transactions, serialization, token builders, and signing;
pkg/vm/script_builder for classic VM scripts;
pkg/cryptography for keys, addresses, signatures, and WIF handling;
pkg/blockchain for classic VM transaction objects.
Lower-level packages such as pkg/io, pkg/jsonrpc, pkg/domain, and pkg/vm remain available for advanced serialization and migration work, but application code should avoid depending on their internals unless it needs that wire-level control.
Script Builder
Building a script is the main entry point for transaction and contract interactions. The script must match the target contract or interop ABI.
The CallContract and CallInterop methods are the primary entry points for creating scripts.
func (s ScriptBuilder) CallContract(contractName, method string, args ...interface{}) ScriptBuilder
func (s ScriptBuilder) CallInterop(method string, args ...interface{}) ScriptBuilder
The available CallInterop functions are listed below.
For CallContract, inspect the ABIs of the smart contracts currently deployed on Phantasma mainnet: Explorer contract list. To see all methods of a contract, for example stake, check the contract methods view: stake contract methods.
The Go builder now resolves labels per instance, emits integer values as VM Number payloads, supports array arguments, and matches the C#/TS/C++ shared script vectors.
Address arguments are intentionally typed as cryptography.Address in high-level helpers. Raw string arguments passed to CallContract or CallInterop are emitted as VM strings; use cryptography.MustAddressFromString(text) or the *Text helpers when the ABI expects a Phantasma address.
Examples
Following code generates script to transfer tokenAmount amount of token tokenSymbol from wallet from to wallet to
from := cryptography.MustAddressFromString("put sender address here") // Phantasma address, starting with capital 'P'
to := cryptography.MustAddressFromString("put recipient address here") // Phantasma address, starting with capital 'P'
tokenAmount := big.NewInt(1000000000) // Token amount in the form of big integer
tokenSymbol := "SOUL"
sb := scriptbuilder.BeginScript()
script := sb.CallContract("gas", "AllowGas", from, cryptography.NullAddress(), big.NewInt(100000), big.NewInt(21000)).
CallInterop("Runtime.TransferTokens", from, to, tokenSymbol, tokenAmount).
CallContract("gas", "SpendGas", from).
EndScript()
And here we generate script to make a call which does not require transaction, for this we use CallContract method:
address := cryptography.MustAddressFromString("put caller address here") // Phantasma address, starting with capital 'P'
tokenAmount := big.NewInt(1000000000) // Token amount in the form of big integer
sb := scriptbuilder.BeginScript().
CallContract("gas", "AllowGas", address, cryptography.NullAddress(), big.NewInt(100000), big.NewInt(21000)).
CallContract("stake", "Stake", address, tokenAmount).
CallContract("gas", "SpendGas", address)
script := sb.EndScript()
Script Builder Extensions
For some widely used contract calls SDK has special extension methods which make code more compact. The typed-address helpers are AllowGas, SpendGas, MintTokens, Stake, Unstake, TransferTokens, TransferBalance, TransferNFT, CrossTransferToken, CrossTransferNFT, and CallNFT.
String-address convenience helpers are AllowGasText, SpendGasText, MintTokensText, StakeText, UnstakeText, TransferTokensText, TransferBalanceText, TransferNFTText, CrossTransferTokenText, and CrossTransferNFTText.
The ordinary *Text helpers parse Phantasma address text before emitting VM address bytes. Cross-chain text helpers parse the Phantasma destination-chain/sender addresses and keep the destination account as a VM string for non-Phantasma destination formats.
Examples
We can rewrite examples from previous section using AllowGas() and SpendGas() extensions:
sb := scriptbuilder.BeginScript()
script := sb.AllowGas(from, cryptography.NullAddress(), big.NewInt(100000), big.NewInt(21000)).
CallInterop("Runtime.TransferTokens", from, to, tokenSymbol, tokenAmount).
SpendGas(from).
EndScript()
sb := scriptbuilder.BeginScript().
AllowGas(address, cryptography.NullAddress(), big.NewInt(100000), big.NewInt(21000)).
CallContract("stake", "Stake", address, tokenAmount).
SpendGas(address)
script := sb.EndScript()
We can also rewrite main contract calls in these examples:
sb := scriptbuilder.BeginScript()
script := sb.AllowGas(from, cryptography.NullAddress(), big.NewInt(100000), big.NewInt(21000)).
TransferTokens(tokenSymbol, from, to, tokenAmount).
SpendGas(from).
EndScript()
sb := scriptbuilder.BeginScript().
AllowGas(address, cryptography.NullAddress(), big.NewInt(100000), big.NewInt(21000)).
Stake(address, tokenAmount).
SpendGas(address)
script := sb.EndScript()
InvokeRawScript and decoding the result
Scripts that do not require a transaction can be sent to the chain directly using InvokeRawScript().
Here's a read-only example that gets SOUL token decimals from the chain:
// Build script
sb := scriptbuilder.BeginScript().
CallInterop("Runtime.GetTokenDecimals", "SOUL")
script := sb.EndScript()
// Before sending script to the chain we need to encode it into Base16 encoding (HEX)
encodedScript := fmt.Sprintf("%x", script)
// Make the call itself
result, err := client.InvokeRawScript(ctx, "main", encodedScript)
if err != nil {
log.Fatalf("script invocation failed: %v", err)
}
value, err := result.DecodeResultWithError()
if err != nil {
log.Fatalf("script result decoding failed: %v", err)
}
fmt.Println("SOUL decimals:", value.AsNumber().String())
Building and sending transaction
Building transaction
To build a transaction you will first need to build a script.
Note, building a transaction is for transactional scripts only. Non transactional scripts should use the RPC function InvokeRawScript().
// Build script
sb := scriptbuilder.BeginScript()
script := sb.AllowGas(keyPair.Address(), cryptography.NullAddress(), big.NewInt(100000), big.NewInt(21000)).
TransferTokens(tokenSymbol, keyPair.Address(), to, tokenAmount).
SpendGas(keyPair.Address()).
EndScript()
// Build transaction
expire := time.Now().UTC().Add(time.Second * time.Duration(30)).Unix()
tx := blockchain.NewTransaction(netSelected, "main", script, uint32(expire), domain.SDKPayload)
// Sign transaction
if err := tx.Sign(keyPair); err != nil {
log.Fatal(err)
}
// Before sending script to the chain we need to encode it into Base16 encoding (HEX)
txHex := hex.EncodeToString(tx.Bytes())
Sending transaction
Broadcasting requires a funded key and sends a transaction to the selected network. Keep broadcast examples separate from read-only examples and only run them intentionally.
txHash, err := client.SendRawTransaction(ctx, txHex)
if err != nil {
log.Fatalf("broadcasting tx failed: %v", err)
} else {
if util.ErrorDetect(txHash) {
log.Fatalf("broadcasting tx failed: %s", txHash)
} else {
fmt.Println("Tx successfully broadcasted! Tx hash: " + txHash)
}
}
Waiting for transaction execution result
We need to wait for transaction to be minted on the chain to get its status:
for {
txResult, _ := client.GetTransaction(ctx, txHash)
if txResult.StateIsSuccess() {
fmt.Println("Transaction was successfully minted, tx hash: " + fmt.Sprint(txResult.Hash))
break // Funds were transferred successfully
}
if txResult.StateIsFault() {
fmt.Println("Transaction failed, tx hash: " + fmt.Sprint(txResult.Hash))
break // Funds were not transferred, transaction failed
}
time.Sleep(200 * time.Millisecond)
}
Staking SOUL token
Following code shows how to stake SOUL token:
// Build script
sb := scriptbuilder.BeginScript().
AllowGas(address, cryptography.NullAddress(), big.NewInt(100000), big.NewInt(21000)).
Stake(address, tokenAmount).
SpendGas(address)
script := sb.EndScript()
// Build transaction
expire := time.Now().UTC().Add(time.Second * time.Duration(30)).Unix()
tx := chain.NewTransaction(netSelected, "main", script, uint32(expire), domain.SDKPayload)
// Sign transaction
if err := tx.Sign(keyPair); err != nil {
log.Fatal(err)
}
// Before sending script to the chain we need to encode it into Base16 encoding (HEX)
txHex := hex.EncodeToString(tx.Bytes())
txHash, err := client.SendRawTransaction(ctx, txHex)
if err != nil {
log.Fatalf("broadcasting tx failed: %v", err)
} else {
if util.ErrorDetect(txHash) {
log.Fatalf("broadcasting tx failed: %s", txHash)
} else {
fmt.Println("Tx successfully broadcasted! Tx hash: " + txHash)
}
}
for {
txResult, _ := client.GetTransaction(ctx, txHash)
if txResult.StateIsSuccess() {
fmt.Println("Transaction was successfully minted, tx hash: " + fmt.Sprint(txResult.Hash))
break // Funds were transferred successfully
}
if txResult.StateIsFault() {
fmt.Println("Transaction failed, tx hash: " + fmt.Sprint(txResult.Hash))
break // Funds were not transferred, transaction failed
}
time.Sleep(200 * time.Millisecond)
}
Scanning the blockchain for incoming transactions
In the following code we monitor the blockchain by checking all the new blocks minted on the blockchain and waiting for TokenReceive event for given address. This event for address means that address has received some tokens.
func onTransactionReceived(address, symbol, amount string) {
fmt.Printf("Address %s received %s %s\n", address, amount, symbol)
}
func waitForIncomingTransfers(address string) {
// Get current block height
height, _ := client.GetBlockHeight(ctx, "main")
for {
// Get block's data by its height
block, err := client.GetBlockByHeight(ctx, "main", height.String())
if err != nil {
log.Fatalf("GetBlockByHeight failed: %v", err)
}
// Iterate throough all transactions in the block
for _, tx := range block.Txs {
// Skip failed trasactions
if !tx.StateIsSuccess() {
continue
}
// Iterate throough all events in the transaction
for _, e := range tx.Events {
if e.Kind == event.TokenReceive.String() && e.Address == address {
// We found TokenReceive event for given address
// Decode event data into event.TokenEventData structure
decoded, _ := hex.DecodeString(e.Data)
data := io.Deserialize[*event.TokenEventData](decoded, &event.TokenEventData{})
// Apply decimals to the token amount
t, ok := getChainToken(data.Symbol)
if !ok {
log.Fatalf("token %s not found", data.Symbol)
}
tokenAmount := util.ConvertDecimals(data.Value, int(t.Decimals))
// Call our callback function
onTransactionReceived(e.Address, data.Symbol, tokenAmount)
}
}
}
// Wait for next block to appear on the blockchain
for {
newHeight, _ := client.GetBlockHeight(ctx, "main")
if newHeight.Cmp(height) == 1 {
// New block was minted (at least 1 new block)
height = height.Add(height, big.NewInt(1))
break
}
// Wait 200 milliseconds before making next RPC call
time.Sleep(200 * time.Millisecond)
}
}
}
Examples
This repository has examples folder with some code which can be easily reused. Examples are grouped into a single console application.
To run this application switch to examples folder and run:
go run .
or
sh run.sh
Application entry point is main() function in main.go source file. Once launched it will display the following menu:

Wallet submenu:

Chain stats submenu:

Contributing
Feel free to contribute to this project after reading the
contributing guidelines.
Before starting to work on a certain topic, create an new issue first,
describing the feature/topic you are going to implement.
License