ERC-721 Tokens (Go)
This guide shows the core steps to use ULedger’s native ERC-721-style token transactions with the ULedger Go SDK.
ERC-721 tokens are non-fungible tokens (NFTs): each token is unique and is identified by a tokenId. They’re commonly used for
collectibles, certificates, tickets, digital art, memberships, and any scenario where each asset needs a unique identity.
In ULedger, ERC-721-style tokens are supported as native protocol transactions. That means you can manage an NFT collection and its tokens using standard transaction types such as:
CREATE_TOKEN— define token collection metadata (name,symbol,baseURI)MINT_NFT— mint a new NFT (tokenId) to a wallet addressTRANSFER_NFT— transfer an NFT (tokenId) between walletsAPPROVE_TOKEN— approve another wallet to transfer a token (spender pattern)TRANSFER_TOKEN(with payloadFrom) — transfer on behalf of an owner using an approval (if supported by your network)BURN_TOKEN— destroy an NFT (if burnable)
Because these actions are normal ULedger transactions, token operations inherit ULedger’s core capabilities: wallet-based identity, cryptographic signatures, tamper-evident history, and an auditable trail of token events across the chain.
In the next guide, we’ll cover ULedger’s other native token standard:
- ERC-1155 (multi-tokens) — a single contract/address can manage both fungible and non-fungible token IDs, including batch mint/transfer.
Your wallet must be registered on-chain (CREATE_WALLET) before it can submit token transactions.
Make sure your wallet is registered.
1) Prerequisites
You need:
- A node endpoint (the examples use
https://your-node-urlas a placeholder, so replace it with the URL of your network) - A
blockchainId(Testnet:08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2) - A wallet you can sign with (recommended: load from
.ukey) - A basic understanding that:
- Token Address = the
TransactionIdreturned byCREATE_TOKEN
- Token Address = the
2) Load a wallet
Load from a .ukey file instead of hardcoding keys:
w, err := wallet.LoadFromFile("./wallets/my_wallet.ukey", "")
if err != nil {
return fmt.Errorf("load wallet: %w", err)
}
3) Create a transaction session
Create a session bound to a node and wallet:
session, err := transaction.NewUL_TransactionSession(nodeEndpoint, w)
if err != nil {
return fmt.Errorf("create session: %w", err)
}
4) Create an ERC-721 collection (CREATE_TOKEN)
4.1 Build the payload
func buildCreateERC721Payload() ([]byte, error) {
return json.Marshal(transaction.CreateTokenPayload{
TokenType: transaction.ERC721_TOKEN_TYPE,
Name: "Collectible Token",
Symbol: "CTK",
BaseURI: "https://api.collectibletoken.com/token/",
Mintable: true,
Burnable: true,
})
}
4.2 Submit the transaction
payloadBytes, err := buildCreateERC721Payload()
if err != nil {
return err
}
input := transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: w.Address,
PayloadType: transaction.CREATE_TOKEN.String(),
Payload: string(payloadBytes),
}
tx, err := session.GenerateTransaction(input)
if err != nil {
return fmt.Errorf("create collection tx: %w", err)
}
tokenAddress := tx.TransactionId // ✅ Save this
fmt.Println("Token address:", tokenAddress)
fmt.Println("Status:", tx.Status, "Output:", tx.Output)
✅ The returned
tx.TransactionIdis used as the collection’s “token address” in future calls.
5) Mint an NFT (MINT_NFT)
:::info Confirm the recipient before you send
The recipient must be a wallet on this same blockchain with a matching key type. Transfers are final, and the network credits the address it is given: an address with no wallet on this chain holds a balance that cannot be spent. Run this check before every mint or transfer in this guide.
curl -o /dev/null -w '%{http_code}\n' \
"https://your-node-url/blockchains/{blockchainId}/wallets/{recipientAddress}"
# 200 = registered here 404 = not on this chain
:::
Mint creates a new token in the collection and assigns it to a wallet address.
5.1 Build the payload
func buildMintERC721Payload(tokenAddress, to string, tokenId uint64, tokenURI string) ([]byte, error) {
return json.Marshal(transaction.MintTokenPayload{
TokenAddress: tokenAddress,
To: to,
TokenId: tokenId,
TokenURI: tokenURI,
})
}
5.2 Submit the transaction
payloadBytes, err := buildMintERC721Payload(
tokenAddress,
toAddress,
1, // tokenId
"https://api.collectibletoken.com/token/1",
)
if err != nil {
return err
}
input := transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: w.Address,
PayloadType: transaction.MINT_NFT.String(),
Payload: string(payloadBytes),
}
tx, err := session.GenerateTransaction(input)
if err != nil {
return fmt.Errorf("mint tx: %w", err)
}
fmt.Println("Mint tx:", tx.TransactionId)
fmt.Println("Status:", tx.Status, "Output:", tx.Output)
6) Transfer an NFT (TRANSFER_NFT)
Transfers a specific tokenId to another wallet address.
6.1 Build the payload
func buildTransferERC721Payload(tokenAddress, to string, tokenId uint64) ([]byte, error) {
return json.Marshal(transaction.TransferTokenPayload{
TokenAddress: tokenAddress,
To: to,
TokenId: tokenId,
})
}
6.2 Submit the transaction
payloadBytes, err := buildTransferERC721Payload(tokenAddress, toAddress, 1)
if err != nil {
return err
}
input := transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: w.Address, // current owner/sender
PayloadType: transaction.TRANSFER_NFT.String(),
Payload: string(payloadBytes),
}
tx, err := session.GenerateTransaction(input)
if err != nil {
return fmt.Errorf("transfer tx: %w", err)
}
fmt.Println("Transfer tx:", tx.TransactionId)
fmt.Println("Status:", tx.Status, "Output:", tx.Output)
7) Approve a spender (APPROVE_TOKEN)
Approvals allow a spender to transfer NFTs on behalf of the owner.
7.1 Build the payload
Most ERC-721 implementations approve a spender for a specific tokenId:
func buildApproveERC721Payload(tokenAddress, spender string, tokenId uint64) ([]byte, error) {
return json.Marshal(transaction.ApproveTokenPayload{
TokenAddress: tokenAddress,
Spender: spender,
TokenId: tokenId,
})
}
7.2 Submit the transaction
payloadBytes, err := buildApproveERC721Payload(tokenAddress, spenderAddress, 1)
if err != nil {
return err
}
input := transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: w.Address, // token owner
PayloadType: transaction.APPROVE_TOKEN.String(),
Payload: string(payloadBytes),
}
tx, err := session.GenerateTransaction(input)
if err != nil {
return fmt.Errorf("approve tx: %w", err)
}
fmt.Println("Approve tx:", tx.TransactionId)
fmt.Println("Status:", tx.Status, "Output:", tx.Output)
8) Transfer using approval (TRANSFER_TOKEN with payload From)
:::warning The recipient must not be the spender
In an allowance transfer the signer is the spender, and the network rejects any transfer whose signer is also the recipient. Send to a third address instead.
The rule applies to ERC-20, NFTs and multi-tokens. A transfer that breaches it is accepted at submit and then not applied while the allowance is unchanged, so check the recipient first if you see that. :::
This pattern submits a transfer where:
- the transaction signer (
input.From) is the spender - the payload From is the token owner whose NFT is being moved
8.1 Build the payload
func buildTransferApprovalERC721Payload(tokenAddress, to, owner string, tokenId uint64) ([]byte, error) {
return json.Marshal(transaction.TransferTokenPayload{
TokenAddress: tokenAddress,
To: to,
From: owner, // owner whose NFT will be moved
TokenId: tokenId, // token being moved
})
}
8.2 Submit the transaction
payloadBytes, err := buildTransferApprovalERC721Payload(tokenAddress, finalRecipient, ownerAddress, 1)
if err != nil {
return err
}
input := transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: spenderWallet.Address, // signer is spender
PayloadType: transaction.TRANSFER_TOKEN.String(),
Payload: string(payloadBytes),
}
spenderSession, err := transaction.NewUL_TransactionSession(nodeEndpoint, spenderWallet)
if err != nil {
return err
}
tx, err := spenderSession.GenerateTransaction(input)
if err != nil {
return fmt.Errorf("transfer_approval tx: %w", err)
}
fmt.Println("Transfer (approval) tx:", tx.TransactionId)
fmt.Println("Status:", tx.Status, "Output:", tx.Output)
9) Burn an NFT (BURN_TOKEN)
9.1 Build the payload
func buildBurnERC721Payload(tokenAddress string, tokenId uint64) ([]byte, error) {
return json.Marshal(transaction.BurnTokenPayload{
TokenAddress: tokenAddress,
TokenId: tokenId,
})
}
9.2 Submit the transaction
payloadBytes, err := buildBurnERC721Payload(tokenAddress, 1)
if err != nil {
return err
}
input := transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: w.Address,
PayloadType: transaction.BURN_TOKEN.String(),
Payload: string(payloadBytes),
}
tx, err := session.GenerateTransaction(input)
if err != nil {
return fmt.Errorf("burn tx: %w", err)
}
fmt.Println("Burn tx:", tx.TransactionId)
fmt.Println("Status:", tx.Status, "Output:", tx.Output)
10) Full example
This single example shows:
- Create collection
- Mint tokenId=1
- Transfer tokenId=1
- Approve spender for tokenId=1
- Transfer using approval (spender moves tokenId=1)
- Burn tokenId=1
package main
import (
"encoding/json"
"fmt"
"github.com/ULedgerInc/golang-sdk/pkg/transaction"
"github.com/ULedgerInc/golang-sdk/pkg/wallet"
)
func main() {
nodeEndpoint := "https://your-node-url"
blockchainId := "08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2"
// Load two wallets (owner + spender) for demonstration
ownerWallet, err := wallet.LoadFromFile("./wallets/owner.ukey", "")
if err != nil { panic(err) }
spenderWallet, err := wallet.LoadFromFile("./wallets/spender.ukey", "")
if err != nil { panic(err) }
ownerSession, err := transaction.NewUL_TransactionSession(nodeEndpoint, ownerWallet)
if err != nil { panic(err) }
// 1) CREATE_TOKEN (ERC721)
createPayload, err := json.Marshal(transaction.CreateTokenPayload{
TokenType: transaction.ERC721_TOKEN_TYPE,
Name: "Collectible Token",
Symbol: "CTK",
BaseURI: "https://api.collectibletoken.com/token/",
Mintable: true,
Burnable: true,
})
if err != nil { panic(err) }
createTx, err := ownerSession.GenerateTransaction(transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: ownerWallet.Address,
PayloadType: transaction.CREATE_TOKEN.String(),
Payload: string(createPayload),
})
if err != nil { panic(err) }
tokenAddress := createTx.TransactionId
fmt.Println("Token address:", tokenAddress)
// 2) MINT_NFT (mint tokenId=1 to owner)
mintPayload, _ := json.Marshal(transaction.MintTokenPayload{
TokenAddress: tokenAddress,
To: ownerWallet.Address,
TokenId: 1,
TokenURI: "https://api.collectibletoken.com/token/1",
})
mintTx, err := ownerSession.GenerateTransaction(transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: ownerWallet.Address,
PayloadType: transaction.MINT_NFT.String(),
Payload: string(mintPayload),
})
if err != nil { panic(err) }
fmt.Println("Mint tx:", mintTx.TransactionId)
// 3) TRANSFER_NFT (owner -> spender)
transferPayload, _ := json.Marshal(transaction.TransferTokenPayload{
TokenAddress: tokenAddress,
To: spenderWallet.Address,
TokenId: 1,
})
transferTx, err := ownerSession.GenerateTransaction(transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: ownerWallet.Address,
PayloadType: transaction.TRANSFER_NFT.String(),
Payload: string(transferPayload),
})
if err != nil { panic(err) }
fmt.Println("Transfer tx:", transferTx.TransactionId)
// 4) APPROVE_TOKEN (spender approved for tokenId=1)
approvePayload, _ := json.Marshal(transaction.ApproveTokenPayload{
TokenAddress: tokenAddress,
Spender: spenderWallet.Address,
TokenId: 1,
})
approveTx, err := ownerSession.GenerateTransaction(transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: ownerWallet.Address,
PayloadType: transaction.APPROVE_TOKEN.String(),
Payload: string(approvePayload),
})
if err != nil { panic(err) }
fmt.Println("Approve tx:", approveTx.TransactionId)
// 5) TRANSFER_TOKEN using approval (spender moves owner's tokenId=1 to final recipient)
spenderSession, err := transaction.NewUL_TransactionSession(nodeEndpoint, spenderWallet)
if err != nil { panic(err) }
transferApprovalPayload, _ := json.Marshal(transaction.TransferTokenPayload{
TokenAddress: tokenAddress,
From: ownerWallet.Address, // owner
To: "DESTINATION_ADDRESS", // replace
TokenId: 1,
})
transferApprovalTx, err := spenderSession.GenerateTransaction(transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: spenderWallet.Address, // signer = spender
PayloadType: transaction.TRANSFER_TOKEN.String(),
Payload: string(transferApprovalPayload),
})
if err != nil { panic(err) }
fmt.Println("Transfer (approval) tx:", transferApprovalTx.TransactionId)
// 6) BURN_TOKEN (burn tokenId=1)
burnPayload, _ := json.Marshal(transaction.BurnTokenPayload{
TokenAddress: tokenAddress,
TokenId: 1,
})
burnTx, err := ownerSession.GenerateTransaction(transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: ownerWallet.Address,
PayloadType: transaction.BURN_TOKEN.String(),
Payload: string(burnPayload),
})
if err != nil { panic(err) }
fmt.Println("Burn tx:", burnTx.TransactionId)
}
Next steps
- Follow the ERC-1155 token guide.