@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 +12 -0
- package/dist/bitcoin.d.mts +68 -36
- package/dist/bitcoin.mjs +73 -53
- package/dist/client.d.mts +15 -2
- package/dist/client.mjs +102 -32
- package/dist/constants.mjs +11 -1
- package/dist/contracts/hashi/utxo_pool.mjs +2 -1
- package/dist/contracts/hashi/withdraw.mjs +2 -1
- package/dist/contracts/hashi/withdrawal_queue.mjs +11 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/types.d.mts +25 -0
- package/dist/util.mjs +11 -1
- package/package.json +2 -2
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
|
package/dist/bitcoin.d.mts
CHANGED
|
@@ -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:
|
|
59
|
+
* Builds a 2-of-2 P2TR script-path-only address:
|
|
60
|
+
* `tr(NUMS, multi_a(2, guardian, derived_mpc))`.
|
|
60
61
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
74
|
-
* tweak
|
|
75
|
-
* outputKey
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
|
133
|
+
* The address scheme is:
|
|
99
134
|
* ```text
|
|
100
|
-
* tr(NUMS,
|
|
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(
|
|
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,
|
|
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
|
-
*
|
|
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
|
|
19
|
-
* whose spending condition is
|
|
20
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* client's
|
|
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,
|
|
35
|
-
* Nothing-Up-My-Sleeve point with no known private key,
|
|
36
|
-
* through the script path (see
|
|
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:
|
|
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
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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
|
|
145
|
-
* tweak
|
|
146
|
-
* outputKey
|
|
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
|
-
*
|
|
150
|
-
*
|
|
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
|
|
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
|
|
157
|
-
if (
|
|
158
|
-
|
|
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(
|
|
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
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
|
207
|
+
* The address scheme is:
|
|
180
208
|
* ```text
|
|
181
|
-
* tr(NUMS,
|
|
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(
|
|
195
|
-
return
|
|
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,
|
|
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
|
|
54
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
452
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
927
|
+
approvalTimestampMs,
|
|
928
|
+
confirmableAtMs: approvalTimestampMs !== null && timeDelayMs !== null ? approvalTimestampMs + timeDelayMs : null
|
|
859
929
|
};
|
|
860
930
|
}
|
|
861
931
|
function parseWithdrawalHistoryItem(content) {
|
package/dist/constants.mjs
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
}
|