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).