@tapforce/pod-bridge-sdk 1.2.3 → 1.2.5
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
CHANGED
|
@@ -110,6 +110,11 @@ const receipt = await tx.wait();
|
|
|
110
110
|
Deposits on Pod require manual claim on ETH with aggregated validator signatures.
|
|
111
111
|
|
|
112
112
|
```typescript
|
|
113
|
+
import {
|
|
114
|
+
PodToSourceChainActionClient,
|
|
115
|
+
extractAggregatedSignaturesWithValidators
|
|
116
|
+
} from '@tapforce/pod-bridge-sdk';
|
|
117
|
+
|
|
113
118
|
const client = new PodToSourceChainActionClient(actionConfig);
|
|
114
119
|
|
|
115
120
|
// Step 1: Deposit on Pod
|
|
@@ -123,17 +128,26 @@ const depositTx = client.deposit({
|
|
|
123
128
|
const tx = await podSigner.sendTransaction(depositTx);
|
|
124
129
|
const receipt = await tx.wait();
|
|
125
130
|
|
|
126
|
-
// Step 2: Get
|
|
127
|
-
|
|
131
|
+
// Step 2: Get the receipt with validator signatures
|
|
132
|
+
const podReceipt = await podProvider.send('eth_getTransactionReceipt', [tx.hash]);
|
|
133
|
+
|
|
134
|
+
// Step 3: Get committee for v-value recovery
|
|
135
|
+
const committee = await podProvider.send('pod_getCommittee', {});
|
|
128
136
|
|
|
129
|
-
// Step
|
|
137
|
+
// Step 4: Extract aggregated signatures (65-byte format with correct v-values)
|
|
138
|
+
const aggregatedSignatures = extractAggregatedSignaturesWithValidators(
|
|
139
|
+
podReceipt,
|
|
140
|
+
committee.validators // [[0, "02..."], [1, "03..."], ...] compressed pubkeys
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Step 5: Claim on ETH
|
|
130
144
|
const claimTx = client.claim({
|
|
131
145
|
claimData: {
|
|
132
146
|
ethTokenAddress: '0xETH_TOKEN_ADDRESS', // Token on ETH (different from Pod)
|
|
133
147
|
amount: ethers.parseEther('1'),
|
|
134
148
|
to: '0x...', // Recipient
|
|
135
149
|
committeeEpoch: 0, // Hardcoded for now
|
|
136
|
-
aggregatedSignatures
|
|
150
|
+
aggregatedSignatures, // 65-byte signatures (r,s,v) concatenated
|
|
137
151
|
depositTxHash: tx.hash // Deposit TX hash from Pod
|
|
138
152
|
},
|
|
139
153
|
from: '0x...'
|
|
@@ -269,30 +283,68 @@ interface ClaimProofData {
|
|
|
269
283
|
|
|
270
284
|
### Signature Recovery Helpers
|
|
271
285
|
|
|
272
|
-
Pod
|
|
273
|
-
The
|
|
286
|
+
Pod returns DER-encoded 64-byte signatures without the parity bit (v), but the ETH contract requires 65-byte (r,s,v) format.
|
|
287
|
+
The SDK handles this by trying both v values (27/28) and verifying against the committee's validator public keys.
|
|
274
288
|
|
|
275
289
|
```typescript
|
|
276
290
|
import {
|
|
291
|
+
extractAggregatedSignaturesWithValidators,
|
|
277
292
|
extractAggregatedSignatures,
|
|
278
|
-
|
|
279
|
-
addressFromPublicKey
|
|
293
|
+
parseDerSignature,
|
|
294
|
+
addressFromPublicKey,
|
|
295
|
+
recoverSignatureWithoutPubkey
|
|
280
296
|
} from '@tapforce/pod-bridge-sdk';
|
|
281
297
|
|
|
282
|
-
// Recommended: Extract
|
|
298
|
+
// Recommended: Extract with validator verification (correct v-values)
|
|
283
299
|
const podReceipt = await podProvider.send('eth_getTransactionReceipt', [depositTxHash]);
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
r, // r component (32 bytes hex)
|
|
289
|
-
s, // s component (32 bytes hex)
|
|
290
|
-
msgHash, // message hash that was signed
|
|
291
|
-
publicKey // signer's public key (to verify recovery)
|
|
300
|
+
const committee = await podProvider.send('pod_getCommittee', {});
|
|
301
|
+
const aggregatedSignatures = extractAggregatedSignaturesWithValidators(
|
|
302
|
+
podReceipt,
|
|
303
|
+
committee.validators // [[index, compressedPubKey], ...]
|
|
292
304
|
);
|
|
293
305
|
|
|
294
|
-
//
|
|
295
|
-
const
|
|
306
|
+
// Alternative: Extract without validator verification (may have wrong v-values)
|
|
307
|
+
const sigs = extractAggregatedSignatures(podReceipt);
|
|
308
|
+
|
|
309
|
+
// Low-level: Parse DER signature to r,s components
|
|
310
|
+
const { r, s } = parseDerSignature(derEncodedSig);
|
|
311
|
+
|
|
312
|
+
// Low-level: Recover 65-byte signature without pubkey (tries v=27 first)
|
|
313
|
+
const sig65 = recoverSignatureWithoutPubkey(r, s, msgHash);
|
|
314
|
+
|
|
315
|
+
// Utility: Derive address from uncompressed public key
|
|
316
|
+
const address = addressFromPublicKey(uncompressedPubKey);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Pod Receipt Format
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
interface PodTransactionReceipt {
|
|
323
|
+
// ... standard fields ...
|
|
324
|
+
attested_tx: {
|
|
325
|
+
hash: string; // Hash signed by validators
|
|
326
|
+
committee_epoch: number;
|
|
327
|
+
};
|
|
328
|
+
signatures: {
|
|
329
|
+
"0": string; // DER-encoded signature from validator 0
|
|
330
|
+
"1": string; // DER-encoded signature from validator 1
|
|
331
|
+
// ...
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### Committee Format
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// pod_getCommittee response
|
|
340
|
+
{
|
|
341
|
+
validators: [
|
|
342
|
+
[0, "024ee7..."], // [index, compressed_secp256k1_pubkey]
|
|
343
|
+
[1, "025c59..."],
|
|
344
|
+
// ...
|
|
345
|
+
],
|
|
346
|
+
quorum_size: 3
|
|
347
|
+
}
|
|
296
348
|
```
|
|
297
349
|
|
|
298
350
|
## Events
|
|
@@ -6,7 +6,7 @@ import { PodBridgeConfig, BridgeRequest, BridgeRequestWithType } from "../../lib
|
|
|
6
6
|
* to provide a unified view of bridge activity across both chains.
|
|
7
7
|
*
|
|
8
8
|
* Bridge Architecture:
|
|
9
|
-
* - ETH -> Pod: deposits on ETH,
|
|
9
|
+
* - ETH -> Pod: deposits on ETH, claimable on Pod once Sepolia block is finalized
|
|
10
10
|
* - Pod -> ETH: deposits on Pod, claims on ETH with aggregated validator signatures
|
|
11
11
|
* - Only ERC20 tokens supported (wrap ETH to WETH)
|
|
12
12
|
*
|
|
@@ -15,7 +15,6 @@ import { PodBridgeConfig, BridgeRequest, BridgeRequestWithType } from "../../lib
|
|
|
15
15
|
* - Claims release ERC20 tokens with validator signatures (Pod -> ETH direction)
|
|
16
16
|
*
|
|
17
17
|
* POD Chain:
|
|
18
|
-
* - Auto-claim for ETH -> Pod deposits (instant finality)
|
|
19
18
|
* - Deposits for Pod -> ETH direction
|
|
20
19
|
*/
|
|
21
20
|
export declare class PodBridgeTrackerClient {
|
|
@@ -46,19 +45,17 @@ export declare class PodBridgeTrackerClient {
|
|
|
46
45
|
/**
|
|
47
46
|
* Check if deposits can be claimed
|
|
48
47
|
*
|
|
49
|
-
*
|
|
50
|
-
* - ETH -> Pod: AUTO-CLAIM on Pod, no manual claim needed (always returns false)
|
|
48
|
+
* - ETH -> Pod: Claimable once the Sepolia deposit block is finalized
|
|
51
49
|
* - Pod -> ETH: Manual claim needed on ETH with aggregated signatures
|
|
52
50
|
*
|
|
53
51
|
* @param deposit The bridge request to check
|
|
54
|
-
* @returns True if the deposit can be
|
|
52
|
+
* @returns True if the deposit can be claimed
|
|
55
53
|
*/
|
|
56
54
|
canBeClaimed(deposit: BridgeRequest): Promise<boolean>;
|
|
57
55
|
/**
|
|
58
56
|
* Batch check claim status for multiple deposits
|
|
59
57
|
*
|
|
60
|
-
*
|
|
61
|
-
* - ETH -> Pod: AUTO-CLAIM on Pod (mark as claimed after finalization)
|
|
58
|
+
* - ETH -> Pod: isClaimed=false, isClaimable=true once Sepolia block is finalized
|
|
62
59
|
* - Pod -> ETH: Check claim events on Source Chain
|
|
63
60
|
*
|
|
64
61
|
* @private
|
|
@@ -67,8 +64,7 @@ export declare class PodBridgeTrackerClient {
|
|
|
67
64
|
/**
|
|
68
65
|
* Batch check if multiple requests have been processed on-chain
|
|
69
66
|
*
|
|
70
|
-
*
|
|
71
|
-
* - ETH -> Pod: Auto-claimed after finalization
|
|
67
|
+
* - ETH -> Pod: Not auto-claimed; isClaimed reflects actual claim status
|
|
72
68
|
* - Pod -> ETH: Check claim events on Source Chain
|
|
73
69
|
*
|
|
74
70
|
* @param deposits Array of deposits to check
|
|
@@ -11,7 +11,7 @@ const pod_tracker_service_1 = require("./pod-tracker.service");
|
|
|
11
11
|
* to provide a unified view of bridge activity across both chains.
|
|
12
12
|
*
|
|
13
13
|
* Bridge Architecture:
|
|
14
|
-
* - ETH -> Pod: deposits on ETH,
|
|
14
|
+
* - ETH -> Pod: deposits on ETH, claimable on Pod once Sepolia block is finalized
|
|
15
15
|
* - Pod -> ETH: deposits on Pod, claims on ETH with aggregated validator signatures
|
|
16
16
|
* - Only ERC20 tokens supported (wrap ETH to WETH)
|
|
17
17
|
*
|
|
@@ -20,7 +20,6 @@ const pod_tracker_service_1 = require("./pod-tracker.service");
|
|
|
20
20
|
* - Claims release ERC20 tokens with validator signatures (Pod -> ETH direction)
|
|
21
21
|
*
|
|
22
22
|
* POD Chain:
|
|
23
|
-
* - Auto-claim for ETH -> Pod deposits (instant finality)
|
|
24
23
|
* - Deposits for Pod -> ETH direction
|
|
25
24
|
*/
|
|
26
25
|
class PodBridgeTrackerClient {
|
|
@@ -107,18 +106,17 @@ class PodBridgeTrackerClient {
|
|
|
107
106
|
/**
|
|
108
107
|
* Check if deposits can be claimed
|
|
109
108
|
*
|
|
110
|
-
*
|
|
111
|
-
* - ETH -> Pod: AUTO-CLAIM on Pod, no manual claim needed (always returns false)
|
|
109
|
+
* - ETH -> Pod: Claimable once the Sepolia deposit block is finalized
|
|
112
110
|
* - Pod -> ETH: Manual claim needed on ETH with aggregated signatures
|
|
113
111
|
*
|
|
114
112
|
* @param deposit The bridge request to check
|
|
115
|
-
* @returns True if the deposit can be
|
|
113
|
+
* @returns True if the deposit can be claimed
|
|
116
114
|
*/
|
|
117
115
|
async canBeClaimed(deposit) {
|
|
118
|
-
// ETH -> Pod deposits have AUTO-CLAIM on Pod, no manual claim needed
|
|
119
116
|
if (deposit.deposit.chain === pod_bridge_types_1.BridgeChain.SOURCE_CHAIN) {
|
|
120
|
-
//
|
|
121
|
-
|
|
117
|
+
// ETH -> Pod: claimable once the deposit block is finalized on Sepolia
|
|
118
|
+
const isFinalized = await this.sourceChainTracker.isBlockFinalized(deposit.deposit.blockNumber);
|
|
119
|
+
return isFinalized && !deposit.isClaimed;
|
|
122
120
|
}
|
|
123
121
|
else {
|
|
124
122
|
// POD -> ETH deposits need manual claim on ETH
|
|
@@ -129,8 +127,7 @@ class PodBridgeTrackerClient {
|
|
|
129
127
|
/**
|
|
130
128
|
* Batch check claim status for multiple deposits
|
|
131
129
|
*
|
|
132
|
-
*
|
|
133
|
-
* - ETH -> Pod: AUTO-CLAIM on Pod (mark as claimed after finalization)
|
|
130
|
+
* - ETH -> Pod: isClaimed=false, isClaimable=true once Sepolia block is finalized
|
|
134
131
|
* - Pod -> ETH: Check claim events on Source Chain
|
|
135
132
|
*
|
|
136
133
|
* @private
|
|
@@ -200,8 +197,7 @@ class PodBridgeTrackerClient {
|
|
|
200
197
|
/**
|
|
201
198
|
* Batch check if multiple requests have been processed on-chain
|
|
202
199
|
*
|
|
203
|
-
*
|
|
204
|
-
* - ETH -> Pod: Auto-claimed after finalization
|
|
200
|
+
* - ETH -> Pod: Not auto-claimed; isClaimed reflects actual claim status
|
|
205
201
|
* - Pod -> ETH: Check claim events on Source Chain
|
|
206
202
|
*
|
|
207
203
|
* @param deposits Array of deposits to check
|
|
@@ -210,8 +206,6 @@ class PodBridgeTrackerClient {
|
|
|
210
206
|
async areRequestsProcessed(deposits) {
|
|
211
207
|
if (deposits.length === 0)
|
|
212
208
|
return [];
|
|
213
|
-
// Get finalized block for ETH -> Pod auto-claim check
|
|
214
|
-
const finalizedBlock = await this.sourceChainTracker.getFinalizedBlockNumber();
|
|
215
209
|
// Group deposits by origin chain
|
|
216
210
|
const podDeposits = deposits.filter(d => d.deposit.chain === pod_bridge_types_1.BridgeChain.POD);
|
|
217
211
|
// Check claims on Source Chain for Pod -> ETH deposits
|
|
@@ -220,8 +214,8 @@ class PodBridgeTrackerClient {
|
|
|
220
214
|
: new Map();
|
|
221
215
|
return deposits.map(d => {
|
|
222
216
|
if (d.deposit.chain === pod_bridge_types_1.BridgeChain.SOURCE_CHAIN) {
|
|
223
|
-
// ETH -> Pod:
|
|
224
|
-
return d.
|
|
217
|
+
// ETH -> Pod: isClaimed is set during updateClaimStatus
|
|
218
|
+
return d.isClaimed;
|
|
225
219
|
}
|
|
226
220
|
else {
|
|
227
221
|
// Pod -> ETH: Check claim event on Source Chain
|
|
@@ -110,11 +110,20 @@ class SourceChainTrackerService {
|
|
|
110
110
|
async getClaimEvents(deposits) {
|
|
111
111
|
const uniqueRecipients = [...new Set(deposits.map(d => d.deposit.destination))];
|
|
112
112
|
const allClaimLogs = [];
|
|
113
|
+
const startBlock = this.config.deploymentBlock ?? 0;
|
|
114
|
+
const currentBlock = await this.provider.getBlockNumber();
|
|
115
|
+
const BLOCK_BATCH_SIZE = 10000;
|
|
113
116
|
for (const recipient of uniqueRecipients) {
|
|
114
117
|
// Event: Claim(bytes32 indexed id, address indexed to, address token, uint256 amount)
|
|
115
118
|
const claimFilter = this.bridge.filters.Claim(null, recipient);
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
for (let start = startBlock; start <= currentBlock; start += BLOCK_BATCH_SIZE) {
|
|
120
|
+
const end = Math.min(start + BLOCK_BATCH_SIZE - 1, currentBlock);
|
|
121
|
+
const claimLogs = await this.bridge.queryFilter(claimFilter, start, end);
|
|
122
|
+
allClaimLogs.push(...claimLogs);
|
|
123
|
+
if (start + BLOCK_BATCH_SIZE <= currentBlock) {
|
|
124
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
118
127
|
}
|
|
119
128
|
console.log(`[SourceChain] Found ${allClaimLogs.length} claims`);
|
|
120
129
|
const claimedMap = new Map();
|