minimal-xec-wallet 2.2.1 → 2.2.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/CHANGES.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.3] - 2025-02-10
4
+
5
+ ### Fixed
6
+
7
+ #### Message Signing BigInt Error (CRITICAL)
8
+
9
+ Fixed `signMessage()` crashing in the browser with `Cannot mix BigInt and other types, use explicit conversions`.
10
+
11
+ **The Problem:**
12
+ The `signRecoverable` method in `initBrowser-shim.js` called `secp256k1.sign()` and accessed `.r`, `.s`, `.recovery` on the result. But `@noble/curves` v2 `sign()` returns a `Uint8Array`, not a Signature object. Accessing `.r` on a Uint8Array yields `undefined`, and `bigIntToBytes(undefined, 32)` triggers the BigInt TypeError.
13
+
14
+ The same issue affected `recoverSig()` which manually reconstructed a Signature via `new Signature(r, s, recoveryId)` — functional but unnecessarily complex.
15
+
16
+ **The Fix:**
17
+ - `signRecoverable`: use `format: 'recovered'` option which makes `sign()` return the 65-byte `[recovery_id, r(32), s(32)]` format directly
18
+ - `recoverSig`: use `secp256k1.recoverPublicKey(sig, msg)` which natively parses the recovered format and returns compressed public key bytes
19
+
20
+ **Note:** The CLI wallet was unaffected because it runs in Node.js where ecash-lib uses native WASM — the browser shim is never invoked.
21
+
22
+ ### Files Updated
23
+ - `browser-shims/initBrowser-shim.js` — Fix signRecoverable and recoverSig for noble curves v2 API
24
+
25
+ ## [2.2.2] - 2025-02-10
26
+
27
+ ### Added
28
+
29
+ #### Message Signing & Verification
30
+ New public API methods for the eCash message signing protocol (compatible with ecash-lib's `signMsg`/`verifyMsg`):
31
+
32
+ - `signMessage(msg)` — Sign a message with the wallet's private key, returns base64 signature
33
+ - `verifyMessage(msg, signature, address)` — Verify a message signature against an address, returns boolean
34
+ - `_magicHash(msg)` (static) — Internal: compute sha256d of prefixed message (`"\x16eCash Signed Message:\n" + varint(len) + msg`)
35
+
36
+ These methods use the already-shimmed pure-JS crypto (via `@noble/curves`) and work in both Node.js and browser environments without WASM.
37
+
38
+ **New test file:** `test/unit/a15-sign-verify-message-unit.js` — 15 unit tests covering validation, happy paths, and edge cases
39
+
40
+ **New example:** `examples/key-management/sign-verify-message.js` — Demonstrates sign + verify round-trip
41
+
42
+ ### Fixed
43
+ - Removed stray `console.error` in `verifyMessage()` (missed during v2.2.1 cleanup)
44
+
3
45
  ## [2.2.1] - 2025-02-10
4
46
 
5
47
  ### Changed
@@ -32074,26 +32074,11 @@ class NobleEcc {
32074
32074
  }
32075
32075
 
32076
32076
  signRecoverable (seckey, msg) {
32077
- const sig = secp256k1.sign(msg, seckey, { lowS: true, prehash: false });
32078
- const r = sig.r;
32079
- const s = sig.s;
32080
- const recovery = sig.recovery;
32081
- // 65 bytes: [recovery_id, r(32), s(32)]
32082
- const result = new Uint8Array(65);
32083
- result[0] = recovery;
32084
- result.set(bigIntToBytes(r, 32), 1);
32085
- result.set(bigIntToBytes(s, 32), 33);
32086
- return result
32077
+ return secp256k1.sign(msg, seckey, { lowS: true, prehash: false, format: 'recovered' })
32087
32078
  }
32088
32079
 
32089
32080
  recoverSig (sig, msg) {
32090
- const recoveryId = sig[0];
32091
- const r = bytesToBigInt(sig.slice(1, 33));
32092
- const s = bytesToBigInt(sig.slice(33, 65));
32093
- const Signature = secp256k1.Signature;
32094
- const recSig = new Signature(r, s, recoveryId);
32095
- const point = recSig.recoverPublicKey(msg);
32096
- return point.toRawBytes(true)
32081
+ return secp256k1.recoverPublicKey(sig, msg, { prehash: false })
32097
32082
  }
32098
32083
  }
32099
32084
 
@@ -45879,6 +45864,9 @@ function requireMinimalXecWallet () {
45879
45864
  const ConsolidateUtxos = requireConsolidateUtxos();
45880
45865
  const KeyDerivation = requireKeyDerivation();
45881
45866
  const HybridTokenManager = requireHybridTokenManager();
45867
+ const { Ecc, fromHex } = requireIndexBrowser();
45868
+ const { sha256d: ecashSha256d, shaRmd160: ecashShaRmd160 } = requireHash();
45869
+ const { decodeCashAddress } = requireCashaddr();
45882
45870
 
45883
45871
  // let this
45884
45872
 
@@ -45988,6 +45976,8 @@ function requireMinimalXecWallet () {
45988
45976
  this._secureWalletInfo = this._secureWalletInfo.bind(this);
45989
45977
  this.exportPrivateKeyAsWIF = this.exportPrivateKeyAsWIF.bind(this);
45990
45978
  this.validateWIF = this.validateWIF.bind(this);
45979
+ this.signMessage = this.signMessage.bind(this);
45980
+ this.verifyMessage = this.verifyMessage.bind(this);
45991
45981
  }
45992
45982
 
45993
45983
  // Private method to validate XEC addresses
@@ -46937,6 +46927,95 @@ function requireMinimalXecWallet () {
46937
46927
  return false
46938
46928
  }
46939
46929
  }
46930
+
46931
+ // Sign a message using the eCash message signing protocol
46932
+ signMessage (msg) {
46933
+ try {
46934
+ if (!this.walletInfo || !this.walletInfo.privateKey) {
46935
+ throw new Error('Wallet not initialized or no private key available')
46936
+ }
46937
+ if (!msg || typeof msg !== 'string') {
46938
+ throw new Error('Message must be a non-empty string')
46939
+ }
46940
+
46941
+ const sk = fromHex(this.walletInfo.privateKey);
46942
+ const hash = MinimalXECWallet._magicHash(msg);
46943
+ const sig = new Ecc().signRecoverable(sk, hash);
46944
+
46945
+ // Convert Uint8Array to base64
46946
+ const binaryString = String.fromCharCode.apply(null, sig);
46947
+ return globalThis.btoa(binaryString)
46948
+ } catch (err) {
46949
+ throw this._sanitizeError(err, 'Message signing failed')
46950
+ }
46951
+ }
46952
+
46953
+ // Verify a message signature against an address
46954
+ verifyMessage (msg, signature, address) {
46955
+ try {
46956
+ if (!msg || typeof msg !== 'string') {
46957
+ throw new Error('Message must be a non-empty string')
46958
+ }
46959
+ if (!signature || typeof signature !== 'string') {
46960
+ throw new Error('Signature must be a non-empty string')
46961
+ }
46962
+ if (!address || typeof address !== 'string') {
46963
+ throw new Error('Address must be a non-empty string')
46964
+ }
46965
+
46966
+ const hash = MinimalXECWallet._magicHash(msg);
46967
+
46968
+ // Decode base64 signature to Uint8Array
46969
+ const binaryString = globalThis.atob(signature);
46970
+ const sig = new Uint8Array(binaryString.length);
46971
+ for (let i = 0; i < binaryString.length; i++) {
46972
+ sig[i] = binaryString.charCodeAt(i);
46973
+ }
46974
+
46975
+ const recoveredPk = new Ecc().recoverSig(sig, hash);
46976
+
46977
+ // Get hash160 of recovered public key as hex
46978
+ const recoveredHash = Array.from(ecashShaRmd160(recoveredPk))
46979
+ .map(b => b.toString(16).padStart(2, '0')).join('');
46980
+
46981
+ // Get hash from the address
46982
+ const decoded = decodeCashAddress(address);
46983
+ const testedHash = decoded.hash;
46984
+
46985
+ return recoveredHash === testedHash
46986
+ } catch (err) {
46987
+ return false
46988
+ }
46989
+ }
46990
+
46991
+ // Prepare message hash per eCash message signing protocol
46992
+ static _magicHash (msg) {
46993
+ const encoder = new TextEncoder();
46994
+ const prefix = encoder.encode('\x16eCash Signed Message:\n');
46995
+ const messageBytes = encoder.encode(msg);
46996
+
46997
+ // Write varint for message length
46998
+ const len = messageBytes.length;
46999
+ let varintBytes;
47000
+ if (len < 0xfd) {
47001
+ varintBytes = new Uint8Array([len]);
47002
+ } else if (len <= 0xffff) {
47003
+ varintBytes = new Uint8Array([0xfd, len & 0xff, (len >> 8) & 0xff]);
47004
+ } else {
47005
+ varintBytes = new Uint8Array([
47006
+ 0xfe, len & 0xff, (len >> 8) & 0xff,
47007
+ (len >> 16) & 0xff, (len >> 24) & 0xff
47008
+ ]);
47009
+ }
47010
+
47011
+ // Concatenate: prefix + varint + message
47012
+ const data = new Uint8Array(prefix.length + varintBytes.length + messageBytes.length);
47013
+ data.set(prefix, 0);
47014
+ data.set(varintBytes, prefix.length);
47015
+ data.set(messageBytes, prefix.length + varintBytes.length);
47016
+
47017
+ return ecashSha256d(data)
47018
+ }
46940
47019
  }
46941
47020
 
46942
47021
  minimalXecWallet = MinimalXECWallet;
@@ -32080,26 +32080,11 @@
32080
32080
  }
32081
32081
 
32082
32082
  signRecoverable (seckey, msg) {
32083
- const sig = secp256k1.sign(msg, seckey, { lowS: true, prehash: false });
32084
- const r = sig.r;
32085
- const s = sig.s;
32086
- const recovery = sig.recovery;
32087
- // 65 bytes: [recovery_id, r(32), s(32)]
32088
- const result = new Uint8Array(65);
32089
- result[0] = recovery;
32090
- result.set(bigIntToBytes(r, 32), 1);
32091
- result.set(bigIntToBytes(s, 32), 33);
32092
- return result
32083
+ return secp256k1.sign(msg, seckey, { lowS: true, prehash: false, format: 'recovered' })
32093
32084
  }
32094
32085
 
32095
32086
  recoverSig (sig, msg) {
32096
- const recoveryId = sig[0];
32097
- const r = bytesToBigInt(sig.slice(1, 33));
32098
- const s = bytesToBigInt(sig.slice(33, 65));
32099
- const Signature = secp256k1.Signature;
32100
- const recSig = new Signature(r, s, recoveryId);
32101
- const point = recSig.recoverPublicKey(msg);
32102
- return point.toRawBytes(true)
32087
+ return secp256k1.recoverPublicKey(sig, msg, { prehash: false })
32103
32088
  }
32104
32089
  }
32105
32090
 
@@ -45885,6 +45870,9 @@ zoo`.split('\n');
45885
45870
  const ConsolidateUtxos = requireConsolidateUtxos();
45886
45871
  const KeyDerivation = requireKeyDerivation();
45887
45872
  const HybridTokenManager = requireHybridTokenManager();
45873
+ const { Ecc, fromHex } = requireIndexBrowser();
45874
+ const { sha256d: ecashSha256d, shaRmd160: ecashShaRmd160 } = requireHash();
45875
+ const { decodeCashAddress } = requireCashaddr();
45888
45876
 
45889
45877
  // let this
45890
45878
 
@@ -45994,6 +45982,8 @@ zoo`.split('\n');
45994
45982
  this._secureWalletInfo = this._secureWalletInfo.bind(this);
45995
45983
  this.exportPrivateKeyAsWIF = this.exportPrivateKeyAsWIF.bind(this);
45996
45984
  this.validateWIF = this.validateWIF.bind(this);
45985
+ this.signMessage = this.signMessage.bind(this);
45986
+ this.verifyMessage = this.verifyMessage.bind(this);
45997
45987
  }
45998
45988
 
45999
45989
  // Private method to validate XEC addresses
@@ -46943,6 +46933,95 @@ zoo`.split('\n');
46943
46933
  return false
46944
46934
  }
46945
46935
  }
46936
+
46937
+ // Sign a message using the eCash message signing protocol
46938
+ signMessage (msg) {
46939
+ try {
46940
+ if (!this.walletInfo || !this.walletInfo.privateKey) {
46941
+ throw new Error('Wallet not initialized or no private key available')
46942
+ }
46943
+ if (!msg || typeof msg !== 'string') {
46944
+ throw new Error('Message must be a non-empty string')
46945
+ }
46946
+
46947
+ const sk = fromHex(this.walletInfo.privateKey);
46948
+ const hash = MinimalXECWallet._magicHash(msg);
46949
+ const sig = new Ecc().signRecoverable(sk, hash);
46950
+
46951
+ // Convert Uint8Array to base64
46952
+ const binaryString = String.fromCharCode.apply(null, sig);
46953
+ return globalThis.btoa(binaryString)
46954
+ } catch (err) {
46955
+ throw this._sanitizeError(err, 'Message signing failed')
46956
+ }
46957
+ }
46958
+
46959
+ // Verify a message signature against an address
46960
+ verifyMessage (msg, signature, address) {
46961
+ try {
46962
+ if (!msg || typeof msg !== 'string') {
46963
+ throw new Error('Message must be a non-empty string')
46964
+ }
46965
+ if (!signature || typeof signature !== 'string') {
46966
+ throw new Error('Signature must be a non-empty string')
46967
+ }
46968
+ if (!address || typeof address !== 'string') {
46969
+ throw new Error('Address must be a non-empty string')
46970
+ }
46971
+
46972
+ const hash = MinimalXECWallet._magicHash(msg);
46973
+
46974
+ // Decode base64 signature to Uint8Array
46975
+ const binaryString = globalThis.atob(signature);
46976
+ const sig = new Uint8Array(binaryString.length);
46977
+ for (let i = 0; i < binaryString.length; i++) {
46978
+ sig[i] = binaryString.charCodeAt(i);
46979
+ }
46980
+
46981
+ const recoveredPk = new Ecc().recoverSig(sig, hash);
46982
+
46983
+ // Get hash160 of recovered public key as hex
46984
+ const recoveredHash = Array.from(ecashShaRmd160(recoveredPk))
46985
+ .map(b => b.toString(16).padStart(2, '0')).join('');
46986
+
46987
+ // Get hash from the address
46988
+ const decoded = decodeCashAddress(address);
46989
+ const testedHash = decoded.hash;
46990
+
46991
+ return recoveredHash === testedHash
46992
+ } catch (err) {
46993
+ return false
46994
+ }
46995
+ }
46996
+
46997
+ // Prepare message hash per eCash message signing protocol
46998
+ static _magicHash (msg) {
46999
+ const encoder = new TextEncoder();
47000
+ const prefix = encoder.encode('\x16eCash Signed Message:\n');
47001
+ const messageBytes = encoder.encode(msg);
47002
+
47003
+ // Write varint for message length
47004
+ const len = messageBytes.length;
47005
+ let varintBytes;
47006
+ if (len < 0xfd) {
47007
+ varintBytes = new Uint8Array([len]);
47008
+ } else if (len <= 0xffff) {
47009
+ varintBytes = new Uint8Array([0xfd, len & 0xff, (len >> 8) & 0xff]);
47010
+ } else {
47011
+ varintBytes = new Uint8Array([
47012
+ 0xfe, len & 0xff, (len >> 8) & 0xff,
47013
+ (len >> 16) & 0xff, (len >> 24) & 0xff
47014
+ ]);
47015
+ }
47016
+
47017
+ // Concatenate: prefix + varint + message
47018
+ const data = new Uint8Array(prefix.length + varintBytes.length + messageBytes.length);
47019
+ data.set(prefix, 0);
47020
+ data.set(varintBytes, prefix.length);
47021
+ data.set(messageBytes, prefix.length + varintBytes.length);
47022
+
47023
+ return ecashSha256d(data)
47024
+ }
46946
47025
  }
46947
47026
 
46948
47027
  minimalXecWallet = MinimalXECWallet;