@mysten-incubation/hashi 0.0.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 +1 -0
- package/dist/bitcoin.d.mts +140 -0
- package/dist/bitcoin.mjs +272 -0
- package/dist/client.d.mts +318 -0
- package/dist/client.mjs +522 -0
- package/dist/constants.mjs +60 -0
- package/dist/contracts/hashi/bitcoin_state.mjs +28 -0
- package/dist/contracts/hashi/committee.mjs +40 -0
- package/dist/contracts/hashi/committee_set.mjs +34 -0
- package/dist/contracts/hashi/config.mjs +28 -0
- package/dist/contracts/hashi/config_value.mjs +21 -0
- package/dist/contracts/hashi/deposit.mjs +67 -0
- package/dist/contracts/hashi/deposit_queue.mjs +33 -0
- package/dist/contracts/hashi/deps/sui/bag.mjs +42 -0
- package/dist/contracts/hashi/deps/sui/balance.mjs +20 -0
- package/dist/contracts/hashi/deps/sui/group_ops.mjs +16 -0
- package/dist/contracts/hashi/deps/sui/object_bag.mjs +25 -0
- package/dist/contracts/hashi/deps/sui/package.mjs +26 -0
- package/dist/contracts/hashi/deps/sui/table.mjs +37 -0
- package/dist/contracts/hashi/deps/sui/vec_map.mjs +36 -0
- package/dist/contracts/hashi/deps/sui/vec_set.mjs +25 -0
- package/dist/contracts/hashi/hashi.mjs +29 -0
- package/dist/contracts/hashi/proposals.mjs +18 -0
- package/dist/contracts/hashi/treasury.mjs +28 -0
- package/dist/contracts/hashi/utxo.mjs +56 -0
- package/dist/contracts/hashi/utxo_pool.mjs +35 -0
- package/dist/contracts/hashi/withdraw.mjs +91 -0
- package/dist/contracts/hashi/withdrawal_queue.mjs +131 -0
- package/dist/contracts/utils/index.d.mts +8 -0
- package/dist/contracts/utils/index.mjs +118 -0
- package/dist/errors.d.mts +118 -0
- package/dist/errors.mjs +118 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +5 -0
- package/dist/types.d.mts +137 -0
- package/dist/util.mjs +50 -0
- package/package.json +64 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @mysten-incubation/hashi
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { BitcoinNetwork } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/bitcoin.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Converts a 33-byte arkworks-compressed secp256k1 point to 33-byte SEC1 compressed format.
|
|
6
|
+
*
|
|
7
|
+
* The on-chain `CommitteeSet.mpc_public_key` is serialised with `ark-serialize`
|
|
8
|
+
* (via `bcs::to_bytes` in the Rust node), which uses a different compressed
|
|
9
|
+
* layout than the SEC1/X9.62 standard that `@noble/curves` expects:
|
|
10
|
+
*
|
|
11
|
+
* | Property | ark-serialize | SEC1 (noble) |
|
|
12
|
+
* |----------------|----------------------------|--------------------------|
|
|
13
|
+
* | Byte order | **little-endian** x | **big-endian** x |
|
|
14
|
+
* | Y-parity | flag in **last** byte | prefix **first** byte |
|
|
15
|
+
* | Parity meaning | "negative" (y > (p-1)/2) | even / odd (y mod 2) |
|
|
16
|
+
*
|
|
17
|
+
* Because the parity conventions differ, we cannot simply remap the flag bit —
|
|
18
|
+
* we must lift the x-coordinate onto the curve to recover y, then check its
|
|
19
|
+
* parity in both systems.
|
|
20
|
+
*
|
|
21
|
+
* @param ark - 33-byte arkworks-compressed point
|
|
22
|
+
* (bytes [0..32] = x in little-endian, byte [32] = flags with bit 7 = y_is_negative)
|
|
23
|
+
* @returns 33-byte SEC1 compressed point (prefix 0x02 | 0x03, then x in big-endian)
|
|
24
|
+
*/
|
|
25
|
+
declare function arkworksToSec1Compressed(ark: Uint8Array): Uint8Array;
|
|
26
|
+
/**
|
|
27
|
+
* Derives a child x-only public key from the MPC master key and a Sui address.
|
|
28
|
+
*
|
|
29
|
+
* This is the core key-derivation step that gives each Sui address its own
|
|
30
|
+
* unique Bitcoin public key. The MPC committee can sign for this child key
|
|
31
|
+
* (using threshold Schnorr with additive tweaking), which is what authorises
|
|
32
|
+
* a withdrawal transaction on the Bitcoin side.
|
|
33
|
+
*
|
|
34
|
+
* Replicates the Rust function
|
|
35
|
+
* `fastcrypto_tbls::threshold_schnorr::key_derivation::derive_verifying_key`:
|
|
36
|
+
*
|
|
37
|
+
* ```text
|
|
38
|
+
* tweak = HKDF-SHA3-256(ikm = parent_x ‖ sui_address, len = 64) mod n
|
|
39
|
+
* child = parent + tweak × G
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* The tweak is derived deterministically from the master key's x-coordinate
|
|
43
|
+
* concatenated with the depositor's Sui address, using HKDF with SHA3-256 as
|
|
44
|
+
* the underlying hash. The 64-byte output is reduced mod n (the secp256k1
|
|
45
|
+
* group order) to produce a scalar, which is then used as an additive tweak
|
|
46
|
+
* on the master public key.
|
|
47
|
+
*
|
|
48
|
+
* The returned value is the 32-byte x-only form of the child key (the
|
|
49
|
+
* x-coordinate only, without a parity prefix). This is the format expected
|
|
50
|
+
* by BIP-340 Schnorr signatures and BIP-341 taproot constructions.
|
|
51
|
+
*
|
|
52
|
+
* @param mpcKeyCompressed - 33-byte SEC1 compressed secp256k1 public key
|
|
53
|
+
* (the MPC master key, after arkworks-to-SEC1 conversion)
|
|
54
|
+
* @param suiAddress - 32-byte Sui address used as the derivation path
|
|
55
|
+
* @returns 32-byte x-only public key of the derived child
|
|
56
|
+
*/
|
|
57
|
+
declare function deriveChildPubkey(mpcKeyCompressed: Uint8Array, suiAddress: Uint8Array): Uint8Array;
|
|
58
|
+
/**
|
|
59
|
+
* Builds a P2TR script-path-only address: `tr(NUMS, pk(pubkey))`.
|
|
60
|
+
*
|
|
61
|
+
* A BIP-341 taproot output has two spending paths: the *key path* (spend with
|
|
62
|
+
* the internal key) and the *script path* (reveal and satisfy a script in the
|
|
63
|
+
* Merkle tree). By using the NUMS (Nothing-Up-My-Sleeve) point as the internal
|
|
64
|
+
* key — a point with no known discrete logarithm — the key path is provably
|
|
65
|
+
* unspendable, forcing every spend through the script path.
|
|
66
|
+
*
|
|
67
|
+
* The script tree contains a single leaf: `<pubkey> OP_CHECKSIG`, meaning the
|
|
68
|
+
* MPC committee must produce a valid Schnorr signature for `pubkey` to spend
|
|
69
|
+
* the output. This is how the committee authorises withdrawals.
|
|
70
|
+
*
|
|
71
|
+
* The taproot output key is computed per BIP-341:
|
|
72
|
+
* ```text
|
|
73
|
+
* leafHash = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(script) ‖ script)
|
|
74
|
+
* tweak = tagged_hash("TapTweak", NUMS ‖ leafHash)
|
|
75
|
+
* outputKey = NUMS + tweak × G
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* The resulting output key is encoded as a SegWit v1 witness program in a
|
|
79
|
+
* bech32m address.
|
|
80
|
+
*
|
|
81
|
+
* @param pubkey - 32-byte x-only public key for the script leaf
|
|
82
|
+
* @param network - Bitcoin network for the bech32m human-readable prefix
|
|
83
|
+
* @returns bech32m-encoded P2TR address (e.g. `bc1p…`, `tb1p…`, `bcrt1p…`)
|
|
84
|
+
*/
|
|
85
|
+
declare function taprootScriptPathAddress(pubkey: Uint8Array, network: BitcoinNetwork): string;
|
|
86
|
+
/**
|
|
87
|
+
* Generates a Bitcoin P2TR deposit address for a Sui address.
|
|
88
|
+
*
|
|
89
|
+
* This is the main entry point for the address derivation pipeline. Given the
|
|
90
|
+
* MPC master key and a Sui address, it produces the unique Bitcoin deposit
|
|
91
|
+
* address where a user should send BTC in order to receive bridged tokens on
|
|
92
|
+
* Sui.
|
|
93
|
+
*
|
|
94
|
+
* Combines {@link deriveChildPubkey} (child key derivation) and
|
|
95
|
+
* {@link taprootScriptPathAddress} (taproot address construction) into a
|
|
96
|
+
* single call.
|
|
97
|
+
*
|
|
98
|
+
* The current devnet address scheme uses a single-key script path:
|
|
99
|
+
* ```text
|
|
100
|
+
* tr(NUMS, pk(derive(H, d)))
|
|
101
|
+
* ```
|
|
102
|
+
* where `H` is the MPC master key and `d` is the depositor's Sui address.
|
|
103
|
+
*
|
|
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
|
+
* @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
|
|
112
|
+
*/
|
|
113
|
+
declare function generateDepositAddress(mpcKeyCompressed: Uint8Array, suiAddress: Uint8Array, network: BitcoinNetwork): string;
|
|
114
|
+
/**
|
|
115
|
+
* Decodes a bech32/bech32m SegWit Bitcoin address into a witness program.
|
|
116
|
+
*
|
|
117
|
+
* Hashi withdrawals send BTC to a witness-program output, so the SDK only
|
|
118
|
+
* accepts the two address types the MPC committee currently supports:
|
|
119
|
+
*
|
|
120
|
+
* - **P2WPKH** — witness version 0, 20-byte program (`bc1q…`, `tb1q…`)
|
|
121
|
+
* - **P2TR** — witness version 1, 32-byte program (`bc1p…`, `tb1p…`)
|
|
122
|
+
*
|
|
123
|
+
* Legacy base58 addresses (`1…`, `3…`) aren't bech32 at all and surface as
|
|
124
|
+
* `"malformed"`. Version-0 32-byte P2WSH is rejected (no committee support).
|
|
125
|
+
*
|
|
126
|
+
* Per BIP-350, v0 must use a bech32 checksum and v1+ must use bech32m. This
|
|
127
|
+
* function enforces that rule strictly — a v0 address encoded as bech32m
|
|
128
|
+
* (or vice versa) fails with `"bad-checksum"`.
|
|
129
|
+
*
|
|
130
|
+
* @param address - User-supplied Bitcoin address string
|
|
131
|
+
* @param network - Expected Bitcoin network; the HRP must match
|
|
132
|
+
* @returns `{ version, program }` — witness version + raw program bytes
|
|
133
|
+
* @throws {@link InvalidBitcoinAddressError} with a structured `code` on any failure
|
|
134
|
+
*/
|
|
135
|
+
declare function bitcoinAddressToWitnessProgram(address: string, network: BitcoinNetwork): {
|
|
136
|
+
version: number;
|
|
137
|
+
program: Uint8Array;
|
|
138
|
+
};
|
|
139
|
+
//#endregion
|
|
140
|
+
export { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress };
|
package/dist/bitcoin.mjs
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { NETWORK_HRP, NUMS_KEY } from "./constants.mjs";
|
|
2
|
+
import { InvalidBitcoinAddressError } from "./errors.mjs";
|
|
3
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
4
|
+
import { sha3_256 } from "@noble/hashes/sha3.js";
|
|
5
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
6
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
7
|
+
import { concatBytes } from "@noble/hashes/utils.js";
|
|
8
|
+
import { bech32, bech32m } from "@scure/base";
|
|
9
|
+
|
|
10
|
+
//#region src/bitcoin.ts
|
|
11
|
+
/**
|
|
12
|
+
* Bitcoin address derivation for Hashi deposit addresses.
|
|
13
|
+
*
|
|
14
|
+
* Hashi bridges Bitcoin and Sui by assigning each Sui address a unique Bitcoin
|
|
15
|
+
* deposit address. When BTC is sent to that address, the Hashi MPC committee
|
|
16
|
+
* detects the deposit and mints equivalent tokens on Sui.
|
|
17
|
+
*
|
|
18
|
+
* The deposit address is a Pay-to-Taproot (P2TR / BIP-341) script-path address
|
|
19
|
+
* whose spending condition is a single `OP_CHECKSIG` against a child public key
|
|
20
|
+
* derived from the MPC committee's master key and the depositor's Sui address.
|
|
21
|
+
*
|
|
22
|
+
* The full derivation pipeline is:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Fetch** the MPC master key from on-chain (`CommitteeSet.mpc_public_key`).
|
|
25
|
+
* The on-chain bytes use the arkworks compressed format (little-endian x +
|
|
26
|
+
* flag byte), so they must first be converted to SEC1 via
|
|
27
|
+
* {@link arkworksToSec1Compressed} — this is done automatically by the
|
|
28
|
+
* client's `view.mpcPublicKey()` method.
|
|
29
|
+
*
|
|
30
|
+
* 2. **Derive** a child key: `child = masterKey + HKDF-SHA3-256(x ‖ suiAddr) × G`
|
|
31
|
+
* (see {@link deriveChildPubkey}). This replicates the Rust function
|
|
32
|
+
* `fastcrypto_tbls::threshold_schnorr::key_derivation::derive_verifying_key`.
|
|
33
|
+
*
|
|
34
|
+
* 3. **Build** the taproot address: `tr(NUMS, pk(child))` where NUMS is a
|
|
35
|
+
* Nothing-Up-My-Sleeve point with no known private key, forcing all spends
|
|
36
|
+
* through the script path (see {@link taprootScriptPathAddress}).
|
|
37
|
+
*
|
|
38
|
+
* The end-to-end helper {@link generateDepositAddress} combines steps 2–3.
|
|
39
|
+
*
|
|
40
|
+
* @see https://mystenlabs.github.io/hashi/design/address-scheme.html
|
|
41
|
+
*/
|
|
42
|
+
const Point = secp256k1.Point;
|
|
43
|
+
const CURVE_ORDER = Point.CURVE().n;
|
|
44
|
+
const NUMS_POINT = Point.fromBytes(concatBytes(new Uint8Array([2]), NUMS_KEY));
|
|
45
|
+
/** BIP-340 tagged hash: SHA256(SHA256(tag) ‖ SHA256(tag) ‖ msg) */
|
|
46
|
+
function taggedHash(tag, ...msgs) {
|
|
47
|
+
const tagHash = sha256(new TextEncoder().encode(tag));
|
|
48
|
+
return sha256(concatBytes(tagHash, tagHash, ...msgs));
|
|
49
|
+
}
|
|
50
|
+
/** Interpret a byte array as a big-endian unsigned integer. */
|
|
51
|
+
function bytesToNumberBE(bytes) {
|
|
52
|
+
let n = 0n;
|
|
53
|
+
for (const byte of bytes) n = n << 8n | BigInt(byte);
|
|
54
|
+
return n;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Converts a 33-byte arkworks-compressed secp256k1 point to 33-byte SEC1 compressed format.
|
|
58
|
+
*
|
|
59
|
+
* The on-chain `CommitteeSet.mpc_public_key` is serialised with `ark-serialize`
|
|
60
|
+
* (via `bcs::to_bytes` in the Rust node), which uses a different compressed
|
|
61
|
+
* layout than the SEC1/X9.62 standard that `@noble/curves` expects:
|
|
62
|
+
*
|
|
63
|
+
* | Property | ark-serialize | SEC1 (noble) |
|
|
64
|
+
* |----------------|----------------------------|--------------------------|
|
|
65
|
+
* | Byte order | **little-endian** x | **big-endian** x |
|
|
66
|
+
* | Y-parity | flag in **last** byte | prefix **first** byte |
|
|
67
|
+
* | Parity meaning | "negative" (y > (p-1)/2) | even / odd (y mod 2) |
|
|
68
|
+
*
|
|
69
|
+
* Because the parity conventions differ, we cannot simply remap the flag bit —
|
|
70
|
+
* we must lift the x-coordinate onto the curve to recover y, then check its
|
|
71
|
+
* parity in both systems.
|
|
72
|
+
*
|
|
73
|
+
* @param ark - 33-byte arkworks-compressed point
|
|
74
|
+
* (bytes [0..32] = x in little-endian, byte [32] = flags with bit 7 = y_is_negative)
|
|
75
|
+
* @returns 33-byte SEC1 compressed point (prefix 0x02 | 0x03, then x in big-endian)
|
|
76
|
+
*/
|
|
77
|
+
function arkworksToSec1Compressed(ark) {
|
|
78
|
+
if (ark.length !== 33) throw new Error(`Expected 33-byte arkworks-compressed key, got ${ark.length}`);
|
|
79
|
+
const yIsNegative = ark[32] >> 7 & 1;
|
|
80
|
+
const xBe = new Uint8Array(ark.slice(0, 32)).reverse();
|
|
81
|
+
const trial = new Uint8Array(33);
|
|
82
|
+
trial[0] = 2;
|
|
83
|
+
trial.set(xBe, 1);
|
|
84
|
+
const trialIsNeg = Point.fromBytes(trial).toAffine().y > (Point.CURVE().p - 1n) / 2n;
|
|
85
|
+
const prefix = yIsNegative === 1 !== trialIsNeg ? 3 : 2;
|
|
86
|
+
const sec1 = new Uint8Array(33);
|
|
87
|
+
sec1[0] = prefix;
|
|
88
|
+
sec1.set(xBe, 1);
|
|
89
|
+
return sec1;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Derives a child x-only public key from the MPC master key and a Sui address.
|
|
93
|
+
*
|
|
94
|
+
* This is the core key-derivation step that gives each Sui address its own
|
|
95
|
+
* unique Bitcoin public key. The MPC committee can sign for this child key
|
|
96
|
+
* (using threshold Schnorr with additive tweaking), which is what authorises
|
|
97
|
+
* a withdrawal transaction on the Bitcoin side.
|
|
98
|
+
*
|
|
99
|
+
* Replicates the Rust function
|
|
100
|
+
* `fastcrypto_tbls::threshold_schnorr::key_derivation::derive_verifying_key`:
|
|
101
|
+
*
|
|
102
|
+
* ```text
|
|
103
|
+
* tweak = HKDF-SHA3-256(ikm = parent_x ‖ sui_address, len = 64) mod n
|
|
104
|
+
* child = parent + tweak × G
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* The tweak is derived deterministically from the master key's x-coordinate
|
|
108
|
+
* concatenated with the depositor's Sui address, using HKDF with SHA3-256 as
|
|
109
|
+
* the underlying hash. The 64-byte output is reduced mod n (the secp256k1
|
|
110
|
+
* group order) to produce a scalar, which is then used as an additive tweak
|
|
111
|
+
* on the master public key.
|
|
112
|
+
*
|
|
113
|
+
* The returned value is the 32-byte x-only form of the child key (the
|
|
114
|
+
* x-coordinate only, without a parity prefix). This is the format expected
|
|
115
|
+
* by BIP-340 Schnorr signatures and BIP-341 taproot constructions.
|
|
116
|
+
*
|
|
117
|
+
* @param mpcKeyCompressed - 33-byte SEC1 compressed secp256k1 public key
|
|
118
|
+
* (the MPC master key, after arkworks-to-SEC1 conversion)
|
|
119
|
+
* @param suiAddress - 32-byte Sui address used as the derivation path
|
|
120
|
+
* @returns 32-byte x-only public key of the derived child
|
|
121
|
+
*/
|
|
122
|
+
function deriveChildPubkey(mpcKeyCompressed, suiAddress) {
|
|
123
|
+
if (mpcKeyCompressed.length !== 33) throw new Error(`Expected 33-byte compressed MPC key, got ${mpcKeyCompressed.length}`);
|
|
124
|
+
if (suiAddress.length !== 32) throw new Error(`Expected 32-byte Sui address, got ${suiAddress.length}`);
|
|
125
|
+
const parentPoint = Point.fromBytes(mpcKeyCompressed);
|
|
126
|
+
const tweakScalar = bytesToNumberBE(hkdf(sha3_256, concatBytes(mpcKeyCompressed.slice(1), suiAddress), void 0, void 0, 64)) % CURVE_ORDER;
|
|
127
|
+
return parentPoint.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Builds a P2TR script-path-only address: `tr(NUMS, pk(pubkey))`.
|
|
131
|
+
*
|
|
132
|
+
* A BIP-341 taproot output has two spending paths: the *key path* (spend with
|
|
133
|
+
* the internal key) and the *script path* (reveal and satisfy a script in the
|
|
134
|
+
* Merkle tree). By using the NUMS (Nothing-Up-My-Sleeve) point as the internal
|
|
135
|
+
* key — a point with no known discrete logarithm — the key path is provably
|
|
136
|
+
* unspendable, forcing every spend through the script path.
|
|
137
|
+
*
|
|
138
|
+
* The script tree contains a single leaf: `<pubkey> OP_CHECKSIG`, meaning the
|
|
139
|
+
* MPC committee must produce a valid Schnorr signature for `pubkey` to spend
|
|
140
|
+
* the output. This is how the committee authorises withdrawals.
|
|
141
|
+
*
|
|
142
|
+
* The taproot output key is computed per BIP-341:
|
|
143
|
+
* ```text
|
|
144
|
+
* leafHash = tagged_hash("TapLeaf", 0xC0 ‖ compact_size(script) ‖ script)
|
|
145
|
+
* tweak = tagged_hash("TapTweak", NUMS ‖ leafHash)
|
|
146
|
+
* outputKey = NUMS + tweak × G
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* The resulting output key is encoded as a SegWit v1 witness program in a
|
|
150
|
+
* bech32m address.
|
|
151
|
+
*
|
|
152
|
+
* @param pubkey - 32-byte x-only public key for the script leaf
|
|
153
|
+
* @param network - Bitcoin network for the bech32m human-readable prefix
|
|
154
|
+
* @returns bech32m-encoded P2TR address (e.g. `bc1p…`, `tb1p…`, `bcrt1p…`)
|
|
155
|
+
*/
|
|
156
|
+
function taprootScriptPathAddress(pubkey, network) {
|
|
157
|
+
if (pubkey.length !== 32) throw new Error(`Expected 32-byte x-only pubkey, got ${pubkey.length}`);
|
|
158
|
+
const tapscript = new Uint8Array(34);
|
|
159
|
+
tapscript[0] = 32;
|
|
160
|
+
tapscript.set(pubkey, 1);
|
|
161
|
+
tapscript[33] = 172;
|
|
162
|
+
const tweakScalar = bytesToNumberBE(taggedHash("TapTweak", NUMS_KEY, taggedHash("TapLeaf", new Uint8Array([192, tapscript.length]), tapscript))) % CURVE_ORDER;
|
|
163
|
+
const outputKey = NUMS_POINT.add(Point.BASE.multiply(tweakScalar)).toBytes(true).slice(1);
|
|
164
|
+
const words = [1, ...bech32m.toWords(outputKey)];
|
|
165
|
+
return bech32m.encode(NETWORK_HRP[network], words);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Generates a Bitcoin P2TR deposit address for a Sui address.
|
|
169
|
+
*
|
|
170
|
+
* This is the main entry point for the address derivation pipeline. Given the
|
|
171
|
+
* MPC master key and a Sui address, it produces the unique Bitcoin deposit
|
|
172
|
+
* address where a user should send BTC in order to receive bridged tokens on
|
|
173
|
+
* Sui.
|
|
174
|
+
*
|
|
175
|
+
* Combines {@link deriveChildPubkey} (child key derivation) and
|
|
176
|
+
* {@link taprootScriptPathAddress} (taproot address construction) into a
|
|
177
|
+
* single call.
|
|
178
|
+
*
|
|
179
|
+
* The current devnet address scheme uses a single-key script path:
|
|
180
|
+
* ```text
|
|
181
|
+
* tr(NUMS, pk(derive(H, d)))
|
|
182
|
+
* ```
|
|
183
|
+
* where `H` is the MPC master key and `d` is the depositor's Sui address.
|
|
184
|
+
*
|
|
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
|
+
* @returns bech32m-encoded P2TR deposit address (e.g. `tb1p…` for signet)
|
|
193
|
+
*/
|
|
194
|
+
function generateDepositAddress(mpcKeyCompressed, suiAddress, network) {
|
|
195
|
+
return taprootScriptPathAddress(deriveChildPubkey(mpcKeyCompressed, suiAddress), network);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Decodes a bech32/bech32m SegWit Bitcoin address into a witness program.
|
|
199
|
+
*
|
|
200
|
+
* Hashi withdrawals send BTC to a witness-program output, so the SDK only
|
|
201
|
+
* accepts the two address types the MPC committee currently supports:
|
|
202
|
+
*
|
|
203
|
+
* - **P2WPKH** — witness version 0, 20-byte program (`bc1q…`, `tb1q…`)
|
|
204
|
+
* - **P2TR** — witness version 1, 32-byte program (`bc1p…`, `tb1p…`)
|
|
205
|
+
*
|
|
206
|
+
* Legacy base58 addresses (`1…`, `3…`) aren't bech32 at all and surface as
|
|
207
|
+
* `"malformed"`. Version-0 32-byte P2WSH is rejected (no committee support).
|
|
208
|
+
*
|
|
209
|
+
* Per BIP-350, v0 must use a bech32 checksum and v1+ must use bech32m. This
|
|
210
|
+
* function enforces that rule strictly — a v0 address encoded as bech32m
|
|
211
|
+
* (or vice versa) fails with `"bad-checksum"`.
|
|
212
|
+
*
|
|
213
|
+
* @param address - User-supplied Bitcoin address string
|
|
214
|
+
* @param network - Expected Bitcoin network; the HRP must match
|
|
215
|
+
* @returns `{ version, program }` — witness version + raw program bytes
|
|
216
|
+
* @throws {@link InvalidBitcoinAddressError} with a structured `code` on any failure
|
|
217
|
+
*/
|
|
218
|
+
function bitcoinAddressToWitnessProgram(address, network) {
|
|
219
|
+
const expectedHrp = NETWORK_HRP[network];
|
|
220
|
+
let decoded;
|
|
221
|
+
let variant;
|
|
222
|
+
try {
|
|
223
|
+
decoded = bech32.decode(address);
|
|
224
|
+
variant = "bech32";
|
|
225
|
+
} catch {}
|
|
226
|
+
if (!decoded) try {
|
|
227
|
+
decoded = bech32m.decode(address);
|
|
228
|
+
variant = "bech32m";
|
|
229
|
+
} catch (cause) {
|
|
230
|
+
throw new InvalidBitcoinAddressError({
|
|
231
|
+
address,
|
|
232
|
+
code: "malformed",
|
|
233
|
+
message: `Bitcoin address "${address}" is not valid bech32 or bech32m.`
|
|
234
|
+
}, { cause });
|
|
235
|
+
}
|
|
236
|
+
if (decoded.words.length === 0) throw new InvalidBitcoinAddressError({
|
|
237
|
+
address,
|
|
238
|
+
code: "malformed",
|
|
239
|
+
message: `Bitcoin address "${address}" has no data payload.`
|
|
240
|
+
});
|
|
241
|
+
const version = decoded.words[0];
|
|
242
|
+
const expectedVariant = version === 0 ? "bech32" : "bech32m";
|
|
243
|
+
if (variant !== expectedVariant) throw new InvalidBitcoinAddressError({
|
|
244
|
+
address,
|
|
245
|
+
code: "bad-checksum",
|
|
246
|
+
message: `Bitcoin address "${address}" has witness version ${version} but a ${variant} checksum; BIP-350 requires ${expectedVariant} for this version.`
|
|
247
|
+
});
|
|
248
|
+
if (decoded.prefix !== expectedHrp) throw new InvalidBitcoinAddressError({
|
|
249
|
+
address,
|
|
250
|
+
code: "wrong-network",
|
|
251
|
+
message: `Bitcoin address "${address}" uses HRP "${decoded.prefix}" but the client is configured for ${network} (expected "${expectedHrp}").`
|
|
252
|
+
});
|
|
253
|
+
if (version !== 0 && version !== 1) throw new InvalidBitcoinAddressError({
|
|
254
|
+
address,
|
|
255
|
+
code: "unsupported-version",
|
|
256
|
+
message: `Bitcoin address "${address}" has witness version ${version}; Hashi supports only v0 (P2WPKH) and v1 (P2TR).`
|
|
257
|
+
});
|
|
258
|
+
const program = bech32.fromWords(decoded.words.slice(1));
|
|
259
|
+
const expectedLen = version === 0 ? 20 : 32;
|
|
260
|
+
if (program.length !== expectedLen) throw new InvalidBitcoinAddressError({
|
|
261
|
+
address,
|
|
262
|
+
code: "bad-program-length",
|
|
263
|
+
message: `Bitcoin address "${address}" has a ${program.length}-byte witness program; v${version} (${version === 0 ? "P2WPKH" : "P2TR"}) requires ${expectedLen} bytes.`
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
version,
|
|
267
|
+
program
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
//#endregion
|
|
272
|
+
export { arkworksToSec1Compressed, bitcoinAddressToWitnessProgram, deriveChildPubkey, generateDepositAddress, taprootScriptPathAddress };
|