@eternl/hub-wallet-sdk

中文文档
0.55.0发布于 2026/11/10BUSL-1.1TypeScriptESM + CJS近 30 天下载 2487

Eternl Hub SDK for wallet developers.

原始使用文档
文档目录27 节
  1. Installation
  2. Runtime requirements
  3. Limitations — module-level singleton state
  4. Configuration
  5. 1. Initialize the SDK
  6. 2. Required NATS configuration — getNatsUrl + getAuthentica…
  7. Quick Start
  8. Primary API — WalletConnectorSession
  9. Options ( WalletConnectorSessionOptions )
  10. Returned API ( WalletConnectorSessionApi )
  11. Remote signing (requester side)
  12. Presence beacon & raw-stream presence
  13. Lower-level composables
  14. useNatsClient
  15. useEncryption
  16. useReplayProtection
  17. useSessionKeyCache
  18. useMessageAuth
  19. useNatsValidation
  20. Storage
  21. Utilities
  22. Canonicalization (RFC 8785)
  23. Secure IDs
  24. Protocol constants
  25. Protocol types & permissions
  26. Security model (summary)
  27. License

@eternl/hub-wallet-sdk

A composables-based SDK for wallet developers to integrate with Eternl Hub. It provides the wallet-side building blocks for secure device/dApp pairing, end-to-end-encrypted messaging, presence, and remote signing over NATS WebSocket transport.

The primary, high-level entry point is createWalletConnectorSession — it composes the symmetric pairing protocol from @eternl/hub-core and adds the wallet-role logic (responding to dApp apiCalls, account switching, presence). Lower-level composables (useNatsClient, useEncryption, useReplayProtection, …) are also exported for advanced or custom integrations.

Status: source-available under BUSL-1.1 (see License). The API is stabilizing toward a 1.0 release; while on 0.x, minor versions may carry breaking changes — check the CHANGELOG before upgrading.

Installation

npm install @eternl/hub-wallet-sdk
# or
pnpm add @eternl/hub-wallet-sdk

It depends on @eternl/hub-core and the modular @nats-io/*@3.x clients (@nats-io/nats-core, @nats-io/jetstream, @nats-io/kv), which are installed automatically as dependencies.

Runtime requirements

The SDK requires a secure WebCrypto environment (a modern browser or Node.js 18+). initHub() calls assertCryptoCapabilities() and throws if crypto.subtle / crypto.getRandomValues are unavailable. There is no Math.random() fallback anywhere in the SDK.

You can probe capabilities yourself:

import { checkCryptoCapabilities } from '@eternl/hub-wallet-sdk'

const caps = checkCryptoCapabilities()
// {
//   hasSubtleCrypto: boolean
//   hasRandomValues: boolean
//   hasRandomUUID:   boolean
//   isSecure:        boolean   // hasSubtleCrypto && hasRandomValues
// }
if (!caps.isSecure) {
  throw new Error('WebCrypto unavailable — SDK cannot run here')
}

Limitations — module-level singleton state

Read this before designing your integration.

Several core modules hold process-wide singleton state, not per-instance state:

  • useNatsClient — one NATS connection, one set of callbacks, and one subscription map per process. Calling useNatsClient() repeatedly returns handles onto the same underlying connection. configure() overwrites the previously-configured providers/callbacks. There is exactly one connection per process.
  • useEncryption — getCurrentKeyPair() / getCurrentSessionKey() track a single "current" key pair / session key per process. generateKeyPair() and deriveSessionKeyHKDF() overwrite them.
  • signingTransport — configureSigningTransport(...) sets one global config consumed by all signing composables.
  • getWalletPresenceBeacon() — returns a process-wide singleton beacon (so all sessions share one lifecycle-dispatcher subscription).
  • useReplayProtection / useSessionKeyCache — module-level caches keyed by pairId / deviceId.

Practical consequence: this SDK is designed for one wallet client per JS context (one browser tab / one Node process). It is not safe to run two independent, differently-configured wallet clients in the same process. Tests that need isolation should reset state via the exported test helpers (e.g. __resetWalletPresenceBeaconForTests, clearAllReplayCaches, clearAllCachedKeys) or run in separate worker processes.

createWalletConnectorSession itself returns an independent per-pairing object, but it composes the singleton NATS transport and (by default) the singleton presence beacon underneath.

Configuration

1. Initialize the SDK

import { initHub, isInitialized, getConfig } from '@eternl/hub-wallet-sdk'

initHub({ network: 'mainnet' })

// With options:
initHub({
  network: 'preprod',                              // 'mainnet' | 'preprod' | 'preview'
  hubUrl: 'wss://custom-hub.example.com/nats',     // default: wss://hub.de-5.eternl.art/nats
  storage: myStorage,                              // optional HubStorage adapter
  storagePrefix: 'my-wallet',                      // optional localStorage key prefix
  constants: { CONNECTION_TIMEOUT_MS: 20_000 },    // optional protocol-constant overrides
})

if (isInitialized()) {
  console.log('network:', getConfig().network)
}

2. Required NATS configuration — getNatsUrl + getAuthenticator

useNatsClient does not read the hub URL from initHub. You must call configure() with at least a getNatsUrl provider before connecting. Production brokers run deny-by-default ACLs, so you must also supply a getAuthenticator that mints a per-pairing JWT.

import { useNatsClient } from '@eternl/hub-wallet-sdk'
import { jwtAuthenticator } from '@nats-io/nats-core'

const nats = useNatsClient()

nats.configure({
  // Required. Returns the broker WebSocket URL.
  getNatsUrl: () => 'wss://hub.de-5.eternl.art/nats',

  // Per-pairing JWT model (R1.5 / C1.e).
  // Called immediately before EVERY connect attempt — including reconnects —
  // so you can mint a fresh per-pairing CLIENTS user JWT (e.g. POST /nats/token
  // on be-hub) each time. Returning `undefined` produces an anonymous
  // connection, which only works against local-dev / pre-R1.2 brokers.
  //
  // YOU are responsible for refresh-before-expiry: when the JWT nears `exp`,
  // call `disconnect()` then `connect()` to swap in a fresh token. The SDK
  // re-invokes this provider on the new connect.
  getAuthenticator: async () => {
    const { jwt, seed } = await mintPerPairingToken()  // your call to be-hub
    return jwtAuthenticator(jwt, new TextEncoder().encode(seed))
  },

  // Optional lifecycle / error callbacks:
  onConnected: () => console.log('connected'),
  onDisconnected: () => console.log('disconnected'),
  onError: (msg: string) => console.error('nats error:', msg),

  // Broker rejected the whole connection (AUTHORIZATION_VIOLATION). The SDK
  // does NOT auto-retry — clear your cached JWT, then either mint a fresh one
  // and call retry(), or surface a re-pair prompt.
  onPairRevoked: () => clearCachedJwtAndPromptRepair(),

  // Broker rejected a publish/subscribe (PERMISSIONS_VIOLATION). The
  // connection stays up; this fires per-subject for ACL denials.
  onPermissionsViolation: (subject?: string) => console.warn('ACL denied:', subject),
})

await nats.connect()

Quick Start

A minimal end-to-end flow: connect, confirm the connection is live, then run a wallet connector session for a pairing.

import {
  initHub,
  useNatsClient,
  configureSigningTransport,
  createWalletConnectorSession,
  type ApiCallResult,
} from '@eternl/hub-wallet-sdk'

// 1. Init
initHub({ network: 'mainnet' })

// 2. Configure + connect transport
const nats = useNatsClient()
nats.configure({
  getNatsUrl: () => 'wss://hub.de-5.eternl.art/nats',
  getAuthenticator: async () => myJwtAuthenticator(),  // see "Required NATS configuration"
})
await nats.connect()

// connectionStatus() is a getter, not getConnectionStatus()
if (nats.connectionStatus() !== 'connected') {
  throw new Error(`NATS not connected: ${nats.connectionStatus()}`)
}

// 3. Run a wallet connector session for an established pairing.
//    `encryption` and `transport` here implement hub-core's SessionEncryptionApi
//    and Transport — typically built from the pairing's derived session key and
//    the NATS client. See "WalletConnectorSession" below.
const session = createWalletConnectorSession({
  pairId,                       // string — the established pairing id
  encryption,                   // SessionEncryptionApi (from @eternl/hub-core)
  transport,                    // Transport (from @eternl/hub-core)
  grantedPermissions,           // Permission[]
  walletName: 'My Wallet',
  apiCallHandler: async ({ method, args, accountId }): Promise<ApiCallResult> => {
    // Invoke the underlying CIP-30 method and return its result.
    const result = await myWallet.invoke(method, args, accountId)
    return { success: true, result }
  },
})

session.on('apiCallReceived', ({ method }) => console.log('dApp called', method))

Primary API — WalletConnectorSession

createWalletConnectorSession(options) builds a wallet-role session over a single pairing. It owns inbound apiCall dispatch, account-switch handling, access/consent state, and presence. The exported WalletConnectorSession class is a thin wrapper that returns the factory result.

Options (WalletConnectorSessionOptions)

interface WalletConnectorSessionOptions {
  pairId: string
  encryption: SessionEncryptionApi          // from @eternl/hub-core
  transport: Transport                       // from @eternl/hub-core
  grantedPermissions: Permission[]           // enforced for signing-method ACLs

  requireConsent?: boolean                   // default false → auto-grants all categories on start
  autoEstablishAllowed?: boolean             // default true
  walletName?: string

  apiCallHandler: ApiCallHandler             // REQUIRED
  accountSwitchHandler?: AccountSwitchHandler

  onPeerDisconnect?: (reason: string) => void
  onPeerPairingRemoved?: (reason: string) => void
  onError?: (err: Error) => void

  // Opt-in raw-stream presence (Option C):
  presenceC?: { deviceId: string; tabId: string; getNc: () => NatsConnection | null }
  presenceBeacon?: WalletPresenceBeaconApi   // override singleton (tests only)
}

type ApiCallResult =
  | { success: true; result: unknown }
  | { success: false; errorCode: string; errorMessage: string }

type ApiCallHandler = (params: {
  method: string
  args: unknown[]
  accountId: string
  requestId: string
}) => Promise<ApiCallResult>

type AccountSwitchHandler = (params: { accountId: string }) => Promise<boolean>

Returned API (WalletConnectorSessionApi)

const session = createWalletConnectorSession(options)

session.pairId                  // string
session.accessGranted           // boolean
session.grantedCategories       // CIP30Category[] (copy)
session.grantedPermissions      // readonly Permission[]

// Consent (only needed when requireConsent: true; otherwise auto-granted on start)
await session.grantAccess(['core', 'governance', 'extension'], /* autoEstablishAllowed */ true)
await session.denyAccess('User declined')

// Push account changes to the dApp
await session.notifyAccountsChanged(accounts, activeAccountId, 'updated')

// Presence
await session.requestPresenceCheck(/* timeoutMs? */ 5000)        // Promise<boolean>
await session.requestPresenceCSnapshot()                         // Option C only; else resolves null
await session.notifyOnline()
await session.notifyOffline('tab hidden')

// Teardown
await session.disconnect('Wallet disconnected')   // sends DISCONNECT, then disposes
await session.remove('Pairing removed')           // sends PAIRING_REMOVED, then disposes
session.dispose()                                 // local cleanup, no message

// Events (returns an unsubscribe fn)
const off = session.on('apiCallReceived', ({ method, accountId, requestId }) => { /* … */ })
session.on('accountSwitchRequested', ({ accountId }) => { /* … */ })
session.on('pingReceived', ({ timestamp }) => { /* … */ })
session.on('heartbeatReceived', ({ timestamp }) => { /* … */ })
session.on('peerPresence', ({ status, reason, sentAt }) => { /* … */ })
session.on('peerPresenceC', (event) => { /* … */ })          // Option C only
session.on('accessStateChange', ({ granted, reason }) => { /* … */ })
session.on('error', (err) => { /* … */ })
off()

Notes:

  • With requireConsent omitted/false, the session auto-grants ['core','governance','extension'] on start.
  • When access is not granted (or the method's category is not granted), inbound apiCalls are rejected with errorCode: '-3' before your apiCallHandler runs.
  • A handler that throws is surfaced to the dApp as errorCode: '-2'.
  • The session implements Symbol.dispose, so using session = createWalletConnectorSession(...) works where supported.

Remote signing (requester side)

useSigningRequestSender lets a wallet ask another paired instance to sign a transaction. It requires configureSigningTransport(...) to be called once at boot, and an active NATS connection.

import {
  configureSigningTransport,
  useSigningRequestSender,
} from '@eternl/hub-wallet-sdk'

// Configure once, globally:
configureSigningTransport({
  getDeviceInfo:        () => myDeviceInfo,
  getMyDeviceId:        () => myDeviceId,
  getMyPeerId:          () => myPeerId,
  getSessionKey:        (deviceId) => sessionKeysByDevice[deviceId],   // base64 raw key | undefined
  getPairCryptoContext: async (deviceId) => ({
    pairingId,
    pinAadBytes,        // Uint8Array — first 8 bytes of SHA-256(pin)
  }),
  getNextSeq:           (pairingId) => nextSeqFor(pairingId),          // bigint, monotonic per pairing
})

const { requestRemoteSigning } = useSigningRequestSender()

const result = await requestRemoteSigning(targetInstance, transactionCBOR, {
  walletPubKeyHash: 'sha256-hex-of-wallet-pubkey',
  // walletId?: explicit id to skip the pubKeyHash lookup
  // timeoutMs?: overrides SIGNING_TIMEOUT_MS (default 120_000)
})

// result.status: 'idle' | 'requesting' | 'waiting' | 'approved' | 'rejected' | 'timeout' | 'error'
if (result.status === 'approved') {
  console.log('signed tx:', result.signedTxCBOR, 'witness:', result.witnessSet)
} else {
  console.error('signing failed:', result.error)
}

For the responder side, see useSigningRequestHandler (decode/decrypt inbound signing.request.encrypted) and useSigningResponseSender (publish the encrypted response).

Presence beacon & raw-stream presence

import {
  getWalletPresenceBeacon,
  createWalletPresenceCClient,
} from '@eternl/hub-wallet-sdk'

// Process-wide beacon — fans tab-visibility / page-unload transitions to all
// registered sessions. WalletConnectorSession registers itself automatically.
const beacon = getWalletPresenceBeacon()
console.log('registered sessions:', beacon.size)

// Standalone raw-stream (Option C) presence client (usually you'd use the
// session's `presenceC` option instead of building this directly):
const presenceC = createWalletPresenceCClient({
  pairId,
  deviceId,
  tabId,
  getNc: () => useNatsClient().getNatsConnection(),  // live nats.ws connection or null
  onPeerEvent: (event) => console.log('peer presence:', event.classification),
})
presenceC.start()
// … later
presenceC.stop()

Lower-level composables

useNatsClient

One process-wide NATS WebSocket client. Status is read via getters, and request/response uses subscribeAndRequest (there is no request() method).

import { useNatsClient } from '@eternl/hub-wallet-sdk'

const nats = useNatsClient()
nats.configure({ getNatsUrl: () => 'wss://hub.de-5.eternl.art/nats', getAuthenticator })
await nats.connect()

// State getters
nats.connectionStatus()   // 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error'
nats.isConnected()        // boolean
nats.errorMessage()       // string
nats.reconnectAttempts()  // number

// Subscribe (JSON-parsed + schema-validated). Returns a Subscription | null.
const sub = nats.subscribe('hub.device.peer-id.message', (data) => {
  console.log('received:', data)
})

// Raw subscribe (no JSON parse/validation) — returns { unsubscribe } | null
const rawSub = nats.subscribeRaw('presence_out.pair.dapp', (bytes: Uint8Array) => { /* … */ })

// Publish
await nats.publish('hub.device.peer-id.message', { type: 'hello' })
await nats.publishRaw('subject', new Uint8Array([1, 2, 3]))

// Request/response (unique reply subject per call)
const res = await nats.subscribeAndRequest<MyResponse>(
  'hub.responses',          // responseSubject (a unique suffix is appended)
  'hub.requests',           // requestSubject
  { query: 'status' },      // requestData (a `replyTo` field is injected)
  { timeout: 5000 },
)
if (res.success) console.log(res.data)
else console.error(res.error)

// Cleanup
nats.unsubscribe('hub.device.peer-id.message')
nats.unsubscribeAll()
await nats.disconnect()

useEncryption

ECDH (P-256) + HKDF-SHA256 session-key derivation and AES-256-GCM AEAD, interoperable with @eternl/hub-core v2. There is no deriveSharedSecret; the v2 pipeline is deriveSharedSecretRaw → deriveSessionKeyHKDF. Encryption is PIN-bound: every frame's AAD is utf8(pairingId) || SHA-256(pin)[:8] || be64(seq).

import { useEncryption } from '@eternl/hub-wallet-sdk'

const enc = useEncryption()

// Handshake
const keyPair    = await enc.generateKeyPair()                       // KeyPair | null
const myPubB64   = await enc.exportPublicKey(keyPair!.publicKey)      // string | null
const peerPub    = await enc.importPublicKey(peerPubB64)             // CryptoKey | null

// v2 derivation
const shared     = await enc.deriveSharedSecretRaw(keyPair!.privateKey, peerPub!) // Uint8Array | null
const sas6       = await enc.deriveSAS6(shared!, pin, pairingId)     // 6-digit string for UI confirm
const session    = await enc.deriveSessionKeyHKDF(shared!, pairingId, pin)        // SessionKey | null
// session.key — non-extractable CryptoKey;  session.raw — base64 raw key (persist with care)

// Encrypt — pass a CryptoKey and a context { pairingId, pin | pinAadBytes, seq }
let seq = 1n
const payload = await enc.encryptJSON(
  { hello: 'world' },
  session!.key,
  { pairingId, pin, seq },          // OR { pairingId, pinAadBytes, seq } for PIN-less callers
)

// Decrypt — seq is read from the payload, so the context omits it
const plain = await enc.decryptJSON<{ hello: string }>(
  payload!,
  session!.key,
  { pairingId, pin },               // OR { pairingId, pinAadBytes }
)

// Re-import a persisted raw key as a non-extractable CryptoKey
const imported = await enc.importSessionKey(session!.raw)            // CryptoKey | null

enc.cleanup()

EncryptedPayload is a v2 envelope: { version: 2, iv, authTag, encrypted, algorithm, timestamp, messageId, pairingId, seq } (where seq is a decimal string). Decrypt hard-fails on any non-v2 payload.

useReplayProtection

In-memory, per-pairId message de-duplication with a TTL window. checkAndRecordMessage is synchronous and returns { valid, error? } (there is no .reason field and nothing to await).

import {
  generateMessageId,
  checkAndRecordMessage,
  clearReplayCache,
} from '@eternl/hub-wallet-sdk'

const messageId = generateMessageId()              // 'msg_…'
const timestamp = new Date().toISOString()

// Signature: checkAndRecordMessage(pairId, messageId, timestamp)
const check = checkAndRecordMessage('pair-id-123', messageId, timestamp)
if (!check.valid) {
  console.warn('rejected:', check.error)           // e.g. 'Replay attack detected: duplicate messageId'
}

clearReplayCache('pair-id-123')

(For naming compatibility, checkAndRecordMessage is also re-exported as checkAndRecordMessageAsync — it is still synchronous despite the name.)

useSessionKeyCache

Caches non-extractable CryptoKeys keyed by deviceId. Note importSessionKey takes deviceId first, then the base64 raw key.

import {
  importSessionKey,
  getCachedKey,
  hasCachedKey,
  clearCachedKey,
  getCacheStats,
} from '@eternl/hub-wallet-sdk'

const key = await importSessionKey(deviceId, rawBase64)   // (deviceId, rawBase64) → CryptoKey | null

if (hasCachedKey(deviceId)) {
  const cached = getCachedKey(deviceId)                   // CryptoKey | null
}

const stats = getCacheStats()                             // { size: number; deviceIds: DeviceId[] }
clearCachedKey(deviceId)

useMessageAuth

HMAC-SHA256 authentication over RFC-8785-canonicalized messages, keyed by the base64 raw session key string.

import {
  signMessage,
  verifyMessage,
  createSignedMessage,
  verifyAndUnwrap,
} from '@eternl/hub-wallet-sdk'

const sig    = await signMessage({ type: 'presence', online: true }, sessionKeyRaw) // string | null
const ok     = await verifyMessage({ type: 'presence', online: true, signature: sig! }, sessionKeyRaw)
const signed = await createSignedMessage({ type: 'presence', online: true }, sessionKeyRaw) // T & { signature } | null
const orig   = await verifyAndUnwrap(signed!, sessionKeyRaw)  // T | null (null if invalid)

useNatsValidation

Message-structure validation and type guards.

import {
  validateNatsMessage,
  isValidEncryptedPayload,
  parseAndValidatePayload,
} from '@eternl/hub-wallet-sdk'

const v = validateNatsMessage(message)   // { valid: boolean; error?: string; type?: string }
if (isValidEncryptedPayload(payload)) { /* … */ }
const parsed = parseAndValidatePayload(jsonString)

Storage

The HubStorage interface uses getItem / setItem / removeItem (with an optional clear), and supports both sync (localStorage) and async (IndexedDB) backends.

import {
  createLocalStorage,
  createMemoryStorage,
  isLocalStorageAvailable,
  type HubStorage,
} from '@eternl/hub-wallet-sdk'

interface HubStorage {
  getItem(key: string): string | null | Promise<string | null>
  setItem(key: string, value: string): void | Promise<void>
  removeItem(key: string): void | Promise<void>
  clear?(): void | Promise<void>
}

// Built-in adapters
initHub({ network: 'mainnet', storage: createLocalStorage('my-wallet') })
initHub({ network: 'mainnet', storage: createMemoryStorage() })

// Custom (e.g. IndexedDB) adapter
const customStorage: HubStorage = {
  getItem:    (key) => myDB.get(key),
  setItem:    (key, value) => myDB.set(key, value),
  removeItem: (key) => myDB.delete(key),
}
initHub({ network: 'mainnet', storage: customStorage })

If no storage is provided, initHub uses localStorage when available and falls back to in-memory storage otherwise.

Utilities

Canonicalization (RFC 8785)

import { canonicalize, createSigningPayload } from '@eternl/hub-wallet-sdk'

canonicalize({ b: 2, a: 1 })                       // '{"a":1,"b":2}'
createSigningPayload(message, ['signature'])       // canonical string with fields excluded

Secure IDs

import { generateSecureId, generateSecureMessageId, assertCryptoCapabilities } from '@eternl/hub-wallet-sdk'

assertCryptoCapabilities()         // throws if WebCrypto is unavailable
const id  = generateSecureId()     // 32-char hex (derived from crypto.randomUUID / getRandomValues)
const mid = generateSecureMessageId()  // 'msg_…' (alias of generateMessageId)

Protocol constants

All protocol constants are exported as getter functions that return the resolved value (defaults, or overrides passed via initHub({ constants: … })):

import {
  CONNECTION_TIMEOUT_MS,           // () => 10_000
  SIGNING_TIMEOUT_MS,              // () => 120_000
  REPLAY_PROTECTION_TTL_MS,        // () => 300_000
  MAX_PAYLOAD_SIZE_BYTES,          // () => 1_048_576
  PRESENCE_HEARTBEAT_INTERVAL_MS,  // () => 5_000
  DEFAULT_HUB_URL,                 // 'wss://hub.de-5.eternl.art/nats' (string constant)
} from '@eternl/hub-wallet-sdk'

const timeout = CONNECTION_TIMEOUT_MS()   // call them

Protocol types & permissions

The SDK re-exports the full set of protocol types from @eternl/hub-core and its own modules, plus the Permission enum and AccountInfo:

import type {
  DeviceId, PeerId, PairId, PairingId,
  DeviceInfo, PairedInstance, WalletRegistryEntry,
  SigningRequestMessage, EncryptedSigningRequestMessage,
  EncryptedPayload, SessionKey, KeyPair,
  ValidationResult, ReplayValidationResult,
  AccountInfo,
} from '@eternl/hub-wallet-sdk'

import { Permission } from '@eternl/hub-wallet-sdk'

const granted = [Permission.ADDRESS_READ, Permission.SIGN_TX, Permission.SIGN_DATA]

Security model (summary)

  • End-to-end encryption — AES-256-GCM with random 12-byte IVs and a 16-byte auth tag.
  • PIN-bound session keys — HKDF-SHA256 over the ECDH (P-256) shared secret, salted with SHA-256(utf8(pin) || pairingId); the AAD binds pairingId, SHA-256(pin)[:8], and a monotonic seq. Without the PIN the session key cannot be derived from the ECDH transcript alone.
  • SAS6 — a 6-digit short authentication string for out-of-band pairing confirmation.
  • Replay protection — per-pairId message de-duplication with a TTL window and clock-skew tolerance.
  • HMAC authentication — HMAC-SHA256 over RFC-8785-canonicalized messages.
  • Payload validation — size limits and message-schema checks on inbound NATS messages.
  • Strict v2 wire — pre-v2 encrypted payloads are hard-rejected (no silent downgrade).

License

Business Source License 1.1 (BUSL-1.1). See LICENSE.

The Licensor is Tastenkunst GmbH. Additional Use Grant: None — you may copy, modify, and make non-production use of this package. Production/commercial use requires a separate license from Tastenkunst. On the Change Date (the fourth anniversary of each version's first publication) that version converts to GPL-2.0-or-later.

Contact: https://tastenkunst.com/

版本与发布记录

@eternl/hub-wallet-sdk 共发布 69 个版本,首次发布于 2026/05/08;过去 12 个月发布了 69 个版本。

版本发布日期距今
0.55.0latest2026/11/103 天前
0.54.02026/11/103 天前
0.53.02026/11/103 天前
0.52.02026/11/103 天前
0.51.02026/11/104 天前
0.50.02026/11/094 天前

关于 @eternl/hub-wallet-sdk 的常见问题

@eternl/hub-wallet-sdk 是什么?

Eternl Hub SDK for wallet developers.

如何安装 @eternl/hub-wallet-sdk?

在项目目录运行 npm install @eternl/hub-wallet-sdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@nats-io/kv(3.4.0)、@nats-io/jetstream(3.4.0)、@nats-io/nats-core(3.4.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@eternl/hub-wallet-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@eternl/hub-wallet-sdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@eternl/hub-wallet-sdk 可以商用吗?

@eternl/hub-wallet-sdk 使用 BUSL-1.1 许可。声明的许可为 BUSL-1.1,请阅读许可原文确认使用条件。以上为概括,不构成法律意见,请以许可原文为准。

@eternl/hub-wallet-sdk 的下载量有多少?

根据 npm 官方下载统计,@eternl/hub-wallet-sdk 在 2026/10/14 – 2026/11/12 这 30 天内被下载 2,487 次,日均约 83 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@eternl/hub-wallet-sdk 还在维护吗?

最新版本 0.55.0 发布于 2026/11/10(3 天前);至今共发布 69 个版本,其中过去 12 个月发布了 69 个;首次发布于 2026/05/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。