quantumcoin 8.0.0 → 8.0.2

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-SDK.md CHANGED
@@ -419,6 +419,8 @@ Core signing implementation.
419
419
  - `getAddress(): Promise<string>`
420
420
  - `signTransaction(tx: TransactionRequest): Promise<string>`
421
421
  - `sendTransaction(tx: TransactionRequest): Promise<TransactionResponse>`
422
+ - `signMessageSync(message: string | Uint8Array, signingContext?: number | null): string` — signs an arbitrary message using the EIP-191 personal-message digest (see [Message signing](#message-signing-eip-191)). Returns an opaque post-quantum signature blob (0x hex) that embeds the signer's public key. The message may be at most 1 MiB once UTF-8 encoded (`INVALID_ARGUMENT` otherwise). Optional `signingContext`: omitted/`null` derives the compact context from the key type (`0` for keyType 3, `1` for keyType 5); `2` selects the full-signature scheme for a keyType 3 wallet.
423
+ - `signMessage(message: string | Uint8Array, signingContext?: number | null): Promise<string>` — async wrapper over `signMessageSync` (honors the ethers `Signer` interface contract; the underlying signing is synchronous).
422
424
 
423
425
  ### `Wallet`
424
426
 
@@ -487,6 +489,38 @@ Address-only signer.
487
489
  - `new VoidSigner(address: string, provider?: AbstractProvider)`
488
490
  - `getAddress(): Promise<string>`
489
491
 
492
+ ### Message signing (EIP-191)
493
+
494
+ Arbitrary-message signing and verification, adapted from ethers for
495
+ QuantumCoin's post-quantum cryptography.
496
+
497
+ - `Wallet.signMessage(message, signingContext?)` / `Wallet.signMessageSync(message, signingContext?)` — see [`BaseWallet`](#basewallet).
498
+ - `hashMessage(message: string | Uint8Array): string` — the EIP-191 digest,
499
+ `keccak256("\x19Ethereum Signed Message:\n" + len + message)` (32 bytes). Same
500
+ prefix as Ethereum, so it matches `personal_sign` in `quantum-coin-go`. Strings
501
+ are UTF-8 encoded; the length prefix counts message **bytes**.
502
+ - `verifyMessage(message: string | Uint8Array, signature: string | Uint8Array): string`
503
+ — **synchronous** (matches ethers; there is no `verifyMessageSync`). Returns the
504
+ recovered 32-byte signer address; throws `INVALID_ARGUMENT` if the signature is
505
+ malformed or does not verify.
506
+
507
+ ```js
508
+ const { Wallet, verifyMessage } = require("quantumcoin");
509
+ const wallet = Wallet.createRandom();
510
+ const sig = await wallet.signMessage("Hello Joe");
511
+ verifyMessage("Hello Joe", sig) === wallet.address; // true
512
+ ```
513
+
514
+ **Key differences vs Ethereum**
515
+
516
+ | Property | Ethereum | QuantumCoin |
517
+ | --- | --- | --- |
518
+ | Message prefix / hash | EIP-191 + keccak256 | Identical EIP-191 + keccak256 |
519
+ | Signature | 65-byte `(r, s, v)` | Opaque multi-KB blob (scheme id byte + **embedded public key**) |
520
+ | Address size | 20 bytes | 32 bytes |
521
+ | Recovery | ECDSA `ecrecover` | Extract embedded public key + PQC verify (no `ecrecover`) |
522
+ | `signTypedData` (EIP-712) | Supported | Not yet supported |
523
+
490
524
  ## Contracts
491
525
 
492
526
  ### `Contract`
@@ -576,24 +610,91 @@ ABI encoding/decoding compatibility layer.
576
610
 
577
611
  - `new Interface(abi: any[] | Interface | null)`
578
612
 
579
- **Methods**
613
+ **Fragment lookup** — each accepts a bare name, a canonical signature
614
+ (`name(type,...)`), or a hex identifier, mirroring ethers.js v6:
615
+ - `getFunction(nameOrSignatureOrSelector: string): FunctionFragment`
616
+ - Resolves by name, `transfer(address,uint256)`, or 4-byte selector `0xa9059cbb`.
617
+ - Throws `INVALID_ARGUMENT` when unresolved or when a bare name is ambiguous
618
+ across overloads (pass the full signature instead).
619
+ - `getEvent(nameOrSignatureOrTopic: string): EventFragment`
620
+ - Resolves by name, signature, or topic0 (`0x` + 64 hex).
621
+ - `getError(nameOrSignatureOrSelector: string): ErrorFragment`
622
+ - Resolves by name, signature, or 4-byte selector.
623
+ - `getConstructor(): ConstructorFragment | null`
624
+ - `getSighash(fragmentOrName): string` — 4-byte function selector.
580
625
  - `formatJson(): string`
581
626
  - `format(format?: string | null): string`
582
- - `getFunction(name: string): FunctionFragment`
583
- - `getEvent(name: string): EventFragment`
584
- - `getError(name: string): ErrorFragment`
585
- - `getConstructor(): ConstructorFragment | null`
586
627
 
587
- **Encoding**
628
+ `FunctionFragment` / `ErrorFragment` expose a `selector` getter and
629
+ `EventFragment` a `topicHash` getter (both require `Initialize()`).
630
+
631
+ **Encoding / decoding**
588
632
  - `encodeFunctionData(functionFragmentOrName, values?: any[] | null): string`
633
+ - `decodeFunctionData(functionFragmentOrName, data: string): Result` — decode a
634
+ call's arguments (selector + args). The data's selector must match the resolved
635
+ function, otherwise it throws `INVALID_ARGUMENT`. `encodeFunctionData(fragment,
636
+ decodeFunctionData(fragment, data))` reproduces `data` byte-for-byte.
589
637
  - `decodeFunctionResult(functionFragmentOrName, data: string): any`
638
+ - `encodeDeploy(values?: any[]): string` — ABI-encode constructor arguments (no
639
+ selector), or `"0x"` when the constructor takes no arguments. Append to the
640
+ creation bytecode for a deployment.
590
641
  - `encodeEventLog(eventFragmentOrName, values?: any[] | null): { topics: string[], data: string }`
591
642
  - `decodeEventLog(eventFragmentOrName, topics: string[], data: string): any`
592
643
 
593
644
  **Parsing**
645
+ - `parseTransaction(tx: { data: string, value?: BigNumberish }): { fragment, name, signature, selector, args: Result, value: bigint }`
646
+ - Resolves the function by the 4-byte selector in `tx.data`, decodes the
647
+ arguments from the real bytes, and reports `value` as a `bigint`.
648
+ - `parseError(data: string): { fragment, name, signature, selector, args: Result }`
649
+ - Resolves a custom error by its 4-byte selector and decodes its arguments.
594
650
  - `parseLog(log: { topics: string[], data: string }): { fragment, name, signature, topic, args }`
595
651
  - Uses signature topic matching and `decodeEventLog(...)`
596
652
 
653
+ > **32-byte addresses:** QuantumCoin addresses are 32 bytes and occupy a full ABI
654
+ > word, whereas Ethereum left-pads a 20-byte address. Canonical signatures are
655
+ > identical strings, so keccak-derived selectors and event topics match Ethereum
656
+ > exactly (e.g. `transfer(address,uint256)` → `0xa9059cbb`); only the contents of
657
+ > an address-bearing calldata word differ.
658
+
659
+ **Example (JS):**
660
+
661
+ ```js
662
+ const { Initialize } = require("quantumcoin/config");
663
+ const { Interface } = require("quantumcoin");
664
+
665
+ await Initialize(null);
666
+ const iface = new Interface([
667
+ { type: "function", name: "transfer",
668
+ inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }],
669
+ outputs: [{ type: "bool" }] },
670
+ ]);
671
+
672
+ const to = "0x" + "ab".repeat(32); // 32-byte address
673
+ const data = iface.encodeFunctionData("transfer", [to, 1000n]);
674
+
675
+ const tx = iface.parseTransaction({ data, value: "0x0" });
676
+ console.log(tx.name, tx.signature, tx.selector); // transfer transfer(address,uint256) 0xa9059cbb
677
+ console.log(tx.args.to, tx.args.amount); // <to> 1000n
678
+
679
+ // "What you see is what you sign": re-encode the decoded args and compare.
680
+ const ok = iface.encodeFunctionData(tx.fragment, Array.from(tx.args)) === data; // true
681
+ ```
682
+
683
+ **Example (TS):**
684
+
685
+ ```ts
686
+ import { Initialize } from "quantumcoin/config";
687
+ import qc from "quantumcoin";
688
+
689
+ await Initialize(null);
690
+ const iface = new qc.Interface([
691
+ { type: "error", name: "InsufficientBalance",
692
+ inputs: [{ name: "have", type: "uint256" }, { name: "want", type: "uint256" }] },
693
+ ]);
694
+ const selector: string = iface.getError("InsufficientBalance").selector;
695
+ // const desc = iface.parseError(revertData); // { name, args: { have, want }, ... }
696
+ ```
697
+
597
698
  ### `AbiCoder`
598
699
 
599
700
  Minimal ABI coder for encoding/decoding tuples of values.
@@ -601,6 +702,8 @@ Minimal ABI coder for encoding/decoding tuples of values.
601
702
  - `encode(types: (string|any)[], values: any[]): string`
602
703
  - `decode(types: (string|any)[], data: string): any`
603
704
  - `getDefaultValue(types: (string|any)[]): any`
705
+ - `static defaultAbiCoder(): AbiCoder` — shared singleton instance (ethers.js v6
706
+ shape): `AbiCoder.defaultAbiCoder().encode(["uint256"], [1n])`.
604
707
 
605
708
  ## Utilities
606
709
 
@@ -679,6 +782,7 @@ const asOutput: Uint256 = 123n;
679
782
  - `sha512(data: BytesLike): string`
680
783
  - `ripemd160(data: BytesLike): string`
681
784
  - `id(text: string): string` (=`keccak256(utf8Bytes(text))`)
785
+ - `hashMessage(message: BytesLike): string` — EIP-191 personal-message digest, `keccak256("\x19Ethereum Signed Message:\n" + len + message)`. See [Message signing](#message-signing-eip-191).
682
786
  - `randomBytes(length: number): Uint8Array`
683
787
  - `computeHmac(algorithm: string, key: BytesLike, data: BytesLike): string`
684
788
  - `pbkdf2(password: BytesLike, salt: BytesLike, iterations: number, keylen: number, algorithm?: string): string`
package/README.md CHANGED
@@ -86,6 +86,52 @@ const restored = Wallet.fromEncryptedJsonSync(json, "mySecurePassword123");
86
86
  console.log(restored.address);
87
87
  ```
88
88
 
89
+ ## Message signing (EIP-191 / `personal_sign`)
90
+
91
+ Sign and verify arbitrary messages using QuantumCoin's post-quantum keys. The
92
+ message digest uses the exact same EIP-191 prefix as Ethereum
93
+ (`keccak256("\x19Ethereum Signed Message:\n" + len + message)`), so it is
94
+ byte-for-byte compatible with `personal_sign` in `quantum-coin-go`.
95
+
96
+ ```js
97
+ const { Wallet, verifyMessage, hashMessage } = require("quantumcoin");
98
+ const { Initialize } = require("quantumcoin/config");
99
+
100
+ await Initialize(null);
101
+
102
+ const wallet = Wallet.createRandom();
103
+
104
+ // async (ethers Signer contract) or sync
105
+ const signature = await wallet.signMessage("Hello Joe");
106
+ const signatureSync = wallet.signMessageSync("Hello Joe");
107
+
108
+ // verifyMessage is synchronous and returns the recovered signer address
109
+ const signer = verifyMessage("Hello Joe", signature);
110
+ console.log(signer === wallet.address); // true
111
+
112
+ // The EIP-191 digest is available on its own if needed
113
+ console.log(hashMessage("Hello Joe")); // 0x...32-byte hash
114
+ ```
115
+
116
+ ### Differences vs Ethereum
117
+
118
+ - **Signature shape:** not a 65-byte `(r, s, v)` value. It is an opaque,
119
+ scheme-dependent multi-kilobyte hex blob whose first byte is the scheme id and
120
+ which **embeds the signer's public key**. There is no `Signature`/`r`/`s`/`v`
121
+ object.
122
+ - **Address size:** recovered addresses are the full 32 bytes (66 hex chars).
123
+ - **No `ecrecover`:** `verifyMessage` does not recover a key from `(digest, sig)`
124
+ cryptographically. It extracts the embedded public key, verifies it against the
125
+ digest, and returns its address; a signature that fails verification throws.
126
+ - **Signing context:** `signMessage`/`signMessageSync` accept an optional
127
+ `signingContext`. When omitted it derives the compact context from the key type
128
+ (`0` for keyType 3, `1` for keyType 5); pass `2` to request the full-signature
129
+ scheme for a keyType 3 wallet.
130
+ - **Message size:** the message must be at most 1 MiB (once UTF-8 encoded);
131
+ larger inputs throw `INVALID_ARGUMENT`. The message is only ever hashed to a
132
+ 32-byte digest, so there is no need for larger payloads.
133
+ - **EIP-712 (`signTypedData`)** is not yet supported.
134
+
89
135
  ## Contracts (read-only)
90
136
 
91
137
  ```js
@@ -102,6 +148,48 @@ const staking = new Contract(address, abi, provider);
102
148
  console.log(await staking.getDepositorCount());
103
149
  ```
104
150
 
151
+ ## ABI encoding & decoding
152
+
153
+ `Interface` and `AbiCoder` provide ethers.js v6-compatible ABI handling. Beyond
154
+ encoding, you can resolve fragments by name, canonical signature, or hex
155
+ identifier, and decode/parse raw calldata — useful for a strict "what you see is
156
+ what you sign" verification (decode the real bytes, then re-encode and compare).
157
+
158
+ ```js
159
+ const { Initialize } = require("quantumcoin/config");
160
+ const { Interface, AbiCoder } = require("quantumcoin");
161
+
162
+ await Initialize(null);
163
+ const iface = new Interface([
164
+ { type: "function", name: "transfer",
165
+ inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }],
166
+ outputs: [{ type: "bool" }] },
167
+ ]);
168
+
169
+ const to = "0x" + "ab".repeat(32); // 32-byte QuantumCoin address
170
+ const data = iface.encodeFunctionData("transfer", [to, 1000n]);
171
+
172
+ const tx = iface.parseTransaction({ data }); // resolves by selector, decodes args
173
+ // tx.name === "transfer", tx.selector === "0xa9059cbb", tx.args.amount === 1000n
174
+
175
+ // Re-encode the decoded args and confirm they reproduce the original calldata.
176
+ const verified = iface.encodeFunctionData(tx.fragment, Array.from(tx.args)) === data;
177
+
178
+ const coder = AbiCoder.defaultAbiCoder();
179
+ coder.encode(["uint256"], [1n]);
180
+ ```
181
+
182
+ Highlights: `getFunction`/`getError` (by name, signature, or 4-byte selector),
183
+ `getEvent` (by name, signature, or topic0), `decodeFunctionData`,
184
+ `parseTransaction`, `parseError`, `getSighash`, `FunctionFragment.selector`,
185
+ `EventFragment.topicHash`, `Interface.encodeDeploy`, and
186
+ `AbiCoder.defaultAbiCoder()`.
187
+
188
+ > QuantumCoin addresses are 32 bytes and fill a full ABI word (Ethereum left-pads
189
+ > a 20-byte address). Canonical signatures are identical, so selectors and event
190
+ > topics match Ethereum exactly; only address-bearing calldata words differ. See
191
+ > [README-SDK.md](./README-SDK.md#interface) for the full API.
192
+
105
193
  ## Typed contract generator
106
194
 
107
195
  This repo includes a generator described in `SPEC.md` section 15.
@@ -156,6 +244,7 @@ Common types:
156
244
  ```bash
157
245
  npm run example
158
246
  npm run example:wallet
247
+ npm run example:sign-message
159
248
  npm run example:contract:read
160
249
  npm run example:events
161
250
  # Run all examples (including SDK generator JS/TS)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quantumcoin",
3
- "version": "8.0.0",
3
+ "version": "8.0.2",
4
4
  "description": "QuantumCoin.js - a post quantum cryptography SDK for QuantumCoin",
5
5
  "main": "index.js",
6
6
  "types": "src/index.d.ts",
@@ -59,12 +59,14 @@
59
59
  "example:events:ts": "npx tsx examples/events.ts",
60
60
  "example:offline-signing": "node examples/offline-signing.js",
61
61
  "example:offline-signing:ts": "npx tsx examples/offline-signing.ts",
62
+ "example:sign-message": "node examples/sign-message.js",
63
+ "example:sign-message:ts": "npx tsx examples/sign-message.ts",
62
64
  "example:generator-js": "node examples/example-generator-sdk-js.js",
63
65
  "example:generator-js:ts": "npx tsx examples/example-generator-sdk-js.ts",
64
66
  "example:generator-ts": "node examples/example-generator-sdk-ts.js",
65
67
  "example:generator-ts:ts": "npx tsx examples/example-generator-sdk-ts.ts",
66
- "examples": "node examples/example.js && node examples/wallet-offline.js && node examples/read-operations.js && node examples/events.js && node examples/example-generator-sdk-js.js && node examples/example-generator-sdk-ts.js",
67
- "examples:ts": "npx tsx examples/example.ts && npx tsx examples/wallet-offline.ts && npx tsx examples/read-operations.ts && npx tsx examples/events.ts && npx tsx examples/example-generator-sdk-js.ts && npx tsx examples/example-generator-sdk-ts.ts",
68
+ "examples": "node examples/example.js && node examples/wallet-offline.js && node examples/sign-message.js && node examples/read-operations.js && node examples/events.js && node examples/example-generator-sdk-js.js && node examples/example-generator-sdk-ts.js",
69
+ "examples:ts": "npx tsx examples/example.ts && npx tsx examples/wallet-offline.ts && npx tsx examples/sign-message.ts && npx tsx examples/read-operations.ts && npx tsx examples/events.ts && npx tsx examples/example-generator-sdk-js.ts && npx tsx examples/example-generator-sdk-ts.ts",
68
70
  "build": "npx -p typescript tsc -p tsconfig.build.json && node scripts/copy-declarations.js"
69
71
  },
70
72
  "repository": {
@@ -29,10 +29,28 @@ export class Fragment {
29
29
  export class NamedFragment extends Fragment {
30
30
  }
31
31
  export class FunctionFragment extends NamedFragment {
32
+ /**
33
+ * The 4-byte function selector (sighash), e.g. "0xa9059cbb". Requires the SDK
34
+ * to be initialized (uses keccak256). Mirrors ethers.js v6.
35
+ * @returns {string}
36
+ */
37
+ get selector(): string;
32
38
  }
33
39
  export class EventFragment extends NamedFragment {
40
+ /**
41
+ * The event topic hash (topic0). Requires the SDK to be initialized (uses
42
+ * keccak256). Mirrors ethers.js v6.
43
+ * @returns {string}
44
+ */
45
+ get topicHash(): string;
34
46
  }
35
47
  export class ErrorFragment extends NamedFragment {
48
+ /**
49
+ * The 4-byte error selector. Requires the SDK to be initialized (uses
50
+ * keccak256). Mirrors ethers.js v6.
51
+ * @returns {string}
52
+ */
53
+ get selector(): string;
36
54
  }
37
55
  export class ConstructorFragment extends Fragment {
38
56
  }
@@ -43,9 +43,52 @@ class Fragment {
43
43
  }
44
44
 
45
45
  class NamedFragment extends Fragment {}
46
- class FunctionFragment extends NamedFragment {}
47
- class EventFragment extends NamedFragment {}
48
- class ErrorFragment extends NamedFragment {}
46
+
47
+ class FunctionFragment extends NamedFragment {
48
+ /**
49
+ * The 4-byte function selector (sighash), e.g. "0xa9059cbb". Requires the SDK
50
+ * to be initialized (uses keccak256). Mirrors ethers.js v6.
51
+ * @returns {string}
52
+ */
53
+ get selector() {
54
+ // Lazy require to keep fragment construction dependency-free.
55
+ // eslint-disable-next-line global-require
56
+ const { functionSelectorHex } = require("./js-abi-coder");
57
+ return functionSelectorHex(this.name, Array.isArray(this.inputs) ? this.inputs : []);
58
+ }
59
+ }
60
+
61
+ class EventFragment extends NamedFragment {
62
+ /**
63
+ * The event topic hash (topic0). Requires the SDK to be initialized (uses
64
+ * keccak256). Mirrors ethers.js v6.
65
+ * @returns {string}
66
+ */
67
+ get topicHash() {
68
+ // eslint-disable-next-line global-require
69
+ const { canonicalType } = require("./js-abi-coder");
70
+ // eslint-disable-next-line global-require
71
+ const { id } = require("../utils/hashing");
72
+ // eslint-disable-next-line global-require
73
+ const { normalizeHex } = require("../internal/hex");
74
+ const inputs = Array.isArray(this.inputs) ? this.inputs : [];
75
+ return normalizeHex(id(`${this.name}(${inputs.map((i) => canonicalType(i)).join(",")})`));
76
+ }
77
+ }
78
+
79
+ class ErrorFragment extends NamedFragment {
80
+ /**
81
+ * The 4-byte error selector. Requires the SDK to be initialized (uses
82
+ * keccak256). Mirrors ethers.js v6.
83
+ * @returns {string}
84
+ */
85
+ get selector() {
86
+ // eslint-disable-next-line global-require
87
+ const { functionSelectorHex } = require("./js-abi-coder");
88
+ return functionSelectorHex(this.name, Array.isArray(this.inputs) ? this.inputs : []);
89
+ }
90
+ }
91
+
49
92
  class ConstructorFragment extends Fragment {}
50
93
  class StructFragment extends Fragment {}
51
94
  class FallbackFragment extends Fragment {}
@@ -31,23 +31,26 @@ export class Interface {
31
31
  */
32
32
  format(format?: string | undefined): string;
33
33
  /**
34
- * Get a function fragment by name (first match).
35
- * @param {string} nameOrSignature
34
+ * Get a function fragment by bare name, canonical signature
35
+ * (`name(type,...)`), or 4-byte selector (`0x` + 8 hex). Mirrors ethers.js v6.
36
+ * @param {string} nameOrSignatureOrSelector
36
37
  * @returns {FunctionFragment}
37
38
  */
38
- getFunction(nameOrSignature: string): FunctionFragment;
39
+ getFunction(nameOrSignatureOrSelector: string): FunctionFragment;
39
40
  /**
40
- * Get an event fragment by name (first match).
41
- * @param {string} nameOrSignature
41
+ * Get an event fragment by bare name, canonical signature, or topic0
42
+ * (`0x` + 64 hex). Mirrors ethers.js v6.
43
+ * @param {string} nameOrSignatureOrTopic
42
44
  * @returns {EventFragment}
43
45
  */
44
- getEvent(nameOrSignature: string): EventFragment;
46
+ getEvent(nameOrSignatureOrTopic: string): EventFragment;
45
47
  /**
46
- * Get an error fragment by name (first match).
47
- * @param {string} nameOrSignature
48
+ * Get an error fragment by bare name, canonical signature, or 4-byte selector.
49
+ * Mirrors ethers.js v6.
50
+ * @param {string} nameOrSignatureOrSelector
48
51
  * @returns {ErrorFragment}
49
52
  */
50
- getError(nameOrSignature: string): ErrorFragment;
53
+ getError(nameOrSignatureOrSelector: string): ErrorFragment;
51
54
  /**
52
55
  * Returns the constructor fragment if present.
53
56
  * @returns {ConstructorFragment|null}
@@ -60,6 +63,23 @@ export class Interface {
60
63
  * @returns {string}
61
64
  */
62
65
  encodeFunctionData(functionFragment: FunctionFragment | string, values: any[]): string;
66
+ /**
67
+ * Encode ABI constructor arguments for a contract deployment (no selector).
68
+ * Returns "0x" when the constructor takes no arguments. The caller appends
69
+ * this to the creation bytecode. Mirrors ethers.js v6.
70
+ * @param {any[]=} values
71
+ * @returns {string}
72
+ */
73
+ encodeDeploy(values?: any[] | undefined): string;
74
+ /**
75
+ * Decode the calldata of a function call (4-byte selector + ABI-encoded
76
+ * arguments), returning the decoded input arguments as a Result. The data's
77
+ * selector must match the resolved function. Mirrors ethers.js v6.
78
+ * @param {FunctionFragment|string} fragment
79
+ * @param {string} data
80
+ * @returns {Result}
81
+ */
82
+ decodeFunctionData(fragment: FunctionFragment | string, data: string): Result;
63
83
  /**
64
84
  * Decode function result using quantum-coin-js-sdk.
65
85
  * @param {FunctionFragment|string} functionFragment
@@ -85,7 +105,25 @@ export class Interface {
85
105
  * @returns {any}
86
106
  */
87
107
  decodeEventLog(eventFragment: EventFragment | any, topics: string[], data: string): any;
88
- parseTransaction(): void;
108
+ /**
109
+ * Parse a transaction's calldata into its function + decoded arguments.
110
+ * Resolves the function by the 4-byte selector in `tx.data`, decodes the
111
+ * arguments from the real bytes, and reports `value` as a bigint. Mirrors
112
+ * ethers.js v6 (returns a TransactionDescription-shaped object).
113
+ * @param {{ data: string, value?: any }} tx
114
+ * @returns {{ fragment: FunctionFragment, name: string, signature: string, selector: string, args: Result, value: bigint }}
115
+ */
116
+ parseTransaction(tx: {
117
+ data: string;
118
+ value?: any;
119
+ }): {
120
+ fragment: FunctionFragment;
121
+ name: string;
122
+ signature: string;
123
+ selector: string;
124
+ args: Result;
125
+ value: bigint;
126
+ };
89
127
  parseLog(...args: any[]): {
90
128
  fragment: EventFragment;
91
129
  name: any;
@@ -93,8 +131,27 @@ export class Interface {
93
131
  topic: string;
94
132
  args: Result;
95
133
  };
96
- parseError(): void;
97
- getSighash(): void;
134
+ /**
135
+ * Parse custom-error return data into its error fragment + decoded arguments.
136
+ * Resolves the error by the 4-byte selector in `data`. Mirrors ethers.js v6
137
+ * (returns an ErrorDescription-shaped object).
138
+ * @param {string} data
139
+ * @returns {{ fragment: ErrorFragment, name: string, signature: string, selector: string, args: Result }}
140
+ */
141
+ parseError(data: string): {
142
+ fragment: ErrorFragment;
143
+ name: string;
144
+ signature: string;
145
+ selector: string;
146
+ args: Result;
147
+ };
148
+ /**
149
+ * Return the 4-byte function selector (sighash) for a function fragment or
150
+ * name. Mirrors ethers.js v5 getSighash / v6 FunctionFragment.selector.
151
+ * @param {FunctionFragment|string} fragmentOrName
152
+ * @returns {string}
153
+ */
154
+ getSighash(fragmentOrName: FunctionFragment | string): string;
98
155
  /**
99
156
  * Compute the topic0 (event signature hash) for an event.
100
157
  * @param {string|EventFragment|any} nameOrFragment
@@ -105,6 +162,11 @@ export class Interface {
105
162
  getReceive(): null;
106
163
  }
107
164
  export class AbiCoder {
165
+ /**
166
+ * Return the shared default AbiCoder instance (ethers.js v6 shape).
167
+ * @returns {AbiCoder}
168
+ */
169
+ static defaultAbiCoder(): AbiCoder;
108
170
  /**
109
171
  * Encode values by types into ABI data.
110
172
  * @param {(string|any)[]} types
@@ -8,12 +8,45 @@
8
8
 
9
9
  const qcsdk = require("quantum-coin-js-sdk");
10
10
  const { makeError, assertArgument } = require("../errors");
11
- const { normalizeHex, arrayify, bytesToHex } = require("../internal/hex");
11
+ const { normalizeHex, arrayify, bytesToHex, isHexString } = require("../internal/hex");
12
12
  const { EventFragment, FunctionFragment, ErrorFragment, ConstructorFragment } = require("./fragments");
13
13
  const { Result } = require("../utils/result");
14
14
  const { id } = require("../utils/hashing");
15
15
  const jsAbi = require("./js-abi-coder");
16
16
 
17
+ // Remove all whitespace (used to normalize a caller-supplied canonical signature
18
+ // like "transfer(address, uint256)" before comparing against the ABI).
19
+ function _stripWhitespace(s) {
20
+ return String(s).replace(/\s+/g, "");
21
+ }
22
+
23
+ // Canonical Solidity signature for a fragment, e.g. "transfer(address,uint256)".
24
+ // Tuples/arrays are expanded via the shared js-abi-coder canonicalizer so it
25
+ // matches the selector/topic hashing used everywhere else.
26
+ function _canonicalSignature(frag) {
27
+ const inputs = Array.isArray(frag.inputs) ? frag.inputs : [];
28
+ return `${frag.name}(${inputs.map((i) => jsAbi.canonicalType(i)).join(",")})`;
29
+ }
30
+
31
+ // 0x + 8 hex => a 4-byte function/error selector.
32
+ function _isSelectorHex(s) {
33
+ return typeof s === "string" && /^0x[0-9a-fA-F]{8}$/.test(s.trim());
34
+ }
35
+
36
+ // 0x + 64 hex => a 32-byte event topic (topic0).
37
+ function _isTopicHex(s) {
38
+ return typeof s === "string" && /^0x[0-9a-fA-F]{64}$/.test(s.trim());
39
+ }
40
+
41
+ // Resolve a function fragment for decode/encode. A fragment object (e.g. the one
42
+ // returned by parseTransaction/getFunction) is used AS-IS so an overloaded name
43
+ // is never re-resolved ambiguously; a string resolves through the ABI.
44
+ function _resolveFunctionFragment(iface, fragment) {
45
+ if (fragment instanceof FunctionFragment) return fragment;
46
+ if (fragment && typeof fragment === "object" && fragment.name) return new FunctionFragment(fragment);
47
+ return iface.getFunction(fragment);
48
+ }
49
+
17
50
  function _requireInitialized() {
18
51
  // eslint-disable-next-line global-require
19
52
  const { isInitialized, getInitializationPromise } = require("../../config");
@@ -289,39 +322,96 @@ class Interface {
289
322
  }
290
323
 
291
324
  /**
292
- * Get a function fragment by name (first match).
293
- * @param {string} nameOrSignature
325
+ * Get a function fragment by bare name, canonical signature
326
+ * (`name(type,...)`), or 4-byte selector (`0x` + 8 hex). Mirrors ethers.js v6.
327
+ * @param {string} nameOrSignatureOrSelector
294
328
  * @returns {FunctionFragment}
295
329
  */
296
- getFunction(nameOrSignature) {
297
- assertArgument(typeof nameOrSignature === "string", "name must be a string", "nameOrSignature", nameOrSignature);
298
- const found = this.abi.find((f) => f && f.type === "function" && f.name === nameOrSignature);
299
- if (!found) throw makeError("function not found", "INVALID_ARGUMENT", { nameOrSignature });
300
- return new FunctionFragment(found);
330
+ getFunction(nameOrSignatureOrSelector) {
331
+ assertArgument(typeof nameOrSignatureOrSelector === "string", "name must be a string", "nameOrSignatureOrSelector", nameOrSignatureOrSelector);
332
+ const key = nameOrSignatureOrSelector.trim();
333
+ const fns = this.abi.filter((f) => f && f.type === "function" && f.name);
334
+
335
+ if (_isSelectorHex(key)) {
336
+ const sel = normalizeHex(key).toLowerCase();
337
+ const found = fns.find((f) => jsAbi.functionSelectorHex(f.name, f.inputs || []).toLowerCase() === sel);
338
+ if (!found) throw makeError("no matching function for selector", "INVALID_ARGUMENT", { selector: key });
339
+ return new FunctionFragment(found);
340
+ }
341
+
342
+ if (key.indexOf("(") >= 0) {
343
+ const norm = _stripWhitespace(key);
344
+ const found = fns.find((f) => _canonicalSignature(f) === norm);
345
+ if (!found) throw makeError("no matching function for signature", "INVALID_ARGUMENT", { signature: key });
346
+ return new FunctionFragment(found);
347
+ }
348
+
349
+ const matches = fns.filter((f) => f.name === key);
350
+ if (matches.length === 0) throw makeError("function not found", "INVALID_ARGUMENT", { nameOrSignatureOrSelector });
351
+ if (matches.length > 1) {
352
+ throw makeError("ambiguous function name; specify the full signature", "INVALID_ARGUMENT", { nameOrSignatureOrSelector });
353
+ }
354
+ return new FunctionFragment(matches[0]);
301
355
  }
302
356
 
303
357
  /**
304
- * Get an event fragment by name (first match).
305
- * @param {string} nameOrSignature
358
+ * Get an event fragment by bare name, canonical signature, or topic0
359
+ * (`0x` + 64 hex). Mirrors ethers.js v6.
360
+ * @param {string} nameOrSignatureOrTopic
306
361
  * @returns {EventFragment}
307
362
  */
308
- getEvent(nameOrSignature) {
309
- assertArgument(typeof nameOrSignature === "string", "name must be a string", "nameOrSignature", nameOrSignature);
310
- const found = this.abi.find((f) => f && f.type === "event" && f.name === nameOrSignature);
311
- if (!found) throw makeError("event not found", "INVALID_ARGUMENT", { nameOrSignature });
312
- return new EventFragment(found);
363
+ getEvent(nameOrSignatureOrTopic) {
364
+ assertArgument(typeof nameOrSignatureOrTopic === "string", "name must be a string", "nameOrSignatureOrTopic", nameOrSignatureOrTopic);
365
+ const key = nameOrSignatureOrTopic.trim();
366
+ const evs = this.abi.filter((f) => f && f.type === "event" && f.name);
367
+
368
+ if (_isTopicHex(key)) {
369
+ const topic = normalizeHex(key);
370
+ const found = evs.find((f) => !f.anonymous && normalizeHex(id(_canonicalSignature(f))) === topic);
371
+ if (!found) throw makeError("no matching event for topic", "INVALID_ARGUMENT", { topic: key });
372
+ return new EventFragment(found);
373
+ }
374
+
375
+ if (key.indexOf("(") >= 0) {
376
+ const norm = _stripWhitespace(key);
377
+ const found = evs.find((f) => _canonicalSignature(f) === norm);
378
+ if (!found) throw makeError("no matching event for signature", "INVALID_ARGUMENT", { signature: key });
379
+ return new EventFragment(found);
380
+ }
381
+
382
+ const matches = evs.filter((f) => f.name === key);
383
+ if (matches.length === 0) throw makeError("event not found", "INVALID_ARGUMENT", { nameOrSignatureOrTopic });
384
+ return new EventFragment(matches[0]);
313
385
  }
314
386
 
315
387
  /**
316
- * Get an error fragment by name (first match).
317
- * @param {string} nameOrSignature
388
+ * Get an error fragment by bare name, canonical signature, or 4-byte selector.
389
+ * Mirrors ethers.js v6.
390
+ * @param {string} nameOrSignatureOrSelector
318
391
  * @returns {ErrorFragment}
319
392
  */
320
- getError(nameOrSignature) {
321
- assertArgument(typeof nameOrSignature === "string", "name must be a string", "nameOrSignature", nameOrSignature);
322
- const found = this.abi.find((f) => f && f.type === "error" && f.name === nameOrSignature);
323
- if (!found) throw makeError("error not found", "INVALID_ARGUMENT", { nameOrSignature });
324
- return new ErrorFragment(found);
393
+ getError(nameOrSignatureOrSelector) {
394
+ assertArgument(typeof nameOrSignatureOrSelector === "string", "name must be a string", "nameOrSignatureOrSelector", nameOrSignatureOrSelector);
395
+ const key = nameOrSignatureOrSelector.trim();
396
+ const errs = this.abi.filter((f) => f && f.type === "error" && f.name);
397
+
398
+ if (_isSelectorHex(key)) {
399
+ const sel = normalizeHex(key).toLowerCase();
400
+ const found = errs.find((f) => jsAbi.functionSelectorHex(f.name, f.inputs || []).toLowerCase() === sel);
401
+ if (!found) throw makeError("no matching error for selector", "INVALID_ARGUMENT", { selector: key });
402
+ return new ErrorFragment(found);
403
+ }
404
+
405
+ if (key.indexOf("(") >= 0) {
406
+ const norm = _stripWhitespace(key);
407
+ const found = errs.find((f) => _canonicalSignature(f) === norm);
408
+ if (!found) throw makeError("no matching error for signature", "INVALID_ARGUMENT", { signature: key });
409
+ return new ErrorFragment(found);
410
+ }
411
+
412
+ const matches = errs.filter((f) => f.name === key);
413
+ if (matches.length === 0) throw makeError("error not found", "INVALID_ARGUMENT", { nameOrSignatureOrSelector });
414
+ return new ErrorFragment(matches[0]);
325
415
  }
326
416
 
327
417
  /**
@@ -341,24 +431,76 @@ class Interface {
341
431
  */
342
432
  encodeFunctionData(functionFragment, values) {
343
433
  _requireInitialized();
344
- const name = typeof functionFragment === "string" ? functionFragment : functionFragment?.name;
434
+ const rawArgs = Array.isArray(values) ? values : [];
435
+
436
+ // When a fragment object is supplied (e.g. from parseTransaction /
437
+ // getFunction), encode directly from its exact inputs via the pure-JS coder.
438
+ // This guarantees encode/decode is a faithful round-trip (including
439
+ // overloaded functions) and matches decodeFunctionData byte-for-byte.
440
+ if (functionFragment && typeof functionFragment === "object") {
441
+ const fname = functionFragment.name;
442
+ assertArgument(typeof fname === "string" && fname.length > 0, "invalid function", "functionFragment", functionFragment);
443
+ const finputs = Array.isArray(functionFragment.inputs) ? functionFragment.inputs : [];
444
+ return jsAbi.encodeFunctionData(fname, finputs, rawArgs);
445
+ }
446
+
447
+ const name = functionFragment;
345
448
  assertArgument(typeof name === "string" && name.length > 0, "invalid function", "functionFragment", functionFragment);
346
449
  const frag = this.getFunction(name);
347
450
  const inputs = Array.isArray(frag.inputs) ? frag.inputs : [];
348
- const rawArgs = Array.isArray(values) ? values : [];
349
451
 
350
452
  // Fallback for complex ABI surfaces where qcsdk packing is unreliable.
351
453
  if (jsAbi.needsJsAbi(inputs)) {
352
- return jsAbi.encodeFunctionData(name, inputs, rawArgs);
454
+ return jsAbi.encodeFunctionData(frag.name, inputs, rawArgs);
353
455
  }
354
456
 
355
457
  const args = inputs.map((p, idx) => _convertInputValueForQcsdk(p, rawArgs[idx]));
356
- const res = qcsdk.packMethodData(this._qcsdkAbiJson, name, ...args);
458
+ const res = qcsdk.packMethodData(this._qcsdkAbiJson, frag.name, ...args);
357
459
  if (!res || typeof res.error !== "string") throw makeError("packMethodData failed", "UNKNOWN_ERROR", {});
358
- if (res.error) throw makeError(res.error, "UNKNOWN_ERROR", { operation: "packMethodData", function: name });
460
+ if (res.error) throw makeError(res.error, "UNKNOWN_ERROR", { operation: "packMethodData", function: frag.name });
359
461
  return normalizeHex(res.result);
360
462
  }
361
463
 
464
+ /**
465
+ * Encode ABI constructor arguments for a contract deployment (no selector).
466
+ * Returns "0x" when the constructor takes no arguments. The caller appends
467
+ * this to the creation bytecode. Mirrors ethers.js v6.
468
+ * @param {any[]=} values
469
+ * @returns {string}
470
+ */
471
+ encodeDeploy(values) {
472
+ _requireInitialized();
473
+ const ctor = this.getConstructor();
474
+ const inputs = ctor && Array.isArray(ctor.inputs) ? ctor.inputs : [];
475
+ const rawArgs = Array.isArray(values) ? values : [];
476
+ if (inputs.length === 0) return "0x";
477
+ return normalizeHex(bytesToHex(jsAbi.encodeTupleLike(inputs, rawArgs)));
478
+ }
479
+
480
+ /**
481
+ * Decode the calldata of a function call (4-byte selector + ABI-encoded
482
+ * arguments), returning the decoded input arguments as a Result. The data's
483
+ * selector must match the resolved function. Mirrors ethers.js v6.
484
+ * @param {FunctionFragment|string} fragment
485
+ * @param {string} data
486
+ * @returns {Result}
487
+ */
488
+ decodeFunctionData(fragment, data) {
489
+ _requireInitialized();
490
+ assertArgument(typeof data === "string" && isHexString(data), "data must be a hex string", "data", data);
491
+ const frag = _resolveFunctionFragment(this, fragment);
492
+ const inputs = Array.isArray(frag.inputs) ? frag.inputs : [];
493
+ const selector = jsAbi.functionSelectorHex(frag.name, inputs).toLowerCase();
494
+ const norm = normalizeHex(data);
495
+ const bytes = arrayify(norm);
496
+ if (bytes.length < 4 || norm.slice(0, 10).toLowerCase() !== selector) {
497
+ throw makeError("data selector does not match function", "INVALID_ARGUMENT", { function: frag.name, data });
498
+ }
499
+ const items = jsAbi.decodeTupleLike(inputs, bytes.slice(4), 0);
500
+ const keys = inputs.map((i) => (i && typeof i.name === "string" && i.name.length ? i.name : null));
501
+ return Result.fromItems(items, keys);
502
+ }
503
+
362
504
  /**
363
505
  * Decode function result using quantum-coin-js-sdk.
364
506
  * @param {FunctionFragment|string} functionFragment
@@ -442,9 +584,40 @@ class Interface {
442
584
  }
443
585
  }
444
586
 
445
- // The following methods exist in ethers.js v6. We provide placeholders to keep API shape.
446
- parseTransaction() {
447
- throw makeError("parseTransaction not implemented", "NOT_IMPLEMENTED", {});
587
+ /**
588
+ * Parse a transaction's calldata into its function + decoded arguments.
589
+ * Resolves the function by the 4-byte selector in `tx.data`, decodes the
590
+ * arguments from the real bytes, and reports `value` as a bigint. Mirrors
591
+ * ethers.js v6 (returns a TransactionDescription-shaped object).
592
+ * @param {{ data: string, value?: any }} tx
593
+ * @returns {{ fragment: FunctionFragment, name: string, signature: string, selector: string, args: Result, value: bigint }}
594
+ */
595
+ parseTransaction(tx) {
596
+ _requireInitialized();
597
+ assertArgument(tx && typeof tx === "object", "tx must be an object", "tx", tx);
598
+ const data = tx.data;
599
+ assertArgument(typeof data === "string" && isHexString(data), "tx.data must be a hex string", "tx.data", data);
600
+ const norm = normalizeHex(data);
601
+ if (arrayify(norm).length < 4) throw makeError("data too short for a function selector", "INVALID_ARGUMENT", { data });
602
+ const selector = norm.slice(0, 10);
603
+ const frag = this.getFunction(selector);
604
+ const args = this.decodeFunctionData(frag, norm);
605
+ let value = 0n;
606
+ if (tx.value != null) {
607
+ try {
608
+ value = BigInt(tx.value);
609
+ } catch {
610
+ value = 0n;
611
+ }
612
+ }
613
+ return {
614
+ fragment: frag,
615
+ name: frag.name,
616
+ signature: _canonicalSignature(frag),
617
+ selector: jsAbi.functionSelectorHex(frag.name, Array.isArray(frag.inputs) ? frag.inputs : []),
618
+ args,
619
+ value,
620
+ };
448
621
  }
449
622
  parseLog() {
450
623
  _requireInitialized();
@@ -512,11 +685,47 @@ class Interface {
512
685
  args,
513
686
  };
514
687
  }
515
- parseError() {
516
- throw makeError("parseError not implemented", "NOT_IMPLEMENTED", {});
688
+ /**
689
+ * Parse custom-error return data into its error fragment + decoded arguments.
690
+ * Resolves the error by the 4-byte selector in `data`. Mirrors ethers.js v6
691
+ * (returns an ErrorDescription-shaped object).
692
+ * @param {string} data
693
+ * @returns {{ fragment: ErrorFragment, name: string, signature: string, selector: string, args: Result }}
694
+ */
695
+ parseError(data) {
696
+ _requireInitialized();
697
+ assertArgument(typeof data === "string" && isHexString(data), "data must be a hex string", "data", data);
698
+ const norm = normalizeHex(data);
699
+ const bytes = arrayify(norm);
700
+ if (bytes.length < 4) throw makeError("data too short for an error selector", "INVALID_ARGUMENT", { data });
701
+ const selector = norm.slice(0, 10).toLowerCase();
702
+ const errs = this.abi.filter((f) => f && f.type === "error" && f.name);
703
+ const found = errs.find((f) => jsAbi.functionSelectorHex(f.name, f.inputs || []).toLowerCase() === selector);
704
+ if (!found) throw makeError("no matching error for selector", "INVALID_ARGUMENT", { selector });
705
+ const inputs = Array.isArray(found.inputs) ? found.inputs : [];
706
+ const items = jsAbi.decodeTupleLike(inputs, bytes.slice(4), 0);
707
+ const keys = inputs.map((i) => (i && typeof i.name === "string" && i.name.length ? i.name : null));
708
+ return {
709
+ fragment: new ErrorFragment(found),
710
+ name: found.name,
711
+ signature: _canonicalSignature(found),
712
+ selector: jsAbi.functionSelectorHex(found.name, inputs),
713
+ args: Result.fromItems(items, keys),
714
+ };
517
715
  }
518
- getSighash() {
519
- throw makeError("getSighash not implemented", "NOT_IMPLEMENTED", {});
716
+
717
+ /**
718
+ * Return the 4-byte function selector (sighash) for a function fragment or
719
+ * name. Mirrors ethers.js v5 getSighash / v6 FunctionFragment.selector.
720
+ * @param {FunctionFragment|string} fragmentOrName
721
+ * @returns {string}
722
+ */
723
+ getSighash(fragmentOrName) {
724
+ const frag =
725
+ fragmentOrName && typeof fragmentOrName === "object" && fragmentOrName.name
726
+ ? fragmentOrName
727
+ : this.getFunction(fragmentOrName);
728
+ return jsAbi.functionSelectorHex(frag.name, Array.isArray(frag.inputs) ? frag.inputs : []);
520
729
  }
521
730
  /**
522
731
  * Compute the topic0 (event signature hash) for an event.
@@ -599,6 +808,15 @@ class AbiCoder {
599
808
  assertArgument(Array.isArray(types), "types must be an array", "types", types);
600
809
  return types.map(() => null);
601
810
  }
811
+
812
+ /**
813
+ * Return the shared default AbiCoder instance (ethers.js v6 shape).
814
+ * @returns {AbiCoder}
815
+ */
816
+ static defaultAbiCoder() {
817
+ if (!AbiCoder._instance) AbiCoder._instance = new AbiCoder();
818
+ return AbiCoder._instance;
819
+ }
602
820
  }
603
821
 
604
822
  module.exports = { Interface, AbiCoder };
@@ -6,3 +6,5 @@ export function functionSelectorHex(name: any, inputs: any): string;
6
6
  export function encodeFunctionData(name: any, inputs: any, values: any): string;
7
7
  export function encodeTupleLike(params: any, values: any, depth: any): Uint8Array<any>;
8
8
  export function decodeFunctionResult(outputs: any, dataHex: any): any[];
9
+ export function decodeTupleLike(params: any, data: any, baseOffset: any, depth: any): any[];
10
+ export function decodeParam(param: any, data: any, offset: any, depth: any): any;
@@ -534,5 +534,7 @@ module.exports = {
534
534
  encodeFunctionData,
535
535
  encodeTupleLike,
536
536
  decodeFunctionResult,
537
+ decodeTupleLike,
538
+ decodeParam,
537
539
  };
538
540
 
package/src/index.d.ts CHANGED
@@ -29,6 +29,7 @@ declare const _exports: {
29
29
  sha512(data: string | Uint8Array): string;
30
30
  ripemd160(data: string | Uint8Array): string;
31
31
  id(text: string): string;
32
+ hashMessage(message: string | Uint8Array): string;
32
33
  randomBytes(length: number): Uint8Array;
33
34
  computeHmac(algorithm: string, key: string | Uint8Array, data: string | Uint8Array): string;
34
35
  pbkdf2(password: string | Uint8Array, salt: string | Uint8Array, iterations: number, keylen: number, algorithm?: string | undefined): string;
@@ -88,6 +89,7 @@ declare const _exports: {
88
89
  ContractTransactionResponse: typeof import("./contract/contract").ContractTransactionResponse;
89
90
  ContractTransactionReceipt: typeof import("./contract/contract").ContractTransactionReceipt;
90
91
  EventLog: typeof import("./contract/contract").EventLog;
92
+ verifyMessage(message: string | Uint8Array, signature: string | Uint8Array): string;
91
93
  SigningKey: typeof import("./wallet/wallet").SigningKey;
92
94
  AbstractSigner: typeof import("./wallet/wallet").AbstractSigner;
93
95
  BaseWallet: typeof import("./wallet/wallet").BaseWallet;
@@ -28,6 +28,24 @@ export function ripemd160(data: string | Uint8Array): string;
28
28
  * @returns {string}
29
29
  */
30
30
  export function id(text: string): string;
31
+ /**
32
+ * Compute the EIP-191 "personal message" digest for a message.
33
+ *
34
+ * QuantumCoin uses the exact same prefix as Ethereum, so this is byte-for-byte
35
+ * compatible with `personal_sign` in `quantum-coin-go`:
36
+ *
37
+ * keccak256("\x19Ethereum Signed Message:\n" + message.length + message)
38
+ *
39
+ * The resulting 32-byte hash is the digest passed to `Wallet.signMessage` /
40
+ * `Wallet.signMessageSync` and re-derived by `verifyMessage`. The decimal length
41
+ * prefix counts message BYTES, not characters. A string message is UTF-8
42
+ * encoded; a Uint8Array (or 0x hex string coerced via `arrayify`) is treated as
43
+ * raw bytes.
44
+ *
45
+ * @param {string|Uint8Array} message The message to hash. Strings are UTF-8 encoded.
46
+ * @returns {string} 0x-prefixed, 32-byte keccak256 digest.
47
+ */
48
+ export function hashMessage(message: string | Uint8Array): string;
31
49
  /**
32
50
  * Generate cryptographically strong random bytes.
33
51
  *
@@ -89,6 +89,32 @@ function id(text) {
89
89
  return keccak256(utf8ToBytes(text));
90
90
  }
91
91
 
92
+ /**
93
+ * Compute the EIP-191 "personal message" digest for a message.
94
+ *
95
+ * QuantumCoin uses the exact same prefix as Ethereum, so this is byte-for-byte
96
+ * compatible with `personal_sign` in `quantum-coin-go`:
97
+ *
98
+ * keccak256("\x19Ethereum Signed Message:\n" + message.length + message)
99
+ *
100
+ * The resulting 32-byte hash is the digest passed to `Wallet.signMessage` /
101
+ * `Wallet.signMessageSync` and re-derived by `verifyMessage`. The decimal length
102
+ * prefix counts message BYTES, not characters. A string message is UTF-8
103
+ * encoded; a Uint8Array (or 0x hex string coerced via `arrayify`) is treated as
104
+ * raw bytes.
105
+ *
106
+ * @param {string|Uint8Array} message The message to hash. Strings are UTF-8 encoded.
107
+ * @returns {string} 0x-prefixed, 32-byte keccak256 digest.
108
+ */
109
+ function hashMessage(message) {
110
+ const msgBytes = typeof message === "string" ? utf8ToBytes(message) : arrayify(message);
111
+ const prefix = utf8ToBytes(`\x19Ethereum Signed Message:\n${msgBytes.length}`);
112
+ const composed = new Uint8Array(prefix.length + msgBytes.length);
113
+ composed.set(prefix, 0);
114
+ composed.set(msgBytes, prefix.length);
115
+ return keccak256(composed);
116
+ }
117
+
92
118
  /**
93
119
  * Generate cryptographically strong random bytes.
94
120
  *
@@ -181,6 +207,7 @@ module.exports = {
181
207
  sha512,
182
208
  ripemd160,
183
209
  id,
210
+ hashMessage,
184
211
  randomBytes,
185
212
  computeHmac,
186
213
  pbkdf2,
@@ -17,6 +17,7 @@ declare const _exports: {
17
17
  sha512(data: string | Uint8Array): string;
18
18
  ripemd160(data: string | Uint8Array): string;
19
19
  id(text: string): string;
20
+ hashMessage(message: string | Uint8Array): string;
20
21
  randomBytes(length: number): Uint8Array;
21
22
  computeHmac(algorithm: string, key: string | Uint8Array, data: string | Uint8Array): string;
22
23
  pbkdf2(password: string | Uint8Array, salt: string | Uint8Array, iterations: number, keylen: number, algorithm?: string | undefined): string;
@@ -1,4 +1,5 @@
1
1
  declare const _exports: {
2
+ verifyMessage(message: string | Uint8Array, signature: string | Uint8Array): string;
2
3
  SigningKey: typeof import("./wallet").SigningKey;
3
4
  AbstractSigner: typeof import("./wallet").AbstractSigner;
4
5
  BaseWallet: typeof import("./wallet").BaseWallet;
@@ -51,6 +51,24 @@ export class BaseWallet extends AbstractSigner {
51
51
  * @returns {Promise<string>}
52
52
  */
53
53
  signTransaction(tx: import("../providers/provider").TransactionRequest): Promise<string>;
54
+ /**
55
+ * Sign an arbitrary message (EIP-191 personal-message scheme), synchronously.
56
+ * Returns an opaque post-quantum signature blob (0x hex) that embeds the
57
+ * signer's public key. Optional signingContext: when omitted/null the compact
58
+ * context is derived from the key type (0 for keyType 3, 1 for keyType 5);
59
+ * pass 2 for the full-signature scheme on a keyType 3 wallet.
60
+ * @param {string|Uint8Array} message
61
+ * @param {number|null=} signingContext
62
+ * @returns {string}
63
+ */
64
+ signMessageSync(message: string | Uint8Array, signingContext?: (number | null) | undefined): string;
65
+ /**
66
+ * Sign an arbitrary message (EIP-191). Async wrapper over signMessageSync.
67
+ * @param {string|Uint8Array} message
68
+ * @param {number|null=} signingContext
69
+ * @returns {Promise<string>}
70
+ */
71
+ signMessage(message: string | Uint8Array, signingContext?: (number | null) | undefined): Promise<string>;
54
72
  /**
55
73
  * Internal: sign a transaction and return both the raw serialized transaction
56
74
  * and the signer-computed transaction hash. The hash is later used to
@@ -225,3 +243,16 @@ export class VoidSigner extends AbstractSigner {
225
243
  _address: string;
226
244
  getAddress(): Promise<string>;
227
245
  }
246
+ /**
247
+ * Recover the signer's 32-byte address from an EIP-191 message signature.
248
+ *
249
+ * QuantumCoin has no ECDSA ecrecover; the post-quantum signature embeds the
250
+ * public key, which is extracted and verified against the message digest before
251
+ * the address is returned. Throws INVALID_ARGUMENT if the signature is malformed
252
+ * or does not verify. Synchronous, matching ethers' verifyMessage.
253
+ *
254
+ * @param {string|Uint8Array} message The original message (strings are UTF-8 encoded).
255
+ * @param {string|Uint8Array} signature The signature produced by signMessage.
256
+ * @returns {string} 0x-prefixed 32-byte signer address.
257
+ */
258
+ export function verifyMessage(message: string | Uint8Array, signature: string | Uint8Array): string;
@@ -12,8 +12,9 @@ const qcsdk = require("quantum-coin-js-sdk");
12
12
  const seedWords = require("seed-words");
13
13
  const { JsonRpcProvider } = require("../providers/json-rpc-provider");
14
14
  const { assertArgument, assertSecretArgument, makeError } = require("../errors");
15
- const { arrayify, bytesToHex, bytesToUtf8, hexToBytes, isHexString, normalizeHex } = require("../internal/hex");
16
- const { getAddress } = require("../utils/address");
15
+ const { arrayify, bytesToHex, bytesToUtf8, hexToBytes, isHexString, normalizeHex, utf8ToBytes } = require("../internal/hex");
16
+ const { computeAddress, getAddress } = require("../utils/address");
17
+ const { hashMessage } = require("../utils/hashing");
17
18
  const { WeiPerEther } = require("../constants");
18
19
 
19
20
  function _requireInitialized() {
@@ -34,6 +35,13 @@ function _bytesToNumberArray(bytes) {
34
35
  return Array.from(bytes);
35
36
  }
36
37
 
38
+ // Upper bound on the message size accepted by signMessage/signMessageSync. The
39
+ // message is only ever hashed to a 32-byte digest, so there is no protocol need
40
+ // for large inputs; this cap is a guardrail against accidentally signing an
41
+ // unbounded buffer (e.g. a whole file passed in by mistake). 1 MiB is far larger
42
+ // than any realistic personal_sign payload.
43
+ const MAX_MESSAGE_BYTES = 1024 * 1024;
44
+
37
45
  /**
38
46
  * Verify that a private/public key pair is internally consistent (the public
39
47
  * key really corresponds to the private key). Constructing a wallet from a
@@ -214,6 +222,74 @@ class BaseWallet extends AbstractSigner {
214
222
  return raw;
215
223
  }
216
224
 
225
+ /**
226
+ * Sign an arbitrary message using QuantumCoin's EIP-191 personal-message
227
+ * scheme, synchronously.
228
+ *
229
+ * The message is hashed with {@link hashMessage} (the same
230
+ * `keccak256("\x19Ethereum Signed Message:\n" + len + message)` digest used by
231
+ * `personal_sign` in quantum-coin-go), producing a 32-byte digest that is then
232
+ * signed with the wallet's post-quantum key.
233
+ *
234
+ * Unlike Ethereum's 65-byte `(r, s, v)` signature, the returned value is an
235
+ * opaque, scheme-dependent multi-kilobyte blob whose first byte is the scheme
236
+ * id and which EMBEDS the signer's public key. This is what allows
237
+ * {@link verifyMessage} to recover the signer address without an ecrecover
238
+ * primitive.
239
+ *
240
+ * @param {string|Uint8Array} message The message to sign (strings are UTF-8 encoded).
241
+ * Must be at most {@link MAX_MESSAGE_BYTES} (1 MiB) once UTF-8 encoded.
242
+ * @param {number|null=} signingContext Optional signing context. When omitted or
243
+ * null, `quantum-coin-js-sdk` derives the compact context from the key type
244
+ * (0 for keyType 3, 1 for keyType 5); pass 2 to request the full-signature
245
+ * scheme for a keyType 3 wallet.
246
+ * @returns {string} 0x-prefixed signature blob (public key embedded).
247
+ * @throws {TypeError} INVALID_ARGUMENT if the message exceeds the size limit.
248
+ */
249
+ signMessageSync(message, signingContext) {
250
+ _requireInitialized();
251
+ const msgBytes = typeof message === "string" ? utf8ToBytes(message) : arrayify(message);
252
+ assertArgument(
253
+ msgBytes.length <= MAX_MESSAGE_BYTES,
254
+ `message too long (max ${MAX_MESSAGE_BYTES} bytes)`,
255
+ "message",
256
+ msgBytes.length,
257
+ );
258
+ const digestArr = _bytesToNumberArray(hexToBytes(hashMessage(msgBytes)));
259
+ const privArr = _bytesToNumberArray(this.signingKey.privateKeyBytes);
260
+ const pubArr = _bytesToNumberArray(this.signingKey.publicKeyBytes);
261
+ const ctx = signingContext == null ? null : signingContext;
262
+ const res = qcsdk.sign(privArr, digestArr, ctx);
263
+ if (!res || typeof res !== "object" || res.resultCode !== 0 || res.signature == null) {
264
+ throw makeError("signMessage failed", "UNKNOWN_ERROR", {
265
+ resultCode: res && typeof res === "object" ? res.resultCode : null,
266
+ });
267
+ }
268
+ // qcsdk.sign returns the signature-only bytes. Combine the public key into
269
+ // the blob (sig + pubKey) so the result is self-describing and verifyMessage
270
+ // can recover the signer without a separately supplied public key.
271
+ const sigArr = Array.from(res.signature);
272
+ const combined = qcsdk.combinePublicKeySignature(pubArr, sigArr);
273
+ if (typeof combined !== "string") {
274
+ throw makeError("signMessage failed to combine public key with signature", "UNKNOWN_ERROR", {});
275
+ }
276
+ return normalizeHex(combined);
277
+ }
278
+
279
+ /**
280
+ * Sign an arbitrary message (EIP-191). Async wrapper over
281
+ * {@link BaseWallet#signMessageSync} that honors the ethers `Signer` interface
282
+ * contract (`Signer.signMessage` is async); the underlying PQC signing is
283
+ * synchronous.
284
+ *
285
+ * @param {string|Uint8Array} message The message to sign (strings are UTF-8 encoded).
286
+ * @param {number|null=} signingContext Optional signing context (see signMessageSync).
287
+ * @returns {Promise<string>} 0x-prefixed signature blob (public key embedded).
288
+ */
289
+ async signMessage(message, signingContext) {
290
+ return this.signMessageSync(message, signingContext);
291
+ }
292
+
217
293
  /**
218
294
  * Internal: sign a transaction and return both the raw serialized transaction
219
295
  * and the signer-computed transaction hash. The hash is later used to
@@ -739,6 +815,39 @@ class VoidSigner extends AbstractSigner {
739
815
  }
740
816
  }
741
817
 
818
+ /**
819
+ * Recover the signer's 32-byte address from an EIP-191 message signature.
820
+ *
821
+ * QuantumCoin has no ECDSA `ecrecover`: signatures are opaque post-quantum blobs
822
+ * that embed the signer's public key. This function re-derives the EIP-191
823
+ * digest with {@link hashMessage}, extracts and cryptographically verifies the
824
+ * embedded public key against that digest via
825
+ * `quantum-coin-js-sdk.publicKeyFromSignature`, and returns the corresponding
826
+ * 32-byte address. If the signature does not verify there is nothing to recover,
827
+ * so the function throws.
828
+ *
829
+ * Synchronous, matching ethers' `verifyMessage` (there is no async variant).
830
+ *
831
+ * @param {string|Uint8Array} message The original message (strings are UTF-8 encoded).
832
+ * @param {string|Uint8Array} signature The signature produced by `signMessage`.
833
+ * @returns {string} 0x-prefixed 32-byte signer address.
834
+ * @throws {TypeError} INVALID_ARGUMENT if the signature is malformed or does not verify.
835
+ */
836
+ function verifyMessage(message, signature) {
837
+ _requireInitialized();
838
+ const digestHex = hashMessage(message);
839
+ const digestArr = _bytesToNumberArray(hexToBytes(digestHex));
840
+ const sigArr = _bytesToNumberArray(arrayify(signature));
841
+ const pubHex = qcsdk.publicKeyFromSignature(digestArr, sigArr);
842
+ if (typeof pubHex !== "string") {
843
+ throw makeError("verifyMessage failed: signature did not verify", "INVALID_ARGUMENT", {
844
+ argument: "signature",
845
+ value: "[signature]",
846
+ });
847
+ }
848
+ return computeAddress(pubHex);
849
+ }
850
+
742
851
  module.exports = {
743
852
  SigningKey,
744
853
  AbstractSigner,
@@ -747,5 +856,6 @@ module.exports = {
747
856
  NonceManager,
748
857
  JsonRpcSigner,
749
858
  VoidSigner,
859
+ verifyMessage,
750
860
  };
751
861