@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.
- package/README.md +293 -0
- package/package.json +32 -0
- package/src/constants.js +31 -0
- package/src/contract.js +207 -0
- package/src/index.js +37 -0
- package/src/provider.js +95 -0
- package/src/transaction.js +566 -0
- package/src/utils/address.js +48 -0
- package/src/utils/rlp.js +56 -0
- package/src/wallet.js +158 -0
- package/src/zk-transaction.js +95 -0
|
@@ -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
|
+
}
|
package/src/utils/rlp.js
ADDED
|
@@ -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
|
+
}
|