@hyperbridge/sdk 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/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/abis/erc6160.d.ts +370 -0
- package/dist/abis/erc6160.js +238 -0
- package/dist/abis/erc6160.js.map +1 -0
- package/dist/abis/evmHost.d.ts +1752 -0
- package/dist/abis/evmHost.js +2250 -0
- package/dist/abis/evmHost.js.map +1 -0
- package/dist/abis/handler.d.ts +580 -0
- package/dist/abis/handler.js +750 -0
- package/dist/abis/handler.js.map +1 -0
- package/dist/abis/pingModule.d.ts +594 -0
- package/dist/abis/pingModule.js +765 -0
- package/dist/abis/pingModule.js.map +1 -0
- package/dist/abis/tokenGateway.d.ts +839 -0
- package/dist/abis/tokenGateway.js +471 -0
- package/dist/abis/tokenGateway.js.map +1 -0
- package/dist/chain.d.ts +83 -0
- package/dist/chain.js +34 -0
- package/dist/chain.js.map +1 -0
- package/dist/chains/evm.d.ts +86 -0
- package/dist/chains/evm.js +249 -0
- package/dist/chains/evm.js.map +1 -0
- package/dist/chains/substrate.d.ts +88 -0
- package/dist/chains/substrate.js +287 -0
- package/dist/chains/substrate.js.map +1 -0
- package/dist/client.d.ts +216 -0
- package/dist/client.js +774 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/queries.d.ts +3 -0
- package/dist/queries.js +78 -0
- package/dist/queries.js.map +1 -0
- package/dist/tests/hyperbridgeRequests.test.d.ts +1 -0
- package/dist/tests/hyperbridgeRequests.test.js +415 -0
- package/dist/tests/hyperbridgeRequests.test.js.map +1 -0
- package/dist/tests/postRequest.test.d.ts +1 -0
- package/dist/tests/postRequest.test.js +293 -0
- package/dist/tests/postRequest.test.js.map +1 -0
- package/dist/tests/setup.d.ts +1 -0
- package/dist/tests/setup.js +6 -0
- package/dist/tests/setup.js.map +1 -0
- package/dist/tests/tokenGateway.test.d.ts +1 -0
- package/dist/tests/tokenGateway.test.js +85 -0
- package/dist/tests/tokenGateway.test.js.map +1 -0
- package/dist/tests/xcmGateway.test.d.ts +1 -0
- package/dist/tests/xcmGateway.test.js +71 -0
- package/dist/tests/xcmGateway.test.js.map +1 -0
- package/dist/types/index.d.ts +238 -0
- package/dist/types/index.js +30 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/mmr.d.ts +13 -0
- package/dist/utils/mmr.js +153 -0
- package/dist/utils/mmr.js.map +1 -0
- package/dist/utils/substrate.d.ts +1913 -0
- package/dist/utils/substrate.js +361 -0
- package/dist/utils/substrate.js.map +1 -0
- package/dist/utils/tokenGateway.d.ts +68 -0
- package/dist/utils/tokenGateway.js +151 -0
- package/dist/utils/tokenGateway.js.map +1 -0
- package/dist/utils/xcmGateway.d.ts +81 -0
- package/dist/utils/xcmGateway.js +218 -0
- package/dist/utils/xcmGateway.js.map +1 -0
- package/dist/utils.d.ts +57 -0
- package/dist/utils.js +96 -0
- package/dist/utils.js.map +1 -0
- package/package.json +74 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { GraphQLClient } from "graphql-request";
|
|
2
|
+
import maxBy from "lodash/maxBy";
|
|
3
|
+
import { pad } from "viem";
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
import mergeRace from "@async-generator/merge-race";
|
|
6
|
+
import { RequestStatus, TimeoutStatus, } from "./types/index.js";
|
|
7
|
+
import { REQUEST_STATUS, STATE_MACHINE_UPDATES_BY_HEIGHT, STATE_MACHINE_UPDATES_BY_TIMESTAMP } from "./queries.js";
|
|
8
|
+
import { COMBINED_STATUS_WEIGHTS, REQUEST_STATUS_WEIGHTS, TIMEOUT_STATUS_WEIGHTS, postRequestCommitment, sleep, } from "./utils.js";
|
|
9
|
+
import { getChain } from "./chain.js";
|
|
10
|
+
/**
|
|
11
|
+
* IndexerClient provides methods for interacting with the Hyperbridge indexer.
|
|
12
|
+
*
|
|
13
|
+
* This client facilitates querying and tracking cross-chain requests and their status
|
|
14
|
+
* through the Hyperbridge protocol. It supports:
|
|
15
|
+
*
|
|
16
|
+
* - Querying state machine updates by block height or timestamp
|
|
17
|
+
* - Retrieving request status information by transaction hash
|
|
18
|
+
* - Monitoring request status changes through streaming interfaces
|
|
19
|
+
* - Handling request timeout flows and related proof generation
|
|
20
|
+
* - Tracking request finalization across source and destination chains
|
|
21
|
+
*
|
|
22
|
+
* The client implements automatic retries with exponential backoff for network
|
|
23
|
+
* resilience and provides both simple query methods and advanced streaming
|
|
24
|
+
* interfaces for real-time status tracking.
|
|
25
|
+
*
|
|
26
|
+
* The URLs provided in the configuration must point to archive nodes to allow the client to query for storage proofs
|
|
27
|
+
* of potentially much older blocks. Regular nodes only store the state for recent blocks and will not be able
|
|
28
|
+
* to provide the necessary proofs for cross-chain verification, especially in timeout scenarios.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const client = new IndexerClient({
|
|
33
|
+
* url: "https://indexer.hyperbridge.xyz/graphql",
|
|
34
|
+
* pollInterval: 2000,
|
|
35
|
+
* source: {
|
|
36
|
+
* stateMachineId: "EVM-1",
|
|
37
|
+
* consensusStateId: "ETH0"
|
|
38
|
+
* rpcUrl: "",
|
|
39
|
+
* host: "0x87ea45..",
|
|
40
|
+
* },
|
|
41
|
+
* dest: {
|
|
42
|
+
* stateMachineId: "EVM-42161",
|
|
43
|
+
* consensusStateId: "ETH0"
|
|
44
|
+
* rpcUrl: "",
|
|
45
|
+
* host: "0x87ea42345..",
|
|
46
|
+
* },
|
|
47
|
+
* hyperbridge: {
|
|
48
|
+
* stateMachineId: "POLKADOT-3367",
|
|
49
|
+
* consensusStateId: "DOT0"
|
|
50
|
+
* wsUrl: "ws://localhost:9944"
|
|
51
|
+
* }
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // Query a request status
|
|
55
|
+
* const status = await client.queryRequestWithStatus("0x1234...");
|
|
56
|
+
*
|
|
57
|
+
* // Stream status updates
|
|
58
|
+
* for await (const update of client.postRequestStatusStream("0x1234...")) {
|
|
59
|
+
* console.log(`Request status: ${update.status}`);
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export class IndexerClient {
|
|
64
|
+
/**
|
|
65
|
+
* Creates a new IndexerClient instance
|
|
66
|
+
*/
|
|
67
|
+
constructor(config) {
|
|
68
|
+
/**
|
|
69
|
+
* Default configuration for retry behavior when network requests fail
|
|
70
|
+
* - maxRetries: Maximum number of retry attempts before failing
|
|
71
|
+
* - backoffMs: Initial backoff time in milliseconds (doubles with each retry)
|
|
72
|
+
*/
|
|
73
|
+
this.defaultRetryConfig = {
|
|
74
|
+
maxRetries: 3,
|
|
75
|
+
backoffMs: 1000,
|
|
76
|
+
};
|
|
77
|
+
this.client = new GraphQLClient(config?.url || "http://localhost:3000/graphql");
|
|
78
|
+
this.config = config;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Query for a single state machine update event greater than or equal to the given height.
|
|
82
|
+
* @params statemachineId - ID of the state machine
|
|
83
|
+
* @params height - Starting block height
|
|
84
|
+
* @params chain - The identifier for the chain where the state machine update should be queried (corresponds to a stateMachineId)
|
|
85
|
+
* @returns Closest state machine update
|
|
86
|
+
*/
|
|
87
|
+
async queryStateMachineUpdateByHeight({ statemachineId, height, chain, }) {
|
|
88
|
+
const response = await this.withRetry(() => this.client.request(STATE_MACHINE_UPDATES_BY_HEIGHT, {
|
|
89
|
+
statemachineId,
|
|
90
|
+
height,
|
|
91
|
+
chain,
|
|
92
|
+
}));
|
|
93
|
+
return response.stateMachineUpdateEvents.nodes[0];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Query for a single state machine update event greater than or equal to the given timestamp.
|
|
97
|
+
* @params statemachineId - ID of the state machine
|
|
98
|
+
* @params timestamp - Starting block timestamp
|
|
99
|
+
* @params chain - The identifier for the chain where the state machine update should be queried (corresponds to a stateMachineId)
|
|
100
|
+
* @returns Closest state machine update
|
|
101
|
+
*/
|
|
102
|
+
async queryStateMachineUpdateByTimestamp({ statemachineId, commitmentTimestamp, chain, }) {
|
|
103
|
+
const response = await this.withRetry(() => this.client.request(STATE_MACHINE_UPDATES_BY_TIMESTAMP, {
|
|
104
|
+
statemachineId,
|
|
105
|
+
commitmentTimestamp: commitmentTimestamp.toString(),
|
|
106
|
+
chain,
|
|
107
|
+
}));
|
|
108
|
+
return response.stateMachineUpdateEvents.nodes[0];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Queries a request by any of its associated hashes and returns it alongside its statuses
|
|
112
|
+
* Statuses will be one of SOURCE, HYPERBRIDGE_DELIVERED and DESTINATION
|
|
113
|
+
* @param hash - Can be commitment, hyperbridge tx hash, source tx hash, destination tx hash, or timeout tx hash
|
|
114
|
+
* @returns Latest status and block metadata of the request
|
|
115
|
+
*/
|
|
116
|
+
async queryRequest(hash) {
|
|
117
|
+
const self = this;
|
|
118
|
+
const response = await self.withRetry(() => self.client.request(REQUEST_STATUS, {
|
|
119
|
+
hash,
|
|
120
|
+
}));
|
|
121
|
+
if (!response.requests.nodes[0])
|
|
122
|
+
return;
|
|
123
|
+
const statuses = response.requests.nodes[0].statusMetadata.nodes.map((item) => ({
|
|
124
|
+
status: item.status,
|
|
125
|
+
metadata: {
|
|
126
|
+
blockHash: item.blockHash,
|
|
127
|
+
blockNumber: parseInt(item.blockNumber),
|
|
128
|
+
transactionHash: item.transactionHash,
|
|
129
|
+
},
|
|
130
|
+
}));
|
|
131
|
+
// sort by ascending order
|
|
132
|
+
const sorted = statuses.sort((a, b) => REQUEST_STATUS_WEIGHTS[a.status] - REQUEST_STATUS_WEIGHTS[b.status]);
|
|
133
|
+
const request = {
|
|
134
|
+
...response.requests.nodes[0],
|
|
135
|
+
statuses: sorted,
|
|
136
|
+
};
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
delete request.statusMetadata;
|
|
139
|
+
return request;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Enhances a request with finality events by querying state machine updates.
|
|
143
|
+
*
|
|
144
|
+
* This method augments a request object with additional inferred status events
|
|
145
|
+
* that represent chain finality confirmations. It adds:
|
|
146
|
+
* - SOURCE_FINALIZED: When the source chain has finalized the request
|
|
147
|
+
* - HYPERBRIDGE_FINALIZED: When Hyperbridge has finalized the delivery confirmation
|
|
148
|
+
*
|
|
149
|
+
* The method also generates appropriate calldata for submitting cross-chain proofs
|
|
150
|
+
* when applicable.
|
|
151
|
+
*
|
|
152
|
+
* @param request - The request to enhance with finality events
|
|
153
|
+
* @returns The request with finality events added
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
async addRequestFinalityEvents(request) {
|
|
157
|
+
const self = this;
|
|
158
|
+
let hyperbridgeDelivered;
|
|
159
|
+
if (request.source === self.config.hyperbridge.stateMachineId) {
|
|
160
|
+
// the first status contains the blocknumber of the initial request
|
|
161
|
+
hyperbridgeDelivered = request.statuses[0];
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// we assume there's always a SOURCE event which contains the blocknumber of the initial request
|
|
165
|
+
const sourceFinality = await self.queryStateMachineUpdateByHeight({
|
|
166
|
+
statemachineId: request.source,
|
|
167
|
+
height: request.statuses[0].metadata.blockNumber,
|
|
168
|
+
chain: self.config.hyperbridge.stateMachineId,
|
|
169
|
+
});
|
|
170
|
+
// no finality event found, return request as is
|
|
171
|
+
if (!sourceFinality)
|
|
172
|
+
return request;
|
|
173
|
+
// Insert finality event into request.statuses at index 1
|
|
174
|
+
request.statuses.push({
|
|
175
|
+
status: RequestStatus.SOURCE_FINALIZED,
|
|
176
|
+
metadata: {
|
|
177
|
+
blockHash: sourceFinality.blockHash,
|
|
178
|
+
blockNumber: sourceFinality.height,
|
|
179
|
+
transactionHash: sourceFinality.transactionHash,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
// check if there's a hyperbridge delivered event
|
|
183
|
+
hyperbridgeDelivered = request.statuses.find((item) => item.status === RequestStatus.HYPERBRIDGE_DELIVERED);
|
|
184
|
+
if (!hyperbridgeDelivered)
|
|
185
|
+
return request;
|
|
186
|
+
}
|
|
187
|
+
// no need to query finality event if destination is hyperbridge
|
|
188
|
+
if (request.dest === self.config.hyperbridge.stateMachineId)
|
|
189
|
+
return request;
|
|
190
|
+
const hyperbridgeFinality = await self.queryStateMachineUpdateByHeight({
|
|
191
|
+
statemachineId: self.config.hyperbridge.stateMachineId,
|
|
192
|
+
height: hyperbridgeDelivered.metadata.blockNumber,
|
|
193
|
+
chain: request.dest,
|
|
194
|
+
});
|
|
195
|
+
if (!hyperbridgeFinality)
|
|
196
|
+
return request;
|
|
197
|
+
// check if request receipt exists on destination chain
|
|
198
|
+
const destChain = await getChain(self.config.dest);
|
|
199
|
+
const hyperbridge = await getChain({
|
|
200
|
+
...self.config.hyperbridge,
|
|
201
|
+
hasher: "Keccak",
|
|
202
|
+
});
|
|
203
|
+
const proof = await hyperbridge.queryRequestsProof([postRequestCommitment(request)], request.dest, BigInt(hyperbridgeFinality.height));
|
|
204
|
+
const calldata = destChain.encode({
|
|
205
|
+
kind: "PostRequest",
|
|
206
|
+
proof: {
|
|
207
|
+
stateMachine: self.config.hyperbridge.stateMachineId,
|
|
208
|
+
consensusStateId: self.config.hyperbridge.consensusStateId,
|
|
209
|
+
proof,
|
|
210
|
+
height: BigInt(hyperbridgeFinality.height),
|
|
211
|
+
},
|
|
212
|
+
requests: [request],
|
|
213
|
+
signer: pad("0x"),
|
|
214
|
+
});
|
|
215
|
+
request.statuses.push({
|
|
216
|
+
status: RequestStatus.HYPERBRIDGE_FINALIZED,
|
|
217
|
+
metadata: {
|
|
218
|
+
blockHash: hyperbridgeFinality.blockHash,
|
|
219
|
+
blockNumber: hyperbridgeFinality.height,
|
|
220
|
+
transactionHash: hyperbridgeFinality.transactionHash,
|
|
221
|
+
calldata,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
return request;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Adds timeout finality events to a request by querying for relevant timeout proofs and
|
|
228
|
+
* chain state necessary for timeout processing.
|
|
229
|
+
*
|
|
230
|
+
* This method enhances a request object with additional status events related to the
|
|
231
|
+
* timeout flow, including:
|
|
232
|
+
* - PENDING_TIMEOUT: When a request has passed its timeout timestamp
|
|
233
|
+
* - DESTINATION_FINALIZED: When the destination chain has finalized the timeout timestamp
|
|
234
|
+
* - HYPERBRIDGE_FINALIZED_TIMEOUT: When hyperbridge has finalized the timeout state
|
|
235
|
+
*
|
|
236
|
+
* The method also generates appropriate calldata for submitting timeout proofs.
|
|
237
|
+
*
|
|
238
|
+
* @param request - Request to fill timeout events for
|
|
239
|
+
* @returns Request with timeout events filled in, including any proof calldata for timeout submissions
|
|
240
|
+
* @private
|
|
241
|
+
*/
|
|
242
|
+
async addTimeoutFinalityEvents(request) {
|
|
243
|
+
const self = this;
|
|
244
|
+
// check if request receipt exists on destination chain
|
|
245
|
+
const destChain = await getChain(self.config.dest);
|
|
246
|
+
const hyperbridge = await getChain({
|
|
247
|
+
...self.config.hyperbridge,
|
|
248
|
+
hasher: "Keccak",
|
|
249
|
+
});
|
|
250
|
+
const commitment = postRequestCommitment(request);
|
|
251
|
+
const reciept = await destChain.queryRequestReceipt(commitment);
|
|
252
|
+
const destTimestamp = await destChain.timestamp();
|
|
253
|
+
// request not timed out
|
|
254
|
+
if (reciept || request.timeoutTimestamp > destTimestamp)
|
|
255
|
+
return request;
|
|
256
|
+
request.statuses.push({
|
|
257
|
+
status: TimeoutStatus.PENDING_TIMEOUT,
|
|
258
|
+
metadata: { blockHash: "0x", blockNumber: 0, transactionHash: "0x" },
|
|
259
|
+
});
|
|
260
|
+
const delivered = request.statuses.find((item) => item.status === RequestStatus.HYPERBRIDGE_DELIVERED);
|
|
261
|
+
let hyperbridgeFinalized;
|
|
262
|
+
if (!delivered) {
|
|
263
|
+
// either the request was never delivered to hyperbridge
|
|
264
|
+
// or hyperbridge was the destination of the request
|
|
265
|
+
hyperbridgeFinalized = await self.queryStateMachineUpdateByTimestamp({
|
|
266
|
+
statemachineId: self.config.hyperbridge.stateMachineId,
|
|
267
|
+
commitmentTimestamp: request.timeoutTimestamp,
|
|
268
|
+
chain: request.source,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
let destFinalized = await self.queryStateMachineUpdateByTimestamp({
|
|
273
|
+
statemachineId: request.dest,
|
|
274
|
+
commitmentTimestamp: request.timeoutTimestamp,
|
|
275
|
+
chain: self.config.hyperbridge.stateMachineId,
|
|
276
|
+
});
|
|
277
|
+
if (!destFinalized)
|
|
278
|
+
return request;
|
|
279
|
+
request.statuses.push({
|
|
280
|
+
status: TimeoutStatus.DESTINATION_FINALIZED_TIMEOUT,
|
|
281
|
+
metadata: {
|
|
282
|
+
blockHash: destFinalized.blockHash,
|
|
283
|
+
blockNumber: destFinalized.blockNumber,
|
|
284
|
+
transactionHash: destFinalized.transactionHash,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
// if the source is the hyperbridge state machine, no further action is needed
|
|
288
|
+
// use the timeout stream to timeout on hyperbridge
|
|
289
|
+
if (request.source === self.config.hyperbridge.stateMachineId)
|
|
290
|
+
return request;
|
|
291
|
+
const hyperbridgeTimedOut = request.statuses.find((item) => item.status === TimeoutStatus.HYPERBRIDGE_TIMED_OUT);
|
|
292
|
+
if (!hyperbridgeTimedOut)
|
|
293
|
+
return request;
|
|
294
|
+
hyperbridgeFinalized = await self.queryStateMachineUpdateByHeight({
|
|
295
|
+
statemachineId: self.config.hyperbridge.stateMachineId,
|
|
296
|
+
height: hyperbridgeTimedOut.metadata.blockNumber,
|
|
297
|
+
chain: request.source,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (!hyperbridgeFinalized)
|
|
301
|
+
return request;
|
|
302
|
+
const proof = await hyperbridge.queryStateProof(BigInt(hyperbridgeFinalized.height), [
|
|
303
|
+
hyperbridge.requestReceiptKey(commitment),
|
|
304
|
+
]);
|
|
305
|
+
const sourceChain = await getChain(self.config.source);
|
|
306
|
+
let calldata = sourceChain.encode({
|
|
307
|
+
kind: "TimeoutPostRequest",
|
|
308
|
+
proof: {
|
|
309
|
+
proof,
|
|
310
|
+
height: BigInt(hyperbridgeFinalized.height),
|
|
311
|
+
stateMachine: self.config.hyperbridge.stateMachineId,
|
|
312
|
+
consensusStateId: self.config.hyperbridge.consensusStateId,
|
|
313
|
+
},
|
|
314
|
+
requests: [
|
|
315
|
+
{
|
|
316
|
+
source: request.source,
|
|
317
|
+
dest: request.dest,
|
|
318
|
+
from: request.from,
|
|
319
|
+
to: request.to,
|
|
320
|
+
nonce: request.nonce,
|
|
321
|
+
body: request.body,
|
|
322
|
+
timeoutTimestamp: request.timeoutTimestamp,
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
});
|
|
326
|
+
request.statuses.push({
|
|
327
|
+
status: TimeoutStatus.HYPERBRIDGE_FINALIZED_TIMEOUT,
|
|
328
|
+
metadata: {
|
|
329
|
+
blockHash: hyperbridgeFinalized.blockHash,
|
|
330
|
+
blockNumber: hyperbridgeFinalized.blockNumber,
|
|
331
|
+
transactionHash: hyperbridgeFinalized.transactionHash,
|
|
332
|
+
calldata,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
return request;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Queries a request by any of its associated hashes and returns it alongside its statuses,
|
|
339
|
+
* including any finalization events.
|
|
340
|
+
* @param hash - Can be commitment, hyperbridge tx hash, source tx hash, destination tx hash, or timeout tx hash
|
|
341
|
+
* @returns Full request data with all inferred status events, including SOURCE_FINALIZED and HYPERBRIDGE_FINALIZED
|
|
342
|
+
* @remarks Unlike queryRequest(), this method adds derived finalization status events by querying state machine updates
|
|
343
|
+
*/
|
|
344
|
+
async queryRequestWithStatus(hash) {
|
|
345
|
+
let request = await this.queryRequest(hash);
|
|
346
|
+
if (!request)
|
|
347
|
+
return;
|
|
348
|
+
request = await this.addRequestFinalityEvents(request);
|
|
349
|
+
request = await this.addTimeoutFinalityEvents(request);
|
|
350
|
+
// ensure all statuses are sorted by weight
|
|
351
|
+
request.statuses = request.statuses.sort((a, b) => COMBINED_STATUS_WEIGHTS[a.status] - COMBINED_STATUS_WEIGHTS[b.status]);
|
|
352
|
+
return request;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Create a Stream of status updates for a post request.
|
|
356
|
+
* Stream ends when either the request reaches the destination or times out.
|
|
357
|
+
* If the stream yields TimeoutStatus.PENDING_TIMEOUT, use postRequestTimeoutStream() to begin timeout processing.
|
|
358
|
+
* @param hash - Can be commitment, hyperbridge tx hash, source tx hash, destination tx hash, or timeout tx hash
|
|
359
|
+
* @returns AsyncGenerator that emits status updates until a terminal state is reached
|
|
360
|
+
* @example
|
|
361
|
+
*
|
|
362
|
+
* let client = new IndexerClient(config)
|
|
363
|
+
* let stream = client.postRequestStatusStream(hash)
|
|
364
|
+
*
|
|
365
|
+
* // you can use a for-await-of loop
|
|
366
|
+
* for await (const status of stream) {
|
|
367
|
+
* console.log(status)
|
|
368
|
+
* }
|
|
369
|
+
*
|
|
370
|
+
* // you can also use a while loop
|
|
371
|
+
* while (true) {
|
|
372
|
+
* const status = await stream.next()
|
|
373
|
+
* if (status.done) {
|
|
374
|
+
* break
|
|
375
|
+
* }
|
|
376
|
+
* console.log(status.value)
|
|
377
|
+
* }
|
|
378
|
+
*
|
|
379
|
+
*/
|
|
380
|
+
async *postRequestStatusStream(hash) {
|
|
381
|
+
const self = this;
|
|
382
|
+
// wait for request to be created
|
|
383
|
+
let request;
|
|
384
|
+
while (!request) {
|
|
385
|
+
await sleep(self.config.pollInterval);
|
|
386
|
+
request = await self.queryRequest(hash);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const chain = await getChain(self.config.dest);
|
|
390
|
+
const timeoutStream = self.timeoutStream(request.timeoutTimestamp, chain);
|
|
391
|
+
const statusStream = self.postRequestStatusStreamInternal(hash);
|
|
392
|
+
const combined = mergeRace(timeoutStream, statusStream);
|
|
393
|
+
let item = await combined.next();
|
|
394
|
+
while (!item.done) {
|
|
395
|
+
yield item.value;
|
|
396
|
+
item = await combined.next();
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
/*
|
|
401
|
+
* Returns a generator that will yield true if the request is timed out
|
|
402
|
+
* If the request does not have a timeout, it will yield never yield
|
|
403
|
+
* @param request - Request to timeout
|
|
404
|
+
*/
|
|
405
|
+
async *timeoutStream(timeoutTimestamp, chain) {
|
|
406
|
+
if (timeoutTimestamp > 0) {
|
|
407
|
+
let timestamp = await chain.timestamp();
|
|
408
|
+
while (timestamp < timeoutTimestamp) {
|
|
409
|
+
const diff = BigInt(timeoutTimestamp) - BigInt(timestamp);
|
|
410
|
+
await sleep(Number(diff));
|
|
411
|
+
timestamp = await chain.timestamp();
|
|
412
|
+
}
|
|
413
|
+
yield {
|
|
414
|
+
status: TimeoutStatus.PENDING_TIMEOUT,
|
|
415
|
+
metadata: { blockHash: "0x", blockNumber: 0, transactionHash: "0x" },
|
|
416
|
+
};
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Create a Stream of status updates
|
|
422
|
+
* @param hash - Can be commitment, hyperbridge tx hash, source tx hash, destination tx hash, or timeout tx hash
|
|
423
|
+
* @returns AsyncGenerator that emits status updates until a terminal state is reached
|
|
424
|
+
*/
|
|
425
|
+
async *postRequestStatusStreamInternal(hash) {
|
|
426
|
+
const self = this;
|
|
427
|
+
let request;
|
|
428
|
+
while (!request) {
|
|
429
|
+
await sleep(self.config.pollInterval);
|
|
430
|
+
request = await self.queryRequest(hash);
|
|
431
|
+
}
|
|
432
|
+
let status = request.source === self.config.hyperbridge.stateMachineId
|
|
433
|
+
? RequestStatus.HYPERBRIDGE_DELIVERED
|
|
434
|
+
: RequestStatus.SOURCE;
|
|
435
|
+
const latestMetadata = request.statuses[request.statuses.length - 1];
|
|
436
|
+
// start with the latest status
|
|
437
|
+
status = maxBy([status, latestMetadata.status], (item) => REQUEST_STATUS_WEIGHTS[item]);
|
|
438
|
+
while (true) {
|
|
439
|
+
switch (status) {
|
|
440
|
+
// request has been dispatched from source chain
|
|
441
|
+
case RequestStatus.SOURCE: {
|
|
442
|
+
let sourceUpdate;
|
|
443
|
+
while (!sourceUpdate) {
|
|
444
|
+
await sleep(self.config.pollInterval);
|
|
445
|
+
sourceUpdate = await self.queryStateMachineUpdateByHeight({
|
|
446
|
+
statemachineId: request.source,
|
|
447
|
+
height: request.statuses[0].metadata.blockNumber,
|
|
448
|
+
chain: self.config.hyperbridge.stateMachineId,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
yield {
|
|
452
|
+
status: RequestStatus.SOURCE_FINALIZED,
|
|
453
|
+
metadata: {
|
|
454
|
+
blockHash: sourceUpdate.blockHash,
|
|
455
|
+
blockNumber: sourceUpdate.height,
|
|
456
|
+
transactionHash: sourceUpdate.transactionHash,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
status = RequestStatus.SOURCE_FINALIZED;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
// finality proofs for request has been verified on Hyperbridge
|
|
463
|
+
case RequestStatus.SOURCE_FINALIZED: {
|
|
464
|
+
// wait for the request to be delivered on Hyperbridge
|
|
465
|
+
while (!request || request.statuses.length < 2) {
|
|
466
|
+
await sleep(self.config.pollInterval);
|
|
467
|
+
request = await self.queryRequest(hash);
|
|
468
|
+
}
|
|
469
|
+
status =
|
|
470
|
+
request.dest === self.config.hyperbridge.stateMachineId
|
|
471
|
+
? RequestStatus.DESTINATION
|
|
472
|
+
: RequestStatus.HYPERBRIDGE_DELIVERED;
|
|
473
|
+
yield {
|
|
474
|
+
status,
|
|
475
|
+
metadata: {
|
|
476
|
+
blockHash: request.statuses[1].metadata.blockHash,
|
|
477
|
+
blockNumber: request.statuses[1].metadata.blockNumber,
|
|
478
|
+
transactionHash: request.statuses[1].metadata.transactionHash,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
// the request has been verified and aggregated on Hyperbridge
|
|
484
|
+
case RequestStatus.HYPERBRIDGE_DELIVERED: {
|
|
485
|
+
// Get the latest state machine update for hyperbridge on the destination chain
|
|
486
|
+
let hyperbridgeFinalized;
|
|
487
|
+
let index = request.source === self.config.hyperbridge.stateMachineId ? 0 : 1;
|
|
488
|
+
while (!hyperbridgeFinalized) {
|
|
489
|
+
await sleep(self.config.pollInterval);
|
|
490
|
+
hyperbridgeFinalized = await self.queryStateMachineUpdateByHeight({
|
|
491
|
+
statemachineId: self.config.hyperbridge.stateMachineId,
|
|
492
|
+
height: request.statuses[index].metadata.blockNumber,
|
|
493
|
+
chain: request.dest,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
const destChain = await getChain(self.config.dest);
|
|
497
|
+
const hyperbridge = await getChain({
|
|
498
|
+
...self.config.hyperbridge,
|
|
499
|
+
hasher: "Keccak",
|
|
500
|
+
});
|
|
501
|
+
const proof = await hyperbridge.queryRequestsProof([postRequestCommitment(request)], request.dest, BigInt(hyperbridgeFinalized.height));
|
|
502
|
+
const calldata = destChain.encode({
|
|
503
|
+
kind: "PostRequest",
|
|
504
|
+
proof: {
|
|
505
|
+
stateMachine: self.config.hyperbridge.stateMachineId,
|
|
506
|
+
consensusStateId: self.config.hyperbridge.consensusStateId,
|
|
507
|
+
proof,
|
|
508
|
+
height: BigInt(hyperbridgeFinalized.height),
|
|
509
|
+
},
|
|
510
|
+
requests: [request],
|
|
511
|
+
signer: pad("0x"),
|
|
512
|
+
});
|
|
513
|
+
yield {
|
|
514
|
+
status: RequestStatus.HYPERBRIDGE_FINALIZED,
|
|
515
|
+
metadata: {
|
|
516
|
+
blockHash: hyperbridgeFinalized.blockHash,
|
|
517
|
+
blockNumber: hyperbridgeFinalized.height,
|
|
518
|
+
transactionHash: hyperbridgeFinalized.transactionHash,
|
|
519
|
+
calldata,
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
status = RequestStatus.HYPERBRIDGE_FINALIZED;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
// request has been finalized by hyperbridge
|
|
526
|
+
case RequestStatus.HYPERBRIDGE_FINALIZED: {
|
|
527
|
+
// wait for the request to be delivered to the destination
|
|
528
|
+
let delivered = request.statuses.find((s) => s.status === RequestStatus.DESTINATION);
|
|
529
|
+
while (!request || !delivered) {
|
|
530
|
+
await sleep(self.config.pollInterval);
|
|
531
|
+
request = await self.queryRequest(hash);
|
|
532
|
+
delivered = request?.statuses.find((s) => s.status === RequestStatus.DESTINATION);
|
|
533
|
+
}
|
|
534
|
+
let index = request.source === self.config.hyperbridge.stateMachineId ? 1 : 2;
|
|
535
|
+
yield {
|
|
536
|
+
status: RequestStatus.DESTINATION,
|
|
537
|
+
metadata: {
|
|
538
|
+
blockHash: request.statuses[index].metadata.blockHash,
|
|
539
|
+
blockNumber: request.statuses[index].metadata.blockNumber,
|
|
540
|
+
transactionHash: request.statuses[index].metadata.transactionHash,
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
status = RequestStatus.DESTINATION;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
case RequestStatus.DESTINATION:
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Create a Stream of status updates for a timed out post request.
|
|
553
|
+
* @param hash - Can be commitment, hyperbridge tx hash, source tx hash, destination tx hash, or timeout tx hash
|
|
554
|
+
* @returns AsyncGenerator that emits status updates until a terminal state is reached
|
|
555
|
+
* @example
|
|
556
|
+
*
|
|
557
|
+
* let client = new IndexerClient(config)
|
|
558
|
+
* let stream = client.postRequestTimeoutStream(hash)
|
|
559
|
+
*
|
|
560
|
+
* // you can use a for-await-of loop
|
|
561
|
+
* for await (const status of stream) {
|
|
562
|
+
* console.log(status)
|
|
563
|
+
* }
|
|
564
|
+
*
|
|
565
|
+
* // you can also use a while loop
|
|
566
|
+
* while (true) {
|
|
567
|
+
* const status = await stream.next()
|
|
568
|
+
* if (status.done) {
|
|
569
|
+
* break
|
|
570
|
+
* }
|
|
571
|
+
* console.log(status.value)
|
|
572
|
+
* }
|
|
573
|
+
*/
|
|
574
|
+
async *postRequestTimeoutStream(hash) {
|
|
575
|
+
const self = this;
|
|
576
|
+
let request = await self.queryRequest(hash);
|
|
577
|
+
if (!request)
|
|
578
|
+
throw new Error(`Request not found`);
|
|
579
|
+
const destChain = await getChain(self.config.dest);
|
|
580
|
+
const destTimestamp = await destChain.timestamp();
|
|
581
|
+
if (request.timeoutTimestamp > destTimestamp)
|
|
582
|
+
throw new Error(`Request not timed out`);
|
|
583
|
+
// if the destination is hyperbridge, then just wait for hyperbridge finality
|
|
584
|
+
let status = request.dest === self.config.hyperbridge.stateMachineId
|
|
585
|
+
? TimeoutStatus.HYPERBRIDGE_TIMED_OUT
|
|
586
|
+
: TimeoutStatus.PENDING_TIMEOUT;
|
|
587
|
+
const commitment = postRequestCommitment(request);
|
|
588
|
+
const hyperbridge = (await getChain({
|
|
589
|
+
...self.config.hyperbridge,
|
|
590
|
+
hasher: "Keccak",
|
|
591
|
+
}));
|
|
592
|
+
const latest = request.statuses[request.statuses.length - 1];
|
|
593
|
+
// we're always interested in the latest status
|
|
594
|
+
status = maxBy([status, latest.status], (item) => TIMEOUT_STATUS_WEIGHTS[item]);
|
|
595
|
+
while (true) {
|
|
596
|
+
switch (status) {
|
|
597
|
+
case TimeoutStatus.PENDING_TIMEOUT: {
|
|
598
|
+
const receipt = await hyperbridge.queryRequestReceipt(commitment);
|
|
599
|
+
if (!receipt && request.source !== self.config.hyperbridge.stateMachineId) {
|
|
600
|
+
status = TimeoutStatus.HYPERBRIDGE_TIMED_OUT;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
let update;
|
|
604
|
+
while (!update) {
|
|
605
|
+
await sleep(self.config.pollInterval);
|
|
606
|
+
update = await self.queryStateMachineUpdateByTimestamp({
|
|
607
|
+
statemachineId: request.dest,
|
|
608
|
+
commitmentTimestamp: request.timeoutTimestamp,
|
|
609
|
+
chain: self.config.hyperbridge.stateMachineId,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
yield {
|
|
613
|
+
status: TimeoutStatus.DESTINATION_FINALIZED_TIMEOUT,
|
|
614
|
+
metadata: {
|
|
615
|
+
blockHash: update.blockHash,
|
|
616
|
+
blockNumber: update.height,
|
|
617
|
+
transactionHash: update.transactionHash,
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
status = TimeoutStatus.DESTINATION_FINALIZED_TIMEOUT;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case TimeoutStatus.DESTINATION_FINALIZED_TIMEOUT: {
|
|
624
|
+
if (request.source !== self.config.hyperbridge.stateMachineId) {
|
|
625
|
+
const receipt = await hyperbridge.queryRequestReceipt(commitment);
|
|
626
|
+
if (!receipt) {
|
|
627
|
+
status = TimeoutStatus.HYPERBRIDGE_TIMED_OUT;
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const update = (await self.queryStateMachineUpdateByTimestamp({
|
|
632
|
+
statemachineId: request.dest,
|
|
633
|
+
commitmentTimestamp: request.timeoutTimestamp,
|
|
634
|
+
chain: self.config.hyperbridge.stateMachineId,
|
|
635
|
+
}));
|
|
636
|
+
const proof = await destChain.queryStateProof(BigInt(update.height), [
|
|
637
|
+
destChain.requestReceiptKey(commitment),
|
|
638
|
+
]);
|
|
639
|
+
let { blockHash, transactionHash, blockNumber } = await hyperbridge.submitUnsigned({
|
|
640
|
+
kind: "TimeoutPostRequest",
|
|
641
|
+
proof: {
|
|
642
|
+
proof,
|
|
643
|
+
height: BigInt(update.height),
|
|
644
|
+
stateMachine: request.dest,
|
|
645
|
+
consensusStateId: self.config.dest.consensusStateId,
|
|
646
|
+
},
|
|
647
|
+
requests: [
|
|
648
|
+
{
|
|
649
|
+
source: request.source,
|
|
650
|
+
dest: request.dest,
|
|
651
|
+
from: request.from,
|
|
652
|
+
to: request.to,
|
|
653
|
+
nonce: request.nonce,
|
|
654
|
+
body: request.body,
|
|
655
|
+
timeoutTimestamp: request.timeoutTimestamp,
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
});
|
|
659
|
+
status =
|
|
660
|
+
request.source === self.config.hyperbridge.stateMachineId
|
|
661
|
+
? TimeoutStatus.TIMED_OUT
|
|
662
|
+
: TimeoutStatus.HYPERBRIDGE_TIMED_OUT;
|
|
663
|
+
yield {
|
|
664
|
+
status,
|
|
665
|
+
metadata: {
|
|
666
|
+
blockHash,
|
|
667
|
+
transactionHash,
|
|
668
|
+
blockNumber,
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
case TimeoutStatus.HYPERBRIDGE_TIMED_OUT: {
|
|
674
|
+
const hasDelivered = request.statuses.some((item) => item.status === RequestStatus.HYPERBRIDGE_DELIVERED);
|
|
675
|
+
let update;
|
|
676
|
+
if (!hasDelivered) {
|
|
677
|
+
// if request was never delivered to Hyperbridge
|
|
678
|
+
// then query for any state machine update > requestTimestamp
|
|
679
|
+
while (!update) {
|
|
680
|
+
await sleep(self.config.pollInterval);
|
|
681
|
+
update = await self.queryStateMachineUpdateByTimestamp({
|
|
682
|
+
statemachineId: self.config.hyperbridge.stateMachineId,
|
|
683
|
+
commitmentTimestamp: request.timeoutTimestamp,
|
|
684
|
+
chain: request.source,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
let req;
|
|
690
|
+
while (!req) {
|
|
691
|
+
await sleep(self.config.pollInterval);
|
|
692
|
+
req = await self.queryRequest(hash);
|
|
693
|
+
}
|
|
694
|
+
const timeout = req.statuses
|
|
695
|
+
.sort((a, b) => COMBINED_STATUS_WEIGHTS[a.status] - COMBINED_STATUS_WEIGHTS[b.status])
|
|
696
|
+
.pop();
|
|
697
|
+
if (!timeout || timeout.status !== TimeoutStatus.HYPERBRIDGE_TIMED_OUT)
|
|
698
|
+
break;
|
|
699
|
+
while (!update) {
|
|
700
|
+
await sleep(self.config.pollInterval);
|
|
701
|
+
update = await self.queryStateMachineUpdateByHeight({
|
|
702
|
+
statemachineId: self.config.hyperbridge.stateMachineId,
|
|
703
|
+
height: timeout.metadata.blockNumber,
|
|
704
|
+
chain: request.source,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const proof = await hyperbridge.queryStateProof(BigInt(update.height), [
|
|
709
|
+
hyperbridge.requestReceiptKey(commitment),
|
|
710
|
+
]);
|
|
711
|
+
const sourceChain = await getChain(self.config.source);
|
|
712
|
+
let calldata = sourceChain.encode({
|
|
713
|
+
kind: "TimeoutPostRequest",
|
|
714
|
+
proof: {
|
|
715
|
+
proof,
|
|
716
|
+
height: BigInt(update.height),
|
|
717
|
+
stateMachine: self.config.hyperbridge.stateMachineId,
|
|
718
|
+
consensusStateId: self.config.hyperbridge.consensusStateId,
|
|
719
|
+
},
|
|
720
|
+
requests: [
|
|
721
|
+
{
|
|
722
|
+
source: request.source,
|
|
723
|
+
dest: request.dest,
|
|
724
|
+
from: request.from,
|
|
725
|
+
to: request.to,
|
|
726
|
+
nonce: request.nonce,
|
|
727
|
+
body: request.body,
|
|
728
|
+
timeoutTimestamp: request.timeoutTimestamp,
|
|
729
|
+
},
|
|
730
|
+
],
|
|
731
|
+
});
|
|
732
|
+
yield {
|
|
733
|
+
status: TimeoutStatus.HYPERBRIDGE_FINALIZED_TIMEOUT,
|
|
734
|
+
metadata: {
|
|
735
|
+
transactionHash: update.transactionHash,
|
|
736
|
+
blockNumber: update.blockNumber,
|
|
737
|
+
blockHash: update.blockHash,
|
|
738
|
+
calldata,
|
|
739
|
+
},
|
|
740
|
+
};
|
|
741
|
+
status = TimeoutStatus.HYPERBRIDGE_FINALIZED_TIMEOUT;
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
case TimeoutStatus.HYPERBRIDGE_FINALIZED_TIMEOUT:
|
|
745
|
+
case TimeoutStatus.TIMED_OUT:
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Executes an async operation with exponential backoff retry
|
|
752
|
+
* @param operation - Async function to execute
|
|
753
|
+
* @param retryConfig - Optional retry configuration
|
|
754
|
+
* @returns Result of the operation
|
|
755
|
+
* @throws Last encountered error after all retries are exhausted
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* const result = await this.withRetry(() => this.queryStatus(hash));
|
|
759
|
+
*/
|
|
760
|
+
async withRetry(operation, retryConfig = this.defaultRetryConfig) {
|
|
761
|
+
let lastError;
|
|
762
|
+
for (let i = 0; i < retryConfig.maxRetries; i++) {
|
|
763
|
+
try {
|
|
764
|
+
return await operation();
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
lastError = error;
|
|
768
|
+
await new Promise((resolve) => setTimeout(resolve, retryConfig.backoffMs * Math.pow(2, i)));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
throw lastError;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
//# sourceMappingURL=client.js.map
|