hd-wallet-wasm 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  * - Transaction building and signing
10
10
  *
11
11
  * @module hd-wallet-wasm
12
- * @version 2.1.2
12
+ * @version 2.1.3
13
13
  */
14
14
 
15
15
  // Import aligned API for batch operations
@@ -1342,6 +1342,59 @@ export async function computeWasmHash(wasmBytes) {
1342
1342
  return bytesToHex(wallet.utils.sha256(bytes));
1343
1343
  }
1344
1344
 
1345
+ /**
1346
+ * ONE WALLET INSTANCE PER REALM AND VERSION.
1347
+ *
1348
+ * A page can hold several copies of this file: the site runtime
1349
+ * (/wallet-wasm/runtime/index.mjs), a bundler's copy inside an app chunk and
1350
+ * another inside sdn-js. Each used to instantiate its own WASM, and every
1351
+ * instance commits its own linear memory. The first init() of a version now
1352
+ * instantiates it and publishes the promise on a realm-global keyed by the
1353
+ * package version, so every copy of the same version in the realm shares
1354
+ * that one instance (each copy keeps its own JS wrapper over it). A different
1355
+ * version never shares: its exports may differ.
1356
+ */
1357
+ const PACKAGE_VERSION = '2.1.3';
1358
+ const REALM_INSTANCES = Symbol.for('hd-wallet-wasm/realm-instances');
1359
+
1360
+ function realmInstances() {
1361
+ const existing = globalThis[REALM_INSTANCES];
1362
+ if (existing && typeof existing === 'object') return existing;
1363
+ const created = Object.create(null);
1364
+ Object.defineProperty(globalThis, REALM_INSTANCES, {
1365
+ value: created,
1366
+ enumerable: false,
1367
+ writable: false,
1368
+ configurable: false,
1369
+ });
1370
+ return created;
1371
+ }
1372
+
1373
+ function sharedWasmModule() {
1374
+ const instances = realmInstances();
1375
+ let pending = instances[PACKAGE_VERSION];
1376
+ if (!pending) {
1377
+ pending = loadWasmModule();
1378
+ instances[PACKAGE_VERSION] = pending;
1379
+ // A failed load is not cached: the next init() tries again.
1380
+ pending.catch(() => {
1381
+ if (instances[PACKAGE_VERSION] === pending) delete instances[PACKAGE_VERSION];
1382
+ });
1383
+ }
1384
+ return pending;
1385
+ }
1386
+
1387
+ /**
1388
+ * A SHORT-LIVED INSTANCE for the password KDF (scrypt dirties 64 MiB, and a
1389
+ * WASM memory never shrinks). It is never shared or published: the caller
1390
+ * moves the derived identity into the realm's instance (sdn-typed.mjs,
1391
+ * identity transfer), zeroes this instance's whole memory and drops it, so
1392
+ * the collector releases the memory.
1393
+ */
1394
+ function createKdfInstance() {
1395
+ return loadWasmModule();
1396
+ }
1397
+
1345
1398
  /**
1346
1399
  * WASM module loader (single-file, isomorphic)
1347
1400
  * @returns {Promise<Object>} Initialized WASM module
@@ -5192,7 +5245,7 @@ function createModule(wasm) {
5192
5245
  };
5193
5246
 
5194
5247
  Object.defineProperty(module, 'sdn', {
5195
- value: createSdnTypedCapabilities(wasm),
5248
+ value: createSdnTypedCapabilities(wasm, { createKdfInstance }),
5196
5249
  enumerable: true,
5197
5250
  writable: false,
5198
5251
  configurable: false,
@@ -5337,9 +5390,14 @@ export const WellKnownCoinType = Object.freeze({
5337
5390
  * Initialize the HD Wallet WASM module
5338
5391
  * @returns {Promise<Object>} Initialized HDWalletModule
5339
5392
  */
5393
+ let realmModule = null;
5340
5394
  export default async function init() {
5341
- const wasm = await loadWasmModule();
5342
- return createModule(wasm);
5395
+ // One wrapper per copy of this file, over the realm's one instance.
5396
+ if (!realmModule) {
5397
+ realmModule = sharedWasmModule().then(createModule);
5398
+ realmModule.catch(() => { realmModule = null; });
5399
+ }
5400
+ return realmModule;
5343
5401
  }
5344
5402
 
5345
5403
  /**
@@ -38,9 +38,20 @@ const RAW_ENTRYPOINTS = Object.freeze([
38
38
  '_hd_sdn_sign_asset_review_authority_activation',
39
39
  '_hd_sdn_sign_asset_review_decision',
40
40
  '_hd_sdn_seal_remembered_identity',
41
+ '_hd_sdn_transfer_begin',
42
+ '_hd_sdn_transfer_seal',
43
+ '_hd_sdn_transfer_open',
41
44
  '_hd_sdn_destroy_identity',
42
45
  ]);
43
46
 
47
+ const TRANSFER_PUBLIC_KEY_BYTES = 32;
48
+ const MAX_TRANSFER_BYTES = 1024;
49
+
50
+ // Each capabilities object's transfer sealer, reachable only from this file:
51
+ // sealing is never a public capability (it would hand out a seed sealed to
52
+ // any key a caller names).
53
+ const transferSealers = new WeakMap();
54
+
44
55
  const textEncoder = new TextEncoder();
45
56
  const textDecoder = new TextDecoder('utf-8', { fatal: true });
46
57
  const SafeUint8Array = Uint8Array;
@@ -528,7 +539,13 @@ function readRequired(scope, pointer) {
528
539
  * Internal per-WASM-instance factory. This is intentionally not a package
529
540
  * export subpath; the wallet-origin wrapper constructs it inside createModule.
530
541
  */
531
- export function createSdnTypedCapabilities(wasm) {
542
+ export function createSdnTypedCapabilities(wasm, options = {}) {
543
+ // createKdfInstance (index.mjs) makes a fresh, unshared WASM instance: the
544
+ // password KDF runs there and the identity is moved into `wasm`.
545
+ const createKdfInstance = isObject(options) &&
546
+ typeof options.createKdfInstance === 'function'
547
+ ? options.createKdfInstance
548
+ : null;
532
549
  if (!isObject(wasm) || typeof wasm._malloc !== 'function' ||
533
550
  typeof wasm._free !== 'function' ||
534
551
  RAW_ENTRYPOINTS.some((name) => typeof wasm[name] !== 'function')) {
@@ -658,6 +675,108 @@ export function createSdnTypedCapabilities(wasm) {
658
675
  }
659
676
  }
660
677
 
678
+ // THE RECEIVER: a fresh X25519 key in this instance (native randomness);
679
+ // only its public half comes back. Its private half stays native for one open.
680
+ function beginTransfer() {
681
+ const scope = createAllocationScope(wasm);
682
+ try {
683
+ const out = scope.allocate(TRANSFER_PUBLIC_KEY_BYTES);
684
+ let status;
685
+ try {
686
+ status = wasm._hd_sdn_transfer_begin(out, TRANSFER_PUBLIC_KEY_BYTES);
687
+ } catch {
688
+ fail('CRYPTO_FAILURE');
689
+ }
690
+ if (status !== 0) throwStatus(status);
691
+ return scope.bytes(out, TRANSFER_PUBLIC_KEY_BYTES);
692
+ } finally {
693
+ if (scope.cleanup()) fail('CRYPTO_FAILURE');
694
+ }
695
+ }
696
+
697
+ // THE SENDER: this instance's identity sealed to the receiver's public key.
698
+ function sealTransfer(handle, recipientPublic) {
699
+ const record = recordFor(handle);
700
+ const scope = createAllocationScope(wasm);
701
+ try {
702
+ const recipient = scope.allocate(recipientPublic);
703
+ const outRequired = scope.allocate(4);
704
+ const outBytes = scope.allocate(MAX_TRANSFER_BYTES);
705
+ let status;
706
+ try {
707
+ status = wasm._hd_sdn_transfer_seal(
708
+ record.nativeValue, recipient, recipientPublic.length,
709
+ outBytes, MAX_TRANSFER_BYTES, outRequired,
710
+ );
711
+ } catch {
712
+ fail('CRYPTO_FAILURE');
713
+ }
714
+ if (status !== 0) throwStatus(status);
715
+ const required = readRequired(scope, outRequired);
716
+ if (required <= TRANSFER_PUBLIC_KEY_BYTES || required > MAX_TRANSFER_BYTES) {
717
+ fail('CRYPTO_FAILURE');
718
+ }
719
+ return scope.bytes(outBytes, required);
720
+ } finally {
721
+ if (scope.cleanup()) fail('CRYPTO_FAILURE');
722
+ }
723
+ }
724
+
725
+ /**
726
+ * THE PASSWORD KDF IN A SHORT-LIVED INSTANCE. scrypt (password-scrypt-v2)
727
+ * dirties 64 MiB, and a WASM memory never shrinks, so deriving here would
728
+ * keep 64 MiB committed for the session. Instead: a fresh instance derives,
729
+ * its identity moves into this one (seed sealed native-to-native: only
730
+ * public keys and sealed bytes pass through JS), the fresh instance's
731
+ * identity is destroyed, its whole memory is zeroed and it is dropped.
732
+ * Nothing awaits between begin, seal and open, so no other transfer can
733
+ * interleave.
734
+ */
735
+ async function deriveInKdfInstance(derive, expectedAccountIndex) {
736
+ let kdfWasm;
737
+ try {
738
+ kdfWasm = await createKdfInstance();
739
+ } catch {
740
+ fail('CRYPTO_FAILURE');
741
+ }
742
+ let kdf;
743
+ let derived;
744
+ try {
745
+ kdf = createSdnTypedCapabilities(kdfWasm);
746
+ derived = await derive(kdf);
747
+ const recipient = beginTransfer();
748
+ const sealed = transferSealers.get(kdf)(derived.handle, recipient);
749
+ const opened = invokeDerive(
750
+ '_hd_sdn_transfer_open',
751
+ ([sealedPointer]) => [sealedPointer, sealed.length],
752
+ [{ bytes: sealed }],
753
+ 'sdn-bip32-slip10-purpose-v1',
754
+ 'password-scrypt-v2',
755
+ expectedAccountIndex,
756
+ () => wipeAll([sealed]),
757
+ );
758
+ if (JSON.stringify(opened.identity) !== JSON.stringify(derived.identity)) {
759
+ capabilities.destroySdnIdentity(opened.handle);
760
+ fail('CRYPTO_FAILURE');
761
+ }
762
+ return opened;
763
+ } finally {
764
+ try {
765
+ if (derived) kdf.destroySdnIdentity(derived.handle);
766
+ } catch {
767
+ // The memory is zeroed below either way.
768
+ }
769
+ try {
770
+ const heap = checkedHeap(kdfWasm);
771
+ new SafeUint8Array(heap.buffer).fill(0);
772
+ } catch {
773
+ // Nothing else holds the instance; the collector still releases it.
774
+ }
775
+ kdf = null;
776
+ kdfWasm = null;
777
+ }
778
+ }
779
+
661
780
  const capabilities = {
662
781
  async derivePasswordIdentity(input) {
663
782
  let callerPassword;
@@ -671,6 +790,13 @@ export function createSdnTypedCapabilities(wasm) {
671
790
  if (username.length > MAX_MODERN_CREDENTIAL_BYTES) fail('INVALID_USERNAME');
672
791
  if (password.length > MAX_MODERN_CREDENTIAL_BYTES) fail('INVALID_PASSWORD');
673
792
  const account = requireAccount(input.accountIndex);
793
+ if (createKdfInstance) {
794
+ return await deriveInKdfInstance((kdf) => kdf.derivePasswordIdentity({
795
+ usernameUtf8: username,
796
+ passwordUtf8: password,
797
+ accountIndex: account,
798
+ }), account);
799
+ }
674
800
  return invokeDerive(
675
801
  '_hd_sdn_derive_password_identity',
676
802
  ([usernamePointer, passwordPointer]) => [
@@ -978,5 +1104,7 @@ export function createSdnTypedCapabilities(wasm) {
978
1104
  },
979
1105
  };
980
1106
 
981
- return Object.freeze(capabilities);
1107
+ const frozen = Object.freeze(capabilities);
1108
+ transferSealers.set(frozen, sealTransfer);
1109
+ return frozen;
982
1110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-wallet-wasm",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "Comprehensive HD Wallet implementation in WebAssembly - BIP-32/39/44, multi-curve, multi-chain support",
5
5
  "type": "module",
6
6
  "main": "./dist/runtime/index.mjs",
@@ -92,5 +92,5 @@
92
92
  "devDependencies": {
93
93
  "@types/node": "^20.0.0"
94
94
  },
95
- "gitHead": "e862d42fcf505cdbc7e928d676bc1a180a167989"
95
+ "gitHead": "226dfe7527fcb54571b745ad9f3ebc0b6cc51898"
96
96
  }