Skip to content

Commit 3236c53

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): expose FixedScriptWallet to API
Add FixedScriptWallet with methods for creating output scripts and addresses based on wallet keys. This allows working with standard wallet script types without needing to create descriptors first. Issue: BTC-2652 Co-authored-by: llm-git <[email protected]>
1 parent 579e4a5 commit 3236c53

File tree

4 files changed

+132
-1
lines changed

4 files changed

+132
-1
lines changed

packages/wasm-utxo/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { Address as WasmAddress } from "./wasm/wasm_utxo";
4444
export { WrapDescriptor as Descriptor } from "./wasm/wasm_utxo";
4545
export { WrapMiniscript as Miniscript } from "./wasm/wasm_utxo";
4646
export { WrapPsbt as Psbt } from "./wasm/wasm_utxo";
47+
export { FixedScriptWallet } from "./wasm/wasm_utxo";
4748

4849
export namespace utxolibCompat {
4950
export const Address = WasmAddress;

packages/wasm-utxo/src/fixed_script_wallet/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,43 @@ pub mod test_utils;
99

1010
pub use wallet_keys::*;
1111
pub use wallet_scripts::*;
12+
use wasm_bindgen::prelude::*;
13+
14+
use crate::error::WasmMiniscriptError;
15+
use crate::try_from_js_value::TryFromJsValue;
16+
use crate::utxolib_compat::Network;
17+
18+
#[wasm_bindgen]
19+
pub struct FixedScriptWallet;
20+
21+
#[wasm_bindgen]
22+
impl FixedScriptWallet {
23+
#[wasm_bindgen(js_name = outputScript)]
24+
pub fn output_script(
25+
keys: JsValue,
26+
chain: u32,
27+
index: u32,
28+
) -> Result<Vec<u8>, WasmMiniscriptError> {
29+
let chain = Chain::try_from(chain)
30+
.map_err(|e| WasmMiniscriptError::new(&format!("Invalid chain: {}", e)))?;
31+
32+
let xpubs = xpub_triple_from_jsvalue(&keys)?;
33+
let scripts = WalletScripts::from_xpubs(&xpubs, chain, index);
34+
Ok(scripts.output_script().to_bytes())
35+
}
36+
37+
#[wasm_bindgen(js_name = address)]
38+
pub fn address(keys: JsValue, chain: u32, index: u32, network: JsValue) -> Result<String, WasmMiniscriptError> {
39+
let network = Network::try_from_js_value(&network)?;
40+
let xpubs = xpub_triple_from_jsvalue(&keys)?;
41+
let chain = Chain::try_from(chain)
42+
.map_err(|e| WasmMiniscriptError::new(&format!("Invalid chain: {}", e)))?;
43+
let scripts = WalletScripts::from_xpubs(&xpubs, chain, index);
44+
let script = scripts.output_script();
45+
let address = crate::address::utxolib_compat::from_output_script_with_network(
46+
&script, &network,
47+
)
48+
.map_err(|e| WasmMiniscriptError::new(&format!("Failed to generate address: {}", e)))?;
49+
Ok(address.to_string())
50+
}
51+
}

packages/wasm-utxo/src/fixed_script_wallet/wallet_keys.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,51 @@
11
use std::convert::TryInto;
2+
use std::str::FromStr;
23

34
use crate::bitcoin::{bip32::Xpub, CompressedPublicKey};
5+
use crate::error::WasmMiniscriptError;
6+
use wasm_bindgen::JsValue;
47

58
pub type XpubTriple = [Xpub; 3];
69

710
pub type PubTriple = [CompressedPublicKey; 3];
811

12+
pub fn xpub_triple_from_jsvalue(keys: &JsValue) -> Result<XpubTriple, WasmMiniscriptError> {
13+
let keys_array = js_sys::Array::from(keys);
14+
if keys_array.length() != 3 {
15+
return Err(WasmMiniscriptError::new("Expected exactly 3 xpub keys"));
16+
}
17+
18+
let key_strings: Result<[String; 3], _> = (0..3)
19+
.map(|i| {
20+
keys_array.get(i).as_string().ok_or_else(|| {
21+
WasmMiniscriptError::new(&format!("Key at index {} is not a string", i))
22+
})
23+
})
24+
.collect::<Result<Vec<_>, _>>()
25+
.and_then(|v| {
26+
v.try_into()
27+
.map_err(|_| WasmMiniscriptError::new("Failed to convert to array"))
28+
});
29+
30+
xpub_triple_from_strings(&key_strings?)
31+
}
32+
33+
pub fn xpub_triple_from_strings(
34+
xpub_strings: &[String; 3],
35+
) -> Result<XpubTriple, WasmMiniscriptError> {
36+
let xpubs: Result<Vec<Xpub>, _> = xpub_strings
37+
.iter()
38+
.map(|s| {
39+
Xpub::from_str(s)
40+
.map_err(|e| WasmMiniscriptError::new(&format!("Failed to parse xpub: {}", e)))
41+
})
42+
.collect();
43+
44+
xpubs?
45+
.try_into()
46+
.map_err(|_| WasmMiniscriptError::new("Expected exactly 3 xpubs"))
47+
}
48+
949
pub fn to_pub_triple(xpubs: &XpubTriple) -> PubTriple {
1050
xpubs
1151
.iter()

packages/wasm-utxo/test/address/utxolibCompat.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,43 @@ import * as fs from "node:fs/promises";
33

44
import * as utxolib from "@bitgo/utxo-lib";
55
import assert from "node:assert";
6-
import { utxolibCompat } from "../../js";
6+
import { utxolibCompat, FixedScriptWallet } from "../../js";
7+
8+
type Triple<T> = [T, T, T];
79

810
type Fixture = [type: string, script: string, address: string];
911

12+
function getAddressUtxoLib(
13+
keys: Triple<utxolib.BIP32Interface>,
14+
chain: number,
15+
index: number,
16+
network: utxolib.Network,
17+
): string {
18+
if (!utxolib.bitgo.isChainCode(chain)) {
19+
throw new Error(`Invalid chain code: ${chain}`);
20+
}
21+
22+
const walletKeys = new utxolib.bitgo.RootWalletKeys(keys);
23+
const derived = walletKeys.deriveForChainAndIndex(chain, index);
24+
const script = utxolib.bitgo.outputScripts.createOutputScript2of3(
25+
derived.publicKeys,
26+
utxolib.bitgo.outputScripts.scriptTypeForChain(chain),
27+
);
28+
const address = utxolib.address.fromOutputScript(script.scriptPubKey, network);
29+
return address;
30+
}
31+
32+
function getAddressWasm(
33+
keys: Triple<utxolib.BIP32Interface>,
34+
chain: number,
35+
index: number,
36+
network: utxolib.Network,
37+
): string {
38+
const xpubs = keys.map((key) => key.neutered().toBase58());
39+
const wasmAddress = FixedScriptWallet.address(xpubs, chain, index, network);
40+
return wasmAddress;
41+
}
42+
1043
async function getFixtures(name: string): Promise<Fixture[]> {
1144
if (name === "bitcoinBitGoSignet") {
1245
name = "bitcoinPublicSignet";
@@ -34,6 +67,23 @@ function runTest(network: utxolib.Network) {
3467
assert.deepStrictEqual(Buffer.from(scriptFromAddress), scriptBuf);
3568
}
3669
});
70+
71+
const keyTriple = utxolib.testutil.getKeyTriple("wasm");
72+
73+
const supportedChainCodes = utxolib.bitgo.chainCodes.filter((chainCode) => {
74+
const scriptType = utxolib.bitgo.outputScripts.scriptTypeForChain(chainCode);
75+
return utxolib.bitgo.outputScripts.isSupportedScriptType(network, scriptType);
76+
});
77+
78+
it(`can recreate address from wallet keys for chain codes ${supportedChainCodes.join(", ")}`, function () {
79+
for (const chainCode of supportedChainCodes) {
80+
for (let index = 0; index < 2; index++) {
81+
const utxolibAddress = getAddressUtxoLib(keyTriple, chainCode, index, network);
82+
const wasmAddress = getAddressWasm(keyTriple, chainCode, index, network);
83+
assert.strictEqual(utxolibAddress, wasmAddress);
84+
}
85+
}
86+
});
3787
});
3888
}
3989

0 commit comments

Comments
 (0)