Skip to main content

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 address
  • TRANSFER_NFT — transfer an NFT (tokenId) between wallets
  • APPROVE_TOKEN — approve another wallet to transfer a token (spender pattern)
  • TRANSFER_TOKEN (with payload From) — 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.
info

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-url as 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 TransactionId returned by CREATE_TOKEN

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.TransactionId is 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:

  1. Create collection
  2. Mint tokenId=1
  3. Transfer tokenId=1
  4. Approve spender for tokenId=1
  5. Transfer using approval (spender moves tokenId=1)
  6. 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.