Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/wasm-utxo/.mocharc.integrationLocalRpc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extensions": ["ts", "tsx", "js", "jsx"],
"node-option": ["import=tsx/esm", "experimental-wasm-modules"],
"timeout": 120000
}
2 changes: 1 addition & 1 deletion packages/wasm-utxo/.mocharc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extensions": ["ts", "tsx", "js", "jsx"],
"ignore": ["test/benchmark/**"],
"ignore": ["test/benchmark/**", "test/integrationLocalRpc/**"],
"node-option": ["import=tsx/esm", "experimental-wasm-modules"]
}
2 changes: 2 additions & 0 deletions packages/wasm-utxo/cli/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub enum NetworkArg {
Tzec,
Pearl,
Tpearl,
Tpearlreg,
}

impl From<NetworkArg> for Network {
Expand All @@ -53,6 +54,7 @@ impl From<NetworkArg> for Network {
NetworkArg::Tzec => Network::ZcashTestnet,
NetworkArg::Pearl => Network::Pearl,
NetworkArg::Tpearl => Network::PearlTestnet,
NetworkArg::Tpearlreg => Network::PearlRegtest,
}
}
}
2 changes: 2 additions & 0 deletions packages/wasm-utxo/js/coinName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const coinNames = [
"tzec",
"pearl",
"tpearl",
"tpearlreg",
] as const;

export type CoinName = (typeof coinNames)[number];
Expand Down Expand Up @@ -53,6 +54,7 @@ export function getMainnet(name: CoinName): CoinName {
case "tzec":
return "zec";
case "tpearl":
case "tpearlreg":
return "pearl";
default:
return name;
Expand Down
40 changes: 33 additions & 7 deletions packages/wasm-utxo/js/testutils/AcidTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,15 @@ export class AcidTest {
}

/**
* Create the actual PSBT with all inputs and outputs
* Create the actual PSBT with all inputs and outputs.
*
* @param options.outpoints - Optional real outpoints (txid/vout/prevTx) to replace
* the synthetic `txid=0…0` placeholders. Used by integrationLocalRpc against a
* live regtest node; length must match `inputs`.
*/
createPsbt(): BitGoPsbt {
createPsbt(options?: {
outpoints?: Array<{ txid: string; vout: number; prevTx?: Uint8Array }>;
}): BitGoPsbt {
// Use ZcashBitGoPsbt for Zcash networks
const isZcash = this.coin === "zec" || this.coin === "tzec";
const psbt = isZcash
Expand Down Expand Up @@ -281,10 +287,21 @@ export class AcidTest {
return tx.toBytes();
};

// Add inputs with deterministic outpoints
if (options?.outpoints && options.outpoints.length !== this.inputs.length) {
throw new Error(
`outpoints length ${options.outpoints.length} !== inputs length ${this.inputs.length}`,
);
}

// Add inputs with deterministic outpoints (or real outpoints when provided)
this.inputs.forEach((input, index) => {
const walletKeys = input.walletKeys ?? this.rootWalletKeys;
const outpoint = { txid: "0".repeat(64), vout: index, value: input.value };
const real = options?.outpoints?.[index];
const outpoint = {
txid: real?.txid ?? "0".repeat(64),
vout: real?.vout ?? index,
value: input.value,
};

// scriptId variant: caller provides explicit chain + index
if (input.scriptId) {
Expand All @@ -295,7 +312,10 @@ export class AcidTest {
this.coin,
);
psbt.addWalletInput(
{ ...outpoint, prevTx: buildPrevTx(index, script, input.value) },
{
...outpoint,
prevTx: real?.prevTx ?? buildPrevTx(index, script, input.value),
},
walletKeys,
{ scriptId: input.scriptId, signPath: { signer: "user", cosigner: "bitgo" } },
);
Expand All @@ -308,7 +328,10 @@ export class AcidTest {
const ecpair = ECPair.fromPublicKey(this.getReplayProtectionKey().publicKey);
const script = p2shP2pkOutputScript(ecpair.publicKey);
psbt.addReplayProtectionInput(
{ ...outpoint, prevTx: buildPrevTx(index, script, input.value) },
{
...outpoint,
prevTx: real?.prevTx ?? buildPrevTx(index, script, input.value),
},
ecpair,
);
return;
Expand All @@ -325,7 +348,10 @@ export class AcidTest {
const script = outputScript(walletKeys, scriptId.chain, scriptId.index, this.coin);

psbt.addWalletInput(
{ ...outpoint, prevTx: buildPrevTx(index, script, input.value) },
{
...outpoint,
prevTx: real?.prevTx ?? buildPrevTx(index, script, input.value),
},
walletKeys,
{ scriptId, signPath },
);
Expand Down
2 changes: 2 additions & 0 deletions packages/wasm-utxo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
"scripts": {
"test": "npm run test:mocha && npm run test:wasm-pack && npm run test:imports",
"test:mocha": "mocha --recursive 'test/**/*.ts'",
"test:integrationLocalRpc": "mocha --config .mocharc.integrationLocalRpc.json --recursive 'test/integrationLocalRpc/**/*.test.ts'",
"test:integrationLocalRpc:generate": "tsx --experimental-wasm-modules test/integrationLocalRpc/generate/main.ts",
"test:benchmark": "mocha test/benchmark/signing.ts --timeout 600000",
"test:wasm-pack": "npm run test:wasm-pack-node && npm run test:wasm-pack-chrome",
"test:wasm-pack-node": "./scripts/wasm-pack-test.sh --node",
Expand Down
3 changes: 1 addition & 2 deletions packages/wasm-utxo/src/address/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,7 @@ pub const ZCASH_TEST: Base58CheckCodec = Base58CheckCodec::new(0x1d25, 0x1cba);
// Bech32Codec handles v0 (bech32) and v1+ (bech32m / BIP-350) automatically.
pub const PEARL_BECH32: Bech32Codec = Bech32Codec::new("prl");
pub const PEARL_TEST_BECH32: Bech32Codec = Bech32Codec::new("tprl");
// Regtest HRP used only in test fixtures; not needed in production code.
#[cfg(test)]
/// Regtest HRP (`rprl`) — used by pearld `--regtest` and CoinName `tpearlreg`.
pub const PEARL_REGTEST_BECH32: Bech32Codec = Bech32Codec::new("rprl");

/// Convert output script to address string (convenience wrapper)
Expand Down
16 changes: 13 additions & 3 deletions packages/wasm-utxo/src/address/networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use super::{
BITCOIN_CASH_TESTNET_CASHADDR, BITCOIN_GOLD, BITCOIN_GOLD_BECH32, BITCOIN_GOLD_TESTNET,
BITCOIN_GOLD_TESTNET_BECH32, BITCOIN_SV, BITCOIN_SV_TESTNET, DASH, DASH_TEST, DOGECOIN,
DOGECOIN_TEST, ECASH, ECASH_CASHADDR, ECASH_TEST, ECASH_TEST_CASHADDR, LITECOIN,
LITECOIN_BECH32, LITECOIN_TEST, LITECOIN_TEST_BECH32, PEARL_BECH32, PEARL_TEST_BECH32, REGTEST,
REGTEST_BECH32, TESTNET, TESTNET_BECH32, ZCASH, ZCASH_TEST,
LITECOIN_BECH32, LITECOIN_TEST, LITECOIN_TEST_BECH32, PEARL_BECH32, PEARL_REGTEST_BECH32,
PEARL_TEST_BECH32, REGTEST, REGTEST_BECH32, TESTNET, TESTNET_BECH32, ZCASH, ZCASH_TEST,
};
use crate::bitcoin::Script;
use crate::fixed_script_wallet::wallet_scripts::OutputScriptType;
Expand Down Expand Up @@ -49,6 +49,7 @@ fn get_decode_codecs(network: Network) -> Vec<&'static dyn AddressCodec> {
// Pearl is Taproot-only; bech32m addresses only, no legacy codecs.
Network::Pearl => vec![&PEARL_BECH32],
Network::PearlTestnet => vec![&PEARL_TEST_BECH32],
Network::PearlRegtest => vec![&PEARL_REGTEST_BECH32],
}
}

Expand Down Expand Up @@ -314,6 +315,7 @@ fn get_encode_codec(
// Pearl is Taproot-only; assert_support() already rejects non-P2TR scripts above.
Network::Pearl => Ok(&PEARL_BECH32),
Network::PearlTestnet => Ok(&PEARL_TEST_BECH32),
Network::PearlRegtest => Ok(&PEARL_REGTEST_BECH32),
}
}

Expand Down Expand Up @@ -876,7 +878,7 @@ mod tests {
fn test_pearl_script_type_support() {
use crate::fixed_script_wallet::wallet_scripts::OutputScriptType::*;

for network in [Network::Pearl, Network::PearlTestnet] {
for network in [Network::Pearl, Network::PearlTestnet, Network::PearlRegtest] {
let support = network.output_script_support();
assert!(!support.legacy, "{network:?}: legacy should be false");
assert!(!support.segwit, "{network:?}: segwit should be false");
Expand Down Expand Up @@ -916,13 +918,21 @@ mod tests {
testnet_addr.starts_with("tprl1p"),
"Expected tprl1p… got {testnet_addr}"
);
let regtest_addr = from_output_script_with_network(&p2tr, Network::PearlRegtest).unwrap();
assert!(
regtest_addr.starts_with("rprl1p"),
"Expected rprl1p… got {regtest_addr}"
);

// Round-trip: address → script
let decoded = to_output_script_with_network(&mainnet_addr, Network::Pearl).unwrap();
assert_eq!(decoded, p2tr);
let decoded_test =
to_output_script_with_network(&testnet_addr, Network::PearlTestnet).unwrap();
assert_eq!(decoded_test, p2tr);
let decoded_reg =
to_output_script_with_network(&regtest_addr, Network::PearlRegtest).unwrap();
assert_eq!(decoded_reg, p2tr);

// P2WPKH (v0) → "does not support segwit"
let wpkh_hash = crate::bitcoin::WPubkeyHash::from_byte_array(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,8 @@ impl BitGoPsbt {
| Network::Litecoin
| Network::LitecoinTestnet
| Network::Pearl
| Network::PearlTestnet => Ok(BitGoPsbt::BitcoinLike(
| Network::PearlTestnet
| Network::PearlRegtest => Ok(BitGoPsbt::BitcoinLike(
Psbt::deserialize(psbt_bytes)?,
network,
)),
Expand Down
15 changes: 13 additions & 2 deletions packages/wasm-utxo/src/networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub enum Network {
// No legacy outputs (no P2PKH/P2SH/P2WPKH/P2WSH). Bech32m only.
Pearl,
PearlTestnet,
PearlRegtest,
}

impl Network {
Expand Down Expand Up @@ -89,6 +90,7 @@ impl Network {
Network::ZcashTestnet,
Network::Pearl,
Network::PearlTestnet,
Network::PearlRegtest,
];

/// Returns the canonical string name of this network
Expand Down Expand Up @@ -118,6 +120,7 @@ impl Network {
Network::ZcashTestnet => "ZcashTestnet",
Network::Pearl => "Pearl",
Network::PearlTestnet => "PearlTestnet",
Network::PearlRegtest => "PearlRegtest",
}
}

Expand Down Expand Up @@ -156,6 +159,7 @@ impl Network {

"Pearl" => Some(Network::Pearl),
"PearlTestnet" => Some(Network::PearlTestnet),
"PearlRegtest" => Some(Network::PearlRegtest),

_ => None,
}
Expand Down Expand Up @@ -188,6 +192,10 @@ impl Network {
"litecoinTest" => Some(Network::LitecoinTestnet),
"zcash" => Some(Network::Zcash),
"zcashTest" => Some(Network::ZcashTestnet),
// Pearl is not yet in @bitgo/utxo-lib; names reserved for future wiring.
"pearl" => Some(Network::Pearl),
"pearlTest" => Some(Network::PearlTestnet),
"pearlRegtest" => Some(Network::PearlRegtest),
_ => None,
}
}
Expand Down Expand Up @@ -219,6 +227,7 @@ impl Network {
// Pearl is not yet in @bitgo/utxo-lib; names reserved for future wiring.
Network::Pearl => "pearl",
Network::PearlTestnet => "pearlTest",
Network::PearlRegtest => "pearlRegtest",
}
}

Expand Down Expand Up @@ -249,6 +258,7 @@ impl Network {
"tzec" => Some(Network::ZcashTestnet),
"pearl" => Some(Network::Pearl),
"tpearl" => Some(Network::PearlTestnet),
"tpearlreg" => Some(Network::PearlRegtest),
_ => None,
}
}
Expand Down Expand Up @@ -280,6 +290,7 @@ impl Network {
Network::ZcashTestnet => "tzec",
Network::Pearl => "pearl",
Network::PearlTestnet => "tpearl",
Network::PearlRegtest => "tpearlreg",
}
}

Expand Down Expand Up @@ -317,7 +328,7 @@ impl Network {
Network::ZcashTestnet => Network::Zcash,

Network::Pearl => Network::Pearl,
Network::PearlTestnet => Network::Pearl,
Network::PearlTestnet | Network::PearlRegtest => Network::Pearl,
}
}

Expand Down Expand Up @@ -453,7 +464,7 @@ mod tests {
#[test]
fn test_all_networks() {
// Verify ALL contains all networks
assert_eq!(Network::ALL.len(), 24);
assert_eq!(Network::ALL.len(), 25);

// Verify no duplicates
for (i, network1) in Network::ALL.iter().enumerate() {
Expand Down
1 change: 1 addition & 0 deletions packages/wasm-utxo/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ macro_rules! test_all_networks {
#[case::zcash_testnet($crate::Network::ZcashTestnet)]
#[case::pearl($crate::Network::Pearl)]
#[case::pearl_testnet($crate::Network::PearlTestnet)]
#[case::pearl_regtest($crate::Network::PearlRegtest)]
fn $test_name(#[case] $network: $crate::Network) $body
};
}
2 changes: 1 addition & 1 deletion packages/wasm-utxo/src/wasm/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1209,7 +1209,7 @@ mod tests {
#[test]
fn test_all_networks_macro_is_complete() {
const _: () = assert!(
Network::ALL.len() == 24,
Network::ALL.len() == 25,
"test_all_networks! macro is out of sync with Network::ALL"
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/wasm-utxo/test/address/compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const taprootGroups: CoinName[][] = [
["tbtc", "tbtc4", "tbtcbgsig", "tbtcsig"],
["tbtcreg"],
["tpearl"],
["tpearlreg"],
];

describe("address compatibility", function () {
Expand Down
58 changes: 58 additions & 0 deletions packages/wasm-utxo/test/integrationLocalRpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# integrationLocalRpc

Local regtest-node tests for `@bitgo/wasm-utxo`, modeled on
[`utxo-lib/test/integration_local_rpc`](https://github.com/BitGo/BitGoJS/tree/master/modules/utxo-lib/test/integration_local_rpc)
and the docker harness in
[`indexer-utxo/containers`](https://github.com/BitGo/indexer-utxo/tree/master/containers).

## Design

- Same AcidTest kitchen-sink withdrawal loop for every wasm-utxo coin (as many
input/output script types as the coin permits), validated against a live
regtest node via RPC.
- **Not run on CI.** Default `npm test` / `test:mocha` ignores this directory.
- When run locally, the generator spins up Docker (or a reachable node), runs
the suite, and writes fixtures under `fixtures/<coin>/` for commit.

## Pearl notes

`pearld` is a walletless btcd fork:

- Mine with `generate` only (no `generatetoaddress`) — coinbases go to `--miningaddr`
- No wallet RPC; fund destinations by signing spends off-node with the known
mining key (`pearlConstants.ts`, privkey `0x01`)
- Taproot-only; regtest uses CoinName `tpearlreg` (HRP `rprl`)
- Default image: `319156457634.dkr.ecr.us-west-2.amazonaws.com/pearl-fullnode:1.0.2`
(override with `WASM_UTXO_PEARL_DOCKER_IMAGE`, e.g. local `prl-pearld:latest`)

### Apple Silicon / amd64 Docker

ZK-PoW mining in `pearld` **segfaults under amd64→arm64 Docker emulation**
(see `coins-sandbox/prl/prl_wasm_indexer_report.md`). On macOS arm64, use the
native binary from Pearl's GitHub release:

```bash
gh release download pearl-wallet-v1.0.0 \
--repo pearl-research-labs/pearl \
--pattern "go-binaries-darwin-arm64*" \
--dir /tmp/pearl
tar -xzf /tmp/pearl/go-binaries-darwin-arm64-v1.0.2.tar.gz -C /tmp/pearl
```

## Commands

```bash
# Offline fixture checks (no Docker) — still local-only, not in default mocha
npm run test:integrationLocalRpc

# Native pearld (recommended on Apple Silicon)
WASM_UTXO_PEARLD_BIN=/path/to/pearld \
WASM_UTXO_TESTS_LOG_DOCKER=1 \
npm run test:integrationLocalRpc:generate

# Docker (linux/amd64 native hosts)
npm run test:integrationLocalRpc:generate

# Local docker image override
WASM_UTXO_PEARL_DOCKER_IMAGE=prl-pearld:latest npm run test:integrationLocalRpc:generate
```
Loading
Loading