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 +42 -0
- package/dist/minimal-xec-wallet.esm.js +96 -17
- package/dist/minimal-xec-wallet.js +96 -17
- package/dist/minimal-xec-wallet.min.js +9 -9
- package/examples/key-management/sign-verify-message.js +80 -0
- package/index.js +94 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|