Saltar al contenido principal

Registra tu Wallet y Crea Transacciones con el SDK de Go

En la sección anterior, creaste y guardaste un UL_Wallet en disco.

En esta sección:

  1. Cargarás ese wallet desde un archivo .ukey.
  2. Abrirás una sesión de transaction contra un node de ULedger.
  3. Usarás esa sesión para:
    • enviar tu primera transaction registrando el wallet en la cadena con TX_CREATE_WALLET,
    • y luego enviar transacciones DATA regulares con el mismo wallet.

Conceptualmente:

  • Registrar wallet = crear tu identidad en la cadena (TX_CREATE_WALLET).
  • Transacciones DATA = usar esa identidad para escribir datos en la cadena (TX_DATA).

Reutilizaremos los mismos helpers (carga de configuración, carga de wallet, creación de sesión) para ambos.


1. Prerrequisitos

Antes de comenzar, debes tener:

  • Un node de ULedger en ejecución (o acceso a nodes de desarrollo como https://tn-w-1.uledger.net/).
  • Al menos un archivo de wallet creado con la guía Crear un Wallet con el SDK de Go
    (por ejemplo, ./wallets/my_wallet.ukey).
  • Un pequeño archivo de configuración que liste los endpoints de node que deseas usar.

En este ejemplo asumiremos una pequeña red de desarrollo con cuatro nodes en total:

  • 3 nodes de consejo – nodes que participan en el consensus y votan.
  • 1 node adicional – p. ej. un node seguidor/observador.

En el modelo de consejo de ULedger normalmente necesitas al menos 3 miembros del consejo en línea para alcanzar un voto válido (quórum). Por eso un consejo de 3 nodes es la configuración más pequeña que puede finalizar bloques.

Para simplificar, nuestro config.json apuntará solo a los tres nodes del consejo, ya que son los que enviaremos transacciones:

{
"nodeEndpoints": [
"https://tn-w-1.uledger.net/",
"https://my.node2.uledger.io",
"https://my.node3.uledger.io"
],
"keyType": 0,
"verbose": true
}

keyType corresponde al tipo de clave criptográfica; 0 generalmente se mapea a secp256k1 en este SDK.

Coloca este config.json en algún lugar conveniente (por ejemplo, la raíz del proyecto) y asegúrate de tener un archivo de wallet, p. ej.: ./wallets/my_wallet.ukey.


2. Cargar configuración y wallet

Comencemos cargando:

  • tu configuración JSON en un TestSuiteConfig, y
  • tu archivo de wallet en un 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.FromJson reconstruye el par de claves (clave pública/privada), la dirección, Parent, Enabled y AuthGroups desde el JSON, de modo que el wallet está listo para firmar y participar en verificaciones de permisos.


3. Crear una sesión de transaction

Ahora crearemos un helper que:

  1. Selecciona un endpoint de node de la configuración.
  2. Crea una UL_TransactionSession para ese node y 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
}

Lo que hace NewUL_TransactionSession por ti:

  • Llama al endpoint /health del node y lee metadatos básicos.
  • Llama a /blockchains para saber qué cadenas existen en este node.
  • Almacena el ID del node como el suggestor.
  • Mantiene tu wallet disponible para firmar.

No necesitas hacer esas llamadas HTTP tú mismo — esto se maneja dentro del SDK.


4. Registrar tu wallet (TX_CREATE_WALLET)

Una transaction de registro de wallet es tu primera transaction: crea una identidad en la cadena a la que otra lógica puede hacer referencia posteriormente.

En la cadena, el wallet se representa mediante un payload como:

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"`
}

Podemos envolver esto en un helper que construya el payload, cree el ULTransactionInput y llame a 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
}

Lo que significa cada campo en CreateWalletPayload:

  • PublicKey – la clave pública en hexadecimal del wallet que estamos registrando.
  • Parent – identificador opcional del propietario/creador. En muchas configuraciones esta es la dirección (o nombre) de un wallet admin/raíz existente.
  • KeyType – qué tipo de clave se usa (p. ej. crypto.KeyTypeSecp256k1).
  • AuthGroups – los grupos de permisos a los que pertenece este wallet (de la sección anterior).

Este payload se almacena como el Payload de la transaction y es interpretado por la lógica de wallet de tu cadena.

Cómo funciona GenerateTransaction (una vez, para ambos flujos)

La primera vez que llamamos a GenerateTransaction es para TX_CREATE_WALLET, pero el mismo mecanismo se usa para DATA y otros tipos:

  1. Completa campos adicionales en el input:

    • Suggestor (ID del node),
    • SenderTimestamp,
    • KeyType,
    • PayloadRoot.
  2. Para TX_CREATE_WALLET, calcula un compromiso no vinculado (raíz Merkle) solo sobre el payload.

  3. Firma ese compromiso con la clave privada del wallet.

  4. Envía:

    POST /blockchains/{blockchainId}/transactions
  5. Parsea la respuesta del node en una ULTransaction con:

    • TransactionId
    • Status
    • Output
    • BlockHeight
    • VectorClock, etc.

Para otros tipos de payload (como TX_DATA) el compromiso incluye más campos (remitente, payload, etc.), pero el flujo general es el mismo.

5. Enviar transacciones DATA

Una vez que el wallet está registrado (normalmente una operación por única vez por wallet por cadena), puedes usarlo para enviar transacciones DATA normales.

Definiremos un pequeño helper similar a 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
}

Esto usa el mismo flujo de GenerateTransaction que viste anteriormente, solo con un PayloadType y cuerpo de payload diferentes.

La respuesta del node te dará:

  • TransactionId
  • Status (p. ej. "SUBMITTED", "ACCEPTED", "REJECTED")
  • Output (p. ej. "SUCCESS", "REJECTED_BY_UNAUTHORIZED")
  • más metadatos como BlockHeight y VectorClock.

6. Uniendo todo

Aquí hay un programa mínimo que:

  1. Carga la configuración y el wallet.
  2. Crea una sesión de transaction.
  3. Registra el wallet con TX_CREATE_WALLET (primera transaction).
  4. Envía algunas transacciones DATA con el mismo 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,
)
}
}

Puedes ejecutar esto con algo como:

go run ./cmd/send_transactions

(o cualquier ruta donde coloques main.go en tu proyecto).