@mysten-incubation/hashi 0.0.2 → 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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @mysten-incubation/hashi
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 75fcdca: Fix btcTxid display values to strip the 0x prefix. Add GraphQL-based discovery of pending deposits to transaction history — confirmed requests still read from the on-chain user_requests index; in-flight deposits are discovered via DepositRequestedEvent queries and deduplicated. Bump GET_OBJECTS_BATCH to 500.
8
+
3
9
  ## 0.0.2
4
10
 
5
11
  ### Patch Changes
package/dist/client.d.mts CHANGED
@@ -36,7 +36,8 @@ declare class HashiClient {
36
36
  hashiObjectId,
37
37
  packageId,
38
38
  bitcoinNetwork,
39
- btcRpcUrl
39
+ btcRpcUrl,
40
+ graphqlUrl
40
41
  }: {
41
42
  client: ClientWithCoreApi;
42
43
  network: SuiNetwork;
@@ -44,6 +45,7 @@ declare class HashiClient {
44
45
  packageId?: string;
45
46
  bitcoinNetwork?: BitcoinNetwork;
46
47
  btcRpcUrl?: string;
48
+ graphqlUrl?: string;
47
49
  });
48
50
  /**
49
51
  * Generates a unique Bitcoin P2TR deposit address for a Sui address.
@@ -344,14 +346,9 @@ declare class HashiClient {
344
346
  withdrawalFees: (sender?: string) => Promise<WithdrawalFees>;
345
347
  /**
346
348
  * Fetch the unified transaction history (deposits + withdrawals) for
347
- * a Sui address. Reads from `BitcoinState.user_requests` a Table
348
- * that maps each user address to a Bag of request IDs — then batch-
349
- * fetches the request objects and dispatches by Move type.
350
- *
351
- * Deposit items include `btcTxid` / `btcVout` extracted from the
352
- * UTXO stored on the `DepositRequest`. Withdrawal items include the
353
- * BTC txid from the linked `WithdrawalTransaction` once the committee
354
- * has committed one.
349
+ * a Sui address. Confirmed requests come from the on-chain
350
+ * `user_requests` index; in-flight deposits are discovered via
351
+ * GraphQL `DepositRequestedEvent` queries (indexed by sender).
355
352
  */
356
353
  transactionHistory: (suiAddress: string) => Promise<TransactionHistoryItem[]>;
357
354
  };
package/dist/client.mjs CHANGED
@@ -21,8 +21,17 @@ import { Transaction, coinWithBalance } from "@mysten/sui/transactions";
21
21
  const OBJECT_BAG_ADDRESS_TYPE = "0x0000000000000000000000000000000000000000000000000000000000000002::dynamic_object_field::Wrapper<address>";
22
22
  /** Max value of an unsigned 32-bit integer; vout is a u32 on the Bitcoin side. */
23
23
  const U32_MAX = 4294967295;
24
- /** Max objects per `getObjects` call — Sui transports typically cap at 50. */
25
- const GET_OBJECTS_BATCH = 50;
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
+ }
26
35
  /**
27
36
  * Recognize the per-object errors that mean "the object (or dynamic field)
28
37
  * genuinely does not exist" — the only Error shape that `findUsedUtxos` is
@@ -80,11 +89,12 @@ var HashiClient = class {
80
89
  #packageId;
81
90
  #bitcoinNetwork;
82
91
  #btcRpcUrl;
83
- constructor({ client, network, hashiObjectId, packageId, bitcoinNetwork, btcRpcUrl }) {
92
+ #graphqlUrl;
93
+ constructor({ client, network, hashiObjectId, packageId, bitcoinNetwork, btcRpcUrl, graphqlUrl }) {
84
94
  this.tx = {
85
95
  deposit: (params) => {
86
96
  const tx = new Transaction();
87
- const internalTxid = reverseTxidBytes(params.txid);
97
+ const internalTxid = `0x${reverseTxidBytes(params.txid)}`;
88
98
  for (const { vout, amountSats } of params.utxos) {
89
99
  const utxoId$1 = tx.add(utxoId({
90
100
  package: this.#packageId,
@@ -205,7 +215,7 @@ var HashiClient = class {
205
215
  const fieldIds = [];
206
216
  for (const u of utxos) {
207
217
  const keyBcs = UtxoId.serialize({
208
- txid: reverseTxidBytes(u.txid),
218
+ txid: `0x${reverseTxidBytes(u.txid)}`,
209
219
  vout: u.vout
210
220
  }).toBytes();
211
221
  fieldIds.push(deriveDynamicFieldID(activePoolId, typeTag, keyBcs), deriveDynamicFieldID(spentPoolId, typeTag, keyBcs));
@@ -384,13 +394,29 @@ var HashiClient = class {
384
394
  },
385
395
  transactionHistory: async (suiAddress) => {
386
396
  const btcState = await this.#fetchBitcoinState();
397
+ const confirmedIds = /* @__PURE__ */ new Set();
398
+ const items = [];
387
399
  const userBagId = await this.#fetchUserRequestsBagId(btcState.user_requests.id, suiAddress);
388
- if (userBagId === null) return [];
389
- const requestIds = await this.#listAllDynamicFieldAddressKeys(userBagId);
390
- if (requestIds.length === 0) return [];
391
- const objects = await this.#batchGetObjects(requestIds, { content: true });
392
- const { items, withdrawalTxnLookups } = this.#classifyRequestObjects(objects);
393
- await this.#populateWithdrawalBtcTxids(items, withdrawalTxnLookups);
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);
408
+ }
409
+ }
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);
417
+ }
418
+ } catch {}
419
+ items.sort((a, b) => Number(b.timestampMs - a.timestampMs));
394
420
  return items;
395
421
  }
396
422
  };
@@ -417,6 +443,7 @@ var HashiClient = class {
417
443
  this.#packageId = resolvedPackageId;
418
444
  this.#bitcoinNetwork = bitcoinNetwork ?? config?.bitcoinNetwork ?? "testnet";
419
445
  this.#btcRpcUrl = btcRpcUrl;
446
+ this.#graphqlUrl = graphqlUrl ?? defaultGraphqlUrl(network);
420
447
  }
421
448
  /**
422
449
  * Generates a unique Bitcoin P2TR deposit address for a Sui address.
@@ -683,26 +710,8 @@ var HashiClient = class {
683
710
  return BitcoinState.parse(new Uint8Array(dynamicField.value.bcs));
684
711
  }
685
712
  /**
686
- * Batch-fetches objects in chunks of `GET_OBJECTS_BATCH`, concatenating
687
- * the results. Each element is either an object or an `Error` (for
688
- * missing / deleted objects).
689
- */
690
- async #batchGetObjects(objectIds, include) {
691
- const results = [];
692
- for (let i = 0; i < objectIds.length; i += GET_OBJECTS_BATCH) {
693
- const batch = objectIds.slice(i, i + GET_OBJECTS_BATCH);
694
- const { objects } = await this.#client.core.getObjects({
695
- objectIds: batch,
696
- include
697
- });
698
- results.push(...objects);
699
- }
700
- return results;
701
- }
702
- /**
703
713
  * Resolve the Bag id holding a user's request IDs from `user_requests`,
704
- * or `null` if the user has never had a request (no Table entry yet —
705
- * `getDynamicField` rejects rather than returning a sentinel).
714
+ * or `null` if the user has never had a request.
706
715
  */
707
716
  async #fetchUserRequestsBagId(tableId, suiAddress) {
708
717
  try {
@@ -720,8 +729,7 @@ var HashiClient = class {
720
729
  }
721
730
  /**
722
731
  * Enumerate every `address`-keyed dynamic field on `parentId`, paginating
723
- * through `listDynamicFields` until exhausted. Decodes each `name.bcs` as
724
- * a Sui address.
732
+ * through `listDynamicFields` until exhausted.
725
733
  */
726
734
  async #listAllDynamicFieldAddressKeys(parentId) {
727
735
  const keys = [];
@@ -737,6 +745,58 @@ var HashiClient = class {
737
745
  return keys;
738
746
  }
739
747
  /**
748
+ * Batch-fetches objects in chunks of `GET_OBJECTS_BATCH`, concatenating
749
+ * the results. Each element is either an object or an `Error` (for
750
+ * missing / deleted objects).
751
+ */
752
+ async #batchGetObjects(objectIds, include) {
753
+ const results = [];
754
+ for (let i = 0; i < objectIds.length; i += GET_OBJECTS_BATCH) {
755
+ const batch = objectIds.slice(i, i + GET_OBJECTS_BATCH);
756
+ const { objects } = await this.#client.core.getObjects({
757
+ objectIds: batch,
758
+ include
759
+ });
760
+ results.push(...objects);
761
+ }
762
+ return results;
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
+ /**
740
800
  * Classify a batch of request objects into deposit / withdrawal history
741
801
  * items. Errors in the batch are skipped (the request object may have
742
802
  * been deleted between the bag enumeration and the fetch). Returns the
package/dist/types.d.mts CHANGED
@@ -18,6 +18,8 @@ interface HashiClientOptions<Name = "HashiClient"> {
18
18
  bitcoinNetwork?: BitcoinNetwork;
19
19
  /** Optional Bitcoin Core JSON-RPC URL for UTXO lookups and confirmation checks. */
20
20
  btcRpcUrl?: string;
21
+ /** Override the Sui GraphQL endpoint URL (defaults to `https://fullnode.{network}.sui.io:443/graphql`). */
22
+ graphqlUrl?: string;
21
23
  }
22
24
  /**
23
25
  * Frozen snapshot of every governance-controlled protocol parameter, returned
package/dist/util.mjs CHANGED
@@ -29,7 +29,7 @@ function assertHex32(value, fieldName) {
29
29
  function reverseTxidBytes(txid) {
30
30
  assertHex32(txid, "txid");
31
31
  const hex = txid.slice(2);
32
- let reversed = "0x";
32
+ let reversed = "";
33
33
  for (let i = hex.length - 2; i >= 0; i -= 2) reversed += hex.slice(i, i + 2);
34
34
  return reversed;
35
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mysten-incubation/hashi",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "TypeScript SDK for the Hashi Sui Move smart contracts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Mysten Labs <build@mystenlabs.com>",