@tapforce/pod-bridge-sdk 1.2.1 → 1.2.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/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@ export { PodToSourceChainActionClient } from './clients/action/pod-to-source-cha
2
2
  export { SourceChainToPodActionClient } from './clients/action/source-chain-to-pod-client';
3
3
  export { PodBridgeTrackerClient } from './clients/tracker/client';
4
4
  export { BRIDGE_ABI, POD_BRIDGE_ABI, SOURCE_CHAIN_BRIDGE_ABI, } from './libs/abi/bridge.abi';
5
- export { recoverSignature65B, recoverAggregatedSignatures65B, computeDepositTxHash, extractAggregatedSignatures, extractAttestationInfo, addressFromPublicKey, } from './libs/helpers/signature-recovery.helper';
5
+ export { recoverSignature65B, recoverAggregatedSignatures65B, computeDepositTxHash, extractAggregatedSignatures, extractSignatureInfo, addressFromPublicKey, parseDerSignature, recoverSignatureWithoutPubkey, } from './libs/helpers/signature-recovery.helper';
6
6
  export { PodBridgeConfig, BridgeRequest, BridgeRequestWithType, BridgeChain, DepositType, UnsignedTransaction, ClaimProofData, PodBridgeActionsClientConfig, PodBridgeChainConfig, } from './libs/types/pod-bridge.types';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DepositType = exports.BridgeChain = exports.addressFromPublicKey = exports.extractAttestationInfo = exports.extractAggregatedSignatures = exports.computeDepositTxHash = exports.recoverAggregatedSignatures65B = exports.recoverSignature65B = exports.SOURCE_CHAIN_BRIDGE_ABI = exports.POD_BRIDGE_ABI = exports.BRIDGE_ABI = exports.PodBridgeTrackerClient = exports.SourceChainToPodActionClient = exports.PodToSourceChainActionClient = void 0;
3
+ exports.DepositType = exports.BridgeChain = exports.recoverSignatureWithoutPubkey = exports.parseDerSignature = exports.addressFromPublicKey = exports.extractSignatureInfo = exports.extractAggregatedSignatures = exports.computeDepositTxHash = exports.recoverAggregatedSignatures65B = exports.recoverSignature65B = exports.SOURCE_CHAIN_BRIDGE_ABI = exports.POD_BRIDGE_ABI = exports.BRIDGE_ABI = exports.PodBridgeTrackerClient = exports.SourceChainToPodActionClient = exports.PodToSourceChainActionClient = void 0;
4
4
  var pod_to_source_chain_client_1 = require("./clients/action/pod-to-source-chain-client");
5
5
  Object.defineProperty(exports, "PodToSourceChainActionClient", { enumerable: true, get: function () { return pod_to_source_chain_client_1.PodToSourceChainActionClient; } });
6
6
  var source_chain_to_pod_client_1 = require("./clients/action/source-chain-to-pod-client");
@@ -16,8 +16,10 @@ Object.defineProperty(exports, "recoverSignature65B", { enumerable: true, get: f
16
16
  Object.defineProperty(exports, "recoverAggregatedSignatures65B", { enumerable: true, get: function () { return signature_recovery_helper_1.recoverAggregatedSignatures65B; } });
17
17
  Object.defineProperty(exports, "computeDepositTxHash", { enumerable: true, get: function () { return signature_recovery_helper_1.computeDepositTxHash; } });
18
18
  Object.defineProperty(exports, "extractAggregatedSignatures", { enumerable: true, get: function () { return signature_recovery_helper_1.extractAggregatedSignatures; } });
19
- Object.defineProperty(exports, "extractAttestationInfo", { enumerable: true, get: function () { return signature_recovery_helper_1.extractAttestationInfo; } });
19
+ Object.defineProperty(exports, "extractSignatureInfo", { enumerable: true, get: function () { return signature_recovery_helper_1.extractSignatureInfo; } });
20
20
  Object.defineProperty(exports, "addressFromPublicKey", { enumerable: true, get: function () { return signature_recovery_helper_1.addressFromPublicKey; } });
21
+ Object.defineProperty(exports, "parseDerSignature", { enumerable: true, get: function () { return signature_recovery_helper_1.parseDerSignature; } });
22
+ Object.defineProperty(exports, "recoverSignatureWithoutPubkey", { enumerable: true, get: function () { return signature_recovery_helper_1.recoverSignatureWithoutPubkey; } });
21
23
  var pod_bridge_types_1 = require("./libs/types/pod-bridge.types");
22
24
  Object.defineProperty(exports, "BridgeChain", { enumerable: true, get: function () { return pod_bridge_types_1.BridgeChain; } });
23
25
  Object.defineProperty(exports, "DepositType", { enumerable: true, get: function () { return pod_bridge_types_1.DepositType; } });
@@ -1,4 +1,27 @@
1
1
  import { PodTransactionReceipt } from '../pod-sdk/src/types/responses';
2
+ /**
3
+ * Parse a DER-encoded ECDSA signature to extract r and s components.
4
+ *
5
+ * DER format:
6
+ * 0x30 [total-length] 0x02 [r-length] [r] 0x02 [s-length] [s]
7
+ *
8
+ * Example: 3044022020749ca9...0220292484ed...
9
+ * - 30 = SEQUENCE tag
10
+ * - 44 = total length (68 bytes)
11
+ * - 02 = INTEGER tag for r
12
+ * - 20 = r length (32 bytes)
13
+ * - [32 bytes of r]
14
+ * - 02 = INTEGER tag for s
15
+ * - 20 = s length (32 bytes)
16
+ * - [32 bytes of s]
17
+ *
18
+ * @param derSignature The DER-encoded signature as hex string
19
+ * @returns Object with r and s as 32-byte hex strings (0x prefixed)
20
+ */
21
+ export declare function parseDerSignature(derSignature: string): {
22
+ r: string;
23
+ s: string;
24
+ };
2
25
  /**
3
26
  * Derive Ethereum address from a public key.
4
27
  *
@@ -65,27 +88,74 @@ export declare function recoverAggregatedSignatures65B(aggregatedSig64: string |
65
88
  */
66
89
  export declare function computeDepositTxHash(domainSeparator: string, bridgeContract: string, token: string, amount: bigint | string, to: string, proof: string): string;
67
90
  /**
68
- * Extract aggregated 65-byte signatures from Pod transaction receipt attestations.
91
+ * Extract aggregated 65-byte signatures from Pod transaction receipt.
69
92
  *
70
- * Exactly matches the Rust implementation - recovers the v (parity bit) by trying
71
- * both recovery IDs and checking which one recovers to the correct public key.
93
+ * Receipt format:
94
+ * - attested_tx.hash: The transaction hash that was signed
95
+ * - signatures: Object with numeric keys containing DER-encoded signatures
72
96
  *
73
97
  * @param receipt The Pod transaction receipt
74
- * @param msgHash The message hash that validators signed (transaction hash)
75
- * @returns Concatenated 65-byte signatures (r || s || v for each attestation)
98
+ * @param msgHash Optional override for the message hash (defaults to attested_tx.hash)
99
+ * @param committeePublicKeys Optional array of validator public keys for v-value recovery
100
+ * @returns Concatenated 65-byte signatures (r || s || v for each signature)
76
101
  */
77
- export declare function extractAggregatedSignatures(receipt: PodTransactionReceipt, msgHash?: string): string;
102
+ export declare function extractAggregatedSignatures(receipt: PodTransactionReceipt, msgHash?: string, committeePublicKeys?: string[]): string;
78
103
  /**
79
- * Extract attestation data from Pod receipt for debugging/verification.
80
- * Uses recoverSignature65B to properly recover v value.
104
+ * Recover a 65-byte signature without knowing the public key.
105
+ * Tries both v values (27 and 28) and returns the first valid recovery.
81
106
  *
82
- * @param receipt The Pod transaction receipt
83
- * @param msgHash The message hash that validators signed
84
- * @returns Array of attestation info with public keys and recovered signatures
107
+ * WARNING: This is less secure as we can't verify the signer.
108
+ * Use recoverSignature65B with public key when possible.
109
+ *
110
+ * @param r The r component
111
+ * @param s The s component
112
+ * @param msgHash The message hash
113
+ * @returns The 65-byte signature with v=27 (tries 27 first, then 28)
85
114
  */
86
- export declare function extractAttestationInfo(receipt: PodTransactionReceipt, msgHash?: string): Array<{
87
- publicKey: string;
88
- address: string;
115
+ export declare function recoverSignatureWithoutPubkey(r: string, s: string, msgHash: string): string;
116
+ /**
117
+ * Recover a 65-byte signature by checking which v-value recovers to a known validator address.
118
+ *
119
+ * @param r The r component
120
+ * @param s The s component
121
+ * @param msgHash The message hash
122
+ * @param validatorAddresses Set of known validator addresses (lowercase)
123
+ * @returns Object with signature and recovered address, or null if no validator match
124
+ */
125
+ export declare function recoverSignatureWithValidators(r: string, s: string, msgHash: string, validatorAddresses: Set<string>): {
89
126
  signature: string;
90
- timestamp: number;
127
+ recoveredAddress: string;
128
+ } | null;
129
+ /**
130
+ * Decompress a compressed secp256k1 public key (33 bytes) to uncompressed format.
131
+ * Compressed format: 02/03 prefix + 32-byte x coordinate
132
+ * Uncompressed format: 04 prefix + 32-byte x + 32-byte y
133
+ *
134
+ * @param compressedPubKey The compressed public key (66 hex chars with 02/03 prefix)
135
+ * @returns The uncompressed public key (130 hex chars with 04 prefix)
136
+ */
137
+ export declare function decompressPublicKey(compressedPubKey: string): string;
138
+ /**
139
+ * Extract aggregated 65-byte signatures using validator public keys for v-value recovery.
140
+ * Committee format: Array of [index, compressed_public_key] tuples
141
+ *
142
+ * @param receipt The Pod transaction receipt
143
+ * @param validators Array of [index, compressedPubKey] tuples from pod_getCommittee
144
+ * @param msgHash Optional override for the message hash
145
+ * @returns Concatenated 65-byte signatures (r || s || v for each signature)
146
+ */
147
+ export declare function extractAggregatedSignaturesWithValidators(receipt: PodTransactionReceipt, validators: [number, string][], msgHash?: string): string;
148
+ /**
149
+ * Extract signature info from Pod receipt for debugging/verification.
150
+ *
151
+ * @param receipt The Pod transaction receipt
152
+ * @param msgHash Optional override for the message hash
153
+ * @returns Array of signature info with recovered 65-byte signatures
154
+ */
155
+ export declare function extractSignatureInfo(receipt: PodTransactionReceipt, msgHash?: string): Array<{
156
+ index: number;
157
+ derSignature: string;
158
+ r: string;
159
+ s: string;
160
+ signature65: string;
91
161
  }>;
@@ -1,12 +1,78 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseDerSignature = parseDerSignature;
3
4
  exports.addressFromPublicKey = addressFromPublicKey;
4
5
  exports.recoverSignature65B = recoverSignature65B;
5
6
  exports.recoverAggregatedSignatures65B = recoverAggregatedSignatures65B;
6
7
  exports.computeDepositTxHash = computeDepositTxHash;
7
8
  exports.extractAggregatedSignatures = extractAggregatedSignatures;
8
- exports.extractAttestationInfo = extractAttestationInfo;
9
+ exports.recoverSignatureWithoutPubkey = recoverSignatureWithoutPubkey;
10
+ exports.recoverSignatureWithValidators = recoverSignatureWithValidators;
11
+ exports.decompressPublicKey = decompressPublicKey;
12
+ exports.extractAggregatedSignaturesWithValidators = extractAggregatedSignaturesWithValidators;
13
+ exports.extractSignatureInfo = extractSignatureInfo;
9
14
  const ethers_1 = require("ethers");
15
+ /**
16
+ * Parse a DER-encoded ECDSA signature to extract r and s components.
17
+ *
18
+ * DER format:
19
+ * 0x30 [total-length] 0x02 [r-length] [r] 0x02 [s-length] [s]
20
+ *
21
+ * Example: 3044022020749ca9...0220292484ed...
22
+ * - 30 = SEQUENCE tag
23
+ * - 44 = total length (68 bytes)
24
+ * - 02 = INTEGER tag for r
25
+ * - 20 = r length (32 bytes)
26
+ * - [32 bytes of r]
27
+ * - 02 = INTEGER tag for s
28
+ * - 20 = s length (32 bytes)
29
+ * - [32 bytes of s]
30
+ *
31
+ * @param derSignature The DER-encoded signature as hex string
32
+ * @returns Object with r and s as 32-byte hex strings (0x prefixed)
33
+ */
34
+ function parseDerSignature(derSignature) {
35
+ const hex = derSignature.replace('0x', '');
36
+ const bytes = Uint8Array.from(Buffer.from(hex, 'hex'));
37
+ let offset = 0;
38
+ // Check SEQUENCE tag (0x30)
39
+ if (bytes[offset++] !== 0x30) {
40
+ throw new Error('Invalid DER signature: expected SEQUENCE tag (0x30)');
41
+ }
42
+ // Skip total length
43
+ const totalLength = bytes[offset++];
44
+ if (totalLength > 127) {
45
+ // Long form length (shouldn't happen for ECDSA sigs)
46
+ throw new Error('Invalid DER signature: unexpected long form length');
47
+ }
48
+ // Parse r
49
+ if (bytes[offset++] !== 0x02) {
50
+ throw new Error('Invalid DER signature: expected INTEGER tag (0x02) for r');
51
+ }
52
+ let rLength = bytes[offset++];
53
+ let rStart = offset;
54
+ offset += rLength;
55
+ // Parse s
56
+ if (bytes[offset++] !== 0x02) {
57
+ throw new Error('Invalid DER signature: expected INTEGER tag (0x02) for s');
58
+ }
59
+ let sLength = bytes[offset++];
60
+ let sStart = offset;
61
+ // Extract r and s, handling potential leading zero padding
62
+ let rBytes = bytes.slice(rStart, rStart + rLength);
63
+ let sBytes = bytes.slice(sStart, sStart + sLength);
64
+ // Remove leading zero if present (DER adds 0x00 prefix for values with high bit set)
65
+ if (rBytes.length === 33 && rBytes[0] === 0) {
66
+ rBytes = rBytes.slice(1);
67
+ }
68
+ if (sBytes.length === 33 && sBytes[0] === 0) {
69
+ sBytes = sBytes.slice(1);
70
+ }
71
+ // Pad to 32 bytes if necessary
72
+ const r = Buffer.from(rBytes).toString('hex').padStart(64, '0');
73
+ const s = Buffer.from(sBytes).toString('hex').padStart(64, '0');
74
+ return { r: '0x' + r, s: '0x' + s };
75
+ }
10
76
  /**
11
77
  * Derive Ethereum address from a public key.
12
78
  *
@@ -148,71 +214,234 @@ function computeDepositTxHash(domainSeparator, bridgeContract, token, amount, to
148
214
  // Compute dataHash = keccak256(selector || token || amount || to)
149
215
  const dataHash = solidityPackedKeccak256(['bytes4', 'address', 'uint256', 'address'], [DEPOSIT_SELECTOR, token, amount, to]);
150
216
  // Compute final hash = keccak256(domainSeparator || bridgeContract || dataHash || proof)
151
- const txHash = (0, ethers_1.keccak256)(abiCoder.encode(['bytes32', 'address', 'bytes32', 'bytes'], [domainSeparator, bridgeContract, dataHash, proof]));
152
- return txHash;
217
+ return (0, ethers_1.keccak256)(abiCoder.encode(['bytes32', 'address', 'bytes32', 'bytes'], [domainSeparator, bridgeContract, dataHash, proof]));
153
218
  }
154
219
  /**
155
- * Extract aggregated 65-byte signatures from Pod transaction receipt attestations.
220
+ * Extract aggregated 65-byte signatures from Pod transaction receipt.
156
221
  *
157
- * Exactly matches the Rust implementation - recovers the v (parity bit) by trying
158
- * both recovery IDs and checking which one recovers to the correct public key.
222
+ * Receipt format:
223
+ * - attested_tx.hash: The transaction hash that was signed
224
+ * - signatures: Object with numeric keys containing DER-encoded signatures
159
225
  *
160
226
  * @param receipt The Pod transaction receipt
161
- * @param msgHash The message hash that validators signed (transaction hash)
162
- * @returns Concatenated 65-byte signatures (r || s || v for each attestation)
227
+ * @param msgHash Optional override for the message hash (defaults to attested_tx.hash)
228
+ * @param committeePublicKeys Optional array of validator public keys for v-value recovery
229
+ * @returns Concatenated 65-byte signatures (r || s || v for each signature)
163
230
  */
164
- function extractAggregatedSignatures(receipt, msgHash) {
165
- const attestations = receipt.pod_metadata?.attestations;
166
- if (!attestations || attestations.length === 0) {
167
- throw new Error('No attestations found in receipt');
231
+ function extractAggregatedSignatures(receipt, msgHash, committeePublicKeys) {
232
+ // Use attested_tx.hash as the message that was signed
233
+ const hashToSign = msgHash || receipt.attested_tx.hash;
234
+ // Get signatures in order (0, 1, 2, ...)
235
+ const sigKeys = Object.keys(receipt.signatures).sort((a, b) => parseInt(a) - parseInt(b));
236
+ if (sigKeys.length === 0) {
237
+ throw new Error('No signatures found in receipt');
168
238
  }
169
- // Use provided msgHash or fall back to transaction hash
170
- const hashToSign = msgHash || receipt.transactionHash;
171
239
  const signatures = [];
172
- for (const attestation of attestations) {
173
- const { r, s } = attestation.signature;
174
- const publicKey = attestation.public_key;
175
- // Use recoverSignature65B to find correct v value - exactly like Rust code
176
- const sig65 = recoverSignature65B(r, s, hashToSign, publicKey);
177
- if (!sig65) {
178
- throw new Error(`Failed to recover signature for public key ${publicKey}`);
240
+ for (let i = 0; i < sigKeys.length; i++) {
241
+ const derSig = receipt.signatures[sigKeys[i]];
242
+ const { r, s } = parseDerSignature(derSig);
243
+ if (committeePublicKeys && committeePublicKeys[i]) {
244
+ // Use public key to recover correct v value
245
+ const sig65 = recoverSignature65B(r, s, hashToSign, committeePublicKeys[i]);
246
+ if (!sig65) {
247
+ throw new Error(`Failed to recover signature ${i} for public key ${committeePublicKeys[i]}`);
248
+ }
249
+ signatures.push(sig65.replace('0x', ''));
250
+ }
251
+ else {
252
+ // No public key available - try both v values and use the first valid one
253
+ const sig65 = recoverSignatureWithoutPubkey(r, s, hashToSign);
254
+ signatures.push(sig65.replace('0x', ''));
179
255
  }
180
- signatures.push(sig65.replace('0x', ''));
181
256
  }
182
257
  return '0x' + signatures.join('');
183
258
  }
184
259
  /**
185
- * Extract attestation data from Pod receipt for debugging/verification.
186
- * Uses recoverSignature65B to properly recover v value.
260
+ * Recover a 65-byte signature without knowing the public key.
261
+ * Tries both v values (27 and 28) and returns the first valid recovery.
262
+ *
263
+ * WARNING: This is less secure as we can't verify the signer.
264
+ * Use recoverSignature65B with public key when possible.
265
+ *
266
+ * @param r The r component
267
+ * @param s The s component
268
+ * @param msgHash The message hash
269
+ * @returns The 65-byte signature with v=27 (tries 27 first, then 28)
270
+ */
271
+ function recoverSignatureWithoutPubkey(r, s, msgHash) {
272
+ const rHex = r.replace('0x', '').padStart(64, '0');
273
+ const sHex = s.replace('0x', '').padStart(64, '0');
274
+ // Try v=27 first, then v=28
275
+ for (const v of [27, 28]) {
276
+ try {
277
+ const signature = { r: '0x' + rHex, s: '0x' + sHex, v };
278
+ const recoveredAddress = (0, ethers_1.recoverAddress)(msgHash, signature);
279
+ // If recovery succeeds, use this v value
280
+ if (recoveredAddress) {
281
+ const vHex = v.toString(16).padStart(2, '0');
282
+ return '0x' + rHex + sHex + vHex;
283
+ }
284
+ }
285
+ catch (e) {
286
+ continue;
287
+ }
288
+ }
289
+ // Default to v=27 if both fail
290
+ return '0x' + rHex + sHex + '1b';
291
+ }
292
+ /**
293
+ * Recover a 65-byte signature by checking which v-value recovers to a known validator address.
294
+ *
295
+ * @param r The r component
296
+ * @param s The s component
297
+ * @param msgHash The message hash
298
+ * @param validatorAddresses Set of known validator addresses (lowercase)
299
+ * @returns Object with signature and recovered address, or null if no validator match
300
+ */
301
+ function recoverSignatureWithValidators(r, s, msgHash, validatorAddresses) {
302
+ const rHex = r.replace('0x', '').padStart(64, '0');
303
+ const sHex = s.replace('0x', '').padStart(64, '0');
304
+ // Try both v values and check if recovered address is a known validator
305
+ for (const v of [27, 28]) {
306
+ try {
307
+ const signature = { r: '0x' + rHex, s: '0x' + sHex, v };
308
+ const recoveredAddress = (0, ethers_1.recoverAddress)(msgHash, signature);
309
+ if (recoveredAddress && validatorAddresses.has(recoveredAddress.toLowerCase())) {
310
+ const vHex = v.toString(16).padStart(2, '0');
311
+ return {
312
+ signature: '0x' + rHex + sHex + vHex,
313
+ recoveredAddress: recoveredAddress.toLowerCase()
314
+ };
315
+ }
316
+ }
317
+ catch (e) {
318
+ continue;
319
+ }
320
+ }
321
+ return null;
322
+ }
323
+ /**
324
+ * Decompress a compressed secp256k1 public key (33 bytes) to uncompressed format.
325
+ * Compressed format: 02/03 prefix + 32-byte x coordinate
326
+ * Uncompressed format: 04 prefix + 32-byte x + 32-byte y
327
+ *
328
+ * @param compressedPubKey The compressed public key (66 hex chars with 02/03 prefix)
329
+ * @returns The uncompressed public key (130 hex chars with 04 prefix)
330
+ */
331
+ function decompressPublicKey(compressedPubKey) {
332
+ const hex = compressedPubKey.replace('0x', '');
333
+ if (hex.length !== 66) {
334
+ throw new Error(`Invalid compressed public key length: ${hex.length}. Expected 66 hex chars.`);
335
+ }
336
+ const prefix = hex.slice(0, 2);
337
+ if (prefix !== '02' && prefix !== '03') {
338
+ throw new Error(`Invalid compressed public key prefix: ${prefix}. Expected 02 or 03.`);
339
+ }
340
+ // secp256k1 curve parameters
341
+ const p = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F');
342
+ const x = BigInt('0x' + hex.slice(2));
343
+ // y² = x³ + 7 (mod p)
344
+ const ySquared = (x ** 3n + 7n) % p;
345
+ // Calculate modular square root using Tonelli-Shanks for p ≡ 3 (mod 4)
346
+ // y = ySquared^((p+1)/4) mod p
347
+ const y = modPow(ySquared, (p + 1n) / 4n, p);
348
+ // Check if we need to negate y based on the prefix
349
+ const isYEven = y % 2n === 0n;
350
+ const needsEvenY = prefix === '02';
351
+ const finalY = isYEven === needsEvenY ? y : p - y;
352
+ // Convert to hex, pad to 64 chars
353
+ const xHex = x.toString(16).padStart(64, '0');
354
+ const yHex = finalY.toString(16).padStart(64, '0');
355
+ return '04' + xHex + yHex;
356
+ }
357
+ /**
358
+ * Modular exponentiation: base^exp mod mod
359
+ */
360
+ function modPow(base, exp, mod) {
361
+ let result = 1n;
362
+ base = base % mod;
363
+ while (exp > 0n) {
364
+ if (exp % 2n === 1n) {
365
+ result = (result * base) % mod;
366
+ }
367
+ exp = exp / 2n;
368
+ base = (base * base) % mod;
369
+ }
370
+ return result;
371
+ }
372
+ /**
373
+ * Extract aggregated 65-byte signatures using validator public keys for v-value recovery.
374
+ * Committee format: Array of [index, compressed_public_key] tuples
187
375
  *
188
376
  * @param receipt The Pod transaction receipt
189
- * @param msgHash The message hash that validators signed
190
- * @returns Array of attestation info with public keys and recovered signatures
377
+ * @param validators Array of [index, compressedPubKey] tuples from pod_getCommittee
378
+ * @param msgHash Optional override for the message hash
379
+ * @returns Concatenated 65-byte signatures (r || s || v for each signature)
191
380
  */
192
- function extractAttestationInfo(receipt, msgHash) {
193
- const attestations = receipt.pod_metadata?.attestations;
194
- if (!attestations || attestations.length === 0) {
195
- return [];
196
- }
197
- const hashToSign = msgHash || receipt.transactionHash;
198
- return attestations.map(att => {
199
- const { r, s } = att.signature;
200
- const publicKey = att.public_key;
201
- // Recover signature with correct v value
202
- const sig65 = recoverSignature65B(r, s, hashToSign, publicKey);
203
- // Derive address from public key
204
- let address = '';
381
+ function extractAggregatedSignaturesWithValidators(receipt, validators, msgHash) {
382
+ const hashToSign = msgHash || receipt.attested_tx.hash;
383
+ // Convert compressed public keys to addresses
384
+ const validatorAddresses = new Map();
385
+ for (const [index, compressedPubKey] of validators) {
205
386
  try {
206
- address = addressFromPublicKey(publicKey);
387
+ const uncompressed = decompressPublicKey(compressedPubKey);
388
+ const address = addressFromPublicKey(uncompressed);
389
+ validatorAddresses.set(index, address.toLowerCase());
207
390
  }
208
391
  catch (e) {
209
- // Ignore
392
+ console.warn(`Failed to decompress public key for validator ${index}:`, e);
393
+ }
394
+ }
395
+ console.log('Validator addresses:', Object.fromEntries(validatorAddresses));
396
+ // Create a set of all validator addresses for lookup
397
+ const validatorSet = new Set(validatorAddresses.values());
398
+ // Get signatures in order (0, 1, 2, ...)
399
+ const sigKeys = Object.keys(receipt.signatures).sort((a, b) => parseInt(a) - parseInt(b));
400
+ if (sigKeys.length === 0) {
401
+ throw new Error('No signatures found in receipt');
402
+ }
403
+ const signatures = [];
404
+ const recoveredValidators = [];
405
+ for (let i = 0; i < sigKeys.length; i++) {
406
+ const sigIndex = parseInt(sigKeys[i]);
407
+ const derSig = receipt.signatures[sigKeys[i]];
408
+ const { r, s } = parseDerSignature(derSig);
409
+ // Try to recover with validator address verification
410
+ const result = recoverSignatureWithValidators(r, s, hashToSign, validatorSet);
411
+ if (result) {
412
+ signatures.push(result.signature.replace('0x', ''));
413
+ recoveredValidators.push(result.recoveredAddress);
414
+ }
415
+ else {
416
+ // Fallback to without validator check (will likely fail on contract)
417
+ console.warn(`Signature ${sigIndex}: Could not match to any validator address`);
418
+ const sig65 = recoverSignatureWithoutPubkey(r, s, hashToSign);
419
+ signatures.push(sig65.replace('0x', ''));
210
420
  }
421
+ }
422
+ console.log('Recovered validators:', recoveredValidators);
423
+ return '0x' + signatures.join('');
424
+ }
425
+ /**
426
+ * Extract signature info from Pod receipt for debugging/verification.
427
+ *
428
+ * @param receipt The Pod transaction receipt
429
+ * @param msgHash Optional override for the message hash
430
+ * @returns Array of signature info with recovered 65-byte signatures
431
+ */
432
+ function extractSignatureInfo(receipt, msgHash) {
433
+ const hashToSign = msgHash || receipt.attested_tx.hash;
434
+ const sigKeys = Object.keys(receipt.signatures).sort((a, b) => parseInt(a) - parseInt(b));
435
+ return sigKeys.map((key, index) => {
436
+ const derSig = receipt.signatures[key];
437
+ const { r, s } = parseDerSignature(derSig);
438
+ const sig65 = recoverSignatureWithoutPubkey(r, s, hashToSign);
211
439
  return {
212
- publicKey,
213
- address,
214
- signature: sig65 || 'recovery_failed',
215
- timestamp: att.timestamp
440
+ index,
441
+ derSignature: derSig,
442
+ r,
443
+ s,
444
+ signature65: sig65
216
445
  };
217
446
  });
218
447
  }
@@ -30,14 +30,17 @@ class PodProvider extends ethers_1.ethers.JsonRpcProvider {
30
30
  if (!rawReceipt) {
31
31
  return null;
32
32
  }
33
- // Store pod_metadata before ethers formatting
34
- const podMetadata = rawReceipt.pod_metadata;
33
+ const attestedTx = rawReceipt.attested_tx;
34
+ const signatures = rawReceipt.signatures;
35
35
  // Use ethers' internal formatter
36
36
  const ethersReceipt = this._wrapTransactionReceipt(rawReceipt, await this.getNetwork());
37
37
  // Attach pod_metadata to the formatted receipt
38
38
  const podReceipt = ethersReceipt;
39
- if (podMetadata) {
40
- podReceipt.pod_metadata = podMetadata;
39
+ if (attestedTx) {
40
+ podReceipt.attested_tx = attestedTx;
41
+ }
42
+ if (signatures) {
43
+ podReceipt.signatures = signatures;
41
44
  }
42
45
  return podReceipt;
43
46
  }
@@ -51,14 +54,17 @@ class PodProvider extends ethers_1.ethers.JsonRpcProvider {
51
54
  if (!rawTx) {
52
55
  return null;
53
56
  }
54
- // Store pod_metadata before ethers formatting
55
- const podMetadata = rawTx.pod_metadata;
57
+ const attestedTx = rawTx.attested_tx;
58
+ const signatures = rawTx.signatures;
56
59
  // Use ethers' internal formatter
57
60
  const ethersTx = this._wrapTransactionResponse(rawTx, await this.getNetwork());
58
61
  // Attach pod_metadata to the formatted transaction
59
62
  const podTx = ethersTx;
60
- if (podMetadata) {
61
- podTx.pod_metadata = podMetadata;
63
+ if (attestedTx) {
64
+ podTx.attested_tx = attestedTx;
65
+ }
66
+ if (signatures) {
67
+ podTx.signatures = signatures;
62
68
  }
63
69
  return podTx;
64
70
  }
@@ -23,7 +23,11 @@ export interface PodTransactionReceipt {
23
23
  transactionHash: string;
24
24
  transactionIndex: string;
25
25
  type: string;
26
- pod_metadata: PodTransactionMetadata;
26
+ attested_tx: {
27
+ hash: string;
28
+ committee_epoch: number;
29
+ };
30
+ signatures: Record<string, string>;
27
31
  }
28
32
  export interface PodTransactionLog {
29
33
  address: string;
@@ -36,38 +40,6 @@ export interface PodTransactionLog {
36
40
  transactionHash: string;
37
41
  transactionIndex: string;
38
42
  }
39
- export interface PodTransactionMetadata {
40
- attestations: PodAttestationData[];
41
- transaction: {
42
- signature: {
43
- r: string;
44
- s: string;
45
- v: string;
46
- yParity: string;
47
- };
48
- signed: {
49
- to: string | null;
50
- value: string;
51
- nonce: number;
52
- access_list: ethers.AccessList;
53
- chain_id?: number | null;
54
- gas_limit: number;
55
- input: string;
56
- max_fee_per_gas: number;
57
- max_priority_fee_per_gas: number;
58
- };
59
- };
60
- }
61
- export interface PodAttestationData {
62
- public_key: string;
63
- signature: {
64
- r: string;
65
- s: string;
66
- v: string;
67
- yParity: string;
68
- };
69
- timestamp: number;
70
- }
71
43
  export interface PodMetrics {
72
44
  gas_price: number;
73
45
  latency: number;
@@ -75,11 +47,16 @@ export interface PodMetrics {
75
47
  validator_uptime: number;
76
48
  }
77
49
  export interface PodTransactionResponse extends ethers.TransactionResponse {
78
- pod_metadata?: PodTransactionMetadata;
50
+ attested_tx?: {
51
+ hash: string;
52
+ committee_epoch: number;
53
+ };
54
+ signatures?: Record<string, string>;
79
55
  }
80
56
  export interface PodTransactionReceiptResponse extends ethers.TransactionReceipt {
81
- pod_metadata: PodTransactionMetadata & {
82
- attestations: PodAttestationData[];
83
- receipt_attestations: PodAttestationData[];
57
+ attested_tx: {
58
+ hash: string;
59
+ committee_epoch: number;
84
60
  };
61
+ signatures: Record<string, string>;
85
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapforce/pod-bridge-sdk",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "SDK for interacting with Bridges between pod and other chains",
5
5
  "keywords": [
6
6
  "pod",