Saltar al contenido principal

Billeteras (TypeScript)

En este tutorial revisaremos cómo crear, guardar, recrear y usar una billetera. Cuando se realiza una transaction, el SDK firma la transaction y la envía a la red. Este sello criptográfico permite a los nodes detectar cualquier modificación en los datos y proporciona trazabilidad a la actividad de la billetera, porque todas las transactions están vinculadas a la billetera.

info

Una billetera se usa para crear y firmar transactions; la billetera también tiene una dirección única. Los nodes validan la firma y la existencia de la billetera on-chain para esas transactions, por lo que es un requisito usar billeteras válidas al crear transactions.

info

Este tutorial asume que ya se dispone de una blockchain ULedger configurada y un endpoint de node disponible.

En ULedger, una billetera representa una identidad on-chain:

  • Contiene un keypair (clave pública/privada).
  • Tiene una dirección única derivada de la clave pública.
  • Se le pueden otorgar permisos mediante authGroups.
  • Se usa para firmar transactions que la red puede verificar.

Una billetera también tiene un key type (keyType) que define qué algoritmo criptográfico utiliza. En el TypeScript SDK, keyType es un enum tipado (KeyType), por lo que se deben pasar valores del enum (no strings sin formato) al seleccionar un key type.

Key types soportados:

  • KeyType.Secp256k1 (predeterminado)
  • KeyType.ED25519
  • KeyType.MlDSA87
  • KeyType.BLS12377

En esta guía se explica cómo:

  1. Generar una nueva billetera + mnemonic (recomendado).
  2. Restaurar una billetera desde un mnemonic existente (cuando ya se tiene uno).
  3. Guardar la billetera en un archivo .ukey.
  4. Cargar una billetera desde el disco.
  5. Recrear una billetera desde claves existentes o JSON.
  6. (Luego) usarlo en una sesión de transaction.

1. Prerrequisitos​

  • Node.js >= 16
  • Instalar el SDK:
npm i @uledgerinc/typescript-sdk

En el código TypeScript se usará principalmente:

import { getULedgerSDK } from "@uledgerinc/typescript-sdk";

2. Estructura de la billetera (¿qué contiene una billetera?)​

En TypeScript, los tipos de billetera reflejan la implementación en Go e incluyen un campo keyType: KeyType (no un string de forma libre).

Un payload completo de billetera típicamente contiene:

  • address: string
  • enabled: boolean
  • parent: string
  • authGroups: Record<string, UL_AuthPermission>
  • keyType: KeyType
  • publicKeyHex: string
  • privateKeyHex?: string
  • mnemonic?: string

KeyType es un enum, por ejemplo: KeyType.Secp256k1 (valor string "secp256k1").


3. Generar una nueva billetera + mnemonic (recomendado)​

La forma más segura y recomendada de crear una billetera es dejar que el SDK genere un mnemonic criptográficamente seguro BIP-39.

El SDK soporta valores de entropía comunes: 128, 160, 192, 224, 256 bits.

Se recomienda 256 bits (24 palabras). En los ejemplos a continuación lo configuramos explícitamente en 256.

import { getULedgerSDK, KeyType } from "@uledgerinc/typescript-sdk";

async function main() {
const sdk = await getULedgerSDK();

const wallet = sdk.generateWallet({
keyType: KeyType.Secp256k1,
entropy: 256, // recommended default (24 words)
password: "", // optional
parent: "", // optional
authGroups: {}, // optional
});

console.log("Address:", wallet.address);
console.log("Mnemonic (store this safely!):", wallet.mnemonic);
}

main().catch(console.error);

⚠️ El mnemonic devuelto debe tratarse como un secreto de alta seguridad. Se recomienda guardarlo offline si es posible.


4. Restaurar una billetera desde un mnemonic existente (cuando ya se tiene uno)​

Si ya se tiene un mnemonic BIP-39 válido (por ejemplo, generado anteriormente por el SDK), se puede recrear la misma billetera.

import { getULedgerSDK, KeyType } from "@uledgerinc/typescript-sdk";

async function main() {
const sdk = await getULedgerSDK();

const mnemonic = "<paste your BIP-39 mnemonic here>";
const passphrase = ""; // optional (must match what was used originally)

// Optional but recommended: validate before restoring
if (!sdk.validateMnemonic(mnemonic)) {
throw new Error("Invalid mnemonic: must be valid BIP-39");
}

const wallet = sdk.walletFromMnemonic(mnemonic, {
keyType: KeyType.Secp256k1, // optional (defaults to Secp256k1)
password: passphrase, // optional (used for key derivation)
});

console.log("Wallet address:", wallet.address);
console.log("Can sign:", wallet.canSign);
}

main().catch(console.error);

walletFromMnemonic conceptualmente:

  • Valida el mnemonic (BIP-39).
  • Deriva una semilla del mnemonic (+ passphrase opcional).
  • Crea un keypair para el key type seleccionado (ej. secp256k1).
  • Calcula la dirección a partir de la clave pública.
  • Devuelve una billetera capaz de firmar.

💡 El mnemonic + passphrase es suficiente para recrear la misma billetera posteriormente. Perderlo significa perder acceso al keypair subyacente.


5. Guardar una billetera en un archivo .ukey​

A diferencia del Go SDK, el TS SDK no incluye un helper SaveToFile, por lo que se persiste usando fs de Node y la exportación JSON de la billetera.

Helper: guardar un .ukey (Node.js)​

import fs from "node:fs";
import path from "node:path";
import { Wallet } from "@uledgerinc/typescript-sdk";

type SaveOptions = {
includePrivateKey?: boolean; // default false
includeMnemonic?: boolean; // default true
};

export function saveWalletToUKeyFile(
wallet: Wallet,
filePath: string,
opts: SaveOptions = {},
) {
const includePrivateKey = opts.includePrivateKey ?? true;
const includeMnemonic = opts.includeMnemonic ?? true;

const finalPath = filePath.endsWith(".ukey") ? filePath : `${filePath}.ukey`;

const data = wallet.toWalletData(includePrivateKey);

// Optionally remove mnemonic before writing
if (!includeMnemonic) {
// @ts-expect-error wallet data is a plain object
delete data.mnemonic;
}

fs.mkdirSync(path.dirname(finalPath), { recursive: true });
fs.writeFileSync(finalPath, JSON.stringify(data, null, 2), { mode: 0o600 });
fs.chmodSync(finalPath, 0o600); // enforce owner-only permissions on *nix
}

Uso:

import { getULedgerSDK, KeyType } from "@uledgerinc/typescript-sdk";
import { saveWalletToUKeyFile } from "./saveWalletToUKeyFile";

async function main() {
const sdk = await getULedgerSDK();

const wallet = sdk.generateWallet({
keyType: KeyType.Secp256k1,
entropy: 256, // recommended
});

// Best practice: do NOT include the private key in the file if you can avoid it.
saveWalletToUKeyFile(wallet, "wallets/my_wallet.ukey", {
includePrivateKey: false,
includeMnemonic: true,
});
}

main().catch(console.error);

Ejemplo de archivo .ukey​

{
"address": "11e4876958847f93a82c39110cc86a2bcdf1adf3f725fa9ad8a70b6f5eae1bf1",
"enabled": true,
"parent": "",
"authGroups": {},
"mnemonic": "kiwi possible help find habit genre math lion alpha page loud arrow ...",
"keyType": "secp256k1",
"publicKeyHex": "0435C4D829A686D05ACE8256CAA73504F69BF5569D3E6122BEC54D64D7C31C8E...",
"privateKeyHex": "2310117A24CD8F086E429D1F52748FEC3EAD5C42E0910187143FE8C63CC0C0C1"
}

Algunos detalles que vale la pena destacar:

  • keyType se almacena como string (ej. "secp256k1").
  • mnemonic puede incluirse por conveniencia y recuperación; se puede omitir si no se quieren mnemonics en los archivos.
  • privateKeyHex es opcional — se debe incluir solo si realmente es necesario.

6. Cargar una billetera desde un archivo .ukey​

Para reconstruir una billetera desde un .ukey, se lee el mnemonic o el JSON y se pasa a la billetera.

import fs from "node:fs";
import { getULedgerSDK, KeyType } from "@uledgerinc/typescript-sdk";

async function main() {
const sdk = await getULedgerSDK();

const raw = fs.readFileSync("wallets/my_wallet.ukey", "utf8");
// get mnemonic from raw
const mnemonic = JSON.parse(raw).mnemonic;
const walletFromMnemonic = sdk.walletFromMnemonic(mnemonic, { keyType: KeyType.Secp256k1}); // password/passphrase if used
const walletFromJson = sdk.walletFromJson(raw); // password if used

console.log("Loaded wallet address:", walletFromMnemonic.address);
console.log("Can sign:", walletFromMnemonic.canSign);
console.log("Loaded wallet address from JSON:", walletFromJson.address);
console.log("Can sign from JSON:", walletFromJson.canSign);
}

main().catch(console.error);

El comportamiento coincide con la intención del flujo en Go:

  • Si el mnemonic está presente → restaurar desde el mnemonic (el mnemonic debe ser BIP-39 válido).
  • Si hay una clave privada presente → reconstruir desde las claves hex.
  • De lo contrario → se puede terminar con una billetera que no puede firmar.

7. Cargar desde JSON sin formato (string)​

Si la billetera ya es un string JSON sin formato (desde una CLI, una API, etc.), se usa:

import { getULedgerSDK } from "@uledgerinc/typescript-sdk";

async function main() {
const sdk = await getULedgerSDK();

const raw = `{
"address": "11e4876958847f93a82c39110cc86a2bcdf1adf3f725fa9ad8a70b6f5eae1bf1",
"enabled": true,
"parent": "",
"authGroups": {},
"mnemonic": "kiwi possible help find habit genre math lion alpha page loud arrow ...",
"keyType": "secp256k1",
"publicKeyHex": "0435C4D829A686D0...",
"privateKeyHex": "2310117A24CD8F08..."
}`;

const wallet = sdk.walletFromJson(raw, "");
console.log("Address:", wallet.address);
}

main().catch(console.error);

8. Importar un keypair existente (hex público/privado)​

Si ya se tienen claves codificadas en hex, se usa walletFromHex:

import { getULedgerSDK, KeyType } from "@uledgerinc/typescript-sdk";

async function main() {
const sdk = await getULedgerSDK();

const publicKeyHex = "04f2f0fd15ba3a7f4ba62cd705c4df80...";
const privateKeyHex = "63f6062f2034bcbcc08bae2eaabee8dd...";

const wallet = sdk.walletFromHex(publicKeyHex, privateKeyHex, KeyType.Secp256k1);

console.log("Imported wallet address:", wallet.address);
console.log("Can sign:", wallet.canSign);
}

main().catch(console.error);

9. Próximos pasos: Registrar billeteras​

Todo lo que hemos hecho hasta ahora existe localmente:

  • Generamos un keypair (y mnemonic).
  • Lo guardamos en un archivo .ukey.
  • Lo cargamos de nuevo en un objeto billetera.

En este punto, la billetera puede no estar registrada en ninguna blockchain aún. Es una identidad criptográfica que puede usarse para firmar transactions, pero los nodes generalmente necesitan ver la clave pública de la billetera y sus metadatos on-chain antes de aceptarla.

Para hacer que una blockchain sea consciente de esta billetera (clave pública, parent, auth groups), se envía una transaction de registro de billetera (ej. TX_CREATE_WALLET):

  • El payload incluye la clave pública de la billetera, el key type, el parent y los auth groups.
  • La transaction se firma y se envía como cualquier otra transaction.
  • Una vez incluida en un bloque, los nodes pueden validar esta billetera cuando firme futuras transactions.

Continuar en: Registrar y Crear Transactions (TypeScript) para construir y enviar la transaction de registro de la billetera.