@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 +12 -0
- package/dist/bitcoin.d.mts +75 -36
- package/dist/bitcoin.mjs +119 -55
- package/dist/client.d.mts +15 -2
- package/dist/client.mjs +83 -23
- 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 +18 -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.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
|
package/dist/bitcoin.d.mts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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 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
|
|
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.
|
|
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
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
90
|
+
* leaf1 = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(two_of_two_script) ‖ two_of_two_script)
|
|
91
|
+
* leaf2 = tagged_hash("TapLeaf", 0xC0 ‖ compact_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
|
-
*
|
|
79
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
|
139
|
+
* The address scheme is:
|
|
99
140
|
* ```text
|
|
100
|
-
* tr(NUMS,
|
|
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(
|
|
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,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
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
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
202
|
+
* leaf1 = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(two_of_two_script) ‖ two_of_two_script)
|
|
203
|
+
* leaf2 = tagged_hash("TapLeaf", 0xC0 ‖ compact_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
|
-
*
|
|
150
|
-
*
|
|
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
|
|
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
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
|
250
|
+
* The address scheme is:
|
|
180
251
|
* ```text
|
|
181
|
-
* tr(NUMS,
|
|
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(
|
|
195
|
-
return
|
|
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,
|
|
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
|
|
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 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
|
-
|
|
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,
|
|
@@ -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
|
|
459
|
-
*
|
|
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
|
-
|
|
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);
|
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;
|
|
@@ -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.
|
|
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
|
}
|