@mysten-incubation/hashi 0.2.0 → 0.3.1

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.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 0af8dfa: Derive Bitcoin deposit addresses with Hashi's delayed MPC recovery taproot leaf.
8
+
9
+ ## 0.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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.
14
+
3
15
  ## 0.2.0
4
16
 
5
17
  ### Minor Changes
@@ -56,61 +56,100 @@ 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 Hashi's P2TR script-path-only deposit address:
60
+ * `tr(NUMS, {multi_a(2, guardian, derived_mpc), and_v(v:older(delay), pk(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 immediate-spend script the bridge's withdrawal path constructs
66
+ * in `crates/hashi-types/src/bitcoin/taproot.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.
82
+ *
83
+ * The recovery leaf script is a BIP-342 `and_v(v:older(delay), pk(derived_mpc))`:
84
+ * after Hashi's 60-day BIP-68 relative timelock, the MPC child key alone can
85
+ * spend the output if the guardian key is unavailable.
70
86
  *
71
87
  * The taproot output key is computed per BIP-341:
88
+ *
72
89
  * ```text
73
- * leafHash = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(script) ‖ script)
74
- * tweak = tagged_hash("TapTweak", NUMSleafHash)
75
- * outputKey = NUMS + tweak × G
90
+ * leaf1 = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(two_of_two_script) ‖ two_of_two_script)
91
+ * leaf2 = tagged_hash("TapLeaf", 0xC0compact_size(recovery_script) ‖ recovery_script)
92
+ * root = tagged_hash("TapBranch", min(leaf1, leaf2) max(leaf1, leaf2))
93
+ * tweak = tagged_hash("TapTweak", NUMS ‖ root)
94
+ * outputKey = NUMS + tweak × G
76
95
  * ```
77
96
  *
78
- * The resulting output key is encoded as a SegWit v1 witness program in a
79
- * bech32m address.
97
+ * **Argument ordering is load-bearing.** Guardian first, derived-MPC second.
98
+ * Swapping them produces a different (but real-looking) `tb1p…` whose
99
+ * withdrawals the bridge cannot spend.
80
100
  *
81
- * @param pubkey - 32-byte x-only public key for the script leaf
101
+ * @param guardianBtcXOnly - 32-byte BIP-340 x-only guardian BTC public key
102
+ * (from the on-chain `guardian_btc_public_key` config)
103
+ * @param derivedMpcXOnly - 32-byte x-only public key for the MPC-derived child
104
+ * (output of {@link deriveChildPubkey})
82
105
  * @param network - Bitcoin network for the bech32m human-readable prefix
83
106
  * @returns bech32m-encoded P2TR address (e.g. `bc1p…`, `tb1p…`, `bcrt1p…`)
84
107
  */
85
- declare function taprootScriptPathAddress(pubkey: Uint8Array, network: BitcoinNetwork): string;
108
+ declare function twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly: Uint8Array, derivedMpcXOnly: Uint8Array, network: BitcoinNetwork): string;
109
+ /**
110
+ * Named-args input bundle for {@link generateDepositAddress}. Using named args
111
+ * avoids the foot-gun of two 32-byte `Uint8Array`s in adjacent positions —
112
+ * swapping `guardianBtcXOnly` and `suiAddress` would silently produce a valid
113
+ * but wrong address.
114
+ */
115
+ interface DepositAddressInputs {
116
+ /**
117
+ * 33-byte SEC1-compressed secp256k1 MPC master key (post-arkworks
118
+ * conversion). See {@link arkworksToSec1Compressed}.
119
+ */
120
+ readonly mpcMasterCompressed: Uint8Array;
121
+ /**
122
+ * 32-byte BIP-340 x-only guardian BTC public key (from the on-chain
123
+ * `guardian_btc_public_key` config).
124
+ */
125
+ readonly guardianBtcXOnly: Uint8Array;
126
+ /** 32-byte Sui address used as the derivation path. */
127
+ readonly suiAddress: Uint8Array;
128
+ /** Bitcoin network (determines the bech32m address prefix). */
129
+ readonly network: BitcoinNetwork;
130
+ }
86
131
  /**
87
132
  * Generates a Bitcoin P2TR deposit address for a Sui address.
88
133
  *
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.
134
+ * Main entry point for the address derivation pipeline. Combines
135
+ * {@link deriveChildPubkey} and {@link twoOfTwoTaprootScriptPathAddress} into
136
+ * a single call. The produced address matches the Rust node's
137
+ * `hashi_types::bitcoin::taproot::taproot_address` byte-for-byte.
97
138
  *
98
- * The current devnet address scheme uses a single-key script path:
139
+ * The address scheme is:
99
140
  * ```text
100
- * tr(NUMS, pk(derive(H, d)))
141
+ * tr(NUMS, {multi_a(2, guardian, derive(mpc_master, sui_address)),
142
+ * and_v(v:older(delay), pk(derive(mpc_master, sui_address)))})
101
143
  * ```
102
- * where `H` is the MPC master key and `d` is the depositor's Sui address.
103
144
  *
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
145
  * @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
112
146
  */
113
- declare function generateDepositAddress(mpcKeyCompressed: Uint8Array, suiAddress: Uint8Array, network: BitcoinNetwork): string;
147
+ declare function generateDepositAddress({
148
+ mpcMasterCompressed,
149
+ guardianBtcXOnly,
150
+ suiAddress,
151
+ network
152
+ }: DepositAddressInputs): string;
114
153
  /**
115
154
  * Decodes a bech32/bech32m SegWit Bitcoin address into a witness program.
116
155
  *
@@ -149,4 +188,4 @@ declare function bitcoinAddressToWitnessProgram(address: string, network: Bitcoi
149
188
  */
150
189
  declare function witnessProgramToAddress(program: Uint8Array, network: BitcoinNetwork): string;
151
190
  //#endregion
152
- export { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress, witnessProgramToAddress };
191
+ export { DepositAddressInputs, arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, twoOfTwoTaprootScriptPathAddress, witnessProgramToAddress };
package/dist/bitcoin.mjs CHANGED
@@ -13,35 +13,45 @@ 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
19
  * 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.
20
+ * with two leaves: an immediate `multi_a(2, guardian_btc_pubkey,
21
+ * derive(mpc_master, sui_address))` spend, and a delayed MPC-only recovery
22
+ * spend after Hashi's BIP-68 relative timelock.
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:
38
+ * `tr(NUMS, {multi_a(2, guardian, child), and_v(v:older(delay), pk(child))})`
39
+ * where NUMS is a Nothing-Up-My-Sleeve point with no known private key,
40
+ * forcing all spends through the script path (see
41
+ * {@link twoOfTwoTaprootScriptPathAddress}).
37
42
  *
38
43
  * The end-to-end helper {@link generateDepositAddress} combines steps 2–3.
39
44
  *
45
+ * Mirrors `taproot_address` in `crates/hashi-types/src/bitcoin/taproot.rs`.
46
+ * Cross-language test vectors live in both this file's unit tests and the
47
+ * matching Rust unit test `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;
43
52
  const CURVE_ORDER = Point.CURVE().n;
44
53
  const NUMS_POINT = Point.fromBytes(concatBytes(new Uint8Array([2]), NUMS_KEY));
54
+ const HASHI_MPC_RECOVERY_DELAY_SEQUENCE = 1 << 22 | Math.ceil(1440 * 60 * 60 / 512);
45
55
  /** BIP-340 tagged hash: SHA256(SHA256(tag) ‖ SHA256(tag) ‖ msg) */
46
56
  function taggedHash(tag, ...msgs) {
47
57
  const tagHash = sha256(new TextEncoder().encode(tag));
@@ -53,6 +63,37 @@ function bytesToNumberBE(bytes) {
53
63
  for (const byte of bytes) n = n << 8n | BigInt(byte);
54
64
  return n;
55
65
  }
66
+ /** CompactSize for scripts small enough to fit in one byte. */
67
+ function compactSize(len) {
68
+ if (len >= 253) throw new Error(`Unsupported script length ${len}`);
69
+ return new Uint8Array([len]);
70
+ }
71
+ function scriptNum(n) {
72
+ if (n === 0) return new Uint8Array();
73
+ const bytes = [];
74
+ let value = n;
75
+ while (value > 0) {
76
+ bytes.push(value & 255);
77
+ value >>= 8;
78
+ }
79
+ if ((bytes[bytes.length - 1] & 128) !== 0) bytes.push(0);
80
+ return new Uint8Array(bytes);
81
+ }
82
+ function pushBytes(bytes) {
83
+ if (bytes.length >= 76) throw new Error(`Unsupported push length ${bytes.length}`);
84
+ return concatBytes(new Uint8Array([bytes.length]), bytes);
85
+ }
86
+ function lexicographicCompare(a, b) {
87
+ const len = Math.min(a.length, b.length);
88
+ for (let i = 0; i < len; i++) if (a[i] !== b[i]) return a[i] - b[i];
89
+ return a.length - b.length;
90
+ }
91
+ function tapLeafHash(script) {
92
+ return taggedHash("TapLeaf", new Uint8Array([192]), compactSize(script.length), script);
93
+ }
94
+ function tapBranchHash(a, b) {
95
+ return lexicographicCompare(a, b) <= 0 ? taggedHash("TapBranch", a, b) : taggedHash("TapBranch", b, a);
96
+ }
56
97
  /**
57
98
  * Converts a 33-byte arkworks-compressed secp256k1 point to 33-byte SEC1 compressed format.
58
99
  *
@@ -127,39 +168,73 @@ function deriveChildPubkey(mpcKeyCompressed, suiAddress) {
127
168
  return parentPoint.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
128
169
  }
129
170
  /**
130
- * Builds a P2TR script-path-only address: `tr(NUMS, pk(pubkey))`.
171
+ * Builds Hashi's P2TR script-path-only deposit address:
172
+ * `tr(NUMS, {multi_a(2, guardian, derived_mpc), and_v(v:older(delay), pk(derived_mpc))})`.
131
173
  *
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.
174
+ * The leaf script is a BIP-342 `multi_a` 2-of-2 both Schnorr signatures must
175
+ * be present to spend, ordered so that the witness stack is
176
+ * `[derived_mpc_sig, guardian_sig, leaf_script, control_block]` (LIFO). This
177
+ * is the exact immediate-spend script the bridge's withdrawal path constructs
178
+ * in `crates/hashi-types/src/bitcoin/taproot.rs`'s `compute_taproot_descriptor`.
137
179
  *
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.
180
+ * The leaf script is exactly 70 bytes:
181
+ *
182
+ * ```text
183
+ * 0x20 ‖ guardian (32) // OP_PUSHBYTES_32 <pk1>
184
+ * 0xAC // OP_CHECKSIG
185
+ * 0x20 ‖ derived_mpc(32) // OP_PUSHBYTES_32 <pk2>
186
+ * 0xBA // OP_CHECKSIGADD
187
+ * 0x52 // OP_2 (push small int 2)
188
+ * 0x9C // OP_NUMEQUAL
189
+ * ```
190
+ *
191
+ * Note `0x52` (OP_2) — not `0x01 0x02` — because `rust-miniscript`'s `multi_a`
192
+ * codegen routes through `Builder::push_int(2)` which emits the small-num
193
+ * opcode. A literal pushdata would change the leaf hash and the address.
194
+ *
195
+ * The recovery leaf script is a BIP-342 `and_v(v:older(delay), pk(derived_mpc))`:
196
+ * after Hashi's 60-day BIP-68 relative timelock, the MPC child key alone can
197
+ * spend the output if the guardian key is unavailable.
141
198
  *
142
199
  * The taproot output key is computed per BIP-341:
200
+ *
143
201
  * ```text
144
- * leafHash = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(script) ‖ script)
145
- * tweak = tagged_hash("TapTweak", NUMSleafHash)
146
- * outputKey = NUMS + tweak × G
202
+ * leaf1 = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(two_of_two_script) ‖ two_of_two_script)
203
+ * leaf2 = tagged_hash("TapLeaf", 0xC0compact_size(recovery_script) ‖ recovery_script)
204
+ * root = tagged_hash("TapBranch", min(leaf1, leaf2) max(leaf1, leaf2))
205
+ * tweak = tagged_hash("TapTweak", NUMS ‖ root)
206
+ * outputKey = NUMS + tweak × G
147
207
  * ```
148
208
  *
149
- * The resulting output key is encoded as a SegWit v1 witness program in a
150
- * bech32m address.
209
+ * **Argument ordering is load-bearing.** Guardian first, derived-MPC second.
210
+ * Swapping them produces a different (but real-looking) `tb1p…` whose
211
+ * withdrawals the bridge cannot spend.
151
212
  *
152
- * @param pubkey - 32-byte x-only public key for the script leaf
213
+ * @param guardianBtcXOnly - 32-byte BIP-340 x-only guardian BTC public key
214
+ * (from the on-chain `guardian_btc_public_key` config)
215
+ * @param derivedMpcXOnly - 32-byte x-only public key for the MPC-derived child
216
+ * (output of {@link deriveChildPubkey})
153
217
  * @param network - Bitcoin network for the bech32m human-readable prefix
154
218
  * @returns bech32m-encoded P2TR address (e.g. `bc1p…`, `tb1p…`, `bcrt1p…`)
155
219
  */
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);
159
- tapscript[0] = 32;
160
- tapscript.set(pubkey, 1);
161
- tapscript[33] = 172;
162
- const tweakScalar = bytesToNumberBE(taggedHash("TapTweak", NUMS_KEY, taggedHash("TapLeaf", new Uint8Array([192, tapscript.length]), tapscript))) % CURVE_ORDER;
220
+ function twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly, derivedMpcXOnly, network) {
221
+ if (guardianBtcXOnly.length !== 32) throw new Error(`Expected 32-byte x-only guardian pubkey, got ${guardianBtcXOnly.length}`);
222
+ if (derivedMpcXOnly.length !== 32) throw new Error(`Expected 32-byte x-only derived MPC pubkey, got ${derivedMpcXOnly.length}`);
223
+ const twoOfTwoScript = new Uint8Array(70);
224
+ twoOfTwoScript[0] = 32;
225
+ twoOfTwoScript.set(guardianBtcXOnly, 1);
226
+ twoOfTwoScript[33] = 172;
227
+ twoOfTwoScript[34] = 32;
228
+ twoOfTwoScript.set(derivedMpcXOnly, 35);
229
+ twoOfTwoScript[67] = 186;
230
+ twoOfTwoScript[68] = 82;
231
+ twoOfTwoScript[69] = 156;
232
+ const recoveryScript = concatBytes(pushBytes(scriptNum(HASHI_MPC_RECOVERY_DELAY_SEQUENCE)), new Uint8Array([
233
+ 178,
234
+ 105,
235
+ 32
236
+ ]), derivedMpcXOnly, new Uint8Array([172]));
237
+ const tweakScalar = bytesToNumberBE(taggedHash("TapTweak", NUMS_KEY, tapBranchHash(tapLeafHash(twoOfTwoScript), tapLeafHash(recoveryScript)))) % CURVE_ORDER;
163
238
  const outputKey = NUMS_POINT.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
164
239
  const words = [1, ...bech32m.toWords(outputKey)];
165
240
  return bech32m.encode(NETWORK_HRP[network], words);
@@ -167,32 +242,21 @@ function taprootScriptPathAddress(pubkey, network) {
167
242
  /**
168
243
  * Generates a Bitcoin P2TR deposit address for a Sui address.
169
244
  *
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.
245
+ * Main entry point for the address derivation pipeline. Combines
246
+ * {@link deriveChildPubkey} and {@link twoOfTwoTaprootScriptPathAddress} into
247
+ * a single call. The produced address matches the Rust node's
248
+ * `hashi_types::bitcoin::taproot::taproot_address` byte-for-byte.
178
249
  *
179
- * The current devnet address scheme uses a single-key script path:
250
+ * The address scheme is:
180
251
  * ```text
181
- * tr(NUMS, pk(derive(H, d)))
252
+ * tr(NUMS, {multi_a(2, guardian, derive(mpc_master, sui_address)),
253
+ * and_v(v:older(delay), pk(derive(mpc_master, sui_address)))})
182
254
  * ```
183
- * where `H` is the MPC master key and `d` is the depositor's Sui address.
184
255
  *
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
256
  * @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
193
257
  */
194
- function generateDepositAddress(mpcKeyCompressed, suiAddress, network) {
195
- return taprootScriptPathAddress(deriveChildPubkey(mpcKeyCompressed, suiAddress), network);
258
+ function generateDepositAddress({ mpcMasterCompressed, guardianBtcXOnly, suiAddress, network }) {
259
+ return twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly, deriveChildPubkey(mpcMasterCompressed, suiAddress), network);
196
260
  }
197
261
  /**
198
262
  * Decodes a bech32/bech32m SegWit Bitcoin address into a witness program.
@@ -292,4 +356,4 @@ function witnessProgramToAddress(program, network) {
292
356
  }
293
357
 
294
358
  //#endregion
295
- export { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress, witnessProgramToAddress };
359
+ 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 the Hashi taproot script tree: an immediate 2-of-2 leaf
56
+ * (`multi_a(2, guardian, derived_mpc)`) plus a delayed MPC-only recovery
57
+ * leaf. The address matches the bridge's on-chain
58
+ * `validate_deposit_request_derivation_path` check 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,
@@ -455,8 +442,21 @@ var HashiClient = class {
455
442
  /**
456
443
  * Generates a unique Bitcoin P2TR deposit address for a Sui address.
457
444
  *
458
- * Fetches the MPC committee public key from on-chain, derives a child key
459
- * 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 the Hashi taproot script tree: an immediate 2-of-2 leaf
448
+ * (`multi_a(2, guardian, derived_mpc)`) plus a delayed MPC-only recovery
449
+ * leaf. The address matches the bridge's on-chain
450
+ * `validate_deposit_request_derivation_path` check 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.
460
460
  *
461
461
  * @example
462
462
  * ```ts
@@ -466,7 +466,13 @@ var HashiClient = class {
466
466
  * ```
467
467
  */
468
468
  async generateDepositAddress({ suiAddress, bitcoinNetwork = this.#bitcoinNetwork }) {
469
- 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
+ });
470
476
  }
471
477
  /**
472
478
  * Submit one or more Bitcoin deposits for committee confirmation, batched
@@ -633,6 +639,22 @@ var HashiClient = class {
633
639
  };
634
640
  const bool = (key) => entry(contents, key, "Bool").Bool;
635
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
+ };
636
658
  const rawDepositMin = u64("bitcoin_deposit_minimum");
637
659
  const rawWithdrawalMin = u64("bitcoin_withdrawal_minimum");
638
660
  const bitcoinDepositMinimum = rawDepositMin < DUST_RELAY_MIN_VALUE ? DUST_RELAY_MIN_VALUE : rawDepositMin;
@@ -646,7 +668,10 @@ var HashiClient = class {
646
668
  withdrawalCancellationCooldownMs: u64("withdrawal_cancellation_cooldown_ms"),
647
669
  bitcoinDepositTimeDelayMs: u64("bitcoin_deposit_time_delay_ms"),
648
670
  depositMinimum: bitcoinDepositMinimum,
649
- 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)
650
675
  };
651
676
  }
652
677
  /**
@@ -696,6 +721,31 @@ var HashiClient = class {
696
721
  return 0n;
697
722
  }
698
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
+ /**
699
749
  * Fetches the `BitcoinState` dynamic field from the Hashi shared object.
700
750
  * Returns the BCS-parsed struct whose nested Bag/Table IDs are used by
701
751
  * `findUsedUtxos` and `transactionHistory`.
@@ -851,6 +901,16 @@ var HashiClient = class {
851
901
  }
852
902
  }
853
903
  };
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
+ }
854
914
  function parseDepositHistoryItem(content, timeDelayMs) {
855
915
  const parsed = DepositRequest.parse(content);
856
916
  const approvalTimestampMs = parsed.approval_timestamp_ms === null ? null : BigInt(parsed.approval_timestamp_ms);
@@ -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;
@@ -39,6 +44,19 @@ interface GovernanceConfig {
39
44
  readonly bitcoinDepositTimeDelayMs: bigint;
40
45
  readonly depositMinimum: bigint;
41
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 immediate 2-of-2 leaf in the on-chain deposit-address
57
+ * descriptor. `null` if unset (deposit-address derivation will fail).
58
+ */
59
+ readonly guardianBtcPublicKey: Uint8Array | null;
42
60
  }
43
61
  /**
44
62
  * A single UTXO output within a Bitcoin transaction used to fund a deposit.
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.2.0",
3
+ "version": "0.3.1",
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
  }