@mysten-incubation/hashi 0.1.1 → 0.3.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,17 @@
1
1
  # @mysten-incubation/hashi
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ced85d2: Derive deposit addresses as 2-of-2 (guardian, MPC-child) taproot to match the on-chain bridge (hashi#609). `generateDepositAddress` (pure helper) now takes a named-args object including `guardianBtcXOnly`; `HashiClient.generateDepositAddress` reads the guardian key from on-chain and fails fast with `HashiConfigError` when the deployment is not guardian-provisioned. `GovernanceConfig` gains `guardianUrl`, `guardianPublicKey`, `guardianBtcPublicKey`. Adds `twoOfTwoTaprootScriptPathAddress` as a public primitive and removes the single-key `taprootScriptPathAddress` helper, which the bridge no longer accepts.
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 5f9f592: Surface deposit time delay: add `bitcoinDepositTimeDelayMs` to `GovernanceConfig`, `approvalTimestampMs` and `confirmableAtMs` to `DepositInfo`, and `confirmableAtMs` to `DepositHistoryItem`
14
+
3
15
  ## 0.1.1
4
16
 
5
17
  ### Patch Changes
@@ -56,61 +56,93 @@ declare function arkworksToSec1Compressed(ark: Uint8Array): Uint8Array;
56
56
  */
57
57
  declare function deriveChildPubkey(mpcKeyCompressed: Uint8Array, suiAddress: Uint8Array): Uint8Array;
58
58
  /**
59
- * Builds a P2TR script-path-only address: `tr(NUMS, pk(pubkey))`.
59
+ * Builds a 2-of-2 P2TR script-path-only address:
60
+ * `tr(NUMS, multi_a(2, guardian, derived_mpc))`.
60
61
  *
61
- * A BIP-341 taproot output has two spending paths: the *key path* (spend with
62
- * the internal key) and the *script path* (reveal and satisfy a script in the
63
- * Merkle tree). By using the NUMS (Nothing-Up-My-Sleeve) point as the internal
64
- * key a point with no known discrete logarithm — the key path is provably
65
- * unspendable, forcing every spend through the script path.
62
+ * The leaf script is a BIP-342 `multi_a` 2-of-2 both Schnorr signatures must
63
+ * be present to spend, ordered so that the witness stack is
64
+ * `[derived_mpc_sig, guardian_sig, leaf_script, control_block]` (LIFO). This
65
+ * is the exact script the bridge's withdrawal path constructs in
66
+ * `crates/hashi-types/src/guardian/bitcoin_utils.rs`'s `compute_taproot_descriptor`.
66
67
  *
67
- * The script tree contains a single leaf: `<pubkey> OP_CHECKSIG`, meaning the
68
- * MPC committee must produce a valid Schnorr signature for `pubkey` to spend
69
- * the output. This is how the committee authorises withdrawals.
68
+ * The leaf script is exactly 70 bytes:
69
+ *
70
+ * ```text
71
+ * 0x20 ‖ guardian (32) // OP_PUSHBYTES_32 <pk1>
72
+ * 0xAC // OP_CHECKSIG
73
+ * 0x20 ‖ derived_mpc(32) // OP_PUSHBYTES_32 <pk2>
74
+ * 0xBA // OP_CHECKSIGADD
75
+ * 0x52 // OP_2 (push small int 2)
76
+ * 0x9C // OP_NUMEQUAL
77
+ * ```
78
+ *
79
+ * Note `0x52` (OP_2) — not `0x01 0x02` — because `rust-miniscript`'s `multi_a`
80
+ * codegen routes through `Builder::push_int(2)` which emits the small-num
81
+ * opcode. A literal pushdata would change the leaf hash and the address.
70
82
  *
71
83
  * The taproot output key is computed per BIP-341:
84
+ *
72
85
  * ```text
73
- * leafHash = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(script) ‖ script)
74
- * tweak = tagged_hash("TapTweak", NUMS ‖ leafHash)
75
- * outputKey = NUMS + tweak × G
86
+ * leafHash = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(script) ‖ script)
87
+ * tweak = tagged_hash("TapTweak", NUMS ‖ leafHash)
88
+ * outputKey = NUMS + tweak × G
76
89
  * ```
77
90
  *
78
- * The resulting output key is encoded as a SegWit v1 witness program in a
79
- * bech32m address.
91
+ * **Argument ordering is load-bearing.** Guardian first, derived-MPC second.
92
+ * Swapping them produces a different (but real-looking) `tb1p…` whose
93
+ * withdrawals the bridge cannot spend.
80
94
  *
81
- * @param pubkey - 32-byte x-only public key for the script leaf
95
+ * @param guardianBtcXOnly - 32-byte BIP-340 x-only guardian BTC public key
96
+ * (from the on-chain `guardian_btc_public_key` config)
97
+ * @param derivedMpcXOnly - 32-byte x-only public key for the MPC-derived child
98
+ * (output of {@link deriveChildPubkey})
82
99
  * @param network - Bitcoin network for the bech32m human-readable prefix
83
100
  * @returns bech32m-encoded P2TR address (e.g. `bc1p…`, `tb1p…`, `bcrt1p…`)
84
101
  */
85
- declare function taprootScriptPathAddress(pubkey: Uint8Array, network: BitcoinNetwork): string;
102
+ declare function twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly: Uint8Array, derivedMpcXOnly: Uint8Array, network: BitcoinNetwork): string;
103
+ /**
104
+ * Named-args input bundle for {@link generateDepositAddress}. Using named args
105
+ * avoids the foot-gun of two 32-byte `Uint8Array`s in adjacent positions —
106
+ * swapping `guardianBtcXOnly` and `suiAddress` would silently produce a valid
107
+ * but wrong address.
108
+ */
109
+ interface DepositAddressInputs {
110
+ /**
111
+ * 33-byte SEC1-compressed secp256k1 MPC master key (post-arkworks
112
+ * conversion). See {@link arkworksToSec1Compressed}.
113
+ */
114
+ readonly mpcMasterCompressed: Uint8Array;
115
+ /**
116
+ * 32-byte BIP-340 x-only guardian BTC public key (from the on-chain
117
+ * `guardian_btc_public_key` config).
118
+ */
119
+ readonly guardianBtcXOnly: Uint8Array;
120
+ /** 32-byte Sui address used as the derivation path. */
121
+ readonly suiAddress: Uint8Array;
122
+ /** Bitcoin network (determines the bech32m address prefix). */
123
+ readonly network: BitcoinNetwork;
124
+ }
86
125
  /**
87
126
  * Generates a Bitcoin P2TR deposit address for a Sui address.
88
127
  *
89
- * This is the main entry point for the address derivation pipeline. Given the
90
- * MPC master key and a Sui address, it produces the unique Bitcoin deposit
91
- * address where a user should send BTC in order to receive bridged tokens on
92
- * Sui.
93
- *
94
- * Combines {@link deriveChildPubkey} (child key derivation) and
95
- * {@link taprootScriptPathAddress} (taproot address construction) into a
96
- * single call.
128
+ * Main entry point for the address derivation pipeline. Combines
129
+ * {@link deriveChildPubkey} and {@link twoOfTwoTaprootScriptPathAddress} into
130
+ * a single call. The produced address matches the Rust node's
131
+ * `bitcoin_utils::two_of_two_taproot_script_path_address` byte-for-byte.
97
132
  *
98
- * The current devnet address scheme uses a single-key script path:
133
+ * The address scheme is:
99
134
  * ```text
100
- * tr(NUMS, pk(derive(H, d)))
135
+ * tr(NUMS, multi_a(2, guardian, derive(mpc_master, sui_address)))
101
136
  * ```
102
- * where `H` is the MPC master key and `d` is the depositor's Sui address.
103
137
  *
104
- * For mainnet the descriptor will include the guardian key:
105
- * `tr(NUMS, multi_a(2, guardian, derive(H, d)))` — not yet implemented here.
106
- *
107
- * @param mpcKeyCompressed - 33-byte SEC1 compressed secp256k1 MPC public key
108
- * (the MPC master key, after arkworks-to-SEC1 conversion)
109
- * @param suiAddress - 32-byte Sui address
110
- * @param network - Bitcoin network (determines the bech32m address prefix)
111
138
  * @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
112
139
  */
113
- declare function generateDepositAddress(mpcKeyCompressed: Uint8Array, suiAddress: Uint8Array, network: BitcoinNetwork): string;
140
+ declare function generateDepositAddress({
141
+ mpcMasterCompressed,
142
+ guardianBtcXOnly,
143
+ suiAddress,
144
+ network
145
+ }: DepositAddressInputs): string;
114
146
  /**
115
147
  * Decodes a bech32/bech32m SegWit Bitcoin address into a witness program.
116
148
  *
@@ -149,4 +181,4 @@ declare function bitcoinAddressToWitnessProgram(address: string, network: Bitcoi
149
181
  */
150
182
  declare function witnessProgramToAddress(program: Uint8Array, network: BitcoinNetwork): string;
151
183
  //#endregion
152
- export { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress, witnessProgramToAddress };
184
+ export { DepositAddressInputs, arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, twoOfTwoTaprootScriptPathAddress, witnessProgramToAddress };
package/dist/bitcoin.mjs CHANGED
@@ -13,30 +13,39 @@ import { concatBytes } from "@noble/hashes/utils.js";
13
13
  *
14
14
  * Hashi bridges Bitcoin and Sui by assigning each Sui address a unique Bitcoin
15
15
  * deposit address. When BTC is sent to that address, the Hashi MPC committee
16
- * detects the deposit and mints equivalent tokens on Sui.
16
+ * and the Hashi guardian co-sign the withdrawal that spends it, and the bridge
17
+ * mints equivalent tokens on Sui.
17
18
  *
18
- * The deposit address is a Pay-to-Taproot (P2TR / BIP-341) script-path address
19
- * whose spending condition is a single `OP_CHECKSIG` against a child public key
20
- * derived from the MPC committee's master key and the depositor's Sui address.
19
+ * The deposit address is a 2-of-2 Pay-to-Taproot (P2TR / BIP-341) script-path
20
+ * address whose spending condition is `multi_a(2, guardian_btc_pubkey,
21
+ * derive(mpc_master, sui_address))` — both the guardian enclave's BIP-340 key
22
+ * and the MPC-derived child key must produce a Schnorr signature to spend.
21
23
  *
22
24
  * The full derivation pipeline is:
23
25
  *
24
- * 1. **Fetch** the MPC master key from on-chain (`CommitteeSet.mpc_public_key`).
25
- * The on-chain bytes use the arkworks compressed format (little-endian x +
26
- * flag byte), so they must first be converted to SEC1 via
27
- * {@link arkworksToSec1Compressed} this is done automatically by the
28
- * client's `view.mpcPublicKey()` method.
26
+ * 1. **Fetch** the MPC master key from on-chain (`CommitteeSet.mpc_public_key`)
27
+ * and the guardian's BTC public key from the on-chain config
28
+ * (`guardian_btc_public_key`). The MPC bytes use the arkworks compressed
29
+ * format and must first be converted to SEC1 via
30
+ * {@link arkworksToSec1Compressed} — done automatically by the client's
31
+ * `view.mpcPublicKey()`. The guardian key is already in BIP-340 x-only form.
29
32
  *
30
- * 2. **Derive** a child key: `child = masterKey + HKDF-SHA3-256(x ‖ suiAddr) × G`
33
+ * 2. **Derive** a child MPC key: `child = masterKey + HKDF-SHA3-256(x ‖ suiAddr) × G`
31
34
  * (see {@link deriveChildPubkey}). This replicates the Rust function
32
35
  * `fastcrypto_tbls::threshold_schnorr::key_derivation::derive_verifying_key`.
33
36
  *
34
- * 3. **Build** the taproot address: `tr(NUMS, pk(child))` where NUMS is a
35
- * Nothing-Up-My-Sleeve point with no known private key, forcing all spends
36
- * through the script path (see {@link taprootScriptPathAddress}).
37
+ * 3. **Build** the taproot address: `tr(NUMS, multi_a(2, guardian, child))`
38
+ * where NUMS is a Nothing-Up-My-Sleeve point with no known private key,
39
+ * forcing all spends through the script path (see
40
+ * {@link twoOfTwoTaprootScriptPathAddress}).
37
41
  *
38
42
  * The end-to-end helper {@link generateDepositAddress} combines steps 2–3.
39
43
  *
44
+ * Mirrors `two_of_two_taproot_script_path_address` in
45
+ * `crates/hashi-types/src/guardian/bitcoin_utils.rs`. Cross-language test
46
+ * vectors live in both this file's unit tests and the matching Rust unit test
47
+ * `cross_lang_2of2_test_vectors`.
48
+ *
40
49
  * @see https://mystenlabs.github.io/hashi/design/address-scheme.html
41
50
  */
42
51
  const Point = secp256k1.Point;
@@ -127,38 +136,61 @@ function deriveChildPubkey(mpcKeyCompressed, suiAddress) {
127
136
  return parentPoint.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
128
137
  }
129
138
  /**
130
- * Builds a P2TR script-path-only address: `tr(NUMS, pk(pubkey))`.
139
+ * Builds a 2-of-2 P2TR script-path-only address:
140
+ * `tr(NUMS, multi_a(2, guardian, derived_mpc))`.
141
+ *
142
+ * The leaf script is a BIP-342 `multi_a` 2-of-2 — both Schnorr signatures must
143
+ * be present to spend, ordered so that the witness stack is
144
+ * `[derived_mpc_sig, guardian_sig, leaf_script, control_block]` (LIFO). This
145
+ * is the exact script the bridge's withdrawal path constructs in
146
+ * `crates/hashi-types/src/guardian/bitcoin_utils.rs`'s `compute_taproot_descriptor`.
147
+ *
148
+ * The leaf script is exactly 70 bytes:
131
149
  *
132
- * A BIP-341 taproot output has two spending paths: the *key path* (spend with
133
- * the internal key) and the *script path* (reveal and satisfy a script in the
134
- * Merkle tree). By using the NUMS (Nothing-Up-My-Sleeve) point as the internal
135
- * key a point with no known discrete logarithm — the key path is provably
136
- * unspendable, forcing every spend through the script path.
150
+ * ```text
151
+ * 0x20 guardian (32) // OP_PUSHBYTES_32 <pk1>
152
+ * 0xAC // OP_CHECKSIG
153
+ * 0x20 derived_mpc(32) // OP_PUSHBYTES_32 <pk2>
154
+ * 0xBA // OP_CHECKSIGADD
155
+ * 0x52 // OP_2 (push small int 2)
156
+ * 0x9C // OP_NUMEQUAL
157
+ * ```
137
158
  *
138
- * The script tree contains a single leaf: `<pubkey> OP_CHECKSIG`, meaning the
139
- * MPC committee must produce a valid Schnorr signature for `pubkey` to spend
140
- * the output. This is how the committee authorises withdrawals.
159
+ * Note `0x52` (OP_2) not `0x01 0x02` because `rust-miniscript`'s `multi_a`
160
+ * codegen routes through `Builder::push_int(2)` which emits the small-num
161
+ * opcode. A literal pushdata would change the leaf hash and the address.
141
162
  *
142
163
  * The taproot output key is computed per BIP-341:
164
+ *
143
165
  * ```text
144
- * leafHash = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(script) ‖ script)
145
- * tweak = tagged_hash("TapTweak", NUMS ‖ leafHash)
146
- * outputKey = NUMS + tweak × G
166
+ * leafHash = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(script) ‖ script)
167
+ * tweak = tagged_hash("TapTweak", NUMS ‖ leafHash)
168
+ * outputKey = NUMS + tweak × G
147
169
  * ```
148
170
  *
149
- * The resulting output key is encoded as a SegWit v1 witness program in a
150
- * bech32m address.
171
+ * **Argument ordering is load-bearing.** Guardian first, derived-MPC second.
172
+ * Swapping them produces a different (but real-looking) `tb1p…` whose
173
+ * withdrawals the bridge cannot spend.
151
174
  *
152
- * @param pubkey - 32-byte x-only public key for the script leaf
175
+ * @param guardianBtcXOnly - 32-byte BIP-340 x-only guardian BTC public key
176
+ * (from the on-chain `guardian_btc_public_key` config)
177
+ * @param derivedMpcXOnly - 32-byte x-only public key for the MPC-derived child
178
+ * (output of {@link deriveChildPubkey})
153
179
  * @param network - Bitcoin network for the bech32m human-readable prefix
154
180
  * @returns bech32m-encoded P2TR address (e.g. `bc1p…`, `tb1p…`, `bcrt1p…`)
155
181
  */
156
- function taprootScriptPathAddress(pubkey, network) {
157
- if (pubkey.length !== 32) throw new Error(`Expected 32-byte x-only pubkey, got ${pubkey.length}`);
158
- const tapscript = new Uint8Array(34);
182
+ function twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly, derivedMpcXOnly, network) {
183
+ if (guardianBtcXOnly.length !== 32) throw new Error(`Expected 32-byte x-only guardian pubkey, got ${guardianBtcXOnly.length}`);
184
+ if (derivedMpcXOnly.length !== 32) throw new Error(`Expected 32-byte x-only derived MPC pubkey, got ${derivedMpcXOnly.length}`);
185
+ const tapscript = new Uint8Array(70);
159
186
  tapscript[0] = 32;
160
- tapscript.set(pubkey, 1);
187
+ tapscript.set(guardianBtcXOnly, 1);
161
188
  tapscript[33] = 172;
189
+ tapscript[34] = 32;
190
+ tapscript.set(derivedMpcXOnly, 35);
191
+ tapscript[67] = 186;
192
+ tapscript[68] = 82;
193
+ tapscript[69] = 156;
162
194
  const tweakScalar = bytesToNumberBE(taggedHash("TapTweak", NUMS_KEY, taggedHash("TapLeaf", new Uint8Array([192, tapscript.length]), tapscript))) % CURVE_ORDER;
163
195
  const outputKey = NUMS_POINT.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
164
196
  const words = [1, ...bech32m.toWords(outputKey)];
@@ -167,32 +199,20 @@ function taprootScriptPathAddress(pubkey, network) {
167
199
  /**
168
200
  * Generates a Bitcoin P2TR deposit address for a Sui address.
169
201
  *
170
- * This is the main entry point for the address derivation pipeline. Given the
171
- * MPC master key and a Sui address, it produces the unique Bitcoin deposit
172
- * address where a user should send BTC in order to receive bridged tokens on
173
- * Sui.
174
- *
175
- * Combines {@link deriveChildPubkey} (child key derivation) and
176
- * {@link taprootScriptPathAddress} (taproot address construction) into a
177
- * single call.
202
+ * Main entry point for the address derivation pipeline. Combines
203
+ * {@link deriveChildPubkey} and {@link twoOfTwoTaprootScriptPathAddress} into
204
+ * a single call. The produced address matches the Rust node's
205
+ * `bitcoin_utils::two_of_two_taproot_script_path_address` byte-for-byte.
178
206
  *
179
- * The current devnet address scheme uses a single-key script path:
207
+ * The address scheme is:
180
208
  * ```text
181
- * tr(NUMS, pk(derive(H, d)))
209
+ * tr(NUMS, multi_a(2, guardian, derive(mpc_master, sui_address)))
182
210
  * ```
183
- * where `H` is the MPC master key and `d` is the depositor's Sui address.
184
211
  *
185
- * For mainnet the descriptor will include the guardian key:
186
- * `tr(NUMS, multi_a(2, guardian, derive(H, d)))` — not yet implemented here.
187
- *
188
- * @param mpcKeyCompressed - 33-byte SEC1 compressed secp256k1 MPC public key
189
- * (the MPC master key, after arkworks-to-SEC1 conversion)
190
- * @param suiAddress - 32-byte Sui address
191
- * @param network - Bitcoin network (determines the bech32m address prefix)
192
212
  * @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
193
213
  */
194
- function generateDepositAddress(mpcKeyCompressed, suiAddress, network) {
195
- return taprootScriptPathAddress(deriveChildPubkey(mpcKeyCompressed, suiAddress), network);
214
+ function generateDepositAddress({ mpcMasterCompressed, guardianBtcXOnly, suiAddress, network }) {
215
+ return twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly, deriveChildPubkey(mpcMasterCompressed, suiAddress), network);
196
216
  }
197
217
  /**
198
218
  * Decodes a bech32/bech32m SegWit Bitcoin address into a witness program.
@@ -292,4 +312,4 @@ function witnessProgramToAddress(program, network) {
292
312
  }
293
313
 
294
314
  //#endregion
295
- export { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress, witnessProgramToAddress };
315
+ export { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, twoOfTwoTaprootScriptPathAddress, witnessProgramToAddress };
package/dist/client.d.mts CHANGED
@@ -50,8 +50,21 @@ declare class HashiClient {
50
50
  /**
51
51
  * Generates a unique Bitcoin P2TR deposit address for a Sui address.
52
52
  *
53
- * Fetches the MPC committee public key from on-chain, derives a child key
54
- * using the Sui address, and builds a taproot script-path address.
53
+ * Fetches the MPC committee public key and the guardian's BTC public key
54
+ * from on-chain, derives an MPC child key against the Sui address, and
55
+ * builds a 2-of-2 taproot script-path address
56
+ * (`tr(NUMS, multi_a(2, guardian, derived_mpc))`). The address matches
57
+ * the bridge's on-chain `validate_deposit_request_derivation_path` check
58
+ * byte-for-byte.
59
+ *
60
+ * The MPC key (`committee_set.mpc_public_key`) and the guardian key
61
+ * (`guardian_btc_public_key` config) come from a single fetch of the
62
+ * Hashi object, and only those two fields are parsed — an unrelated
63
+ * malformed config entry can't block deposit-address generation.
64
+ *
65
+ * Throws `HashiConfigError` if the deployment isn't guardian-provisioned
66
+ * yet (no `guardian_btc_public_key` on chain). The SDK refuses to fall
67
+ * back to a single-key address because the bridge validator rejects it.
55
68
  *
56
69
  * @example
57
70
  * ```ts
package/dist/client.mjs CHANGED
@@ -6,13 +6,13 @@ import { WithdrawalRequest, WithdrawalTransaction } from "./contracts/hashi/with
6
6
  import { BitcoinState, BitcoinStateKey } from "./contracts/hashi/bitcoin_state.mjs";
7
7
  import { deposit } from "./contracts/hashi/deposit.mjs";
8
8
  import { cancelWithdrawal, requestWithdrawal } from "./contracts/hashi/withdraw.mjs";
9
- import { DUST_RELAY_MIN_VALUE, NETWORK_CONFIG } from "./constants.mjs";
9
+ import { DUST_RELAY_MIN_VALUE, GUARDIAN_BTC_PUBLIC_KEY_LEN, GUARDIAN_PUBLIC_KEY_LEN, 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
12
  import { getTxConfirmations, lookupAllVouts, lookupVout } from "./btc-rpc.mjs";
13
- import { assertHex32, entry, reverseTxidBytes } from "./util.mjs";
13
+ import { assertHex32, configBytes, entry, reverseTxidBytes } from "./util.mjs";
14
14
  import { TypeTagSerializer, bcs } from "@mysten/sui/bcs";
15
- import { deriveDynamicFieldID, fromHex } from "@mysten/sui/utils";
15
+ import { deriveDynamicFieldID, fromHex, normalizeSuiAddress } from "@mysten/sui/utils";
16
16
  import { base58 } from "@scure/base";
17
17
  import { Transaction, coinWithBalance } from "@mysten/sui/transactions";
18
18
 
@@ -176,26 +176,13 @@ var HashiClient = class {
176
176
  };
177
177
  this.view = {
178
178
  mpcPublicKey: async () => {
179
- const result = await Hashi.get({
179
+ return parseMpcPublicKey((await Hashi.get({
180
180
  client: this.#client,
181
181
  objectId: this.#hashiObjectId
182
- });
183
- const mpcKey = new Uint8Array(result.json.committee_set.mpc_public_key);
184
- if (mpcKey.length === 0) throw new Error("MPC public key not available on-chain. Has the committee completed DKG?");
185
- return arkworksToSec1Compressed(mpcKey);
182
+ })).json.committee_set.mpc_public_key);
186
183
  },
187
184
  all: async () => {
188
- let result;
189
- try {
190
- result = await Hashi.get({
191
- client: this.#client,
192
- objectId: this.#hashiObjectId
193
- });
194
- } catch (cause) {
195
- throw new HashiFetchError(`Failed to fetch Hashi shared object ${this.#hashiObjectId}.`, this.#hashiObjectId, { cause });
196
- }
197
- const contents = result.json?.config?.config?.contents;
198
- if (!Array.isArray(contents)) throw new HashiFetchError(`Hashi object ${this.#hashiObjectId} returned an unexpected shape: config.config.contents is not an array.`, this.#hashiObjectId);
185
+ const { contents } = await this.#fetchHashiObject();
199
186
  return this.#parseConfig(contents);
200
187
  },
201
188
  paused: async () => (await this.view.all()).paused,
@@ -269,12 +256,17 @@ var HashiClient = class {
269
256
  if (!depositEvent?.json) return null;
270
257
  const parsed = depositEvent.json;
271
258
  let status = "unknown";
259
+ let approvalTimestampMs = null;
260
+ let confirmableAtMs = null;
272
261
  try {
273
- await DepositRequest.get({
262
+ const reqObj = await DepositRequest.get({
274
263
  client: this.#client,
275
264
  objectId: parsed.request_id
276
265
  });
277
- const requestsBagId = (await this.#fetchBitcoinState()).deposit_queue.requests.id;
266
+ if (reqObj.json.approval_timestamp_ms != null) approvalTimestampMs = BigInt(reqObj.json.approval_timestamp_ms);
267
+ const [btcState, config$1] = await Promise.all([this.#fetchBitcoinState(), this.view.all().catch(() => null)]);
268
+ if (approvalTimestampMs !== null && config$1) confirmableAtMs = approvalTimestampMs + config$1.bitcoinDepositTimeDelayMs;
269
+ const requestsBagId = btcState.deposit_queue.requests.id;
278
270
  status = (await this.#client.core.getDynamicField({
279
271
  parentId: requestsBagId,
280
272
  name: {
@@ -293,6 +285,8 @@ var HashiClient = class {
293
285
  btcTxid: reverseTxidBytes(parsed.utxo_id.txid),
294
286
  btcVout: parsed.utxo_id.vout,
295
287
  timestampMs: BigInt(parsed.timestamp_ms),
288
+ approvalTimestampMs,
289
+ confirmableAtMs,
296
290
  status,
297
291
  suiTxDigest
298
292
  };
@@ -393,7 +387,7 @@ var HashiClient = class {
393
387
  };
394
388
  },
395
389
  transactionHistory: async (suiAddress) => {
396
- const btcState = await this.#fetchBitcoinState();
390
+ const [btcState, timeDelayMs] = await Promise.all([this.#fetchBitcoinState(), this.view.all().then((c) => c.bitcoinDepositTimeDelayMs, () => null)]);
397
391
  const confirmedIds = /* @__PURE__ */ new Set();
398
392
  const items = [];
399
393
  const userBagId = await this.#fetchUserRequestsBagId(btcState.user_requests.id, suiAddress);
@@ -401,7 +395,7 @@ var HashiClient = class {
401
395
  const requestIds = await this.#listAllDynamicFieldAddressKeys(userBagId);
402
396
  if (requestIds.length > 0) {
403
397
  const objects = await this.#batchGetObjects(requestIds, { content: true });
404
- const classified = this.#classifyRequestObjects(objects);
398
+ const classified = this.#classifyRequestObjects(objects, timeDelayMs);
405
399
  items.push(...classified.items);
406
400
  await this.#populateWithdrawalBtcTxids(items, classified.withdrawalTxnLookups);
407
401
  for (const id of requestIds) confirmedIds.add(id);
@@ -412,7 +406,7 @@ var HashiClient = class {
412
406
  const pendingIds = (await this.#queryEventRequestIds(suiAddress, depositEventType)).filter((id) => !confirmedIds.has(id));
413
407
  if (pendingIds.length > 0) {
414
408
  const objects = await this.#batchGetObjects(pendingIds, { content: true });
415
- const classified = this.#classifyRequestObjects(objects);
409
+ const classified = this.#classifyRequestObjects(objects, timeDelayMs);
416
410
  items.push(...classified.items);
417
411
  }
418
412
  } catch {}
@@ -448,8 +442,21 @@ var HashiClient = class {
448
442
  /**
449
443
  * Generates a unique Bitcoin P2TR deposit address for a Sui address.
450
444
  *
451
- * Fetches the MPC committee public key from on-chain, derives a child key
452
- * using the Sui address, and builds a taproot script-path address.
445
+ * Fetches the MPC committee public key and the guardian's BTC public key
446
+ * from on-chain, derives an MPC child key against the Sui address, and
447
+ * builds a 2-of-2 taproot script-path address
448
+ * (`tr(NUMS, multi_a(2, guardian, derived_mpc))`). The address matches
449
+ * the bridge's on-chain `validate_deposit_request_derivation_path` check
450
+ * byte-for-byte.
451
+ *
452
+ * The MPC key (`committee_set.mpc_public_key`) and the guardian key
453
+ * (`guardian_btc_public_key` config) come from a single fetch of the
454
+ * Hashi object, and only those two fields are parsed — an unrelated
455
+ * malformed config entry can't block deposit-address generation.
456
+ *
457
+ * Throws `HashiConfigError` if the deployment isn't guardian-provisioned
458
+ * yet (no `guardian_btc_public_key` on chain). The SDK refuses to fall
459
+ * back to a single-key address because the bridge validator rejects it.
453
460
  *
454
461
  * @example
455
462
  * ```ts
@@ -459,7 +466,13 @@ var HashiClient = class {
459
466
  * ```
460
467
  */
461
468
  async generateDepositAddress({ suiAddress, bitcoinNetwork = this.#bitcoinNetwork }) {
462
- return generateDepositAddress(await this.view.mpcPublicKey(), fromHex(suiAddress), bitcoinNetwork);
469
+ const { json, contents } = await this.#fetchHashiObject();
470
+ return generateDepositAddress({
471
+ mpcMasterCompressed: parseMpcPublicKey(json.committee_set.mpc_public_key),
472
+ guardianBtcXOnly: configBytes(contents, "guardian_btc_public_key", GUARDIAN_BTC_PUBLIC_KEY_LEN),
473
+ suiAddress: fromHex(normalizeSuiAddress(suiAddress)),
474
+ network: bitcoinNetwork
475
+ });
463
476
  }
464
477
  /**
465
478
  * Submit one or more Bitcoin deposits for committee confirmation, batched
@@ -626,6 +639,22 @@ var HashiClient = class {
626
639
  };
627
640
  const bool = (key) => entry(contents, key, "Bool").Bool;
628
641
  const addr = (key) => entry(contents, key, "Address").Address;
642
+ const optionalString = (key) => {
643
+ try {
644
+ return entry(contents, key, "String").String;
645
+ } catch (e) {
646
+ if (e instanceof HashiConfigError && e.actualVariant === void 0) return null;
647
+ throw e;
648
+ }
649
+ };
650
+ const optionalBytes = (key, expectedLen) => {
651
+ try {
652
+ return configBytes(contents, key, expectedLen);
653
+ } catch (e) {
654
+ if (e instanceof HashiConfigError && e.actualVariant === void 0) return null;
655
+ throw e;
656
+ }
657
+ };
629
658
  const rawDepositMin = u64("bitcoin_deposit_minimum");
630
659
  const rawWithdrawalMin = u64("bitcoin_withdrawal_minimum");
631
660
  const bitcoinDepositMinimum = rawDepositMin < DUST_RELAY_MIN_VALUE ? DUST_RELAY_MIN_VALUE : rawDepositMin;
@@ -637,8 +666,12 @@ var HashiClient = class {
637
666
  bitcoinWithdrawalMinimum,
638
667
  bitcoinConfirmationThreshold: u64("bitcoin_confirmation_threshold"),
639
668
  withdrawalCancellationCooldownMs: u64("withdrawal_cancellation_cooldown_ms"),
669
+ bitcoinDepositTimeDelayMs: u64("bitcoin_deposit_time_delay_ms"),
640
670
  depositMinimum: bitcoinDepositMinimum,
641
- worstCaseNetworkFee: bitcoinWithdrawalMinimum - DUST_RELAY_MIN_VALUE
671
+ worstCaseNetworkFee: bitcoinWithdrawalMinimum - DUST_RELAY_MIN_VALUE,
672
+ guardianUrl: optionalString("guardian_url"),
673
+ guardianPublicKey: optionalBytes("guardian_public_key", GUARDIAN_PUBLIC_KEY_LEN),
674
+ guardianBtcPublicKey: optionalBytes("guardian_btc_public_key", GUARDIAN_BTC_PUBLIC_KEY_LEN)
642
675
  };
643
676
  }
644
677
  /**
@@ -688,6 +721,31 @@ var HashiClient = class {
688
721
  return 0n;
689
722
  }
690
723
  /**
724
+ * Fetches the Hashi shared object once and returns its decoded `json`
725
+ * alongside the validated governance-config `contents` array. Wraps
726
+ * transport failures and unexpected shapes in `HashiFetchError`. Shared by
727
+ * `view.all()` and `generateDepositAddress` so a single round-trip serves
728
+ * both the committee key (from `json`) and the config reads (from
729
+ * `contents`).
730
+ */
731
+ async #fetchHashiObject() {
732
+ let result;
733
+ try {
734
+ result = await Hashi.get({
735
+ client: this.#client,
736
+ objectId: this.#hashiObjectId
737
+ });
738
+ } catch (cause) {
739
+ throw new HashiFetchError(`Failed to fetch Hashi shared object ${this.#hashiObjectId}.`, this.#hashiObjectId, { cause });
740
+ }
741
+ const contents = result.json?.config?.config?.contents;
742
+ if (!Array.isArray(contents)) throw new HashiFetchError(`Hashi object ${this.#hashiObjectId} returned an unexpected shape: config.config.contents is not an array.`, this.#hashiObjectId);
743
+ return {
744
+ json: result.json,
745
+ contents
746
+ };
747
+ }
748
+ /**
691
749
  * Fetches the `BitcoinState` dynamic field from the Hashi shared object.
692
750
  * Returns the BCS-parsed struct whose nested Bag/Table IDs are used by
693
751
  * `findUsedUtxos` and `transactionHistory`.
@@ -803,14 +861,14 @@ var HashiClient = class {
803
861
  * items plus the indices that need a follow-up `WithdrawalTransaction`
804
862
  * fetch to populate `btcTxid`.
805
863
  */
806
- #classifyRequestObjects(objects) {
864
+ #classifyRequestObjects(objects, timeDelayMs) {
807
865
  const items = [];
808
866
  const withdrawalTxnLookups = [];
809
867
  const depositRequestType = `${this.#packageId}::deposit_queue::DepositRequest`;
810
868
  const withdrawalRequestType = `${this.#packageId}::withdrawal_queue::WithdrawalRequest`;
811
869
  for (const obj of objects) {
812
870
  if (obj instanceof Error) continue;
813
- if (obj.type === depositRequestType) items.push(parseDepositHistoryItem(obj.content));
871
+ if (obj.type === depositRequestType) items.push(parseDepositHistoryItem(obj.content, timeDelayMs));
814
872
  else if (obj.type === withdrawalRequestType) {
815
873
  const item = parseWithdrawalHistoryItem(obj.content);
816
874
  items.push(item);
@@ -843,8 +901,19 @@ var HashiClient = class {
843
901
  }
844
902
  }
845
903
  };
846
- function parseDepositHistoryItem(content) {
904
+ /**
905
+ * Convert the on-chain `committee_set.mpc_public_key` (arkworks-compressed)
906
+ * into a 33-byte SEC1 key. Throws `HashiConfigError` if DKG hasn't populated
907
+ * it yet (empty vector).
908
+ */
909
+ function parseMpcPublicKey(raw) {
910
+ const mpcKey = new Uint8Array(raw);
911
+ if (mpcKey.length === 0) throw HashiConfigError.missing("committee_set.mpc_public_key", "Bytes");
912
+ return arkworksToSec1Compressed(mpcKey);
913
+ }
914
+ function parseDepositHistoryItem(content, timeDelayMs) {
847
915
  const parsed = DepositRequest.parse(content);
916
+ const approvalTimestampMs = parsed.approval_timestamp_ms === null ? null : BigInt(parsed.approval_timestamp_ms);
848
917
  return {
849
918
  kind: "deposit",
850
919
  requestId: parsed.id,
@@ -855,7 +924,8 @@ function parseDepositHistoryItem(content) {
855
924
  btcTxid: reverseTxidBytes(parsed.utxo.id.txid),
856
925
  btcVout: parsed.utxo.id.vout,
857
926
  approved: parsed.approval_cert !== null,
858
- approvalTimestampMs: parsed.approval_timestamp_ms === null ? null : BigInt(parsed.approval_timestamp_ms)
927
+ approvalTimestampMs,
928
+ confirmableAtMs: approvalTimestampMs !== null && timeDelayMs !== null ? approvalTimestampMs + timeDelayMs : null
859
929
  };
860
930
  }
861
931
  function parseWithdrawalHistoryItem(content) {
@@ -50,6 +50,16 @@ const NUMS_KEY = new Uint8Array([
50
50
  * `hashi::btc_config`.
51
51
  */
52
52
  const DUST_RELAY_MIN_VALUE = 546n;
53
+ /**
54
+ * Length of the Guardian's Ed25519 attestation public key, in bytes. Matches
55
+ * `GUARDIAN_PUBLIC_KEY_LEN` in `hashi::config`.
56
+ */
57
+ const GUARDIAN_PUBLIC_KEY_LEN = 32;
58
+ /**
59
+ * Length of the Guardian's BIP-340 x-only BTC public key, in bytes. Matches
60
+ * `GUARDIAN_BTC_PUBLIC_KEY_LEN` in `hashi::config`.
61
+ */
62
+ const GUARDIAN_BTC_PUBLIC_KEY_LEN = 32;
53
63
  const NETWORK_CONFIG = { devnet: {
54
64
  hashiObjectId: "0x61f1acbf8dd79d284cb312853389747a55a158b1b12a68554afcade05dec5f40",
55
65
  packageId: "0xc377616c626d73556cae080706acf37e862d24d26c690cc7be5a957f53fadf17",
@@ -57,4 +67,4 @@ const NETWORK_CONFIG = { devnet: {
57
67
  } };
58
68
 
59
69
  //#endregion
60
- export { DUST_RELAY_MIN_VALUE, NETWORK_CONFIG, NETWORK_HRP, NUMS_KEY };
70
+ export { DUST_RELAY_MIN_VALUE, GUARDIAN_BTC_PUBLIC_KEY_LEN, GUARDIAN_PUBLIC_KEY_LEN, NETWORK_CONFIG, NETWORK_HRP, NUMS_KEY };
@@ -20,7 +20,8 @@ const UtxoRecord = new MoveStruct({
20
20
  fields: {
21
21
  utxo: Utxo,
22
22
  produced_by: bcs.option(bcs.Address),
23
- locked_by: bcs.option(bcs.Address)
23
+ locked_by: bcs.option(bcs.Address),
24
+ spent_epoch: bcs.option(bcs.u64())
24
25
  }
25
26
  });
26
27
  const UtxoSpentEvent = new MoveStruct({
@@ -27,7 +27,8 @@ const WithdrawalSignedMessage = new MoveStruct({
27
27
  fields: {
28
28
  withdrawal_id: bcs.Address,
29
29
  request_ids: bcs.vector(bcs.Address),
30
- signatures: bcs.vector(bcs.vector(bcs.u8()))
30
+ signatures: bcs.vector(bcs.vector(bcs.u8())),
31
+ guardian_signatures: bcs.vector(bcs.vector(bcs.u8()))
31
32
  }
32
33
  });
33
34
  const WithdrawalConfirmationMessage = new MoveStruct({
@@ -61,6 +61,7 @@ const WithdrawalTransaction = new MoveStruct({
61
61
  timestamp_ms: bcs.u64(),
62
62
  randomness: bcs.vector(bcs.u8()),
63
63
  signatures: bcs.option(bcs.vector(bcs.vector(bcs.u8()))),
64
+ guardian_signatures: bcs.option(bcs.vector(bcs.vector(bcs.u8()))),
64
65
  presig_start_index: bcs.u64(),
65
66
  epoch: bcs.u64()
66
67
  }
@@ -105,7 +106,16 @@ const WithdrawalSignedEvent = new MoveStruct({
105
106
  fields: {
106
107
  withdrawal_txn_id: bcs.Address,
107
108
  request_ids: bcs.vector(bcs.Address),
108
- signatures: bcs.vector(bcs.vector(bcs.u8()))
109
+ signatures: bcs.vector(bcs.vector(bcs.u8())),
110
+ guardian_signatures: bcs.vector(bcs.vector(bcs.u8()))
111
+ }
112
+ });
113
+ const WithdrawalPresigsReassignedEvent = new MoveStruct({
114
+ name: `${$moduleName}::WithdrawalPresigsReassignedEvent`,
115
+ fields: {
116
+ withdrawal_txn_id: bcs.Address,
117
+ epoch: bcs.u64(),
118
+ presig_start_index: bcs.u64()
109
119
  }
110
120
  });
111
121
  const WithdrawalConfirmedEvent = new MoveStruct({
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
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, 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 };
4
+ import { DepositAddressInputs, arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, twoOfTwoTaprootScriptPathAddress, witnessProgramToAddress } from "./bitcoin.mjs";
5
+ export { AmountBelowMinimumError, type AmountViolation, type BitcoinNetwork, type CancelWithdrawalParams, type DepositAddressInputs, 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, twoOfTwoTaprootScriptPathAddress, 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, witnessProgramToAddress } from "./bitcoin.mjs";
2
+ import { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, twoOfTwoTaprootScriptPathAddress, 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, witnessProgramToAddress };
5
+ export { AmountBelowMinimumError, HashiClient, HashiConfigError, HashiFetchError, HashiPausedError, InvalidBitcoinAddressError, InvalidParamsError, arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, hashi, twoOfTwoTaprootScriptPathAddress, witnessProgramToAddress };
package/dist/types.d.mts CHANGED
@@ -28,6 +28,11 @@ interface HashiClientOptions<Name = "HashiClient"> {
28
28
  * state. `depositMinimum` is a Move-side alias of `bitcoinDepositMinimum`;
29
29
  * `worstCaseNetworkFee` is derived as `bitcoinWithdrawalMinimum - 546` (the
30
30
  * dust relay floor) and is always ≥ 1.
31
+ *
32
+ * The three `guardian*` fields are `null` on deployments where the guardian
33
+ * config hasn't been written yet (pre-feature chains or in-flight rollouts).
34
+ * `guardianBtcPublicKey` is required for {@link HashiClient.generateDepositAddress}
35
+ * to succeed.
31
36
  */
32
37
  interface GovernanceConfig {
33
38
  readonly paused: boolean;
@@ -36,8 +41,22 @@ interface GovernanceConfig {
36
41
  readonly bitcoinWithdrawalMinimum: bigint;
37
42
  readonly bitcoinConfirmationThreshold: bigint;
38
43
  readonly withdrawalCancellationCooldownMs: bigint;
44
+ readonly bitcoinDepositTimeDelayMs: bigint;
39
45
  readonly depositMinimum: bigint;
40
46
  readonly worstCaseNetworkFee: bigint;
47
+ /** Guardian gRPC/HTTP endpoint from on-chain config. `null` if unset. */
48
+ readonly guardianUrl: string | null;
49
+ /**
50
+ * Guardian's Ed25519 attestation key (32 bytes), used to verify signed
51
+ * `GetGuardianInfo` responses. `null` if unset.
52
+ */
53
+ readonly guardianPublicKey: Uint8Array | null;
54
+ /**
55
+ * Guardian's BIP-340 x-only secp256k1 BTC public key (32 bytes), the
56
+ * `pk1` slot of the on-chain 2-of-2 deposit-address descriptor. `null`
57
+ * if unset (deposit-address derivation will fail).
58
+ */
59
+ readonly guardianBtcPublicKey: Uint8Array | null;
41
60
  }
42
61
  /**
43
62
  * A single UTXO output within a Bitcoin transaction used to fund a deposit.
@@ -120,6 +139,8 @@ interface DepositHistoryItem {
120
139
  /** `true` once the committee has approved the deposit. */
121
140
  readonly approved: boolean;
122
141
  readonly approvalTimestampMs: bigint | null;
142
+ /** Earliest wall-clock time (ms since epoch) at which the deposit can be confirmed. `null` until approved. */
143
+ readonly confirmableAtMs: bigint | null;
123
144
  }
124
145
  type WithdrawalStatus = "Requested" | "Approved" | "Processing" | "Signed" | "Confirmed";
125
146
  interface WithdrawalHistoryItem {
@@ -157,6 +178,10 @@ interface DepositInfo {
157
178
  readonly btcVout: number;
158
179
  /** Request timestamp (ms since epoch). */
159
180
  readonly timestampMs: bigint;
181
+ /** Timestamp (ms since epoch) when the committee approved this deposit. `null` if not yet approved. */
182
+ readonly approvalTimestampMs: bigint | null;
183
+ /** Earliest wall-clock time (ms since epoch) at which the deposit can be confirmed. `null` until approved or if the config could not be read. */
184
+ readonly confirmableAtMs: bigint | null;
160
185
  /** Current deposit status. */
161
186
  readonly status: DepositStatus;
162
187
  /** Sui transaction digest that created this request. */
package/dist/util.mjs CHANGED
@@ -45,6 +45,16 @@ function entry(contents, key, expectedVariant) {
45
45
  if (e.value.$kind !== expectedVariant) throw HashiConfigError.wrongVariant(key, expectedVariant, e.value.$kind);
46
46
  return e.value;
47
47
  }
48
+ /**
49
+ * Read a `Bytes` config entry and assert its byte length. Throws
50
+ * `HashiConfigError` if the key is missing, holds a non-`Bytes` variant, or
51
+ * has the wrong length.
52
+ */
53
+ function configBytes(contents, key, expectedLen) {
54
+ const v = entry(contents, key, "Bytes");
55
+ if (v.Bytes.length !== expectedLen) throw HashiConfigError.malformedPayload(key, "Bytes", `expected ${expectedLen} bytes, got ${v.Bytes.length}`);
56
+ return new Uint8Array(v.Bytes);
57
+ }
48
58
 
49
59
  //#endregion
50
- export { assertHex32, entry, reverseTxidBytes };
60
+ export { assertHex32, configBytes, entry, reverseTxidBytes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mysten-incubation/hashi",
3
- "version": "0.1.1",
3
+ "version": "0.3.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>",
@@ -59,7 +59,7 @@
59
59
  "test": "vitest --project=unit",
60
60
  "test:integration": "vitest run --project=integration",
61
61
  "coverage": "vitest run --project=unit --coverage",
62
- "codegen": "sui-ts-codegen generate",
62
+ "codegen": "sui-ts-codegen generate && prettier --ignore-path /dev/null --tab-width 4 --print-width 100 --write 'src/contracts/**/*.ts'",
63
63
  "format": "prettier --write \"**/*.{ts,json,md}\""
64
64
  }
65
65
  }