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 +110 -6
- package/README.md +89 -0
- package/package.json +5 -3
- package/src/abi/fragments.d.ts +18 -0
- package/src/abi/fragments.js +46 -3
- package/src/abi/interface.d.ts +74 -12
- package/src/abi/interface.js +252 -34
- package/src/abi/js-abi-coder.d.ts +2 -0
- package/src/abi/js-abi-coder.js +2 -0
- package/src/index.d.ts +2 -0
- package/src/utils/hashing.d.ts +18 -0
- package/src/utils/hashing.js +27 -0
- package/src/utils/index.d.ts +1 -0
- package/src/wallet/index.d.ts +1 -0
- package/src/wallet/wallet.d.ts +31 -0
- package/src/wallet/wallet.js +112 -2
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
|
-
**
|
|
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
|
-
|
|
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.
|
|
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": {
|
package/src/abi/fragments.d.ts
CHANGED
|
@@ -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
|
}
|
package/src/abi/fragments.js
CHANGED
|
@@ -43,9 +43,52 @@ class Fragment {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
class NamedFragment extends Fragment {}
|
|
46
|
-
|
|
47
|
-
class
|
|
48
|
-
|
|
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 {}
|
package/src/abi/interface.d.ts
CHANGED
|
@@ -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
|
|
35
|
-
*
|
|
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(
|
|
39
|
+
getFunction(nameOrSignatureOrSelector: string): FunctionFragment;
|
|
39
40
|
/**
|
|
40
|
-
* Get an event fragment by name
|
|
41
|
-
*
|
|
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(
|
|
46
|
+
getEvent(nameOrSignatureOrTopic: string): EventFragment;
|
|
45
47
|
/**
|
|
46
|
-
* Get an error fragment by name
|
|
47
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
package/src/abi/interface.js
CHANGED
|
@@ -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
|
|
293
|
-
*
|
|
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(
|
|
297
|
-
assertArgument(typeof
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
305
|
-
*
|
|
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(
|
|
309
|
-
assertArgument(typeof
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
317
|
-
*
|
|
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(
|
|
321
|
-
assertArgument(typeof
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
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;
|
package/src/abi/js-abi-coder.js
CHANGED
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;
|
package/src/utils/hashing.d.ts
CHANGED
|
@@ -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
|
*
|
package/src/utils/hashing.js
CHANGED
|
@@ -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,
|
package/src/utils/index.d.ts
CHANGED
|
@@ -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;
|
package/src/wallet/index.d.ts
CHANGED
|
@@ -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;
|
package/src/wallet/wallet.d.ts
CHANGED
|
@@ -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;
|
package/src/wallet/wallet.js
CHANGED
|
@@ -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
|
|