nyx_wallet
v0.5.0
Published
SDK de navegador de Nyx Wallet v3: genera la llave en el dispositivo, la reparte 2-de-3 y firma en el cliente
Readme
Nyx Wallet SDK
SDK ESM para integrar Nyx Wallet en una aplicación web o PWA. Crea y opera cuentas inteligentes ERC-4337; la clave se genera en el navegador, se divide en tres fragmentos y Nyx solo custodia uno de ellos. Las operaciones sensibles requieren una ceremonia WebAuthn.
[email protected]se publicó con un defecto de empaquetado: no contiene el SDK compilado. No debe utilizarse. La línea0.1.xconserva compatibilidad conethersv5; para una app conethersv6 instala[email protected]o posterior dentro de0.2.x.
Instalación
El paquete publicado es nyx_wallet en npmjs.org. No requiere .npmrc, GitHub Packages ni un
token de GitHub para instalarlo. La licencia es propietaria: disponibilidad en npm no concede una
licencia de redistribución, modificación o uso fuera de los términos acordados con Ledgit.
bun add [email protected] ethers@^6.0.0 shamir-secret-sharing@^0.0.4ethers debe ser v6. ethers y shamir-secret-sharing son dependencias de pares para que la
aplicación controle una sola copia de cada una. No fuerces la línea 0.1.x sobre una app v6 con
overrides, ni rebajes toda la app a v5. 0.4.0 todavía no está publicada en npm (latest es
0.3.0); ver el estado y por qué las versiones publicadas no sirven para crear wallets en
../docs/README-DOCS.md.
Este SDK es solo de navegador. Se importa desde el código cliente de la PWA; nunca desde Adonis, un route handler, un Server Component, un job ni otro proceso Node. La API key identifica al cliente durante login y el JWT de Nyx es por usuario y sesión: ninguno es un secreto global de backend ni permite firmar fuera del dispositivo.
Requisitos de la integración
Antes de crear una wallet, la aplicación debe disponer de lo siguiente:
- Un navegador en contexto seguro (
https) concrypto.getRandomValues, IndexedDB y WebAuthn. - Una sesión autenticada contra la API de Nyx.
- El descriptor real de los contratos Safe/EntryPoint desplegados en la red objetivo.
- Una credencial WebAuthn registrada para el usuario: su identificador y clave publica P-256 en JWK. El SDK la verifica en cada operación protegida.
- Un proveedor de clave de recuperación.
createPasskeyRecoveryKeyProvider()usa WebAuthn PRF; si la plataforma no lo soporta, la aplicación debe llevar al usuario por su flujo de guardianes. - Un almacén local para el fragmento del dispositivo y una bóveda de recuperación que proteja el
material antes de persistirlo. Nunca use
localStoragepara ninguno de los dos.
Los valores del despliegue no son valores de ejemplo: deben venir de la configuración verificada de cada red. Inventarlos puede producir una dirección válida en apariencia, pero que nadie controla.
Crear una wallet
import {
createDeviceStore,
createPasskeyRecoveryKeyProvider,
createWallet,
type BiometricCredential,
type RecoveryVault,
type SafeDeployment,
} from "nyx_wallet";
const deployment: SafeDeployment = {
chainId: 80002,
proxyFactory: contracts.safeProxyFactory,
singleton: contracts.safeSingleton,
module: contracts.safe4337Module,
moduleSetup: contracts.safeModuleSetup,
entryPoint: contracts.entryPointV07,
proxyCreationCode: contracts.safeProxyCreationCode,
// Opcional: solo para wallets NUEVAS tras auditar y desplegar NyxOnboardingBatchModule.
// onboardingBatchModule: contracts.nyxOnboardingBatchModule,
};
// IndexedDB, particionado por usuario. El SDK proporciona este almacén para el fragmento local.
const deviceStore = createDeviceStore({ userId: session.userId });
// Implementación de la aplicación: cifra el valor antes de persistirlo y ofrece read/write/wipe.
// No debe enviar nunca el valor sin cifrar a Nyx ni a un servicio de analítica.
const recoveryVault: RecoveryVault = createEncryptedRecoveryVault();
// Resultado del registro WebAuthn de este usuario, recuperado de la fuente de confianza.
const biometricCredential: BiometricCredential = await api.getWalletCredential();
const wallet = await createWallet(
{
apiBaseUrl: "https://api.example.com",
accessToken: session.accessToken,
deviceStore,
recoveryVault,
recoveryKeyProvider: createPasskeyRecoveryKeyProvider(),
biometricCredential,
// Indicarlo cuando el RP de la credencial no sea el dominio que sirve la aplicación.
biometricRpId: "auth.example.com",
},
{
name: "Cuenta principal",
blockchain: "polygon",
network: "testnet",
deployment,
},
);
console.log(wallet.walletId, wallet.address, wallet.signerAddress);createWallet crea una cuenta inteligente; wallet.address es la dirección de esa cuenta, no
la dirección del firmante interno. La cuenta se registra junto con un único fragmento para Nyx y
un sobre de recuperación cifrado en el cliente.
La dirección del firmante es wallet.signerAddress (EIP-55), disponible desde 0.5.0 en los
manejadores que devuelven createWallet y openWallet. Es el valor que espera signer en
POST /api/account/address y ownerSigner en el roster de guardianes. Leerla no pide
biometría ni va a la red: el manejador ya la tiene calculada. No se debe obtener firmando un
mensaje y recuperando el firmante de la firma —verifyMessage(m, await wallet.signMessage(m))—
ni exportando la clave: las dos rutas cuestan una ceremonia para llegar a un dato público que
signerAddress entrega directamente. recoverWallet devuelve el mismo dato en
material.signerAddress, para registrar al dueño desde el dispositivo nuevo.
Operar una cuenta
Cada operación sensible solicita verificación biométrica. Las operaciones de cuenta se envían a la API de Nyx, que las retransmite al bundler; la firma se produce en el dispositivo.
await wallet.deployAccount();
const operation = await wallet.sendFunds({
to: "0x...",
value: "1000000000000000", // wei
nonce: accountNonce, // consultar antes al EntryPoint o al servicio de cuentas
});
console.log(operation.userOpHash);Para seguir esa operación, el navegador no recibe SMART_ACCOUNT_BUNDLER_URL. Consulta Nyx
con el JWT de la sesión mediante el helper; null significa que el bundler todavía no la ha
incluido, no que haya sido rechazada:
import { getUserOperationReceipt } from "nyx_wallet";
const receipt = await getUserOperationReceipt({
apiBaseUrl: "https://api.example.com",
accessToken: session.accessToken,
userOpHash: operation.userOpHash,
});Nyx hace eth_getUserOperationReceipt en el servidor. La URL y cualquier credencial del bundler
permanecen fuera del frontend y no deben declararse como variables públicas de la aplicación.
Por defecto, las tres operaciones de cuenta (deployAccount, sendFunds y rotateSigner) piden
antes a Nyx el patrocinio de gas. La cotización se obtiene antes de firmar porque el paymaster
forma parte de los bytes firmados. Si Nyx la deniega, alcanza su límite o no está disponible, la
operación sigue enviándose sin patrocinio; el resultado lo declara en operation.sponsored y, si
procede, operation.sponsorshipReason. Para no consultar el patrocinio en una llamada concreta,
pasa sponsorship: false:
const operation = await wallet.sendFunds({
to: "0x...",
value: "1000000000000000",
nonce: accountNonce,
sponsorship: false,
});El patrocinio evita que el usuario tenga que adquirir gas cuando está disponible; no sustituye un bundler configurado ni garantiza que Nyx cubra cada operación.
Onboarding atómico con una biometría
wallet.executeOnboardingBatch(intent) ejecuta dos o más llamadas como una sola
UserOperation Safe/ERC-4337. El SDK pide exactamente una ceremonia WebAuthn fresca sobre la
UserOperation final —incluye llamadas, valores, gas, paymaster, factory y firma— y no acepta
skipBiometric, un testigo ni una aserción aportada por la aplicación.
La firma Safe comienza con validAfter (6 bytes) || validUntil (6 bytes) || ownerSignature.
Para este método, validAfter es 0 y validUntil es exactamente intent.expiresAt; el BFF
debe rechazar cualquier otra ventana antes del relay.
La capacidad es opt-in y no modifica Safes existentes. Requiere que el descriptor usado al crear
la wallet incluya la dirección verificable y auditada de NyxOnboardingBatchModule; ese módulo
queda habilitado en el initializer de la Safe. Si falta, executeOnboardingBatch aborta antes de
abrir WebAuthn. Nyx no proporciona una dirección por defecto ni activa/despliega ese módulo al
instalar el SDK.
const intent = {
// Id aleatorio de un solo uso emitido por el backend/BFF del integrador.
intentId: crypto.randomUUID(),
chainId: deployment.chainId,
sender: wallet.address,
nonce: accountNonce,
// Cada llamada queda ordenada y dentro de la calldata autenticada.
calls: [
{ to: approvedToken, value: "0", data: approveExactAmount },
{ to: protocolRouter, value: "0", data: executeQuotedAction },
],
// Obligatorio: también se firma como `validUntil` de la SafeOp.
expiresAt: Math.floor(Date.now() / 1000) + 300,
// `true` solo para la primera UserOperation counterfactual (nonce 0).
deployAccount: false,
};
const result = await wallet.executeOnboardingBatch(intent);
console.log(result.userOpHash, result.sponsored);El módulo ejecuta las llamadas desde la Safe mediante permisos de módulo y revierte el batch
entero si una falla: no existe un estado approve exitoso con la acción siguiente fallida. Es un
módulo Safe privilegiado; su despliegue, auditoría, dirección por red y activación son una
decisión explícita de seguridad, no una variable que pueda inventar el frontend.
Demo experimental QA en Polygon Amoy
El repositorio incluye NyxOnboardingBatchModule y una utilidad de despliegue solo para la demo
QA de Amoy (chainId 80002). El flujo se documenta en
docs/DEPLOY-NYX-ONBOARDING-BATCH-AMOY.md: compila
el fuente con solc 0.8.23-fixed, comprueba la red y queda en dry-run si no se confirman
explícitamente --broadcast --confirm-amoy-80002.
Las variables NYX_QA_AMOY_DEPLOYER_PRIVATE_KEY (local) y los secrets del environment de GitHub
llamado qa-batch-deploy (AMOY_DEPLOYER_PRIVATE_KEY/AMOY_RPC_URL) pertenecen solo al operador
de despliegue QA. Ese environment aún requiere que un administrador configure revisores
obligatorios y política de ramas en GitHub Settings antes de cualquier uso tipo producción. No son
configuración de una aplicación integradora, no van en el frontend, no se publican en npm y no se
reutilizan en producción. Tras el despliegue, un operador debe registrar la dirección en la
configuración QA de SafeDeployment.onboardingBatchModule antes de crear nuevas Safes.
Esta entrega contiene el SDK y el módulo para QA; no contiene ni activa el BFF de Indahouse. El BFF sigue siendo un componente separado que emite/persiste intents y valida la UserOperation antes de relay como se describe abajo. No se debe anunciar la capacidad como lista para producción hasta completar auditoría independiente, configuración por red y aprobación de release.
Addendum técnico: orden real de firma y biometría
La implementación actual construye y firma primero la UserOperation en memoria y pide la única
ceremonia WebAuthn inmediatamente después. Es un orden intencional y distinto de una formulación
estricta de “biometría antes de reconstruir material”: el desafío WebAuthn del relay es la huella
de la UserOperation final, incluida su firma, factory, gas y paymaster. Pedir la aserción antes
impediría que cubriera esos bytes exactos; pedir una segunda ceremonia para la firma no es
aceptable.
Esto no convierte la firma previa en autorización remota: no sale una UserOperation firmada del dispositivo hasta que la aserción WebAuthn fresca se obtiene y se canjea. Si WebAuthn no existe, el usuario cancela o falta la aserción completa, el SDK descarta la operación y no llama al relay. La cotización de patrocinio que puede ocurrir antes contiene sólo el borrador sin firma. Si un despliegue exige que la biometría impida incluso reconstruir material, requiere un rediseño del protocolo; esta versión no afirma satisfacer esa variante más fuerte.
Contrato para un BFF de integrador
El navegador puede enviar la UserOperation ya firmada junto con intentId; el BFF nunca firma ni
recibe material de clave. Al emitir el intent, el BFF debe persistir su forma canónica y
hashOnboardingIntent(intent), asociadas a usuario, sesión y wallet, con TTL corto (normalmente
2–5 minutos). Antes de relay debe comparar el objeto recibido con ese registro mediante el
validador puro assertUserOperationMatchesOnboardingIntent:
import {
assertUserOperationMatchesOnboardingIntent,
hashOnboardingIntent,
} from "nyx_wallet/onboarding";
// Al emitir: guardar intent, hash, usuario/sesión y estado `issued` con TTL.
const intentHash = hashOnboardingIntent(intent);
// Antes de relay: recuperar por intentId y comprobar igualdad, no reinterpretar calldata.
assertUserOperationMatchesOnboardingIntent({ intent, userOperation, deployment });deployment no puede venir de la petición del navegador: el BFF lo carga de su propio registro
por chainId, con factory, singleton, bytecode y módulos previamente verificados on-chain. El
validador demuestra la correspondencia dentro de esa configuración confiable; no puede convertir
una configuración manipulada o un contrato no auditado en una garantía de seguridad.
Para deployAccount: true, el validador no acepta un factoryData meramente no vacío: decodifica
SafeProxyFactory.createProxyWithNonce, exige el singleton y el initializer Safe configurados
(incluidos Safe4337Module y el módulo batch), recalcula CREATE2 y exige que derive exactamente
el sender del intent. Eso enlaza el despliegue counterfactual a la Safe prevista; no reemplaza
la verificación de la firma, que siguen haciendo Safe y el EntryPoint.
El consumo debe ser atómico: issued -> submitted una sola vez y guardar el userOpHash. Un
reintento del mismo intent devuelve ese mismo hash; después del recibo pasa a completed o
failed, y al vencer sin enviar a expired. No se implementa ningún BFF en este paquete. El alta
inicial de WebAuthn conserva su propio gesto de navegador; la única biometría de este método cubre
solo las llamadas on-chain del batch.
sendFunds mueve fondos desde wallet.address. No se debe usar signTransaction para ese
fin: firma una transacción EOA desde la dirección del firmante interno —wallet.signerAddress—
que no es la cuenta inteligente. signTransaction y signMessage existen para integraciones que
necesitan una firma de ese firmante.
También están disponibles:
wallet.rotateSigner({ newSigner, nonce })para cambiar el firmante sin cambiar la dirección de la cuenta.wallet.exportPrivateKey()para la exportación soberana. No debe exponerse como acción por defecto en la interfaz.wallet.close()para invalidar el manejador y soltar sus referencias en memoria al terminar la sesión.walletId,addressysignerAddresssiguen legibles después: son datos públicos de la wallet, no material.
Abrir y recuperar una wallet
En el mismo dispositivo, se abre una wallet existente proporcionando el mismo despliegue usado al crearla:
const wallet = await openWallet(config, walletId, {
deployment,
saltNonce,
});En un dispositivo nuevo, se debe usar la recuperación, no openWallet. recoverWallet y
recoverWalletWithPasskey reconstruyen la clave en el cliente, generan tres fragmentos nuevos y
actualizan el fragmento de Nyx y el sobre cifrado. El fragmento del dispositivo perdido queda
inutilizable. La aplicación debe guardar el campo device que devuelve la recuperación en su
DeviceStore antes de continuar.
recoverWalletWithPasskey devuelve needs-guardians cuando WebAuthn PRF no está disponible o
el passkey de este dispositivo no puede abrir el sobre. Ese resultado es un estado esperado de
producto y debe llevar al flujo de guardianes; los errores de red, autenticación o integridad no
se convierten en un falso flujo de recuperación social.
Modelo de seguridad y límites
El SDK impone estas propiedades:
- La clave se genera con el CSPRNG de la plataforma; si no puede comprobarlo, aborta.
- Nyx recibe un solo fragmento. Las firmas y la exportación de clave se realizan en el cliente.
- Un sobre de recuperación se cifra en el cliente con la clave aportada por
RecoveryKeyProvider; Nyx no recibe el fragmento de recuperación en claro. - Las operaciones protegidas validan la credencial WebAuthn configurada, no solo un indicador de biometría.
También hay límites que la integración debe asumir:
- Un XSS en la aplicación puede usar una wallet abierta. El SDK no sustituye CSP, aislamiento de origen, revisión de dependencias ni una política de seguridad del frontend.
- JavaScript no permite borrar de forma fiable las cadenas que haya creado una librería.
close()invalida el manejador y suelta referencias, pero no garantiza la sobrescritura de todo el heap. - La disponibilidad de WebAuthn PRF depende del autenticador, navegador y plataforma. Debe probarse en los dispositivos que vaya a soportar el producto antes de ofrecer la recuperación por passkey como única vía.
API pública principal
| API | Uso |
| --- | --- |
| createWallet(config, options) | Crea la cuenta, registra el fragmento de Nyx y sella el sobre de recuperación. |
| openWallet(config, walletId, options) | Abre una wallet desde material disponible en el dispositivo actual. |
| recoverWallet(config, walletId) | Recupera desde el sobre, redivide los fragmentos y revoca matemáticamente el juego anterior. |
| recoverWalletWithPasskey(config, walletId) | Variante que expresa la falta de portabilidad de PRF como needs-guardians. |
| createDeviceStore({ userId }) | Crea el almacén IndexedDB para el fragmento del dispositivo. |
| createPasskeyRecoveryKeyProvider() | Crea el proveedor de clave de recuperación basado en WebAuthn PRF. |
| computeAccountAddress(identity) | Calcula y permite verificar la dirección CREATE2 de una cuenta. |
| wallet.signerAddress | Dirección EIP-55 del firmante (EOA) de la cuenta. Sin biometría ni red. Desde 0.5.0. |
| wallet.executeOnboardingBatch(intent) | Firma y retransmite un batch Safe/ERC-4337 atómico con una biometría fresca. |
| nyx_wallet/onboarding | Canonicaliza/hashea intents y permite a un BFF contrastar una UserOperation sin API de firma. |
Licencia
Este software es propietario y confidencial. Su uso, copia, modificación, distribución, ingeniería inversa o explotación comercial requiere autorización escrita de Nyx Wallet. Consulta LICENSE para el texto completo.
