Register your Wallet and Create Transactions with the Go SDK
In the previous section, you created and saved a UL_Wallet to disk.
In this section, you’ll:
- Load that wallet from a
.ukeyfile. - Open a transaction session against a ULedger node.
- Use that session to:
- send your first transaction by registering the wallet on-chain with
TX_CREATE_WALLET, - and then send regular
DATAtransactions with the same wallet.
- send your first transaction by registering the wallet on-chain with
Conceptually:
- Register wallet = create your identity on-chain (
TX_CREATE_WALLET). - DATA transactions = use that identity to write data to the chain (
TX_DATA).
We’ll reuse the same helpers (config loading, wallet loading, session creation) for both.
1. Prerequisites
Before you start, you should have:
- A running ULedger node (or access to dev nodes like
https://tn-w-1.uledger.net/). - At least one wallet file created with the Create a Wallet with the Go SDK guide
(for example,./wallets/my_wallet.ukey). - A small config file that lists the node endpoints you want to use.
In this example we’ll assume a small dev network with four nodes in total:
- 3 council nodes – nodes that participate in consensus and vote.
- 1 extra node – e.g. a follower/observer node.
In ULedger’s council model you typically need at least 3 council members online to reach a valid vote (quorum). That’s why a 3-node council is the smallest setup that can actually finalize blocks.
For simplicity, our config.json will point only at the three council nodes, since those are the ones we’ll send transactions to:
{
"nodeEndpoints": [
"https://tn-w-1.uledger.net/",
"https://my.node2.uledger.io",
"https://my.node3.uledger.io"
],
"keyType": 0,
"verbose": true
}
keyTypecorresponds to the crypto key type;0usually maps tosecp256k1in this SDK.
Place this config.json somewhere convenient (for example, the project root) and make sure you have a wallet file, e.g.: ./wallets/my_wallet.ukey.
2. Load configuration and wallet
Let’s start by loading:
- your JSON config into a
TestSuiteConfig, and - your wallet file into a
UL_Wallet.
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/ULedgerInc/go-sdk/pkg/test_suite"
"github.com/ULedgerInc/go-sdk/pkg/wallet"
)
func loadConfig(path string) (test_suite.TestSuiteConfig, error) {
var cfg test_suite.TestSuiteConfig
data, err := os.ReadFile(path)
if err != nil {
return cfg, fmt.Errorf("read config file: %w", err)
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, fmt.Errorf("parse config file: %w", err)
}
if len(cfg.NodeEndpoints) == 0 {
return cfg, fmt.Errorf("config must contain at least one node endpoint")
}
return cfg, nil
}
func loadWallet(path, passphrase string) (*wallet.UL_Wallet, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read wallet file: %w", err)
}
w, err := wallet.FromJson(string(data), passphrase)
if err != nil {
return nil, fmt.Errorf("parse wallet JSON: %w", err)
}
return w, nil
}
wallet.FromJsonreconstructs the keypair (public/private key), address,Parent,EnabledandAuthGroupsfrom the JSON, so the wallet is ready to sign and participate in permission checks.
3. Create a transaction session
Now we’ll create a helper that:
- Picks a node endpoint from the config.
- Creates a
UL_TransactionSessionfor that node and wallet.
import (
"fmt"
"math/rand/v2"
"github.com/ULedgerInc/go-sdk/pkg/test_suite"
"github.com/ULedgerInc/go-sdk/pkg/transaction"
"github.com/ULedgerInc/go-sdk/pkg/wallet"
)
func newRandomSession(cfg test_suite.TestSuiteConfig, w *wallet.UL_Wallet) (transaction.UL_TransactionSession, error) {
// Pick a random node from the config
idx := rand.IntN(len(cfg.NodeEndpoints))
nodeEndpoint := cfg.NodeEndpoints[idx]
// NewUL_TransactionSession internally calls /health and /blockchains
// and stores the nodeId as the "suggestor"
session, err := transaction.NewUL_TransactionSession(nodeEndpoint, *w)
if err != nil {
return transaction.UL_TransactionSession{}, fmt.Errorf("create transaction session: %w", err)
}
fmt.Printf("Using node %s\n", nodeEndpoint)
return session, nil
}
What NewUL_TransactionSession does for you:
- Calls the node’s
/healthendpoint and reads basic metadata. - Calls
/blockchainsto know what chains exist on this node. - Stores the node ID as the suggestor.
- Keeps your wallet handy for signing.
You don’t need to make those HTTP calls yourself—this is handled inside the SDK.
4. Register your wallet (TX_CREATE_WALLET)
A register wallet transaction is your first transaction: it creates an on-chain identity that other logic can refer to later.
On-chain, the wallet is represented by a payload like:
import (
"encoding/json"
"github.com/ULedgerInc/go-sdk/pkg/crypto"
"github.com/ULedgerInc/go-sdk/pkg/transaction"
"github.com/ULedgerInc/go-sdk/pkg/wallet"
)
type CreateWalletPayload struct {
PublicKey string `json:"publicKey"`
Parent string `json:"parent"`
KeyType crypto.KeyType `json:"keyType"`
AuthGroups map[string]wallet.UL_AuthPermission `json:"authGroups,omitempty"`
}
We can wrap this in a helper that builds the payload, constructs the ULTransactionInput and calls GenerateTransaction:
func registerWallet(
session transaction.UL_TransactionSession,
w *wallet.UL_Wallet,
blockchainId string,
) (*transaction.ULTransaction, error) {
// 1) Build the create-wallet payload
payloadBody := CreateWalletPayload{
PublicKey: w.GetKey().GetPublicKeyHex(false),
Parent: w.Parent,
KeyType: w.GetKey().GetType(),
AuthGroups: w.AuthGroups,
}
payloadBytes, err := json.Marshal(payloadBody)
if err != nil {
return nil, fmt.Errorf("marshal create-wallet payload: %w", err)
}
// 2) Build the ULTransactionInput for TX_CREATE_WALLET
input := transaction.ULTransactionInput{
BlockchainId: blockchainId,
To: w.Address, // wallet being created
From: w.Parent, // author / creator (see note below)
Payload: string(payloadBytes),
PayloadType: transaction.TX_CREATE_WALLET.String(),
}
// 3) Generate and submit the transaction
tx, err := session.GenerateTransaction(input)
if err != nil {
return nil, fmt.Errorf("generate TX_CREATE_WALLET: %w", err)
}
if tx.TransactionId == "" {
return nil, fmt.Errorf("TX_CREATE_WALLET returned empty transaction ID")
}
return &tx, nil
}
What each field in CreateWalletPayload means:
PublicKey– the hex public key of the wallet we’re registering.Parent– optional owner/creator identifier. In many setups this is the address (or name) of an existing admin/root wallet.KeyType– which key type is used (e.g.crypto.KeyTypeSecp256k1).AuthGroups– the permission groups this wallet belongs to (from the previous section).
This payload is stored as the Payload of the transaction and interpreted by your chain’s wallet logic.
How GenerateTransaction works (once, for both flows)
The first time we call GenerateTransaction is for TX_CREATE_WALLET, but the same mechanism is used for DATA and other types:
-
Fills in extra fields on the input:
Suggestor(node ID),SenderTimestamp,KeyType,PayloadRoot.
-
For
TX_CREATE_WALLET, computes an unbound commitment (Merkle root) over the payload only. -
Signs that commitment with the wallet’s private key.
-
Sends:
POST /blockchains/{blockchainId}/transactions -
Parses the node’s response into a
ULTransactionwith:TransactionIdStatusOutputBlockHeightVectorClock, etc.
For other payload types (like TX_DATA) the commitment includes more fields (sender, payload, etc.), but the overall flow is the same.
5. Send DATA transactions
Once the wallet is registered (usually a one-time operation per wallet per chain), you can use it to send normal DATA transactions.
We’ll define a small helper similar to registerWallet:
func sendDataTransaction(
session transaction.UL_TransactionSession,
w *wallet.UL_Wallet,
blockchainId string,
payload string,
) (*transaction.ULTransaction, error) {
input := transaction.ULTransactionInput{
Payload: payload,
From: w.Address,
To: w.Address, // self-transfer (good for testing)
BlockchainId: blockchainId, // the chain you want to hit
PayloadType: transaction.TX_DATA.String(),
}
tx, err := session.GenerateTransaction(input)
if err != nil {
return nil, fmt.Errorf("generate DATA transaction: %w", err)
}
if tx.TransactionId == "" {
return nil, fmt.Errorf("generated transaction has empty transaction ID")
}
return &tx, nil
}
This uses the same GenerateTransaction flow you saw above, just with a different PayloadType and payload body.
The node’s response will give you:
TransactionIdStatus(e.g."SUBMITTED","ACCEPTED","REJECTED")Output(e.g."SUCCESS","REJECTED_BY_UNAUTHORIZED")- plus metadata like
BlockHeightandVectorClock.
6. Putting it all together
Here is a minimal program that:
- Loads config and wallet.
- Creates a transaction session.
- Registers the wallet with
TX_CREATE_WALLET(first transaction). - Sends a few
DATAtransactions with the same wallet.
package main
import (
"encoding/json"
"fmt"
"math/rand/v2"
"os"
"github.com/ULedgerInc/go-sdk/pkg/crypto"
"github.com/ULedgerInc/go-sdk/pkg/test_suite"
"github.com/ULedgerInc/go-sdk/pkg/transaction"
"github.com/ULedgerInc/go-sdk/pkg/wallet"
)
type CreateWalletPayload struct {
PublicKey string `json:"publicKey"`
Parent string `json:"parent"`
KeyType crypto.KeyType `json:"keyType"`
AuthGroups map[string]wallet.UL_AuthPermission `json:"authGroups,omitempty"`
}
func loadConfig(path string) (test_suite.TestSuiteConfig, error) {
var cfg test_suite.TestSuiteConfig
data, err := os.ReadFile(path)
if err != nil {
return cfg, fmt.Errorf("read config file: %w", err)
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, fmt.Errorf("parse config file: %w", err)
}
if len(cfg.NodeEndpoints) == 0 {
return cfg, fmt.Errorf("config must contain at least one node endpoint")
}
return cfg, nil
}
func loadWallet(path, passphrase string) (*wallet.UL_Wallet, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read wallet file: %w", err)
}
w, err := wallet.FromJson(string(data), passphrase)
if err != nil {
return nil, fmt.Errorf("parse wallet JSON: %w", err)
}
return w, nil
}
func newRandomSession(cfg test_suite.TestSuiteConfig, w *wallet.UL_Wallet) (transaction.UL_TransactionSession, error) {
idx := rand.IntN(len(cfg.NodeEndpoints))
nodeEndpoint := cfg.NodeEndpoints[idx]
session, err := transaction.NewUL_TransactionSession(nodeEndpoint, *w)
if err != nil {
return transaction.UL_TransactionSession{}, fmt.Errorf("create transaction session: %w", err)
}
fmt.Printf("Using node %s\n", nodeEndpoint)
return session, nil
}
func registerWallet(
session transaction.UL_TransactionSession,
w *wallet.UL_Wallet,
blockchainId string,
) (*transaction.ULTransaction, error) {
payloadBody := CreateWalletPayload{
PublicKey: w.GetKey().GetPublicKeyHex(false),
Parent: w.Parent,
KeyType: w.GetKey().GetType(),
AuthGroups: w.AuthGroups,
}
payloadBytes, err := json.Marshal(payloadBody)
if err != nil {
return nil, fmt.Errorf("marshal create-wallet payload: %w", err)
}
input := transaction.ULTransactionInput{
BlockchainId: blockchainId,
To: w.Address, // wallet being created
From: w.Parent, // creator / parent
Payload: string(payloadBytes),
PayloadType: transaction.TX_CREATE_WALLET.String(),
}
tx, err := session.GenerateTransaction(input)
if err != nil {
return nil, fmt.Errorf("generate TX_CREATE_WALLET: %w", err)
}
if tx.TransactionId == "" {
return nil, fmt.Errorf("TX_CREATE_WALLET returned empty transaction ID")
}
return &tx, nil
}
func sendDataTransaction(
session transaction.UL_TransactionSession,
w *wallet.UL_Wallet,
blockchainId string,
payload string,
) (*transaction.ULTransaction, error) {
input := transaction.ULTransactionInput{
Payload: payload,
From: w.Address,
To: w.Address,
BlockchainId: blockchainId,
PayloadType: transaction.TX_DATA.String(),
}
tx, err := session.GenerateTransaction(input)
if err != nil {
return nil, fmt.Errorf("generate DATA transaction: %w", err)
}
if tx.TransactionId == "" {
return nil, fmt.Errorf("generated transaction has empty transaction ID")
}
return &tx, nil
}
func main() {
// Adjust these paths/IDs for your environment
const (
configPath = "config.json"
walletPath = "./wallets/my_wallet.ukey"
passphrase = "" // or your mnemonic passphrase if you used one
blockchainId = "your-blockchain-id-here"
txCount = 3
)
cfg, err := loadConfig(configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Config error: %v\n", err)
os.Exit(1)
}
w, err := loadWallet(walletPath, passphrase)
if err != nil {
fmt.Fprintf(os.Stderr, "Wallet error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Loaded wallet: %s (parent=%q)\n", w.Address, w.Parent)
session, err := newRandomSession(cfg, w)
if err != nil {
fmt.Fprintf(os.Stderr, "Session error: %v\n", err)
os.Exit(1)
}
// First: register the wallet on-chain (usually one-time per wallet/chain).
regTx, err := registerWallet(session, w, blockchainId)
if err != nil {
fmt.Fprintf(os.Stderr, "Register wallet error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Registered wallet %s via TX %s (status=%s, output=%s)\n",
w.Address, regTx.TransactionId, regTx.Status, regTx.Output)
// Then: send some DATA transactions using the same wallet.
for i := 0; i < txCount; i++ {
payload := fmt.Sprintf("test payload from %s, tx #%d", w.Address, i+1)
tx, err := sendDataTransaction(session, w, blockchainId, payload)
if err != nil {
fmt.Fprintf(os.Stderr, "DATA transaction error: %v\n", err)
os.Exit(1)
}
fmt.Printf(
"Created DATA transaction %s (status=%s, payloadRoot=%s)\n",
tx.TransactionId,
tx.Status,
tx.PayloadRoot,
)
}
}
You can run this with something like:
go run ./cmd/send_transactions
(or whatever path you place main.go under in your project).