Tokens ERC-20 (Go)
Esta guía muestra los pasos principales para usar las transacciones de tokens ERC-20 nativas de ULedger con el ULedger Go SDK.
Los tokens ERC-20 son tokens fungibles: cada unidad es intercambiable, como la moneda en el sentido convencional. Se usan comúnmente para pagos, balances dentro de aplicaciones, tokens de gobernanza/staking, y activos tokenizados donde "1 unidad" es equivalente a cualquier otra unidad.
En ULedger, los tokens de tipo ERC-20 están soportados como transacciones nativas del protocolo. Esto significa que se puede gestionar el ciclo de vida de un token usando tipos de transacción estándar como:
CREATE_TOKEN— define los metadatos del token (name,symbol,decimals) y opcionalmente acuña un suministro inicialTRANSFER_TOKEN— envía tokens entre billeterasAPPROVE_TOKEN— otorga una asignación al que gastaTRANSFER_TOKEN(con payloadFrom) — transfiere en nombre de un propietario usando una asignación/aprobaciónBURN_TOKEN— destruye tokens para reducir el suministro (si es quemable)
Dado que estas acciones son transacciones normales de ULedger, las operaciones con tokens heredan las capacidades principales de ULedger: identidad basada en billeteras, firmas criptográficas, historial a prueba de manipulaciones y un rastro auditable de eventos de tokens en la cadena.
En las próximas guías, cubriremos los otros estándares de tokens nativos de ULedger:
- ERC-721 (NFTs) — tokens no fungibles donde cada token tiene un
tokenIdúnico (coleccionables, certificados, activos únicos). - ERC-1155 (multi-tokens) — un único contrato/dirección puede gestionar tanto IDs de tokens fungibles como no fungibles, incluyendo acuñación/transferencia por lotes (objetos de juego, sistemas de inventario, catálogos de activos mixtos).
La billetera debe estar registrada en la cadena (CREATE_WALLET) antes de poder enviar transacciones de tokens.
Se debe verificar que la billetera esté registrada.
1) Prerrequisitos
Se necesita:
- Un endpoint de node (los ejemplos usan
https://your-node-urlcomo marcador de posición, por lo que se debe reemplazar por la URL de la red que se use) - Un
blockchainId(Testnet:08c28f29a62819120958984b761ddf8ccb45951612731409873994958fd150a2) - Una billetera con la que se pueda firmar (recomendado: cargar desde
.ukey) - Una comprensión básica de que:
- Token Address = el
TransactionIddevuelto porCREATE_TOKEN
- Token Address = el
2) Cargar una billetera
Se carga desde un archivo .ukey en lugar de codificar las claves directamente:
w, err := wallet.LoadFromFile("./wallets/my_wallet.ukey", "")
if err != nil {
return fmt.Errorf("load wallet: %w", err)
}
3) Crear una sesión de transacción
Se crea una sesión vinculada a un node y una billetera:
session, err := transaction.NewUL_TransactionSession(nodeEndpoint, w)
if err != nil {
return fmt.Errorf("create session: %w", err)
}
4) Crear un token ERC-20 (CREATE_TOKEN)
4.1 Construir el payload
func buildCreateERC20Payload() ([]byte, error) {
return json.Marshal(transaction.CreateTokenPayload{
TokenType: transaction.ERC20_TOKEN_TYPE,
Name: "Sotoken",
Symbol: "SOTOK",
Decimals: 18,
InitialSupply: 1000000000000000000, // 1 token if decimals=18 (in wei-like units)
Mintable: true,
Burnable: true,
})
}
4.2 Enviar la transacción
payloadBytes, err := buildCreateERC20Payload()
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 token tx: %w", err)
}
tokenAddress := tx.TransactionId // ✅ Se debe guardar este valor
fmt.Println("Token address:", tokenAddress)
fmt.Println("Status:", tx.Status, "Output:", tx.Output)
✅ El
tx.TransactionIddevuelto se usa como la "dirección" del token en llamadas futuras.
5) Transferir tokens (TRANSFER_TOKEN)
:::warning Las direcciones de destino no se validan
La red acredita la dirección que se le indica y no verifica que pertenezca a una billetera registrada en esta cadena. Las transferencias son definitivas.
Un destinatario solo puede gastar lo que recibe si tiene una billetera en la misma cadena. Por lo tanto:
- una dirección cuyo tipo de clave coincide con la cadena pero que aún no está registrada conserva tokens seguros, porque registrar la billetera después los vuelve gastables;
- una dirección cuyo tipo de clave no coincide con la cadena, o cuya clave nadie posee, conserva tokens que nunca podrán volver a moverse.
Ambos casos son idénticos en el momento de la transferencia: la transaction se completa con éxito y el saldo aparece a nombre del destinatario. Se debe verificar al destinatario antes de enviar:
curl -o /dev/null -w '%{http_code}\n' \
"https://your-node-url/blockchains/{blockchainId}/wallets/{recipientAddress}"
# 200 = registered on this chain 404 = not registered here
:::
5.1 Construir el payload
func buildTransferERC20Payload(tokenAddress, to string, amount uint64) ([]byte, error) {
return json.Marshal(transaction.TransferTokenPayload{
TokenAddress: tokenAddress,
To: to,
Amount: amount,
})
}
5.2 Enviar la transacción
payloadBytes, err := buildTransferERC20Payload(tokenAddress, toAddress, 5000)
if err != nil {
return err
}
input := transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: w.Address,
PayloadType: transaction.TRANSFER_TOKEN.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)
6) Aprobar al que gasta (APPROVE_TOKEN)
Las aprobaciones permiten que el que gasta transfiera tokens en nombre del propietario del token (estilo asignación).
:::info El que gasta debe ser una billetera distinta
Una billetera no puede aprobarse a sí misma. Si el firmante y Spender son la misma dirección, la transaction se
rechaza como autoaprobación y no se crea ninguna asignación (allowance).
Además, una aprobación por sí sola no mueve nada, porque el allowance solo se consume mediante una transferencia que incluya
From en su payload (paso 7). Una transferencia simple siempre mueve los tokens del propio firmante y nunca
afecta a un allowance.
:::
6.1 Construir el payload
func buildApproveERC20Payload(tokenAddress, spender string, amount uint64) ([]byte, error) {
return json.Marshal(transaction.ApproveTokenPayload{
TokenAddress: tokenAddress,
Spender: spender,
Amount: amount,
})
}
6.2 Enviar la transacción
payloadBytes, err := buildApproveERC20Payload(tokenAddress, spenderAddress, 5000)
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)
7) Transferir usando aprobación (TRANSFER_TOKEN con payload From)
Este patrón envía una transferencia donde:
- el firmante de la transacción (
input.From) es el que gasta - el payload From es el propietario del token cuyo balance está siendo movido
:::warning El destinatario no debe ser el que gasta
La red rechaza una transferencia cuyo firmante es también el destinatario. En una transferencia por allowance el firmante es el que gasta, por lo que el que gasta no puede enviar tokens a su propia dirección: la transaction se acepta al enviarse y luego no se aplica, y el allowance permanece sin cambios.
Se debe enviar a una tercera dirección:
// rejected, signer (spender) is also the recipient
buildTransferApprovalERC20Payload(tokenAddress, spenderAddress, ownerAddress, 3000)
// works, a third party receives the tokens
buildTransferApprovalERC20Payload(tokenAddress, finalRecipient, ownerAddress, 3000)
Si es necesario que el que gasta termine con los tokens, se pueden enrutar a través de una tercera billetera, o hacer que el propietario transfiera directamente en lugar de usar un allowance. :::
7.1 Construir el payload
func buildTransferApprovalERC20Payload(tokenAddress, to, owner string, amount uint64) ([]byte, error) {
return json.Marshal(transaction.TransferTokenPayload{
TokenAddress: tokenAddress,
To: to,
From: owner, // owner whose funds will be moved
Amount: amount, // amount to spend from allowance
})
}
7.2 Enviar la transacción
payloadBytes, err := buildTransferApprovalERC20Payload(tokenAddress, finalRecipient, ownerAddress, 3000)
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)
8) Quemar tokens (BURN_TOKEN)
:::info Usar BURN_TOKEN para destruir suministro
BURN_TOKEN es la forma admitida de retirar tokens de circulación: reduce totalSupply y
deja una transaction BURN_TOKEN auditable.
Enviar tokens a una dirección que nadie controla también los vuelve inutilizables, pero no es una quema: el suministro no cambia y el saldo sigue apareciendo en esa dirección, por lo que los informes y las conciliaciones seguirán contándolo. :::
8.1 Construir el payload
func buildBurnERC20Payload(tokenAddress string, amount uint64) ([]byte, error) {
return json.Marshal(transaction.BurnTokenPayload{
TokenAddress: tokenAddress,
Amount: amount,
})
}
8.2 Enviar la transacción
payloadBytes, err := buildBurnERC20Payload(tokenAddress, 5000)
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)
9) Ejemplo completo
Este ejemplo único muestra:
- Crear token
- Transferir
- Aprobar
- Transferir usando aprobación
- Quemar
package main
import (
"encoding/json"
"fmt"
"github.com/ULedgerInc/go-sdk/pkg/crypto"
"github.com/ULedgerInc/go-sdk/pkg/transaction"
"github.com/ULedgerInc/go-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
createPayload, err := json.Marshal(transaction.CreateTokenPayload{
TokenType: transaction.ERC20_TOKEN_TYPE,
Name: "Sotoken",
Symbol: "SOTOK",
Decimals: 18,
InitialSupply: 1000000000000000000,
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),
KeyType: crypto.KeyTypeSecp256k1,
})
if err != nil { panic(err) }
tokenAddress := createTx.TransactionId
fmt.Println("Token address:", tokenAddress)
// 2) TRANSFER_TOKEN (owner -> someone)
transferPayload, _ := json.Marshal(transaction.TransferTokenPayload{
TokenAddress: tokenAddress,
To: spenderWallet.Address,
Amount: 5000,
})
transferTx, err := ownerSession.GenerateTransaction(transaction.ULTransactionInput{
BlockchainId: blockchainId,
From: ownerWallet.Address,
PayloadType: transaction.TRANSFER_TOKEN.String(),
Payload: string(transferPayload),
})
if err != nil { panic(err) }
fmt.Println("Transfer tx:", transferTx.TransactionId)
// 3) APPROVE_TOKEN (owner approves spender)
approvePayload, _ := json.Marshal(transaction.ApproveTokenPayload{
TokenAddress: tokenAddress,
Spender: spenderWallet.Address,
Amount: 5000,
})
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)
// 4) TRANSFER_TOKEN using approval (spender spends owner's allowance)
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
Amount: 3000,
})
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)
// 5) BURN_TOKEN
burnPayload, _ := json.Marshal(transaction.BurnTokenPayload{
TokenAddress: tokenAddress,
Amount: 1000,
})
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)
}
Próximos pasos
- Crear una guía de ERC-721 (patrones de acuñación/transferencia/aprobación de NFTs)
- Crear una guía de ERC-1155 (multi-token + patrones de transferencia por lotes)