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.
- package/README.md +1 -1
- package/dist/hd-wallet-wasi.wasm +0 -0
- package/dist/hd-wallet.js +1 -1
- package/dist/runtime/index.mjs +62 -4
- package/dist/runtime/sdn-typed.mjs +130 -2
- package/package.json +2 -2
package/dist/runtime/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Transaction building and signing
|
|
10
10
|
*
|
|
11
11
|
* @module hd-wallet-wasm
|
|
12
|
-
* @version 2.1.
|
|
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
|
-
|
|
5342
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
95
|
+
"gitHead": "226dfe7527fcb54571b745ad9f3ebc0b6cc51898"
|
|
96
96
|
}
|