@mysten-incubation/hashi 0.3.0 → 0.4.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 +16 -0
- package/README.md +1 -1
- package/dist/bitcoin.d.mts +15 -8
- package/dist/bitcoin.mjs +71 -27
- package/dist/client.d.mts +37 -11
- package/dist/client.mjs +90 -15
- package/dist/constants.mjs +2 -2
- package/dist/contracts/hashi/bitcoin_state.mjs +8 -0
- package/dist/contracts/hashi/committee.mjs +10 -3
- package/dist/contracts/hashi/committee_set.mjs +32 -3
- package/dist/contracts/hashi/config.mjs +10 -10
- package/dist/contracts/hashi/config_value.mjs +10 -0
- package/dist/contracts/hashi/deposit.mjs +18 -9
- package/dist/contracts/hashi/deposit_queue.mjs +11 -3
- package/dist/contracts/hashi/hashi.mjs +12 -2
- package/dist/contracts/hashi/mpc_signing.mjs +56 -0
- package/dist/contracts/hashi/proposals.mjs +6 -0
- package/dist/contracts/hashi/treasury.mjs +10 -4
- package/dist/contracts/hashi/utxo.mjs +7 -0
- package/dist/contracts/hashi/utxo_pool.mjs +10 -3
- package/dist/contracts/hashi/versioning.mjs +28 -0
- package/dist/contracts/hashi/withdraw.mjs +17 -1
- package/dist/contracts/hashi/withdrawal_queue.mjs +44 -24
- package/dist/errors.d.mts +28 -1
- package/dist/errors.mjs +17 -1
- package/dist/guardian.d.mts +26 -0
- package/dist/guardian.mjs +101 -0
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +3 -2
- package/dist/types.d.mts +64 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @mysten-incubation/hashi
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 874ec08: feat: add a `client.hashi.guardian.*` namespace (`info`, `limiterStatus`, `canWithdraw`) that reads the guardian's rate-limiter headroom from its read-only `/info` endpoint, resolving the guardian URL from `guardianUrl`, a `guardianInfoProvider`, or the on-chain `guardian_url` config
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 8f7606f: Track the redeployed devnet contracts: regenerate bindings against hashi's `testnet` tip (`0e67b619`) (`config_value::Value` gained `U128`/`U256`, shifting the BCS tags the SDK decodes the on-chain config with), follow the `DepositRequested`/`WithdrawalRequested` event renames and request-object field renames, and point `NETWORK_CONFIG.devnet` at the new package and Hashi object.
|
|
12
|
+
|
|
13
|
+
## 0.3.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 0af8dfa: Derive Bitcoin deposit addresses with Hashi's delayed MPC recovery taproot leaf.
|
|
18
|
+
|
|
3
19
|
## 0.3.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ await client.hashi.deposit({
|
|
|
45
45
|
});
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
End-user actions only: **deposit**, **request withdrawal**, **cancel withdrawal**.
|
|
48
|
+
End-user actions only: **deposit**, **request withdrawal**, **cancel withdrawal**. An optional `client.hashi.guardian.*` namespace reads the guardian's rate-limiter headroom (`limiterStatus`, `canWithdraw`) — see the root README.
|
|
49
49
|
|
|
50
50
|
## Documentation
|
|
51
51
|
|
package/dist/bitcoin.d.mts
CHANGED
|
@@ -56,14 +56,14 @@ declare function arkworksToSec1Compressed(ark: Uint8Array): Uint8Array;
|
|
|
56
56
|
*/
|
|
57
57
|
declare function deriveChildPubkey(mpcKeyCompressed: Uint8Array, suiAddress: Uint8Array): Uint8Array;
|
|
58
58
|
/**
|
|
59
|
-
* Builds
|
|
60
|
-
* `tr(NUMS, multi_a(2, guardian, derived_mpc))`.
|
|
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))})`.
|
|
61
61
|
*
|
|
62
62
|
* The leaf script is a BIP-342 `multi_a` 2-of-2 — both Schnorr signatures must
|
|
63
63
|
* be present to spend, ordered so that the witness stack is
|
|
64
64
|
* `[derived_mpc_sig, guardian_sig, leaf_script, control_block]` (LIFO). This
|
|
65
|
-
* is the exact script the bridge's withdrawal path constructs
|
|
66
|
-
* `crates/hashi-types/src/
|
|
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`.
|
|
67
67
|
*
|
|
68
68
|
* The leaf script is exactly 70 bytes:
|
|
69
69
|
*
|
|
@@ -80,11 +80,17 @@ declare function deriveChildPubkey(mpcKeyCompressed: Uint8Array, suiAddress: Uin
|
|
|
80
80
|
* codegen routes through `Builder::push_int(2)` which emits the small-num
|
|
81
81
|
* opcode. A literal pushdata would change the leaf hash and the address.
|
|
82
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.
|
|
86
|
+
*
|
|
83
87
|
* The taproot output key is computed per BIP-341:
|
|
84
88
|
*
|
|
85
89
|
* ```text
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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)
|
|
88
94
|
* outputKey = NUMS + tweak × G
|
|
89
95
|
* ```
|
|
90
96
|
*
|
|
@@ -128,11 +134,12 @@ interface DepositAddressInputs {
|
|
|
128
134
|
* Main entry point for the address derivation pipeline. Combines
|
|
129
135
|
* {@link deriveChildPubkey} and {@link twoOfTwoTaprootScriptPathAddress} into
|
|
130
136
|
* a single call. The produced address matches the Rust node's
|
|
131
|
-
* `
|
|
137
|
+
* `hashi_types::bitcoin::taproot::taproot_address` byte-for-byte.
|
|
132
138
|
*
|
|
133
139
|
* The address scheme is:
|
|
134
140
|
* ```text
|
|
135
|
-
* tr(NUMS, multi_a(2, guardian, derive(mpc_master, sui_address))
|
|
141
|
+
* tr(NUMS, {multi_a(2, guardian, derive(mpc_master, sui_address)),
|
|
142
|
+
* and_v(v:older(delay), pk(derive(mpc_master, sui_address)))})
|
|
136
143
|
* ```
|
|
137
144
|
*
|
|
138
145
|
* @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
|
package/dist/bitcoin.mjs
CHANGED
|
@@ -16,10 +16,10 @@ import { concatBytes } from "@noble/hashes/utils.js";
|
|
|
16
16
|
* and the Hashi guardian co-sign the withdrawal that spends it, and the bridge
|
|
17
17
|
* mints equivalent tokens on Sui.
|
|
18
18
|
*
|
|
19
|
-
* The deposit address is a
|
|
20
|
-
*
|
|
21
|
-
* derive(mpc_master, sui_address))`
|
|
22
|
-
*
|
|
19
|
+
* The deposit address is a Pay-to-Taproot (P2TR / BIP-341) script-path 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.
|
|
23
23
|
*
|
|
24
24
|
* The full derivation pipeline is:
|
|
25
25
|
*
|
|
@@ -34,23 +34,24 @@ import { concatBytes } from "@noble/hashes/utils.js";
|
|
|
34
34
|
* (see {@link deriveChildPubkey}). This replicates the Rust function
|
|
35
35
|
* `fastcrypto_tbls::threshold_schnorr::key_derivation::derive_verifying_key`.
|
|
36
36
|
*
|
|
37
|
-
* 3. **Build** the taproot address:
|
|
37
|
+
* 3. **Build** the taproot address:
|
|
38
|
+
* `tr(NUMS, {multi_a(2, guardian, child), and_v(v:older(delay), pk(child))})`
|
|
38
39
|
* where NUMS is a Nothing-Up-My-Sleeve point with no known private key,
|
|
39
40
|
* forcing all spends through the script path (see
|
|
40
41
|
* {@link twoOfTwoTaprootScriptPathAddress}).
|
|
41
42
|
*
|
|
42
43
|
* The end-to-end helper {@link generateDepositAddress} combines steps 2–3.
|
|
43
44
|
*
|
|
44
|
-
* Mirrors `
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `cross_lang_2of2_test_vectors`.
|
|
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
48
|
*
|
|
49
49
|
* @see https://mystenlabs.github.io/hashi/design/address-scheme.html
|
|
50
50
|
*/
|
|
51
51
|
const Point = secp256k1.Point;
|
|
52
52
|
const CURVE_ORDER = Point.CURVE().n;
|
|
53
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);
|
|
54
55
|
/** BIP-340 tagged hash: SHA256(SHA256(tag) ‖ SHA256(tag) ‖ msg) */
|
|
55
56
|
function taggedHash(tag, ...msgs) {
|
|
56
57
|
const tagHash = sha256(new TextEncoder().encode(tag));
|
|
@@ -62,6 +63,37 @@ function bytesToNumberBE(bytes) {
|
|
|
62
63
|
for (const byte of bytes) n = n << 8n | BigInt(byte);
|
|
63
64
|
return n;
|
|
64
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
|
+
}
|
|
65
97
|
/**
|
|
66
98
|
* Converts a 33-byte arkworks-compressed secp256k1 point to 33-byte SEC1 compressed format.
|
|
67
99
|
*
|
|
@@ -136,14 +168,14 @@ function deriveChildPubkey(mpcKeyCompressed, suiAddress) {
|
|
|
136
168
|
return parentPoint.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
|
|
137
169
|
}
|
|
138
170
|
/**
|
|
139
|
-
* Builds
|
|
140
|
-
* `tr(NUMS, multi_a(2, guardian, derived_mpc))`.
|
|
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))})`.
|
|
141
173
|
*
|
|
142
174
|
* The leaf script is a BIP-342 `multi_a` 2-of-2 — both Schnorr signatures must
|
|
143
175
|
* be present to spend, ordered so that the witness stack is
|
|
144
176
|
* `[derived_mpc_sig, guardian_sig, leaf_script, control_block]` (LIFO). This
|
|
145
|
-
* is the exact script the bridge's withdrawal path constructs
|
|
146
|
-
* `crates/hashi-types/src/
|
|
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`.
|
|
147
179
|
*
|
|
148
180
|
* The leaf script is exactly 70 bytes:
|
|
149
181
|
*
|
|
@@ -160,11 +192,17 @@ function deriveChildPubkey(mpcKeyCompressed, suiAddress) {
|
|
|
160
192
|
* codegen routes through `Builder::push_int(2)` which emits the small-num
|
|
161
193
|
* opcode. A literal pushdata would change the leaf hash and the address.
|
|
162
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.
|
|
198
|
+
*
|
|
163
199
|
* The taproot output key is computed per BIP-341:
|
|
164
200
|
*
|
|
165
201
|
* ```text
|
|
166
|
-
*
|
|
167
|
-
*
|
|
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)
|
|
168
206
|
* outputKey = NUMS + tweak × G
|
|
169
207
|
* ```
|
|
170
208
|
*
|
|
@@ -182,16 +220,21 @@ function deriveChildPubkey(mpcKeyCompressed, suiAddress) {
|
|
|
182
220
|
function twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly, derivedMpcXOnly, network) {
|
|
183
221
|
if (guardianBtcXOnly.length !== 32) throw new Error(`Expected 32-byte x-only guardian pubkey, got ${guardianBtcXOnly.length}`);
|
|
184
222
|
if (derivedMpcXOnly.length !== 32) throw new Error(`Expected 32-byte x-only derived MPC pubkey, got ${derivedMpcXOnly.length}`);
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
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;
|
|
195
238
|
const outputKey = NUMS_POINT.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
|
|
196
239
|
const words = [1, ...bech32m.toWords(outputKey)];
|
|
197
240
|
return bech32m.encode(NETWORK_HRP[network], words);
|
|
@@ -202,11 +245,12 @@ function twoOfTwoTaprootScriptPathAddress(guardianBtcXOnly, derivedMpcXOnly, net
|
|
|
202
245
|
* Main entry point for the address derivation pipeline. Combines
|
|
203
246
|
* {@link deriveChildPubkey} and {@link twoOfTwoTaprootScriptPathAddress} into
|
|
204
247
|
* a single call. The produced address matches the Rust node's
|
|
205
|
-
* `
|
|
248
|
+
* `hashi_types::bitcoin::taproot::taproot_address` byte-for-byte.
|
|
206
249
|
*
|
|
207
250
|
* The address scheme is:
|
|
208
251
|
* ```text
|
|
209
|
-
* tr(NUMS, multi_a(2, guardian, derive(mpc_master, sui_address))
|
|
252
|
+
* tr(NUMS, {multi_a(2, guardian, derive(mpc_master, sui_address)),
|
|
253
|
+
* and_v(v:older(delay), pk(derive(mpc_master, sui_address)))})
|
|
210
254
|
* ```
|
|
211
255
|
*
|
|
212
256
|
* @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
|
package/dist/client.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RawTransactionArgument } from "./contracts/utils/index.mjs";
|
|
2
|
-
import { BitcoinNetwork, CancelWithdrawalParams, DepositFees, DepositInfo, DepositParams, GovernanceConfig, HashiClientOptions, HbtcBalance, SuiNetwork, TransactionHistoryItem, UtxoId, UtxoLookupResult, UtxoUsageResult, WaitOptions, WithdrawalFees, WithdrawalInfo, WithdrawalParams } from "./types.mjs";
|
|
2
|
+
import { BitcoinNetwork, CancelWithdrawalParams, DepositFees, DepositInfo, DepositParams, GovernanceConfig, GuardianInfoProvider, GuardianLimiterSnapshot, GuardianWithdrawCheck, HashiClientOptions, HbtcBalance, RawGuardianInfo, SuiNetwork, TransactionHistoryItem, UtxoId, UtxoLookupResult, UtxoUsageResult, WaitOptions, WithdrawalFees, WithdrawalInfo, WithdrawalParams } from "./types.mjs";
|
|
3
3
|
import * as _mysten_sui_transactions0 from "@mysten/sui/transactions";
|
|
4
4
|
import { Transaction } from "@mysten/sui/transactions";
|
|
5
5
|
import { ClientWithCoreApi, SuiClientTypes } from "@mysten/sui/client";
|
|
@@ -37,7 +37,9 @@ declare class HashiClient {
|
|
|
37
37
|
packageId,
|
|
38
38
|
bitcoinNetwork,
|
|
39
39
|
btcRpcUrl,
|
|
40
|
-
graphqlUrl
|
|
40
|
+
graphqlUrl,
|
|
41
|
+
guardianUrl,
|
|
42
|
+
guardianInfoProvider
|
|
41
43
|
}: {
|
|
42
44
|
client: ClientWithCoreApi;
|
|
43
45
|
network: SuiNetwork;
|
|
@@ -46,16 +48,18 @@ declare class HashiClient {
|
|
|
46
48
|
bitcoinNetwork?: BitcoinNetwork;
|
|
47
49
|
btcRpcUrl?: string;
|
|
48
50
|
graphqlUrl?: string;
|
|
51
|
+
guardianUrl?: string;
|
|
52
|
+
guardianInfoProvider?: GuardianInfoProvider;
|
|
49
53
|
});
|
|
50
54
|
/**
|
|
51
55
|
* Generates a unique Bitcoin P2TR deposit address for a Sui address.
|
|
52
56
|
*
|
|
53
57
|
* Fetches the MPC committee public key and the guardian's BTC public key
|
|
54
58
|
* from on-chain, derives an MPC child key against the Sui address, and
|
|
55
|
-
* builds
|
|
56
|
-
* (`
|
|
57
|
-
* the bridge's on-chain
|
|
58
|
-
* byte-for-byte.
|
|
59
|
+
* builds the Hashi taproot script tree: an immediate 2-of-2 leaf
|
|
60
|
+
* (`multi_a(2, guardian, derived_mpc)`) plus a delayed MPC-only recovery
|
|
61
|
+
* leaf. The address matches the bridge's on-chain
|
|
62
|
+
* `validate_deposit_request_derivation_path` check byte-for-byte.
|
|
59
63
|
*
|
|
60
64
|
* The MPC key (`committee_set.mpc_public_key`) and the guardian key
|
|
61
65
|
* (`guardian_btc_public_key` config) come from a single fetch of the
|
|
@@ -85,7 +89,7 @@ declare class HashiClient {
|
|
|
85
89
|
* into a single Sui PTB. Signs with `signer` and submits, returning the
|
|
86
90
|
* execution result (`$kind: "Transaction" | "FailedTransaction"`). The
|
|
87
91
|
* result includes `effects` and `events` so callers can confirm
|
|
88
|
-
* `
|
|
92
|
+
* `DepositRequested` without an extra round-trip.
|
|
89
93
|
*
|
|
90
94
|
* The method runs three preflight stages before signing:
|
|
91
95
|
*
|
|
@@ -124,7 +128,7 @@ declare class HashiClient {
|
|
|
124
128
|
* from the signer's balance and enqueues a request for the committee to
|
|
125
129
|
* send `amountSats` to `bitcoinAddress` on the Bitcoin network. Signs
|
|
126
130
|
* with `signer` and submits, returning the execution result including
|
|
127
|
-
* `effects` and `events` (`
|
|
131
|
+
* `effects` and `events` (`WithdrawalRequested`).
|
|
128
132
|
*
|
|
129
133
|
* The method runs three preflight stages before signing:
|
|
130
134
|
*
|
|
@@ -331,7 +335,7 @@ declare class HashiClient {
|
|
|
331
335
|
/**
|
|
332
336
|
* Get the status and details of a deposit by its Sui transaction digest.
|
|
333
337
|
*
|
|
334
|
-
* Fetches the `
|
|
338
|
+
* Fetches the `DepositRequested` event from the transaction, extracts the
|
|
335
339
|
* request ID, then probes on-chain state to determine whether the deposit
|
|
336
340
|
* is pending (still in `requests` ObjectBag), confirmed (object exists
|
|
337
341
|
* but not in requests), or expired (object destroyed).
|
|
@@ -340,7 +344,7 @@ declare class HashiClient {
|
|
|
340
344
|
/**
|
|
341
345
|
* Get the status and details of a withdrawal by its Sui transaction digest.
|
|
342
346
|
*
|
|
343
|
-
* Fetches the `
|
|
347
|
+
* Fetches the `WithdrawalRequested` event from the transaction, extracts the
|
|
344
348
|
* request ID, then reads the `WithdrawalRequest` object to determine the
|
|
345
349
|
* current lifecycle state. If a `WithdrawalTransaction` is linked, its
|
|
346
350
|
* Bitcoin txid is populated.
|
|
@@ -361,7 +365,7 @@ declare class HashiClient {
|
|
|
361
365
|
* Fetch the unified transaction history (deposits + withdrawals) for
|
|
362
366
|
* a Sui address. Confirmed requests come from the on-chain
|
|
363
367
|
* `user_requests` index; in-flight deposits are discovered via
|
|
364
|
-
* GraphQL `
|
|
368
|
+
* GraphQL `DepositRequested` event queries (indexed by sender).
|
|
365
369
|
*/
|
|
366
370
|
transactionHistory: (suiAddress: string) => Promise<TransactionHistoryItem[]>;
|
|
367
371
|
};
|
|
@@ -396,6 +400,28 @@ declare class HashiClient {
|
|
|
396
400
|
*/
|
|
397
401
|
confirmations: (txid: string) => Promise<number>;
|
|
398
402
|
};
|
|
403
|
+
guardian: {
|
|
404
|
+
/**
|
|
405
|
+
* Fetch the guardian's curated `/info` (identity + limiter). `limiter`
|
|
406
|
+
* is `null` when the guardian is not yet provisioned/activated; this
|
|
407
|
+
* method never throws for that state, so it can detect an uninitialized
|
|
408
|
+
* guardian without a try/catch.
|
|
409
|
+
*/
|
|
410
|
+
info: () => Promise<RawGuardianInfo>;
|
|
411
|
+
/**
|
|
412
|
+
* Fetch the limiter and compute derived fields: capacity projected to
|
|
413
|
+
* now, bucket fill percentage, and the refill-to-full ETA. Throws
|
|
414
|
+
* `HashiGuardianError` (`code: "not-initialized"`) if the guardian has
|
|
415
|
+
* no limiter yet.
|
|
416
|
+
*/
|
|
417
|
+
limiterStatus: () => Promise<GuardianLimiterSnapshot>;
|
|
418
|
+
/**
|
|
419
|
+
* Check whether the guardian can sign a withdrawal of `amountSats` right
|
|
420
|
+
* now, with an estimated wait if not. Throws `HashiGuardianError`
|
|
421
|
+
* (`code: "not-initialized"`) if the guardian has no limiter yet.
|
|
422
|
+
*/
|
|
423
|
+
canWithdraw: (amountSats: bigint) => Promise<GuardianWithdrawCheck>;
|
|
424
|
+
};
|
|
399
425
|
}
|
|
400
426
|
//#endregion
|
|
401
427
|
export { HashiClient, hashi };
|
package/dist/client.mjs
CHANGED
|
@@ -7,9 +7,10 @@ import { BitcoinState, BitcoinStateKey } from "./contracts/hashi/bitcoin_state.m
|
|
|
7
7
|
import { deposit } from "./contracts/hashi/deposit.mjs";
|
|
8
8
|
import { cancelWithdrawal, requestWithdrawal } from "./contracts/hashi/withdraw.mjs";
|
|
9
9
|
import { DUST_RELAY_MIN_VALUE, GUARDIAN_BTC_PUBLIC_KEY_LEN, GUARDIAN_PUBLIC_KEY_LEN, NETWORK_CONFIG } from "./constants.mjs";
|
|
10
|
-
import { AmountBelowMinimumError, HashiConfigError, HashiFetchError, HashiPausedError, InvalidParamsError } from "./errors.mjs";
|
|
10
|
+
import { AmountBelowMinimumError, HashiConfigError, HashiFetchError, HashiGuardianError, 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 { estimateWaitSecs, fetchGuardianInfo, projectCapacity } from "./guardian.mjs";
|
|
13
14
|
import { assertHex32, configBytes, entry, reverseTxidBytes } from "./util.mjs";
|
|
14
15
|
import { TypeTagSerializer, bcs } from "@mysten/sui/bcs";
|
|
15
16
|
import { deriveDynamicFieldID, fromHex, normalizeSuiAddress } from "@mysten/sui/utils";
|
|
@@ -90,7 +91,10 @@ var HashiClient = class {
|
|
|
90
91
|
#bitcoinNetwork;
|
|
91
92
|
#btcRpcUrl;
|
|
92
93
|
#graphqlUrl;
|
|
93
|
-
|
|
94
|
+
#guardianUrl;
|
|
95
|
+
#guardianInfoProvider;
|
|
96
|
+
#resolvedGuardianUrl;
|
|
97
|
+
constructor({ client, network, hashiObjectId, packageId, bitcoinNetwork, btcRpcUrl, graphqlUrl, guardianUrl, guardianInfoProvider }) {
|
|
94
98
|
this.tx = {
|
|
95
99
|
deposit: (params) => {
|
|
96
100
|
const tx = new Transaction();
|
|
@@ -252,7 +256,7 @@ var HashiClient = class {
|
|
|
252
256
|
});
|
|
253
257
|
const txData = txResult.Transaction ?? txResult.FailedTransaction;
|
|
254
258
|
if (!txData?.events) return null;
|
|
255
|
-
const depositEvent = txData.events.find((e) => e.eventType.includes("::deposit::
|
|
259
|
+
const depositEvent = txData.events.find((e) => e.eventType.includes("::deposit::DepositRequested"));
|
|
256
260
|
if (!depositEvent?.json) return null;
|
|
257
261
|
const parsed = depositEvent.json;
|
|
258
262
|
let status = "unknown";
|
|
@@ -263,7 +267,7 @@ var HashiClient = class {
|
|
|
263
267
|
client: this.#client,
|
|
264
268
|
objectId: parsed.request_id
|
|
265
269
|
});
|
|
266
|
-
if (reqObj.json.
|
|
270
|
+
if (reqObj.json.approved_timestamp_ms != null) approvalTimestampMs = BigInt(reqObj.json.approved_timestamp_ms);
|
|
267
271
|
const [btcState, config$1] = await Promise.all([this.#fetchBitcoinState(), this.view.all().catch(() => null)]);
|
|
268
272
|
if (approvalTimestampMs !== null && config$1) confirmableAtMs = approvalTimestampMs + config$1.bitcoinDepositTimeDelayMs;
|
|
269
273
|
const requestsBagId = btcState.deposit_queue.requests.id;
|
|
@@ -298,7 +302,7 @@ var HashiClient = class {
|
|
|
298
302
|
});
|
|
299
303
|
const txData = txResult.Transaction ?? txResult.FailedTransaction;
|
|
300
304
|
if (!txData?.events) return null;
|
|
301
|
-
const withdrawEvent = txData.events.find((e) => e.eventType.includes("::withdrawal_queue::
|
|
305
|
+
const withdrawEvent = txData.events.find((e) => e.eventType.includes("::withdrawal_queue::WithdrawalRequested"));
|
|
302
306
|
if (!withdrawEvent?.json) return null;
|
|
303
307
|
const parsed = withdrawEvent.json;
|
|
304
308
|
let status = "Requested";
|
|
@@ -402,7 +406,7 @@ var HashiClient = class {
|
|
|
402
406
|
}
|
|
403
407
|
}
|
|
404
408
|
try {
|
|
405
|
-
const depositEventType = `${this.#packageId}::deposit::
|
|
409
|
+
const depositEventType = `${this.#packageId}::deposit::DepositRequested`;
|
|
406
410
|
const pendingIds = (await this.#queryEventRequestIds(suiAddress, depositEventType)).filter((id) => !confirmedIds.has(id));
|
|
407
411
|
if (pendingIds.length > 0) {
|
|
408
412
|
const objects = await this.#batchGetObjects(pendingIds, { content: true });
|
|
@@ -428,6 +432,38 @@ var HashiClient = class {
|
|
|
428
432
|
return getTxConfirmations(this.#btcRpcUrl, txid);
|
|
429
433
|
}
|
|
430
434
|
};
|
|
435
|
+
this.guardian = {
|
|
436
|
+
info: async () => {
|
|
437
|
+
return (await this.#resolveGuardianProvider())();
|
|
438
|
+
},
|
|
439
|
+
limiterStatus: async () => {
|
|
440
|
+
const { state, config: config$1 } = this.#requireLimiter(await this.guardian.info());
|
|
441
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
442
|
+
const availableNowSats = projectCapacity(config$1, state, nowSecs);
|
|
443
|
+
const max = config$1.maxBucketCapacitySats;
|
|
444
|
+
const bucketFillPercent = max > 0n ? Number(availableNowSats) / Number(max) * 100 : 0;
|
|
445
|
+
let fullAtSecs = null;
|
|
446
|
+
if (availableNowSats < max && config$1.refillRateSatsPerSec > 0n) fullAtSecs = nowSecs + (max - availableNowSats + config$1.refillRateSatsPerSec - 1n) / config$1.refillRateSatsPerSec;
|
|
447
|
+
return {
|
|
448
|
+
state,
|
|
449
|
+
config: config$1,
|
|
450
|
+
availableNowSats,
|
|
451
|
+
bucketFillPercent,
|
|
452
|
+
fullAtSecs
|
|
453
|
+
};
|
|
454
|
+
},
|
|
455
|
+
canWithdraw: async (amountSats) => {
|
|
456
|
+
const { state, config: config$1 } = this.#requireLimiter(await this.guardian.info());
|
|
457
|
+
const nowSecs = BigInt(Math.floor(Date.now() / 1e3));
|
|
458
|
+
const availableNowSats = projectCapacity(config$1, state, nowSecs);
|
|
459
|
+
const estimatedWaitSecs = estimateWaitSecs(config$1, state, amountSats, nowSecs);
|
|
460
|
+
return {
|
|
461
|
+
allowed: availableNowSats >= amountSats,
|
|
462
|
+
availableNowSats,
|
|
463
|
+
estimatedWaitSecs
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
};
|
|
431
467
|
const config = NETWORK_CONFIG[network];
|
|
432
468
|
const resolvedObjectId = hashiObjectId ?? config?.hashiObjectId;
|
|
433
469
|
const resolvedPackageId = packageId ?? config?.packageId;
|
|
@@ -438,16 +474,18 @@ var HashiClient = class {
|
|
|
438
474
|
this.#bitcoinNetwork = bitcoinNetwork ?? config?.bitcoinNetwork ?? "testnet";
|
|
439
475
|
this.#btcRpcUrl = btcRpcUrl;
|
|
440
476
|
this.#graphqlUrl = graphqlUrl ?? defaultGraphqlUrl(network);
|
|
477
|
+
this.#guardianUrl = guardianUrl;
|
|
478
|
+
this.#guardianInfoProvider = guardianInfoProvider;
|
|
441
479
|
}
|
|
442
480
|
/**
|
|
443
481
|
* Generates a unique Bitcoin P2TR deposit address for a Sui address.
|
|
444
482
|
*
|
|
445
483
|
* Fetches the MPC committee public key and the guardian's BTC public key
|
|
446
484
|
* from on-chain, derives an MPC child key against the Sui address, and
|
|
447
|
-
* builds
|
|
448
|
-
* (`
|
|
449
|
-
* the bridge's on-chain
|
|
450
|
-
* byte-for-byte.
|
|
485
|
+
* builds the Hashi taproot script tree: an immediate 2-of-2 leaf
|
|
486
|
+
* (`multi_a(2, guardian, derived_mpc)`) plus a delayed MPC-only recovery
|
|
487
|
+
* leaf. The address matches the bridge's on-chain
|
|
488
|
+
* `validate_deposit_request_derivation_path` check byte-for-byte.
|
|
451
489
|
*
|
|
452
490
|
* The MPC key (`committee_set.mpc_public_key`) and the guardian key
|
|
453
491
|
* (`guardian_btc_public_key` config) come from a single fetch of the
|
|
@@ -479,7 +517,7 @@ var HashiClient = class {
|
|
|
479
517
|
* into a single Sui PTB. Signs with `signer` and submits, returning the
|
|
480
518
|
* execution result (`$kind: "Transaction" | "FailedTransaction"`). The
|
|
481
519
|
* result includes `effects` and `events` so callers can confirm
|
|
482
|
-
* `
|
|
520
|
+
* `DepositRequested` without an extra round-trip.
|
|
483
521
|
*
|
|
484
522
|
* The method runs three preflight stages before signing:
|
|
485
523
|
*
|
|
@@ -530,7 +568,7 @@ var HashiClient = class {
|
|
|
530
568
|
* from the signer's balance and enqueues a request for the committee to
|
|
531
569
|
* send `amountSats` to `bitcoinAddress` on the Bitcoin network. Signs
|
|
532
570
|
* with `signer` and submits, returning the execution result including
|
|
533
|
-
* `effects` and `events` (`
|
|
571
|
+
* `effects` and `events` (`WithdrawalRequested`).
|
|
534
572
|
*
|
|
535
573
|
* The method runs three preflight stages before signing:
|
|
536
574
|
*
|
|
@@ -705,6 +743,43 @@ var HashiClient = class {
|
|
|
705
743
|
#requireBtcRpc() {
|
|
706
744
|
if (!this.#btcRpcUrl) throw new Error("btcRpcUrl is required for Bitcoin RPC operations. Pass it in HashiClientOptions.");
|
|
707
745
|
}
|
|
746
|
+
#requireLimiter(info) {
|
|
747
|
+
if (info.limiter === null) throw new HashiGuardianError({
|
|
748
|
+
message: "Guardian rate limiter is not initialized (limiter is null).",
|
|
749
|
+
code: "not-initialized"
|
|
750
|
+
});
|
|
751
|
+
return info.limiter;
|
|
752
|
+
}
|
|
753
|
+
async #resolveGuardianProvider() {
|
|
754
|
+
if (this.#guardianInfoProvider) return this.#guardianInfoProvider;
|
|
755
|
+
const url = this.#guardianUrl || await this.#resolveOnChainGuardianUrl();
|
|
756
|
+
if (!url) throw new HashiGuardianError({
|
|
757
|
+
message: "Guardian URL is not configured. Pass `guardianUrl` or `guardianInfoProvider` to hashi({...}), or set `guardian_url` in the on-chain config.",
|
|
758
|
+
code: "not-configured"
|
|
759
|
+
});
|
|
760
|
+
return () => fetchGuardianInfo(url);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Resolve the guardian origin from the on-chain `guardian_url`, caching only
|
|
764
|
+
* a found URL. `guardian_url` is absent until launch (`finish_publish`
|
|
765
|
+
* publishes it), so a missing value is left uncached and each call re-reads
|
|
766
|
+
* the chain — a client created before launch starts working once the URL is
|
|
767
|
+
* published, rather than caching the absence forever (mirrors the node's
|
|
768
|
+
* lazy `guardian_client()` resolution). Returns `undefined` while unresolved.
|
|
769
|
+
*/
|
|
770
|
+
async #resolveOnChainGuardianUrl() {
|
|
771
|
+
if (this.#resolvedGuardianUrl) return this.#resolvedGuardianUrl;
|
|
772
|
+
let onChain;
|
|
773
|
+
try {
|
|
774
|
+
onChain = (await this.view.all()).guardianUrl;
|
|
775
|
+
} catch (cause) {
|
|
776
|
+
throw new HashiGuardianError({
|
|
777
|
+
message: "Could not read the on-chain guardian_url config. Pass `guardianUrl` or `guardianInfoProvider` to bypass the on-chain read.",
|
|
778
|
+
code: "not-configured"
|
|
779
|
+
}, { cause });
|
|
780
|
+
}
|
|
781
|
+
return this.#resolvedGuardianUrl = onChain || void 0;
|
|
782
|
+
}
|
|
708
783
|
async #estimateGas(tx) {
|
|
709
784
|
try {
|
|
710
785
|
const result = await this.#client.core.simulateTransaction({
|
|
@@ -913,12 +988,12 @@ function parseMpcPublicKey(raw) {
|
|
|
913
988
|
}
|
|
914
989
|
function parseDepositHistoryItem(content, timeDelayMs) {
|
|
915
990
|
const parsed = DepositRequest.parse(content);
|
|
916
|
-
const approvalTimestampMs = parsed.
|
|
991
|
+
const approvalTimestampMs = parsed.approved_timestamp_ms === null ? null : BigInt(parsed.approved_timestamp_ms);
|
|
917
992
|
return {
|
|
918
993
|
kind: "deposit",
|
|
919
994
|
requestId: parsed.id,
|
|
920
995
|
sender: parsed.sender,
|
|
921
|
-
timestampMs: BigInt(parsed.
|
|
996
|
+
timestampMs: BigInt(parsed.created_timestamp_ms),
|
|
922
997
|
suiTxDigest: base58.encode(new Uint8Array(parsed.sui_tx_digest)),
|
|
923
998
|
amountSats: BigInt(parsed.utxo.amount),
|
|
924
999
|
btcTxid: reverseTxidBytes(parsed.utxo.id.txid),
|
|
@@ -936,7 +1011,7 @@ function parseWithdrawalHistoryItem(content) {
|
|
|
936
1011
|
sender: parsed.sender,
|
|
937
1012
|
btcAmountSats: BigInt(parsed.btc_amount),
|
|
938
1013
|
bitcoinAddress: new Uint8Array(parsed.bitcoin_address),
|
|
939
|
-
timestampMs: BigInt(parsed.
|
|
1014
|
+
timestampMs: BigInt(parsed.created_timestamp_ms),
|
|
940
1015
|
suiTxDigest: base58.encode(new Uint8Array(parsed.sui_tx_digest)),
|
|
941
1016
|
status: parsed.status.$kind,
|
|
942
1017
|
withdrawalTxnId: parsed.withdrawal_txn_id ?? null,
|
package/dist/constants.mjs
CHANGED
|
@@ -61,8 +61,8 @@ const GUARDIAN_PUBLIC_KEY_LEN = 32;
|
|
|
61
61
|
*/
|
|
62
62
|
const GUARDIAN_BTC_PUBLIC_KEY_LEN = 32;
|
|
63
63
|
const NETWORK_CONFIG = { devnet: {
|
|
64
|
-
hashiObjectId: "
|
|
65
|
-
packageId: "
|
|
64
|
+
hashiObjectId: "0x84081242ebb05eac5e09ab2a930a60b1357d3d8bc6f927380979f72de991ccca",
|
|
65
|
+
packageId: "0xa877d4d97b6a8bae1da982a84980c502c5ad2ead4b24e6c8e50c57cd6ddc3771",
|
|
66
66
|
bitcoinNetwork: "signet"
|
|
67
67
|
} };
|
|
68
68
|
|
|
@@ -9,6 +9,13 @@ import { bcs } from "@mysten/sui/bcs";
|
|
|
9
9
|
/**************************************************************
|
|
10
10
|
* THIS FILE IS GENERATED AND SHOULD NOT BE MANUALLY MODIFIED *
|
|
11
11
|
**************************************************************/
|
|
12
|
+
/**
|
|
13
|
+
* Per-chain Bitcoin state, attached to the `Hashi` shared object as a dynamic
|
|
14
|
+
* field keyed by `BitcoinStateKey`. Bundles the deposit queue, withdrawal queue,
|
|
15
|
+
* and UTXO pool behind package-only accessors, and maintains a per-user index of
|
|
16
|
+
* request IDs so clients can discover all deposits and withdrawals belonging to an
|
|
17
|
+
* address.
|
|
18
|
+
*/
|
|
12
19
|
const $moduleName = "@local-pkg/hashi::bitcoin_state";
|
|
13
20
|
const BitcoinStateKey = new MoveStruct({
|
|
14
21
|
name: `${$moduleName}::BitcoinStateKey`,
|
|
@@ -17,6 +24,7 @@ const BitcoinStateKey = new MoveStruct({
|
|
|
17
24
|
const BitcoinState = new MoveStruct({
|
|
18
25
|
name: `${$moduleName}::BitcoinState`,
|
|
19
26
|
fields: {
|
|
27
|
+
id: bcs.Address,
|
|
20
28
|
deposit_queue: DepositRequestQueue,
|
|
21
29
|
withdrawal_queue: WithdrawalRequestQueue,
|
|
22
30
|
utxo_pool: UtxoPool,
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { MoveStruct } from "../utils/index.mjs";
|
|
2
2
|
import { Element } from "./deps/sui/group_ops.mjs";
|
|
3
|
+
import { Config } from "./config.mjs";
|
|
3
4
|
import { bcs } from "@mysten/sui/bcs";
|
|
4
5
|
|
|
5
6
|
//#region src/contracts/hashi/committee.ts
|
|
6
7
|
/**************************************************************
|
|
7
8
|
* THIS FILE IS GENERATED AND SHOULD NOT BE MANUALLY MODIFIED *
|
|
8
9
|
**************************************************************/
|
|
10
|
+
/**
|
|
11
|
+
* BLS signing committees and certificate verification. A `Committee` pins an
|
|
12
|
+
* epoch's members (validator addresses, BLS public keys, encryption keys, voting
|
|
13
|
+
* weights) together with the MPC parameters snapshotted at reconfig time.
|
|
14
|
+
* `verify_certificate` checks an aggregate BLS12-381 min-pk signature against a
|
|
15
|
+
* signers bitmap, enforces the stake threshold, and wraps the payload in a
|
|
16
|
+
* `CertifiedMessage` as proof of committee approval.
|
|
17
|
+
*/
|
|
9
18
|
const $moduleName = "@local-pkg/hashi::committee";
|
|
10
19
|
const CommitteeMember = new MoveStruct({
|
|
11
20
|
name: `${$moduleName}::CommitteeMember`,
|
|
@@ -22,9 +31,7 @@ const Committee = new MoveStruct({
|
|
|
22
31
|
epoch: bcs.u64(),
|
|
23
32
|
members: bcs.vector(CommitteeMember),
|
|
24
33
|
total_weight: bcs.u64(),
|
|
25
|
-
|
|
26
|
-
mpc_weight_reduction_allowed_delta: bcs.u64(),
|
|
27
|
-
mpc_max_faulty_in_basis_points: bcs.u64()
|
|
34
|
+
config: Config
|
|
28
35
|
}
|
|
29
36
|
});
|
|
30
37
|
const CommitteeSignature = new MoveStruct({
|