@mysten-incubation/hashi 0.0.1 → 0.1.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/CHANGELOG.md +12 -0
- package/LICENSE +201 -0
- package/README.md +58 -0
- package/dist/bitcoin.d.mts +13 -1
- package/dist/bitcoin.mjs +25 -2
- package/dist/btc-rpc.mjs +39 -0
- package/dist/client.d.mts +80 -10
- package/dist/client.mjs +442 -73
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/types.d.mts +72 -1
- package/dist/util.mjs +1 -1
- package/package.json +63 -62
package/dist/client.mjs
CHANGED
|
@@ -9,16 +9,54 @@ import { cancelWithdrawal, requestWithdrawal } from "./contracts/hashi/withdraw.
|
|
|
9
9
|
import { DUST_RELAY_MIN_VALUE, NETWORK_CONFIG } from "./constants.mjs";
|
|
10
10
|
import { AmountBelowMinimumError, HashiConfigError, HashiFetchError, HashiPausedError, InvalidParamsError } from "./errors.mjs";
|
|
11
11
|
import { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, generateDepositAddress } from "./bitcoin.mjs";
|
|
12
|
+
import { getTxConfirmations, lookupAllVouts, lookupVout } from "./btc-rpc.mjs";
|
|
12
13
|
import { assertHex32, entry, reverseTxidBytes } from "./util.mjs";
|
|
13
14
|
import { TypeTagSerializer, bcs } from "@mysten/sui/bcs";
|
|
14
|
-
import { deriveDynamicFieldID, fromHex
|
|
15
|
+
import { deriveDynamicFieldID, fromHex } from "@mysten/sui/utils";
|
|
16
|
+
import { base58 } from "@scure/base";
|
|
15
17
|
import { Transaction, coinWithBalance } from "@mysten/sui/transactions";
|
|
16
18
|
|
|
17
19
|
//#region src/client.ts
|
|
20
|
+
/** ObjectBag dynamic field name type (Wrapper<address> for dynamic_object_field lookups). */
|
|
21
|
+
const OBJECT_BAG_ADDRESS_TYPE = "0x0000000000000000000000000000000000000000000000000000000000000002::dynamic_object_field::Wrapper<address>";
|
|
18
22
|
/** Max value of an unsigned 32-bit integer; vout is a u32 on the Bitcoin side. */
|
|
19
23
|
const U32_MAX = 4294967295;
|
|
20
|
-
/** Max objects per `getObjects` call
|
|
21
|
-
const GET_OBJECTS_BATCH =
|
|
24
|
+
/** Max objects per `getObjects` call. */
|
|
25
|
+
const GET_OBJECTS_BATCH = 500;
|
|
26
|
+
const GRAPHQL_URLS = {
|
|
27
|
+
devnet: "https://fullnode.devnet.sui.io:443/graphql",
|
|
28
|
+
testnet: "https://fullnode.testnet.sui.io:443/graphql",
|
|
29
|
+
mainnet: "https://fullnode.mainnet.sui.io:443/graphql",
|
|
30
|
+
localnet: "http://127.0.0.1:9000/graphql"
|
|
31
|
+
};
|
|
32
|
+
function defaultGraphqlUrl(network) {
|
|
33
|
+
return GRAPHQL_URLS[network] ?? GRAPHQL_URLS["mainnet"];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Recognize the per-object errors that mean "the object (or dynamic field)
|
|
37
|
+
* genuinely does not exist" — the only Error shape that `findUsedUtxos` is
|
|
38
|
+
* allowed to treat as a pool miss. Anything else (deleted, displayError,
|
|
39
|
+
* unknown) must propagate so other per-object failures can't be silently
|
|
40
|
+
* downgraded to "not used."
|
|
41
|
+
*
|
|
42
|
+
* Two transports, two shapes:
|
|
43
|
+
* - JSON-RPC returns a typed `ObjectError` whose `.code` is `notExists` or
|
|
44
|
+
* `dynamicFieldNotFound`. `ObjectError` is not re-exported from
|
|
45
|
+
* `@mysten/sui/client`, so the guard duck-types the field.
|
|
46
|
+
* - gRPC stringifies the per-object error into `new Error(message)` with no
|
|
47
|
+
* code (see the `TODO: improve error handling` in `@mysten/sui/grpc/core.ts`).
|
|
48
|
+
* For now the only signal is the message, which the Sui ledger service
|
|
49
|
+
* returns as exactly `Object <id> not found` for missing objects.
|
|
50
|
+
*
|
|
51
|
+
* Transport-level failures don't show up here — they reject the whole
|
|
52
|
+
* `getObjects` promise rather than appearing in the result array.
|
|
53
|
+
*/
|
|
54
|
+
const GRPC_NOT_FOUND_MESSAGE_RE = /^Object 0x[0-9a-f]+ not found$/i;
|
|
55
|
+
function isObjectNotFoundError(err) {
|
|
56
|
+
const code = err.code;
|
|
57
|
+
if (code === "notExists" || code === "dynamicFieldNotFound") return true;
|
|
58
|
+
return code === void 0 && GRPC_NOT_FOUND_MESSAGE_RE.test(err.message);
|
|
59
|
+
}
|
|
22
60
|
function hashi({ name = "hashi", ...options }) {
|
|
23
61
|
return {
|
|
24
62
|
name,
|
|
@@ -50,11 +88,13 @@ var HashiClient = class {
|
|
|
50
88
|
#hashiObjectId;
|
|
51
89
|
#packageId;
|
|
52
90
|
#bitcoinNetwork;
|
|
53
|
-
|
|
91
|
+
#btcRpcUrl;
|
|
92
|
+
#graphqlUrl;
|
|
93
|
+
constructor({ client, network, hashiObjectId, packageId, bitcoinNetwork, btcRpcUrl, graphqlUrl }) {
|
|
54
94
|
this.tx = {
|
|
55
95
|
deposit: (params) => {
|
|
56
96
|
const tx = new Transaction();
|
|
57
|
-
const internalTxid = reverseTxidBytes(params.txid)
|
|
97
|
+
const internalTxid = `0x${reverseTxidBytes(params.txid)}`;
|
|
58
98
|
for (const { vout, amountSats } of params.utxos) {
|
|
59
99
|
const utxoId$1 = tx.add(utxoId({
|
|
60
100
|
package: this.#packageId,
|
|
@@ -175,12 +215,14 @@ var HashiClient = class {
|
|
|
175
215
|
const fieldIds = [];
|
|
176
216
|
for (const u of utxos) {
|
|
177
217
|
const keyBcs = UtxoId.serialize({
|
|
178
|
-
txid: reverseTxidBytes(u.txid)
|
|
218
|
+
txid: `0x${reverseTxidBytes(u.txid)}`,
|
|
179
219
|
vout: u.vout
|
|
180
220
|
}).toBytes();
|
|
181
221
|
fieldIds.push(deriveDynamicFieldID(activePoolId, typeTag, keyBcs), deriveDynamicFieldID(spentPoolId, typeTag, keyBcs));
|
|
182
222
|
}
|
|
183
223
|
const objects = await this.#batchGetObjects(fieldIds);
|
|
224
|
+
if (objects.length !== fieldIds.length) throw new HashiFetchError(`findUsedUtxos: getObjects returned ${objects.length} results, expected ${fieldIds.length}`, this.#hashiObjectId);
|
|
225
|
+
for (const result of objects) if (result instanceof Error && !isObjectNotFoundError(result)) throw result;
|
|
184
226
|
return utxos.map((u, i) => {
|
|
185
227
|
const inActivePool = !(objects[i * 2] instanceof Error);
|
|
186
228
|
const inSpentPool = !(objects[i * 2 + 1] instanceof Error);
|
|
@@ -192,86 +234,206 @@ var HashiClient = class {
|
|
|
192
234
|
};
|
|
193
235
|
});
|
|
194
236
|
},
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
237
|
+
balance: async (owner) => {
|
|
238
|
+
const btcType = `${this.#packageId}::btc::BTC`;
|
|
239
|
+
const { balance } = await this.#client.core.getBalance({
|
|
240
|
+
owner,
|
|
241
|
+
coinType: btcType
|
|
242
|
+
});
|
|
243
|
+
let coinObjectCount = 0;
|
|
244
|
+
let cursor = null;
|
|
245
|
+
let hasNextPage = true;
|
|
246
|
+
while (hasNextPage) {
|
|
247
|
+
const page = await this.#client.core.listCoins({
|
|
248
|
+
owner,
|
|
249
|
+
coinType: btcType,
|
|
250
|
+
cursor: cursor ?? void 0
|
|
251
|
+
});
|
|
252
|
+
coinObjectCount += page.objects.length;
|
|
253
|
+
cursor = page.cursor;
|
|
254
|
+
hasNextPage = page.hasNextPage;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
totalBalance: BigInt(balance.balance ?? "0"),
|
|
258
|
+
coinObjectCount
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
depositStatus: async (suiTxDigest) => {
|
|
262
|
+
const txResult = await this.#client.core.getTransaction({
|
|
263
|
+
digest: suiTxDigest,
|
|
264
|
+
include: { events: true }
|
|
265
|
+
});
|
|
266
|
+
const txData = txResult.Transaction ?? txResult.FailedTransaction;
|
|
267
|
+
if (!txData?.events) return null;
|
|
268
|
+
const depositEvent = txData.events.find((e) => e.eventType.includes("::deposit::DepositRequestedEvent"));
|
|
269
|
+
if (!depositEvent?.json) return null;
|
|
270
|
+
const parsed = depositEvent.json;
|
|
271
|
+
let status = "unknown";
|
|
198
272
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
273
|
+
await DepositRequest.get({
|
|
274
|
+
client: this.#client,
|
|
275
|
+
objectId: parsed.request_id
|
|
276
|
+
});
|
|
277
|
+
const requestsBagId = (await this.#fetchBitcoinState()).deposit_queue.requests.id;
|
|
278
|
+
status = (await this.#client.core.getDynamicField({
|
|
279
|
+
parentId: requestsBagId,
|
|
201
280
|
name: {
|
|
202
|
-
type:
|
|
203
|
-
bcs: bcs.Address.serialize(
|
|
281
|
+
type: OBJECT_BAG_ADDRESS_TYPE,
|
|
282
|
+
bcs: bcs.Address.serialize(parsed.request_id).toBytes()
|
|
204
283
|
}
|
|
284
|
+
}).catch(() => null))?.dynamicField ? "pending" : "confirmed";
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (err instanceof Error && isObjectNotFoundError(err)) status = "expired";
|
|
287
|
+
else throw err;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
requestId: parsed.request_id,
|
|
291
|
+
amountSats: BigInt(parsed.amount),
|
|
292
|
+
recipient: parsed.derivation_path,
|
|
293
|
+
btcTxid: reverseTxidBytes(parsed.utxo_id.txid),
|
|
294
|
+
btcVout: parsed.utxo_id.vout,
|
|
295
|
+
timestampMs: BigInt(parsed.timestamp_ms),
|
|
296
|
+
status,
|
|
297
|
+
suiTxDigest
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
withdrawalStatus: async (suiTxDigest) => {
|
|
301
|
+
const txResult = await this.#client.core.getTransaction({
|
|
302
|
+
digest: suiTxDigest,
|
|
303
|
+
include: { events: true }
|
|
304
|
+
});
|
|
305
|
+
const txData = txResult.Transaction ?? txResult.FailedTransaction;
|
|
306
|
+
if (!txData?.events) return null;
|
|
307
|
+
const withdrawEvent = txData.events.find((e) => e.eventType.includes("::withdrawal_queue::WithdrawalRequestedEvent"));
|
|
308
|
+
if (!withdrawEvent?.json) return null;
|
|
309
|
+
const parsed = withdrawEvent.json;
|
|
310
|
+
let status = "Requested";
|
|
311
|
+
let btcTxid = null;
|
|
312
|
+
try {
|
|
313
|
+
const reqObj = await WithdrawalRequest.get({
|
|
314
|
+
client: this.#client,
|
|
315
|
+
objectId: parsed.request_id
|
|
205
316
|
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
317
|
+
status = reqObj.json.status.$kind;
|
|
318
|
+
const withdrawalTxnId = reqObj.json.withdrawal_txn_id;
|
|
319
|
+
if (withdrawalTxnId && (status === "Processing" || status === "Signed" || status === "Confirmed")) try {
|
|
320
|
+
btcTxid = reverseTxidBytes((await WithdrawalTransaction.get({
|
|
321
|
+
client: this.#client,
|
|
322
|
+
objectId: withdrawalTxnId
|
|
323
|
+
})).json.txid);
|
|
324
|
+
} catch {}
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (err instanceof Error && isObjectNotFoundError(err)) status = "cancelled";
|
|
327
|
+
else throw err;
|
|
209
328
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
329
|
+
return {
|
|
330
|
+
requestId: parsed.request_id,
|
|
331
|
+
btcAmountSats: BigInt(parsed.btc_amount),
|
|
332
|
+
bitcoinAddress: new Uint8Array(parsed.bitcoin_address),
|
|
333
|
+
sender: parsed.requester_address,
|
|
334
|
+
timestampMs: BigInt(parsed.timestamp_ms),
|
|
335
|
+
status,
|
|
336
|
+
suiTxDigest,
|
|
337
|
+
btcTxid
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
depositGasEstimate: async (sender) => {
|
|
341
|
+
const dummyAmount = (await this.view.all()).bitcoinDepositMinimum + 1n;
|
|
342
|
+
const dummyTxid = "0x" + "01".repeat(32);
|
|
343
|
+
const tx = new Transaction();
|
|
344
|
+
const utxoId$1 = tx.add(utxoId({
|
|
345
|
+
package: this.#packageId,
|
|
346
|
+
arguments: {
|
|
347
|
+
txid: dummyTxid,
|
|
348
|
+
vout: 0
|
|
349
|
+
}
|
|
350
|
+
}));
|
|
351
|
+
const utxo$1 = tx.add(utxo({
|
|
352
|
+
package: this.#packageId,
|
|
353
|
+
arguments: {
|
|
354
|
+
utxoId: utxoId$1,
|
|
355
|
+
amount: dummyAmount,
|
|
356
|
+
derivationPath: sender
|
|
357
|
+
}
|
|
358
|
+
}));
|
|
359
|
+
tx.add(this.call.deposit({ utxo: utxo$1 }));
|
|
360
|
+
tx.setSender(sender);
|
|
361
|
+
return { gasEstimateMist: await this.#estimateGas(tx) };
|
|
362
|
+
},
|
|
363
|
+
withdrawalFees: async (sender) => {
|
|
364
|
+
const snap = await this.view.all();
|
|
365
|
+
let gasEstimateMist = 0n;
|
|
366
|
+
if (sender) {
|
|
367
|
+
const dummyAmount = snap.bitcoinWithdrawalMinimum + 1n;
|
|
368
|
+
const btcType = `${this.#packageId}::btc::BTC`;
|
|
369
|
+
const tx = new Transaction();
|
|
370
|
+
const coin = tx.add(coinWithBalance({
|
|
371
|
+
type: btcType,
|
|
372
|
+
balance: dummyAmount,
|
|
373
|
+
useGasCoin: false
|
|
374
|
+
}));
|
|
375
|
+
const [balance] = tx.moveCall({
|
|
376
|
+
package: "0x2",
|
|
377
|
+
module: "coin",
|
|
378
|
+
function: "into_balance",
|
|
379
|
+
typeArguments: [btcType],
|
|
380
|
+
arguments: [coin]
|
|
216
381
|
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
382
|
+
tx.add(this.call.requestWithdrawal({
|
|
383
|
+
btc: balance,
|
|
384
|
+
bitcoinAddress: Array(20).fill(0)
|
|
385
|
+
}));
|
|
386
|
+
tx.setSender(sender);
|
|
387
|
+
gasEstimateMist = await this.#estimateGas(tx);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
worstCaseNetworkFeeSats: snap.worstCaseNetworkFee,
|
|
391
|
+
withdrawalMinimumSats: snap.bitcoinWithdrawalMinimum,
|
|
392
|
+
gasEstimateMist
|
|
393
|
+
};
|
|
394
|
+
},
|
|
395
|
+
transactionHistory: async (suiAddress) => {
|
|
396
|
+
const btcState = await this.#fetchBitcoinState();
|
|
397
|
+
const confirmedIds = /* @__PURE__ */ new Set();
|
|
222
398
|
const items = [];
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
timestampMs: BigInt(parsed.timestamp_ms),
|
|
233
|
-
suiTxDigest: `0x${toHex(new Uint8Array(parsed.sui_tx_digest))}`,
|
|
234
|
-
amountSats: BigInt(parsed.utxo.amount),
|
|
235
|
-
btcTxid: reverseTxidBytes(parsed.utxo.id.txid),
|
|
236
|
-
btcVout: parsed.utxo.id.vout,
|
|
237
|
-
approved: parsed.approval_cert !== null,
|
|
238
|
-
approvalTimestampMs: parsed.approval_timestamp_ms !== null ? BigInt(parsed.approval_timestamp_ms) : null
|
|
239
|
-
});
|
|
240
|
-
} else if (obj.type.includes("::withdrawal_queue::WithdrawalRequest")) {
|
|
241
|
-
const parsed = WithdrawalRequest.parse(obj.content);
|
|
242
|
-
const txnId = parsed.withdrawal_txn_id ?? null;
|
|
243
|
-
items.push({
|
|
244
|
-
kind: "withdrawal",
|
|
245
|
-
requestId: parsed.id,
|
|
246
|
-
sender: parsed.sender,
|
|
247
|
-
btcAmountSats: BigInt(parsed.btc_amount),
|
|
248
|
-
bitcoinAddress: new Uint8Array(parsed.bitcoin_address),
|
|
249
|
-
timestampMs: BigInt(parsed.timestamp_ms),
|
|
250
|
-
suiTxDigest: `0x${toHex(new Uint8Array(parsed.sui_tx_digest))}`,
|
|
251
|
-
status: parsed.status.$kind,
|
|
252
|
-
withdrawalTxnId: txnId,
|
|
253
|
-
btcTxid: null
|
|
254
|
-
});
|
|
255
|
-
if (txnId) withdrawalTxnLookups.push({
|
|
256
|
-
itemIndex: items.length - 1,
|
|
257
|
-
txnId
|
|
258
|
-
});
|
|
399
|
+
const userBagId = await this.#fetchUserRequestsBagId(btcState.user_requests.id, suiAddress);
|
|
400
|
+
if (userBagId !== null) {
|
|
401
|
+
const requestIds = await this.#listAllDynamicFieldAddressKeys(userBagId);
|
|
402
|
+
if (requestIds.length > 0) {
|
|
403
|
+
const objects = await this.#batchGetObjects(requestIds, { content: true });
|
|
404
|
+
const classified = this.#classifyRequestObjects(objects);
|
|
405
|
+
items.push(...classified.items);
|
|
406
|
+
await this.#populateWithdrawalBtcTxids(items, classified.withdrawalTxnLookups);
|
|
407
|
+
for (const id of requestIds) confirmedIds.add(id);
|
|
259
408
|
}
|
|
260
409
|
}
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const item = items[withdrawalTxnLookups[i].itemIndex];
|
|
269
|
-
item.btcTxid = reverseTxidBytes(parsed.txid);
|
|
410
|
+
try {
|
|
411
|
+
const depositEventType = `${this.#packageId}::deposit::DepositRequestedEvent`;
|
|
412
|
+
const pendingIds = (await this.#queryEventRequestIds(suiAddress, depositEventType)).filter((id) => !confirmedIds.has(id));
|
|
413
|
+
if (pendingIds.length > 0) {
|
|
414
|
+
const objects = await this.#batchGetObjects(pendingIds, { content: true });
|
|
415
|
+
const classified = this.#classifyRequestObjects(objects);
|
|
416
|
+
items.push(...classified.items);
|
|
270
417
|
}
|
|
271
|
-
}
|
|
418
|
+
} catch {}
|
|
419
|
+
items.sort((a, b) => Number(b.timestampMs - a.timestampMs));
|
|
272
420
|
return items;
|
|
273
421
|
}
|
|
274
422
|
};
|
|
423
|
+
this.bitcoin = {
|
|
424
|
+
lookupVout: async (txid, depositAddress) => {
|
|
425
|
+
this.#requireBtcRpc();
|
|
426
|
+
return lookupVout(this.#btcRpcUrl, txid, depositAddress);
|
|
427
|
+
},
|
|
428
|
+
lookupAllVouts: async (txid, depositAddress) => {
|
|
429
|
+
this.#requireBtcRpc();
|
|
430
|
+
return lookupAllVouts(this.#btcRpcUrl, txid, depositAddress);
|
|
431
|
+
},
|
|
432
|
+
confirmations: async (txid) => {
|
|
433
|
+
this.#requireBtcRpc();
|
|
434
|
+
return getTxConfirmations(this.#btcRpcUrl, txid);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
275
437
|
const config = NETWORK_CONFIG[network];
|
|
276
438
|
const resolvedObjectId = hashiObjectId ?? config?.hashiObjectId;
|
|
277
439
|
const resolvedPackageId = packageId ?? config?.packageId;
|
|
@@ -280,6 +442,8 @@ var HashiClient = class {
|
|
|
280
442
|
this.#hashiObjectId = resolvedObjectId;
|
|
281
443
|
this.#packageId = resolvedPackageId;
|
|
282
444
|
this.#bitcoinNetwork = bitcoinNetwork ?? config?.bitcoinNetwork ?? "testnet";
|
|
445
|
+
this.#btcRpcUrl = btcRpcUrl;
|
|
446
|
+
this.#graphqlUrl = graphqlUrl ?? defaultGraphqlUrl(network);
|
|
283
447
|
}
|
|
284
448
|
/**
|
|
285
449
|
* Generates a unique Bitcoin P2TR deposit address for a Sui address.
|
|
@@ -478,6 +642,52 @@ var HashiClient = class {
|
|
|
478
642
|
};
|
|
479
643
|
}
|
|
480
644
|
/**
|
|
645
|
+
* Poll deposit status until it reaches a terminal state (confirmed or expired).
|
|
646
|
+
*/
|
|
647
|
+
async waitForDeposit(suiTxDigest, options) {
|
|
648
|
+
const intervalMs = options?.intervalMs ?? 15e3;
|
|
649
|
+
const signal = options?.signal;
|
|
650
|
+
while (!signal?.aborted) {
|
|
651
|
+
const info = await this.view.depositStatus(suiTxDigest);
|
|
652
|
+
if (!info) throw new Error(`Deposit not found for digest: ${suiTxDigest}`);
|
|
653
|
+
if (info.status === "confirmed" || info.status === "expired") return info;
|
|
654
|
+
await sleep(intervalMs, signal);
|
|
655
|
+
}
|
|
656
|
+
throw new Error("Polling aborted");
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Poll withdrawal status until it reaches a terminal state (confirmed or cancelled).
|
|
660
|
+
*/
|
|
661
|
+
async waitForWithdrawal(suiTxDigest, options) {
|
|
662
|
+
const intervalMs = options?.intervalMs ?? 15e3;
|
|
663
|
+
const signal = options?.signal;
|
|
664
|
+
while (!signal?.aborted) {
|
|
665
|
+
const info = await this.view.withdrawalStatus(suiTxDigest);
|
|
666
|
+
if (!info) throw new Error(`Withdrawal not found for digest: ${suiTxDigest}`);
|
|
667
|
+
if (info.status === "Confirmed" || info.status === "cancelled") return info;
|
|
668
|
+
await sleep(intervalMs, signal);
|
|
669
|
+
}
|
|
670
|
+
throw new Error("Polling aborted");
|
|
671
|
+
}
|
|
672
|
+
#requireBtcRpc() {
|
|
673
|
+
if (!this.#btcRpcUrl) throw new Error("btcRpcUrl is required for Bitcoin RPC operations. Pass it in HashiClientOptions.");
|
|
674
|
+
}
|
|
675
|
+
async #estimateGas(tx) {
|
|
676
|
+
try {
|
|
677
|
+
const result = await this.#client.core.simulateTransaction({
|
|
678
|
+
transaction: tx,
|
|
679
|
+
include: { effects: true }
|
|
680
|
+
});
|
|
681
|
+
const simTx = result.Transaction ?? result.FailedTransaction;
|
|
682
|
+
if (simTx?.effects?.gasUsed) {
|
|
683
|
+
const gas = simTx.effects.gasUsed;
|
|
684
|
+
const total = BigInt(gas.computationCost) + BigInt(gas.storageCost) - BigInt(gas.storageRebate);
|
|
685
|
+
return total > 0n ? total * 120n / 100n : 0n;
|
|
686
|
+
}
|
|
687
|
+
} catch {}
|
|
688
|
+
return 0n;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
481
691
|
* Fetches the `BitcoinState` dynamic field from the Hashi shared object.
|
|
482
692
|
* Returns the BCS-parsed struct whose nested Bag/Table IDs are used by
|
|
483
693
|
* `findUsedUtxos` and `transactionHistory`.
|
|
@@ -500,6 +710,41 @@ var HashiClient = class {
|
|
|
500
710
|
return BitcoinState.parse(new Uint8Array(dynamicField.value.bcs));
|
|
501
711
|
}
|
|
502
712
|
/**
|
|
713
|
+
* Resolve the Bag id holding a user's request IDs from `user_requests`,
|
|
714
|
+
* or `null` if the user has never had a request.
|
|
715
|
+
*/
|
|
716
|
+
async #fetchUserRequestsBagId(tableId, suiAddress) {
|
|
717
|
+
try {
|
|
718
|
+
const { dynamicField } = await this.#client.core.getDynamicField({
|
|
719
|
+
parentId: tableId,
|
|
720
|
+
name: {
|
|
721
|
+
type: "address",
|
|
722
|
+
bcs: bcs.Address.serialize(suiAddress).toBytes()
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
return Bag.parse(new Uint8Array(dynamicField.value.bcs)).id;
|
|
726
|
+
} catch {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Enumerate every `address`-keyed dynamic field on `parentId`, paginating
|
|
732
|
+
* through `listDynamicFields` until exhausted.
|
|
733
|
+
*/
|
|
734
|
+
async #listAllDynamicFieldAddressKeys(parentId) {
|
|
735
|
+
const keys = [];
|
|
736
|
+
let cursor = null;
|
|
737
|
+
do {
|
|
738
|
+
const page = await this.#client.core.listDynamicFields({
|
|
739
|
+
parentId,
|
|
740
|
+
cursor: cursor ?? void 0
|
|
741
|
+
});
|
|
742
|
+
for (const df of page.dynamicFields) keys.push(bcs.Address.parse(new Uint8Array(df.name.bcs)));
|
|
743
|
+
cursor = page.hasNextPage ? page.cursor : null;
|
|
744
|
+
} while (cursor);
|
|
745
|
+
return keys;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
503
748
|
* Batch-fetches objects in chunks of `GET_OBJECTS_BATCH`, concatenating
|
|
504
749
|
* the results. Each element is either an object or an `Error` (for
|
|
505
750
|
* missing / deleted objects).
|
|
@@ -516,7 +761,131 @@ var HashiClient = class {
|
|
|
516
761
|
}
|
|
517
762
|
return results;
|
|
518
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Query the Sui GraphQL endpoint for events of a given type emitted by
|
|
766
|
+
* a sender. Returns the `request_id` from each event's JSON payload,
|
|
767
|
+
* paginating through all results.
|
|
768
|
+
*/
|
|
769
|
+
async #queryEventRequestIds(sender, eventType) {
|
|
770
|
+
const ids = [];
|
|
771
|
+
let cursor = null;
|
|
772
|
+
let hasMore = true;
|
|
773
|
+
while (hasMore) {
|
|
774
|
+
const query = `{
|
|
775
|
+
events(
|
|
776
|
+
filter: { sender: "${sender}", type: "${eventType}" }
|
|
777
|
+
first: 50${cursor ? `, after: "${cursor}"` : ""}
|
|
778
|
+
) {
|
|
779
|
+
nodes { contents { json } }
|
|
780
|
+
pageInfo { hasNextPage endCursor }
|
|
781
|
+
}
|
|
782
|
+
}`;
|
|
783
|
+
const res = await fetch(this.#graphqlUrl, {
|
|
784
|
+
method: "POST",
|
|
785
|
+
headers: { "Content-Type": "application/json" },
|
|
786
|
+
body: JSON.stringify({ query })
|
|
787
|
+
});
|
|
788
|
+
if (!res.ok) throw new Error(`GraphQL request failed: ${res.status}`);
|
|
789
|
+
const body = await res.json();
|
|
790
|
+
if (body.errors?.length) throw new Error(`GraphQL error: ${body.errors[0].message}`);
|
|
791
|
+
const events = body.data?.events;
|
|
792
|
+
if (!events) break;
|
|
793
|
+
for (const node of events.nodes) ids.push(node.contents.json.request_id);
|
|
794
|
+
hasMore = events.pageInfo.hasNextPage;
|
|
795
|
+
cursor = events.pageInfo.endCursor ?? null;
|
|
796
|
+
}
|
|
797
|
+
return ids;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Classify a batch of request objects into deposit / withdrawal history
|
|
801
|
+
* items. Errors in the batch are skipped (the request object may have
|
|
802
|
+
* been deleted between the bag enumeration and the fetch). Returns the
|
|
803
|
+
* items plus the indices that need a follow-up `WithdrawalTransaction`
|
|
804
|
+
* fetch to populate `btcTxid`.
|
|
805
|
+
*/
|
|
806
|
+
#classifyRequestObjects(objects) {
|
|
807
|
+
const items = [];
|
|
808
|
+
const withdrawalTxnLookups = [];
|
|
809
|
+
const depositRequestType = `${this.#packageId}::deposit_queue::DepositRequest`;
|
|
810
|
+
const withdrawalRequestType = `${this.#packageId}::withdrawal_queue::WithdrawalRequest`;
|
|
811
|
+
for (const obj of objects) {
|
|
812
|
+
if (obj instanceof Error) continue;
|
|
813
|
+
if (obj.type === depositRequestType) items.push(parseDepositHistoryItem(obj.content));
|
|
814
|
+
else if (obj.type === withdrawalRequestType) {
|
|
815
|
+
const item = parseWithdrawalHistoryItem(obj.content);
|
|
816
|
+
items.push(item);
|
|
817
|
+
if (item.withdrawalTxnId) withdrawalTxnLookups.push({
|
|
818
|
+
itemIndex: items.length - 1,
|
|
819
|
+
txnId: item.withdrawalTxnId
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
items,
|
|
825
|
+
withdrawalTxnLookups
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Batch-fetch `WithdrawalTransaction` objects for the withdrawal items
|
|
830
|
+
* that have a linked txn and overwrite their `btcTxid` in place. Errors
|
|
831
|
+
* in the batch leave `btcTxid` at the initial `null`.
|
|
832
|
+
*/
|
|
833
|
+
async #populateWithdrawalBtcTxids(items, lookups) {
|
|
834
|
+
if (lookups.length === 0) return;
|
|
835
|
+
const txnIds = lookups.map((l) => l.txnId);
|
|
836
|
+
const txnObjects = await this.#batchGetObjects(txnIds, { content: true });
|
|
837
|
+
for (let i = 0; i < lookups.length; i++) {
|
|
838
|
+
const txnObj = txnObjects[i];
|
|
839
|
+
if (txnObj instanceof Error) continue;
|
|
840
|
+
const parsed = WithdrawalTransaction.parse(txnObj.content);
|
|
841
|
+
const item = items[lookups[i].itemIndex];
|
|
842
|
+
item.btcTxid = reverseTxidBytes(parsed.txid);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
519
845
|
};
|
|
846
|
+
function parseDepositHistoryItem(content) {
|
|
847
|
+
const parsed = DepositRequest.parse(content);
|
|
848
|
+
return {
|
|
849
|
+
kind: "deposit",
|
|
850
|
+
requestId: parsed.id,
|
|
851
|
+
sender: parsed.sender,
|
|
852
|
+
timestampMs: BigInt(parsed.timestamp_ms),
|
|
853
|
+
suiTxDigest: base58.encode(new Uint8Array(parsed.sui_tx_digest)),
|
|
854
|
+
amountSats: BigInt(parsed.utxo.amount),
|
|
855
|
+
btcTxid: reverseTxidBytes(parsed.utxo.id.txid),
|
|
856
|
+
btcVout: parsed.utxo.id.vout,
|
|
857
|
+
approved: parsed.approval_cert !== null,
|
|
858
|
+
approvalTimestampMs: parsed.approval_timestamp_ms === null ? null : BigInt(parsed.approval_timestamp_ms)
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function parseWithdrawalHistoryItem(content) {
|
|
862
|
+
const parsed = WithdrawalRequest.parse(content);
|
|
863
|
+
return {
|
|
864
|
+
kind: "withdrawal",
|
|
865
|
+
requestId: parsed.id,
|
|
866
|
+
sender: parsed.sender,
|
|
867
|
+
btcAmountSats: BigInt(parsed.btc_amount),
|
|
868
|
+
bitcoinAddress: new Uint8Array(parsed.bitcoin_address),
|
|
869
|
+
timestampMs: BigInt(parsed.timestamp_ms),
|
|
870
|
+
suiTxDigest: base58.encode(new Uint8Array(parsed.sui_tx_digest)),
|
|
871
|
+
status: parsed.status.$kind,
|
|
872
|
+
withdrawalTxnId: parsed.withdrawal_txn_id ?? null,
|
|
873
|
+
btcTxid: null
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function sleep(ms, signal) {
|
|
877
|
+
return new Promise((resolve, reject) => {
|
|
878
|
+
if (signal?.aborted) {
|
|
879
|
+
reject(/* @__PURE__ */ new Error("Aborted"));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const timer = setTimeout(resolve, ms);
|
|
883
|
+
signal?.addEventListener("abort", () => {
|
|
884
|
+
clearTimeout(timer);
|
|
885
|
+
reject(/* @__PURE__ */ new Error("Aborted"));
|
|
886
|
+
}, { once: true });
|
|
887
|
+
});
|
|
888
|
+
}
|
|
520
889
|
|
|
521
890
|
//#endregion
|
|
522
891
|
export { HashiClient, hashi };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BitcoinNetwork, CancelWithdrawalParams, DepositHistoryItem, DepositParams, GovernanceConfig, HashiClientOptions, NetworkConfig, SuiNetwork, TransactionHistoryItem, UtxoId, UtxoOutput, UtxoUsageResult, WithdrawalHistoryItem, WithdrawalParams, WithdrawalStatus } from "./types.mjs";
|
|
1
|
+
import { BitcoinNetwork, CancelWithdrawalParams, DepositFees, DepositHistoryItem, DepositInfo, DepositParams, DepositStatus, GovernanceConfig, HashiClientOptions, HbtcBalance, NetworkConfig, SuiNetwork, TransactionHistoryItem, UtxoId, UtxoLookupResult, UtxoOutput, UtxoUsageResult, WaitOptions, WithdrawalFees, WithdrawalHistoryItem, WithdrawalInfo, WithdrawalParams, WithdrawalStatus } from "./types.mjs";
|
|
2
2
|
import { HashiClient, hashi } from "./client.mjs";
|
|
3
3
|
import { AmountBelowMinimumError, AmountViolation, HashiConfigError, HashiFetchError, HashiPausedError, InvalidBitcoinAddressCode, InvalidBitcoinAddressError, InvalidParamsError } from "./errors.mjs";
|
|
4
|
-
import { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress } from "./bitcoin.mjs";
|
|
5
|
-
export { AmountBelowMinimumError, type AmountViolation, type BitcoinNetwork, type CancelWithdrawalParams, type DepositHistoryItem, type DepositParams, type GovernanceConfig, HashiClient, type HashiClientOptions, HashiConfigError, HashiFetchError, HashiPausedError, type InvalidBitcoinAddressCode, InvalidBitcoinAddressError, InvalidParamsError, type NetworkConfig, type SuiNetwork, type TransactionHistoryItem, type UtxoId, type UtxoOutput, type UtxoUsageResult, type WithdrawalHistoryItem, type WithdrawalParams, type WithdrawalStatus, arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, hashi, taprootScriptPathAddress };
|
|
4
|
+
import { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress, witnessProgramToAddress } from "./bitcoin.mjs";
|
|
5
|
+
export { AmountBelowMinimumError, type AmountViolation, type BitcoinNetwork, type CancelWithdrawalParams, type DepositFees, type DepositHistoryItem, type DepositInfo, type DepositParams, type DepositStatus, type GovernanceConfig, HashiClient, type HashiClientOptions, HashiConfigError, HashiFetchError, HashiPausedError, type HbtcBalance, type InvalidBitcoinAddressCode, InvalidBitcoinAddressError, InvalidParamsError, type NetworkConfig, type SuiNetwork, type TransactionHistoryItem, type UtxoId, type UtxoLookupResult, type UtxoOutput, type UtxoUsageResult, type WaitOptions, type WithdrawalFees, type WithdrawalHistoryItem, type WithdrawalInfo, type WithdrawalParams, type WithdrawalStatus, arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, hashi, taprootScriptPathAddress, witnessProgramToAddress };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AmountBelowMinimumError, HashiConfigError, HashiFetchError, HashiPausedError, InvalidBitcoinAddressError, InvalidParamsError } from "./errors.mjs";
|
|
2
|
-
import { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress } from "./bitcoin.mjs";
|
|
2
|
+
import { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress, witnessProgramToAddress } from "./bitcoin.mjs";
|
|
3
3
|
import { HashiClient, hashi } from "./client.mjs";
|
|
4
4
|
|
|
5
|
-
export { AmountBelowMinimumError, HashiClient, HashiConfigError, HashiFetchError, HashiPausedError, InvalidBitcoinAddressError, InvalidParamsError, arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, hashi, taprootScriptPathAddress };
|
|
5
|
+
export { AmountBelowMinimumError, HashiClient, HashiConfigError, HashiFetchError, HashiPausedError, InvalidBitcoinAddressError, InvalidParamsError, arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, hashi, taprootScriptPathAddress, witnessProgramToAddress };
|