Skip to content

Commit 9e5ab15

Browse files
authored
Merge pull request #66 from BitGo/BTC-2786.parse-input-type
feat(wasm-utxo): add input script type detection
2 parents bd07bb2 + 79b8133 commit 9e5ab15

File tree

5 files changed

+143
-4
lines changed

5 files changed

+143
-4
lines changed

packages/wasm-utxo/js/fixedScriptWallet.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,21 @@ type ReplayProtection =
5656

5757
export type ScriptId = { chain: number; index: number };
5858

59+
export type InputScriptType =
60+
| "p2shP2pk"
61+
| "p2sh"
62+
| "p2shP2wsh"
63+
| "p2wsh"
64+
| "p2trLegacy"
65+
| "p2trMusig2ScriptPath"
66+
| "p2trMusig2KeyPath";
67+
5968
export type ParsedInput = {
6069
address: string;
6170
script: Uint8Array;
6271
value: bigint;
6372
scriptId: ScriptId | null;
73+
scriptType: InputScriptType;
6474
};
6575

6676
export type ParsedOutput = {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ pub enum BitGoPsbt {
9797
}
9898

9999
// Re-export types from submodules for convenience
100-
pub use psbt_wallet_input::{ParsedInput, ScriptId};
100+
pub use psbt_wallet_input::{InputScriptType, ParsedInput, ScriptId};
101101
pub use psbt_wallet_output::ParsedOutput;
102102

103103
/// Parsed transaction with wallet information

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,13 +518,75 @@ pub struct ScriptId {
518518
pub index: u32,
519519
}
520520

521+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
522+
pub enum InputScriptType {
523+
P2shP2pk,
524+
P2sh,
525+
P2shP2wsh,
526+
P2wsh,
527+
P2trLegacy,
528+
P2trMusig2ScriptPath,
529+
P2trMusig2KeyPath,
530+
}
531+
532+
impl InputScriptType {
533+
pub fn from_script_id(script_id: ScriptId, psbt_input: &Input) -> Result<Self, String> {
534+
let chain = Chain::try_from(script_id.chain).map_err(|e| e.to_string())?;
535+
match chain {
536+
Chain::P2shExternal | Chain::P2shInternal => Ok(InputScriptType::P2sh),
537+
Chain::P2shP2wshExternal | Chain::P2shP2wshInternal => Ok(InputScriptType::P2shP2wsh),
538+
Chain::P2wshExternal | Chain::P2wshInternal => Ok(InputScriptType::P2wsh),
539+
Chain::P2trInternal | Chain::P2trExternal => Ok(InputScriptType::P2trLegacy),
540+
Chain::P2trMusig2Internal | Chain::P2trMusig2External => {
541+
// check if tap_script_sigs or tap_scripts are set
542+
if !psbt_input.tap_script_sigs.is_empty() || !psbt_input.tap_scripts.is_empty() {
543+
Ok(InputScriptType::P2trMusig2ScriptPath)
544+
} else {
545+
Ok(InputScriptType::P2trMusig2KeyPath)
546+
}
547+
}
548+
}
549+
}
550+
551+
/// Detects the script type from a script_id chain and PSBT input metadata
552+
///
553+
/// # Arguments
554+
/// - `script_id`: Optional script ID containing chain information (None for replay protection inputs)
555+
/// - `psbt_input`: The PSBT input containing signature metadata
556+
/// - `output_script`: The output script being spent
557+
/// - `replay_protection`: Replay protection configuration
558+
///
559+
/// # Returns
560+
/// - `Ok(InputScriptType)` with the detected script type
561+
/// - `Err(String)` if the script type cannot be determined
562+
pub fn detect(
563+
script_id: Option<ScriptId>,
564+
psbt_input: &Input,
565+
output_script: &ScriptBuf,
566+
replay_protection: &ReplayProtection,
567+
) -> Result<Self, String> {
568+
// For replay protection inputs (no script_id), detect from output script
569+
match script_id {
570+
Some(id) => Self::from_script_id(id, psbt_input),
571+
None => {
572+
if replay_protection.is_replay_protection_input(output_script) {
573+
Ok(InputScriptType::P2shP2pk)
574+
} else {
575+
Err("Input without script_id is not a replay protection input".to_string())
576+
}
577+
}
578+
}
579+
}
580+
}
581+
521582
/// Parsed input from a PSBT transaction
522583
#[derive(Debug, Clone)]
523584
pub struct ParsedInput {
524585
pub address: String,
525586
pub script: Vec<u8>,
526587
pub value: u64,
527588
pub script_id: Option<ScriptId>,
589+
pub script_type: InputScriptType,
528590
}
529591

530592
impl ParsedInput {
@@ -576,11 +638,17 @@ impl ParsedInput {
576638
)
577639
.map_err(ParseInputError::Address)?;
578640

641+
// Detect the script type using script_id chain information
642+
let script_type =
643+
InputScriptType::detect(script_id, psbt_input, output_script, replay_protection)
644+
.map_err(ParseInputError::ScriptTypeDetection)?;
645+
579646
Ok(Self {
580647
address,
581648
script: output_script.to_bytes(),
582649
value: value.to_sat(),
583650
script_id,
651+
script_type,
584652
})
585653
}
586654
}
@@ -598,6 +666,8 @@ pub enum ParseInputError {
598666
WalletValidation(String),
599667
/// Failed to generate address for input
600668
Address(crate::address::AddressError),
669+
/// Failed to detect script type for input
670+
ScriptTypeDetection(String),
601671
}
602672

603673
impl std::fmt::Display for ParseInputError {
@@ -618,6 +688,9 @@ impl std::fmt::Display for ParseInputError {
618688
ParseInputError::Address(error) => {
619689
write!(f, "failed to generate address: {}", error)
620690
}
691+
ParseInputError::ScriptTypeDetection(error) => {
692+
write!(f, "failed to detect script type: {}", error)
693+
}
621694
}
622695
}
623696
}

packages/wasm-utxo/src/wasm/try_into_js_value.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,29 @@ impl TryIntoJsValue for crate::fixed_script_wallet::bitgo_psbt::ScriptId {
317317
}
318318
}
319319

320+
impl TryIntoJsValue for crate::fixed_script_wallet::bitgo_psbt::InputScriptType {
321+
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
322+
use crate::fixed_script_wallet::bitgo_psbt::InputScriptType;
323+
let script_type = match self {
324+
InputScriptType::P2shP2pk => "p2shP2pk",
325+
InputScriptType::P2sh => "p2sh",
326+
InputScriptType::P2shP2wsh => "p2shP2wsh",
327+
InputScriptType::P2wsh => "p2wsh",
328+
InputScriptType::P2trLegacy => "p2trLegacy",
329+
InputScriptType::P2trMusig2ScriptPath => "p2trMusig2ScriptPath",
330+
InputScriptType::P2trMusig2KeyPath => "p2trMusig2KeyPath",
331+
};
332+
Ok(JsValue::from_str(script_type))
333+
}
334+
}
335+
320336
impl TryIntoJsValue for crate::fixed_script_wallet::bitgo_psbt::ParsedInput {
321337
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
322338
js_obj!(
323339
"address" => self.address.clone(),
324340
"value" => self.value,
325-
"scriptId" => self.script_id
341+
"scriptId" => self.script_id,
342+
"scriptType" => self.script_type
326343
)
327344
}
328345
}

packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
import assert from "node:assert";
22
import * as utxolib from "@bitgo/utxo-lib";
33
import { fixedScriptWallet } from "../../js/index.js";
4-
import { BitGoPsbt } from "../../js/fixedScriptWallet.js";
4+
import { BitGoPsbt, InputScriptType } from "../../js/fixedScriptWallet.js";
55
import { loadPsbtFixture, loadWalletKeysFromFixture, getPsbtBuffer } from "./fixtureUtil.js";
66

7+
function getExpectedInputScriptType(fixtureScriptType: string): InputScriptType {
8+
// Map fixture types to InputScriptType values
9+
// Based on the Rust mapping in src/fixed_script_wallet/test_utils/fixtures.rs
10+
switch (fixtureScriptType) {
11+
case "p2shP2pk":
12+
case "p2sh":
13+
case "p2shP2wsh":
14+
case "p2wsh":
15+
return fixtureScriptType;
16+
case "p2tr":
17+
return "p2trLegacy";
18+
case "p2trMusig2":
19+
return "p2trMusig2ScriptPath";
20+
case "taprootKeyPathSpend":
21+
return "p2trMusig2KeyPath";
22+
default:
23+
throw new Error(`Unknown fixture script type: ${fixtureScriptType}`);
24+
}
25+
}
26+
727
function getOtherWalletKeys(): utxolib.bitgo.RootWalletKeys {
828
const otherWalletKeys = utxolib.testutil.getKeyTriple("too many secrets");
929
return new utxolib.bitgo.RootWalletKeys(otherWalletKeys);
@@ -34,9 +54,11 @@ describe("parseTransactionWithWalletKeys", function () {
3454
let fullsignedPsbtBytes: Buffer;
3555
let bitgoPsbt: BitGoPsbt;
3656
let rootWalletKeys: utxolib.bitgo.RootWalletKeys;
57+
let fixture: ReturnType<typeof loadPsbtFixture>;
3758

3859
before(function () {
39-
fullsignedPsbtBytes = getPsbtBuffer(loadPsbtFixture(networkName, "fullsigned"));
60+
fixture = loadPsbtFixture(networkName, "fullsigned");
61+
fullsignedPsbtBytes = getPsbtBuffer(fixture);
4062
bitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(fullsignedPsbtBytes, networkName);
4163
rootWalletKeys = loadWalletKeysFromFixture(networkName);
4264
});
@@ -117,6 +139,23 @@ describe("parseTransactionWithWalletKeys", function () {
117139
assert.ok(parsed.virtualSize > 0, "Virtual size should be > 0");
118140
});
119141

142+
it("should parse inputs with correct scriptType", function () {
143+
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
144+
outputScripts: [replayProtectionScript],
145+
});
146+
147+
// Verify all inputs have scriptType matching fixture
148+
parsed.inputs.forEach((input, i) => {
149+
const fixtureInput = fixture.psbtInputs[i];
150+
const expectedScriptType = getExpectedInputScriptType(fixtureInput.type);
151+
assert.strictEqual(
152+
input.scriptType,
153+
expectedScriptType,
154+
`Input ${i} scriptType should be ${expectedScriptType}, got ${input.scriptType}`,
155+
);
156+
});
157+
});
158+
120159
it("should fail to parse with other wallet keys", function () {
121160
assert.throws(
122161
() => {

0 commit comments

Comments
 (0)