@cheny56/node-client 1.0.0

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.
@@ -0,0 +1,566 @@
1
+ /**
2
+ * Transaction Builders
3
+ * Supports Legacy, AccessList, DynamicFee transactions with ECDSA, PQC, and Hybrid signatures
4
+ */
5
+
6
+ import { ethers } from 'ethers';
7
+ import { ml_dsa65 } from '@noble/post-quantum/ml-dsa';
8
+ import { TX_TYPE, PQC_TYPE, DEFAULT_CHAIN_ID } from './constants.js';
9
+ import { encodeUint64, encodeBigInt, encodeSignature } from './utils/rlp.js';
10
+
11
+ /**
12
+ * Base Transaction class
13
+ */
14
+ class BaseTransaction {
15
+ constructor(params) {
16
+ this.chainId = params.chainId || DEFAULT_CHAIN_ID;
17
+ this.nonce = params.nonce || 0;
18
+ this.to = params.to;
19
+ this.value = params.value || 0n;
20
+ this.data = params.data || '0x';
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Legacy ECDSA Transaction (Type 0)
26
+ */
27
+ export class LegacyTransaction extends BaseTransaction {
28
+ constructor(params) {
29
+ super(params);
30
+ this.type = TX_TYPE.LEGACY;
31
+ this.gasPrice = params.gasPrice || 0n;
32
+ this.gasLimit = params.gasLimit || 21000n;
33
+ }
34
+
35
+ /**
36
+ * Sign with ECDSA wallet
37
+ * @param {ECDSAWallet} wallet - ECDSA wallet
38
+ * @returns {Promise<string>} Signed transaction (hex)
39
+ */
40
+ async sign(wallet) {
41
+ const tx = {
42
+ type: 0,
43
+ nonce: this.nonce,
44
+ gasPrice: this.gasPrice,
45
+ gasLimit: this.gasLimit,
46
+ to: this.to,
47
+ value: this.value,
48
+ data: this.data,
49
+ chainId: this.chainId,
50
+ };
51
+ return await wallet.wallet.signTransaction(tx);
52
+ }
53
+
54
+ /**
55
+ * Get hex-encoded serialized transaction
56
+ * @param {ECDSAWallet} wallet - ECDSA wallet
57
+ * @returns {Promise<string>} Serialized transaction (hex)
58
+ */
59
+ async getHex(wallet) {
60
+ return await this.sign(wallet);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Legacy PQC-only Transaction (Type 0 with PQC)
66
+ */
67
+ export class PQCLegacyTransaction extends BaseTransaction {
68
+ constructor(params) {
69
+ super(params);
70
+ this.type = TX_TYPE.LEGACY;
71
+ this.gasPrice = params.gasPrice || 0n;
72
+ this.gasLimit = params.gasLimit || 21000n;
73
+ this.pqcType = PQC_TYPE.DILITHIUM;
74
+ this.pqcPubKey = null;
75
+ this.pqcSig = null;
76
+ this.v = 0;
77
+ this.r = '0x0';
78
+ this.s = '0x0';
79
+ }
80
+
81
+ getSigningHash() {
82
+ const nonceBytes = this.nonce === 0n || this.nonce === 0
83
+ ? new Uint8Array(0)
84
+ : ethers.getBytes(ethers.toBeHex(this.nonce));
85
+ const gasLimitBytes = this.gasLimit === 0n || this.gasLimit === 0
86
+ ? new Uint8Array(0)
87
+ : ethers.getBytes(ethers.toBeHex(this.gasLimit));
88
+ const chainIdBytes = this.chainId === 0n || this.chainId === 0
89
+ ? new Uint8Array(0)
90
+ : ethers.getBytes(ethers.toBeHex(this.chainId));
91
+
92
+ const encoded = ethers.encodeRlp([
93
+ nonceBytes,
94
+ this.gasPrice === 0n ? new Uint8Array(0) : ethers.getBytes(ethers.toBeHex(this.gasPrice)),
95
+ gasLimitBytes,
96
+ this.to || '0x',
97
+ this.value === 0n ? new Uint8Array(0) : ethers.getBytes(ethers.toBeHex(this.value)),
98
+ this.data,
99
+ chainIdBytes,
100
+ new Uint8Array(0),
101
+ new Uint8Array(0),
102
+ ]);
103
+ return ethers.keccak256(encoded);
104
+ }
105
+
106
+ /**
107
+ * Sign with PQC wallet
108
+ * @param {PQCWallet} wallet - PQC wallet
109
+ * @returns {PQCLegacyTransaction} This transaction
110
+ */
111
+ sign(wallet) {
112
+ const hash = ethers.getBytes(this.getSigningHash());
113
+ this.pqcPubKey = wallet.publicKey;
114
+ const ctx = new TextEncoder().encode('ML-DSA-65');
115
+ this.pqcSig = ml_dsa65.sign(wallet.secretKey, hash, ctx);
116
+ this.v = Number(this.chainId) * 2 + 35;
117
+ this.r = '0x0';
118
+ this.s = '0x0';
119
+ return this;
120
+ }
121
+
122
+ verify() {
123
+ if (!this.pqcSig || !this.pqcPubKey) return false;
124
+ const hash = ethers.getBytes(this.getSigningHash());
125
+ const ctx = new TextEncoder().encode('ML-DSA-65');
126
+ return ml_dsa65.verify(this.pqcPubKey, hash, this.pqcSig, ctx);
127
+ }
128
+
129
+ serialize() {
130
+ if (!this.pqcSig) throw new Error('Transaction not signed');
131
+ if (!this.pqcPubKey || this.pqcPubKey.length === 0 || !this.pqcSig || this.pqcSig.length === 0) {
132
+ throw new Error('PQC fields are required');
133
+ }
134
+
135
+ const nonceHex = encodeUint64(this.nonce);
136
+ const gasPriceHex = encodeBigInt(this.gasPrice);
137
+ const gasLimitHex = encodeUint64(this.gasLimit);
138
+ const valueHex = encodeBigInt(this.value);
139
+ const vHex = encodeBigInt(this.v || 0);
140
+ const rHex = encodeSignature(this.r || '0x0');
141
+ const sHex = encodeSignature(this.s || '0x0');
142
+ const pqcTypeHex = encodeUint64(this.pqcType);
143
+
144
+ const encoded = ethers.encodeRlp([
145
+ nonceHex,
146
+ gasPriceHex,
147
+ gasLimitHex,
148
+ this.to || '0x',
149
+ valueHex,
150
+ this.data || '0x',
151
+ vHex,
152
+ rHex,
153
+ sHex,
154
+ pqcTypeHex,
155
+ this.pqcPubKey,
156
+ this.pqcSig,
157
+ ]);
158
+ return ethers.getBytes(encoded);
159
+ }
160
+
161
+ getHex() {
162
+ return ethers.hexlify(this.serialize());
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Legacy Hybrid Transaction (Type 0 with ECDSA + PQC)
168
+ */
169
+ export class HybridLegacyTransaction extends BaseTransaction {
170
+ constructor(params) {
171
+ super(params);
172
+ this.type = TX_TYPE.LEGACY;
173
+ this.gasPrice = params.gasPrice || 0n;
174
+ this.gasLimit = params.gasLimit || 21000n;
175
+ this.pqcType = PQC_TYPE.DILITHIUM;
176
+ this.pqcPubKey = null;
177
+ this.pqcSig = null;
178
+ this.v = null;
179
+ this.r = null;
180
+ this.s = null;
181
+ }
182
+
183
+ getSigningHash() {
184
+ const nonceBytes = this.nonce === 0n || this.nonce === 0
185
+ ? new Uint8Array(0)
186
+ : ethers.getBytes(ethers.toBeHex(this.nonce));
187
+ const gasLimitBytes = this.gasLimit === 0n || this.gasLimit === 0
188
+ ? new Uint8Array(0)
189
+ : ethers.getBytes(ethers.toBeHex(this.gasLimit));
190
+ const chainIdBytes = this.chainId === 0n || this.chainId === 0
191
+ ? new Uint8Array(0)
192
+ : ethers.getBytes(ethers.toBeHex(this.chainId));
193
+
194
+ const encoded = ethers.encodeRlp([
195
+ nonceBytes,
196
+ this.gasPrice === 0n ? new Uint8Array(0) : ethers.getBytes(ethers.toBeHex(this.gasPrice)),
197
+ gasLimitBytes,
198
+ this.to || '0x',
199
+ this.value === 0n ? new Uint8Array(0) : ethers.getBytes(ethers.toBeHex(this.value)),
200
+ this.data,
201
+ chainIdBytes,
202
+ new Uint8Array(0),
203
+ new Uint8Array(0),
204
+ ]);
205
+ return ethers.keccak256(encoded);
206
+ }
207
+
208
+ /**
209
+ * Sign with both ECDSA and PQC wallets
210
+ * @param {ECDSAWallet} ecdsaWallet - ECDSA wallet
211
+ * @param {PQCWallet} pqcWallet - PQC wallet
212
+ * @returns {HybridLegacyTransaction} This transaction
213
+ */
214
+ sign(ecdsaWallet, pqcWallet) {
215
+ const hash = this.getSigningHash();
216
+ const hashBytes = ethers.getBytes(hash);
217
+
218
+ // ECDSA signature
219
+ const ecdsaSig = ecdsaWallet.wallet.signingKey.sign(hash);
220
+ const recoveryId = ecdsaSig.v >= 27 ? ecdsaSig.v - 27 : ecdsaSig.v;
221
+ this.v = Number(this.chainId) * 2 + 35 + recoveryId;
222
+ this.r = ecdsaSig.r;
223
+ this.s = ecdsaSig.s;
224
+
225
+ // PQC signature
226
+ const ctx = new TextEncoder().encode('ML-DSA-65');
227
+ this.pqcPubKey = pqcWallet.publicKey;
228
+ this.pqcSig = ml_dsa65.sign(pqcWallet.secretKey, hashBytes, ctx);
229
+
230
+ return this;
231
+ }
232
+
233
+ verify() {
234
+ if (!this.pqcSig || !this.pqcPubKey) return false;
235
+ const hash = ethers.getBytes(this.getSigningHash());
236
+ const ctx = new TextEncoder().encode('ML-DSA-65');
237
+ return ml_dsa65.verify(this.pqcPubKey, hash, this.pqcSig, ctx);
238
+ }
239
+
240
+ serialize() {
241
+ if (!this.pqcSig || !this.r) throw new Error('Transaction not signed');
242
+ if (!this.pqcPubKey || this.pqcPubKey.length === 0 || !this.pqcSig || this.pqcSig.length === 0) {
243
+ throw new Error('PQC fields are required');
244
+ }
245
+
246
+ const nonceHex = encodeUint64(this.nonce);
247
+ const gasPriceHex = encodeBigInt(this.gasPrice);
248
+ const gasLimitHex = encodeUint64(this.gasLimit);
249
+ const valueHex = encodeBigInt(this.value);
250
+ const vHex = encodeBigInt(this.v);
251
+ const rHex = encodeSignature(this.r);
252
+ const sHex = encodeSignature(this.s);
253
+ const pqcTypeHex = encodeUint64(this.pqcType);
254
+
255
+ const encoded = ethers.encodeRlp([
256
+ nonceHex,
257
+ gasPriceHex,
258
+ gasLimitHex,
259
+ this.to || '0x',
260
+ valueHex,
261
+ this.data || '0x',
262
+ vHex,
263
+ rHex,
264
+ sHex,
265
+ pqcTypeHex,
266
+ this.pqcPubKey,
267
+ this.pqcSig,
268
+ ]);
269
+ return ethers.getBytes(encoded);
270
+ }
271
+
272
+ getHex() {
273
+ return ethers.hexlify(this.serialize());
274
+ }
275
+ }
276
+
277
+ /**
278
+ * AccessList Transaction (Type 1) - ECDSA
279
+ */
280
+ export class AccessListTransaction extends BaseTransaction {
281
+ constructor(params) {
282
+ super(params);
283
+ this.type = TX_TYPE.ACCESS_LIST;
284
+ this.gasPrice = params.gasPrice || 0n;
285
+ this.gasLimit = params.gasLimit || 21000n;
286
+ this.accessList = params.accessList || [];
287
+ }
288
+
289
+ async sign(wallet) {
290
+ const tx = {
291
+ type: 1,
292
+ nonce: this.nonce,
293
+ gasPrice: this.gasPrice,
294
+ gasLimit: this.gasLimit,
295
+ to: this.to,
296
+ value: this.value,
297
+ data: this.data,
298
+ accessList: this.accessList,
299
+ chainId: this.chainId,
300
+ };
301
+ return await wallet.wallet.signTransaction(tx);
302
+ }
303
+
304
+ async getHex(wallet) {
305
+ return await this.sign(wallet);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * AccessList PQC Transaction (Type 1 with PQC)
311
+ */
312
+ export class PQCAccessListTransaction extends BaseTransaction {
313
+ constructor(params) {
314
+ super(params);
315
+ this.type = TX_TYPE.ACCESS_LIST;
316
+ this.gasPrice = params.gasPrice || 0n;
317
+ this.gasLimit = params.gasLimit || 21000n;
318
+ this.accessList = params.accessList || [];
319
+ this.pqcType = PQC_TYPE.DILITHIUM;
320
+ this.pqcPubKey = null;
321
+ this.pqcSig = null;
322
+ this.v = 0;
323
+ this.r = '0x0';
324
+ this.s = '0x0';
325
+ }
326
+
327
+ getSigningHash() {
328
+ const chainIdHex = this.chainId === 0n || this.chainId === 0 ? '0x' : ethers.toBeHex(this.chainId);
329
+ const nonceHex = this.nonce === 0n || this.nonce === 0 ? '0x' : ethers.toBeHex(this.nonce);
330
+ const gasPriceHex = this.gasPrice === 0n || this.gasPrice === 0 ? '0x' : ethers.toBeHex(this.gasPrice);
331
+ const gasHex = this.gasLimit === 0n || this.gasLimit === 0 ? '0x' : ethers.toBeHex(this.gasLimit);
332
+ const valueHex = this.value === 0n || this.value === 0 ? '0x' : ethers.toBeHex(this.value);
333
+
334
+ const rlpData = [
335
+ chainIdHex,
336
+ nonceHex,
337
+ gasPriceHex,
338
+ gasHex,
339
+ this.to || null,
340
+ valueHex,
341
+ this.data || '0x',
342
+ this.accessList || [],
343
+ ];
344
+
345
+ const encodedHex = ethers.encodeRlp(rlpData);
346
+ const encoded = ethers.getBytes(encodedHex);
347
+
348
+ const typeByte = TX_TYPE.ACCESS_LIST;
349
+ const prefixed = new Uint8Array(1 + encoded.length);
350
+ prefixed[0] = typeByte;
351
+ prefixed.set(encoded, 1);
352
+
353
+ return ethers.keccak256(ethers.hexlify(prefixed));
354
+ }
355
+
356
+ sign(wallet) {
357
+ const hash = ethers.getBytes(this.getSigningHash());
358
+ this.pqcPubKey = wallet.publicKey;
359
+ const ctx = new TextEncoder().encode('ML-DSA-65');
360
+ this.pqcSig = ml_dsa65.sign(wallet.secretKey, hash, ctx);
361
+ return this;
362
+ }
363
+
364
+ verify() {
365
+ if (!this.pqcSig || !this.pqcPubKey) return false;
366
+ const hash = ethers.getBytes(this.getSigningHash());
367
+ const ctx = new TextEncoder().encode('ML-DSA-65');
368
+ return ml_dsa65.verify(this.pqcPubKey, hash, this.pqcSig, ctx);
369
+ }
370
+
371
+ serialize() {
372
+ if (!this.pqcSig) throw new Error('Transaction not signed');
373
+ if (!this.pqcPubKey || this.pqcPubKey.length === 0 || !this.pqcSig || this.pqcSig.length === 0) {
374
+ throw new Error('PQC fields are required');
375
+ }
376
+
377
+ const toAddress = this.to && this.to !== '0x' && this.to !== null ? this.to : null;
378
+ const chainIdHex = encodeBigInt(this.chainId);
379
+ const nonceHex = encodeUint64(this.nonce);
380
+ const gasPriceHex = encodeBigInt(this.gasPrice);
381
+ const gasLimitHex = encodeUint64(this.gasLimit);
382
+ const valueHex = encodeBigInt(this.value);
383
+ const vHex = encodeBigInt(this.v || 0);
384
+ const rHex = encodeSignature(this.r || '0x0');
385
+ const sHex = encodeSignature(this.s || '0x0');
386
+ const pqcTypeHex = encodeUint64(this.pqcType);
387
+
388
+ const encoded = ethers.encodeRlp([
389
+ chainIdHex,
390
+ nonceHex,
391
+ gasPriceHex,
392
+ gasLimitHex,
393
+ toAddress,
394
+ valueHex,
395
+ this.data || '0x',
396
+ this.accessList || [],
397
+ vHex,
398
+ rHex,
399
+ sHex,
400
+ pqcTypeHex,
401
+ this.pqcPubKey,
402
+ this.pqcSig,
403
+ ]);
404
+
405
+ const encodedBytes = ethers.getBytes(encoded);
406
+ const typeByte = new Uint8Array([TX_TYPE.ACCESS_LIST]);
407
+ const prefixed = new Uint8Array(typeByte.length + encodedBytes.length);
408
+ prefixed.set(typeByte, 0);
409
+ prefixed.set(encodedBytes, typeByte.length);
410
+
411
+ return prefixed;
412
+ }
413
+
414
+ getHex() {
415
+ return ethers.hexlify(this.serialize());
416
+ }
417
+ }
418
+
419
+ /**
420
+ * DynamicFee Transaction (Type 2) - ECDSA
421
+ */
422
+ export class DynamicFeeTransaction extends BaseTransaction {
423
+ constructor(params) {
424
+ super(params);
425
+ this.type = TX_TYPE.DYNAMIC_FEE;
426
+ this.maxPriorityFeePerGas = params.maxPriorityFeePerGas || 0n;
427
+ this.maxFeePerGas = params.maxFeePerGas || 0n;
428
+ this.gasLimit = params.gasLimit || 21000n;
429
+ this.accessList = params.accessList || [];
430
+ }
431
+
432
+ async sign(wallet) {
433
+ const tx = {
434
+ type: 2,
435
+ nonce: this.nonce,
436
+ maxPriorityFeePerGas: this.maxPriorityFeePerGas,
437
+ maxFeePerGas: this.maxFeePerGas,
438
+ gasLimit: this.gasLimit,
439
+ to: this.to,
440
+ value: this.value,
441
+ data: this.data,
442
+ accessList: this.accessList,
443
+ chainId: this.chainId,
444
+ };
445
+ return await wallet.wallet.signTransaction(tx);
446
+ }
447
+
448
+ async getHex(wallet) {
449
+ return await this.sign(wallet);
450
+ }
451
+ }
452
+
453
+ /**
454
+ * DynamicFee PQC Transaction (Type 2 with PQC)
455
+ */
456
+ export class PQCDynamicFeeTransaction extends BaseTransaction {
457
+ constructor(params) {
458
+ super(params);
459
+ this.type = TX_TYPE.DYNAMIC_FEE;
460
+ this.maxPriorityFeePerGas = params.maxPriorityFeePerGas || 0n;
461
+ this.maxFeePerGas = params.maxFeePerGas || 0n;
462
+ this.gasLimit = params.gasLimit || 21000n;
463
+ this.accessList = params.accessList || [];
464
+ this.pqcType = PQC_TYPE.DILITHIUM;
465
+ this.pqcPubKey = null;
466
+ this.pqcSig = null;
467
+ this.v = 0;
468
+ this.r = '0x0';
469
+ this.s = '0x0';
470
+ }
471
+
472
+ getSigningHash() {
473
+ const chainIdHex = this.chainId === 0n || this.chainId === 0 ? '0x' : ethers.toBeHex(this.chainId);
474
+ const nonceHex = this.nonce === 0n || this.nonce === 0 ? '0x' : ethers.toBeHex(this.nonce);
475
+ const gasTipCapHex = this.maxPriorityFeePerGas === 0n || this.maxPriorityFeePerGas === 0 ? '0x' : ethers.toBeHex(this.maxPriorityFeePerGas);
476
+ const gasFeeCapHex = this.maxFeePerGas === 0n || this.maxFeePerGas === 0 ? '0x' : ethers.toBeHex(this.maxFeePerGas);
477
+ const gasHex = this.gasLimit === 0n || this.gasLimit === 0 ? '0x' : ethers.toBeHex(this.gasLimit);
478
+ const valueHex = this.value === 0n || this.value === 0 ? '0x' : ethers.toBeHex(this.value);
479
+
480
+ const rlpData = [
481
+ chainIdHex,
482
+ nonceHex,
483
+ gasTipCapHex,
484
+ gasFeeCapHex,
485
+ gasHex,
486
+ this.to || null,
487
+ valueHex,
488
+ this.data || '0x',
489
+ this.accessList || [],
490
+ ];
491
+
492
+ const encodedHex = ethers.encodeRlp(rlpData);
493
+ const encoded = ethers.getBytes(encodedHex);
494
+
495
+ const typeByte = TX_TYPE.DYNAMIC_FEE;
496
+ const prefixed = new Uint8Array(1 + encoded.length);
497
+ prefixed[0] = typeByte;
498
+ prefixed.set(encoded, 1);
499
+
500
+ return ethers.keccak256(ethers.hexlify(prefixed));
501
+ }
502
+
503
+ sign(wallet) {
504
+ const hash = ethers.getBytes(this.getSigningHash());
505
+ this.pqcPubKey = wallet.publicKey;
506
+ const ctx = new TextEncoder().encode('ML-DSA-65');
507
+ this.pqcSig = ml_dsa65.sign(wallet.secretKey, hash, ctx);
508
+ return this;
509
+ }
510
+
511
+ verify() {
512
+ if (!this.pqcSig || !this.pqcPubKey) return false;
513
+ const hash = ethers.getBytes(this.getSigningHash());
514
+ const ctx = new TextEncoder().encode('ML-DSA-65');
515
+ return ml_dsa65.verify(this.pqcPubKey, hash, this.pqcSig, ctx);
516
+ }
517
+
518
+ serialize() {
519
+ if (!this.pqcSig) throw new Error('Transaction not signed');
520
+ if (!this.pqcPubKey || this.pqcPubKey.length === 0 || !this.pqcSig || this.pqcSig.length === 0) {
521
+ throw new Error('PQC fields are required');
522
+ }
523
+
524
+ const toAddress = this.to && this.to !== '0x' && this.to !== null ? this.to : null;
525
+ const chainIdHex = encodeBigInt(this.chainId);
526
+ const nonceHex = encodeUint64(this.nonce);
527
+ const gasTipCapHex = encodeBigInt(this.maxPriorityFeePerGas);
528
+ const gasFeeCapHex = encodeBigInt(this.maxFeePerGas);
529
+ const gasLimitHex = encodeUint64(this.gasLimit);
530
+ const valueHex = encodeBigInt(this.value);
531
+ const vHex = encodeBigInt(this.v || 0);
532
+ const rHex = encodeSignature(this.r || '0x0');
533
+ const sHex = encodeSignature(this.s || '0x0');
534
+ const pqcTypeHex = encodeUint64(this.pqcType);
535
+
536
+ const encoded = ethers.encodeRlp([
537
+ chainIdHex,
538
+ nonceHex,
539
+ gasTipCapHex,
540
+ gasFeeCapHex,
541
+ gasLimitHex,
542
+ toAddress,
543
+ valueHex,
544
+ this.data || '0x',
545
+ this.accessList || [],
546
+ vHex,
547
+ rHex,
548
+ sHex,
549
+ pqcTypeHex,
550
+ this.pqcPubKey,
551
+ this.pqcSig,
552
+ ]);
553
+
554
+ const encodedBytes = ethers.getBytes(encoded);
555
+ const typeByte = new Uint8Array([TX_TYPE.DYNAMIC_FEE]);
556
+ const prefixed = new Uint8Array(typeByte.length + encodedBytes.length);
557
+ prefixed.set(typeByte, 0);
558
+ prefixed.set(encodedBytes, typeByte.length);
559
+
560
+ return prefixed;
561
+ }
562
+
563
+ getHex() {
564
+ return ethers.hexlify(this.serialize());
565
+ }
566
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Address Utilities
3
+ * Handles address derivation for PQC and Hybrid addresses
4
+ */
5
+
6
+ import { ethers } from 'ethers';
7
+
8
+ /**
9
+ * Derive PQC address from Dilithium public key
10
+ * @param {Uint8Array} publicKey - Dilithium public key
11
+ * @returns {string} Ethereum address
12
+ */
13
+ export function derivePQCAddress(publicKey) {
14
+ const hash = ethers.keccak256(publicKey);
15
+ return ethers.getAddress('0x' + hash.slice(-40));
16
+ }
17
+
18
+ /**
19
+ * Derive Hybrid address from ECDSA and Dilithium public keys
20
+ * @param {Uint8Array} ecdsaPublicKey - ECDSA public key (65 bytes, remove 0x04 prefix)
21
+ * @param {Uint8Array} dilithiumPublicKey - Dilithium public key
22
+ * @returns {string} Hybrid Ethereum address
23
+ */
24
+ export function deriveHybridAddress(ecdsaPublicKey, dilithiumPublicKey) {
25
+ // Remove 0x04 prefix from ECDSA public key if present
26
+ let ecdsaBytes = ecdsaPublicKey;
27
+ if (ecdsaBytes.length === 65 && ecdsaBytes[0] === 4) {
28
+ ecdsaBytes = ecdsaBytes.slice(1); // 64 bytes
29
+ }
30
+
31
+ // Concatenate ECDSA (64 bytes) + Dilithium public key
32
+ const concat = new Uint8Array(ecdsaBytes.length + dilithiumPublicKey.length);
33
+ concat.set(ecdsaBytes, 0);
34
+ concat.set(dilithiumPublicKey, ecdsaBytes.length);
35
+
36
+ // Hash and derive address
37
+ const hash = ethers.keccak256(concat);
38
+ return ethers.getAddress('0x' + hash.slice(-40));
39
+ }
40
+
41
+ /**
42
+ * Validate Ethereum address format
43
+ * @param {string} address - Address to validate
44
+ * @returns {boolean}
45
+ */
46
+ export function isValidAddress(address) {
47
+ return ethers.utils.isAddress(address);
48
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * RLP Encoding Utilities
3
+ * Handles proper encoding for Go RLP compatibility
4
+ */
5
+
6
+ import { ethers } from 'ethers';
7
+
8
+ /**
9
+ * Encode uint64 value for RLP as hex string
10
+ * In Go RLP, uint64(0) encodes as empty bytes, non-zero as canonical bytes
11
+ */
12
+ export function encodeUint64(value) {
13
+ if (value === 0 || value === 0n || value === null || value === undefined) {
14
+ return '0x';
15
+ }
16
+ const bigValue = typeof value === 'bigint' ? value : BigInt(value);
17
+ return ethers.toBeHex(bigValue);
18
+ }
19
+
20
+ /**
21
+ * Encode *big.Int value for RLP as hex string
22
+ * Go RLP encodes *big.Int(0) as 0x80 (empty bytes), NOT 0x00!
23
+ */
24
+ export function encodeBigInt(value) {
25
+ if (value === 0 || value === 0n || value === null || value === undefined) {
26
+ return '0x';
27
+ }
28
+ const bigValue = typeof value === 'bigint' ? value : BigInt(value);
29
+ return ethers.toBeHex(bigValue);
30
+ }
31
+
32
+ /**
33
+ * Encode signature value (R, S) for RLP as hex string
34
+ * Removes leading zeros for canonical form (required for *big.Int in Go RLP)
35
+ */
36
+ export function encodeSignature(value) {
37
+ if (!value || value === '0x0' || value === '0x' || value === '0x00') {
38
+ return '0x';
39
+ }
40
+
41
+ const hex = value.startsWith('0x') ? value : '0x' + value;
42
+ let bytes = ethers.getBytes(hex);
43
+
44
+ // Remove leading zeros for canonical form
45
+ let startIdx = 0;
46
+ while (startIdx < bytes.length && bytes[startIdx] === 0) {
47
+ startIdx++;
48
+ }
49
+
50
+ if (startIdx === bytes.length) {
51
+ return '0x';
52
+ }
53
+
54
+ bytes = bytes.slice(startIdx);
55
+ return ethers.hexlify(bytes);
56
+ }