Skip to content
Open
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
30 changes: 20 additions & 10 deletions packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ export type AddWalletOutputOptions = {
value: bigint;
};

export type ParseTransactionOptions = {
replayProtection: ReplayProtectionArg;
payGoPubkeys?: ECPairArg[];
};

export type ParseOutputsOptions = {
payGoPubkeys?: ECPairArg[];
};

export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
protected constructor(protected _wasm: WasmBitGoPsbt) {}

Expand Down Expand Up @@ -424,18 +433,18 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
/**
* Parse transaction with wallet keys to identify wallet inputs/outputs
* @param walletKeys - The wallet keys to use for identification
* @param replayProtection - Scripts that are allowed as inputs without wallet validation
* @param payGoPubkeys - Optional public keys for PayGo attestation verification
* @param options - Options for parsing
* @param options.replayProtection - Scripts that are allowed as inputs without wallet validation
* @param options.payGoPubkeys - Optional public keys for PayGo attestation verification
* @returns Parsed transaction information
*/
parseTransactionWithWalletKeys(
walletKeys: WalletKeysArg,
replayProtection: ReplayProtectionArg,
payGoPubkeys?: ECPairArg[],
options: ParseTransactionOptions,
): ParsedTransaction {
const keys = RootWalletKeys.from(walletKeys);
const rp = ReplayProtection.from(replayProtection, this._wasm.network());
const pubkeys = payGoPubkeys?.map((arg) => ECPair.from(arg).wasm);
const rp = ReplayProtection.from(options.replayProtection, this._wasm.network());
const pubkeys = options.payGoPubkeys?.map((arg) => ECPair.from(arg).wasm);
return this._wasm.parse_transaction_with_wallet_keys(
keys.wasm,
rp.wasm,
Expand All @@ -451,16 +460,17 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
* wallet than the inputs.
*
* @param walletKeys - The wallet keys to use for identification
* @param payGoPubkeys - Optional public keys for PayGo attestation verification
* @param options - Optional options for parsing
* @param options.payGoPubkeys - Optional public keys for PayGo attestation verification
* @returns Array of parsed outputs
* @note This method does NOT validate wallet inputs. It only parses outputs.
*/
parseOutputsWithWalletKeys(
walletKeys: WalletKeysArg,
payGoPubkeys?: ECPairArg[],
options?: ParseOutputsOptions,
): ParsedOutput[] {
const keys = RootWalletKeys.from(walletKeys);
const pubkeys = payGoPubkeys?.map((arg) => ECPair.from(arg).wasm);
const pubkeys = options?.payGoPubkeys?.map((arg) => ECPair.from(arg).wasm);
return this._wasm.parse_outputs_with_wallet_keys(keys.wasm, pubkeys) as ParsedOutput[];
}

Expand Down Expand Up @@ -719,7 +729,7 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
* const counterpartyPsbt = BitGoPsbt.fromBytes(counterpartyPsbtBytes, network);
* psbt.combineMusig2Nonces(counterpartyPsbt);
* // Sign MuSig2 key path inputs
* const parsed = psbt.parseTransactionWithWalletKeys(walletKeys, replayProtection);
* const parsed = psbt.parseTransactionWithWalletKeys(walletKeys, { replayProtection });
* for (let i = 0; i < parsed.inputs.length; i++) {
* if (parsed.inputs[i].scriptType === "p2trMusig2KeyPath") {
* psbt.sign(i, userXpriv);
Expand Down
2 changes: 2 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export {
type AddOutputOptions,
type AddWalletInputOptions,
type AddWalletOutputOptions,
type ParseTransactionOptions,
type ParseOutputsOptions,
} from "./BitGoPsbt.js";

// Zcash-specific PSBT subclass
Expand Down
21 changes: 9 additions & 12 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3979,7 +3979,7 @@ mod tests {
psbt.unsigned_tx.output.push(miniscript::bitcoin::TxOut {
value: miniscript::bitcoin::Amount::from_sat(10000),
script_pubkey: miniscript::bitcoin::ScriptBuf::from_hex(
"76a91479b000887626b294a914501a4cd226b58b23598388ac",
"76a9147f90f63fed017815f1da8bea299da27945a17bda88ac",
)
.unwrap(),
});
Expand All @@ -3998,7 +3998,7 @@ mod tests {
assert!(result.is_ok(), "Should add attestation successfully");

// Extract and verify
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c";
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c";
let psbt = bitgo_psbt.psbt();

// Verify it was added (with address, no verification)
Expand Down Expand Up @@ -4091,9 +4091,9 @@ mod tests {
psbt.unsigned_tx.output.push(miniscript::bitcoin::TxOut {
value: miniscript::bitcoin::Amount::from_sat(10000),
script_pubkey: miniscript::bitcoin::ScriptBuf::from_hex(
"76a91479b000887626b294a914501a4cd226b58b23598388ac",
"76a9147f90f63fed017815f1da8bea299da27945a17bda88ac",
)
.unwrap(), // Address: 1CdWUVacSQQJ617HuNWByGiisEGXGNx2c
.unwrap(), // Address: 1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c
});

// Add PayGo attestation
Expand Down Expand Up @@ -4122,15 +4122,12 @@ mod tests {
.unwrap();
let pubkey = secp256k1::PublicKey::from_slice(&pubkey_bytes).unwrap();

// Note: Signature verification with bitcoinjs-message format is not fully working yet
// So parsing with pubkey will fail validation
let parsed_result = bitgo_psbt.parse_outputs_with_wallet_keys(&wallet_keys, &[pubkey]);
let parsed_result = bitgo_psbt
.parse_outputs_with_wallet_keys(&wallet_keys, &[pubkey])
.unwrap();

// We expect this to fail validation for now
assert!(
parsed_result.is_err(),
"Expected verification to fail with current signature format"
);
// The PayGo output should have paygo: true (verified)
assert!(parsed_result[output_index].paygo);
}

crate::test_psbt_fixtures!(test_parse_transaction_with_wallet_keys, network, format, {
Expand Down
16 changes: 8 additions & 8 deletions packages/wasm-utxo/src/paygo/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ mod tests {
fn test_new_valid_entropy() {
let entropy = vec![0u8; 64];
let signature = vec![1u8; 65];
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c".to_string();
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c".to_string();

let attestation =
PayGoAttestation::new(entropy.clone(), signature.clone(), address.clone());
Expand All @@ -81,7 +81,7 @@ mod tests {
fn test_new_invalid_entropy_length() {
let entropy = vec![0u8; 32]; // Wrong length
let signature = vec![1u8; 65];
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c".to_string();
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c".to_string();

let result = PayGoAttestation::new(entropy, signature, address);
assert!(result.is_err());
Expand All @@ -99,23 +99,23 @@ mod tests {
b722b6d0d9adbab782d2d0d66402794b6bd6449dc26f634035ee388a2b5e7b53f6",
)
.unwrap();
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c".to_string();
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c".to_string();

let attestation = PayGoAttestation::new(entropy, signature, address.clone()).unwrap();
let message = attestation.to_message();

// Message should be: 64 bytes entropy + 33 bytes address + 36 bytes UUID = 133 bytes
assert_eq!(message.len(), 133);
// Message should be: 64 bytes entropy + 34 bytes address + 36 bytes UUID = 134 bytes
assert_eq!(message.len(), 134);

// Verify components
let entropy_part = &message[0..64];
let address_part = &message[64..97];
let uuid_part = &message[97..133];
let address_part = &message[64..98];
let uuid_part = &message[98..134];

assert_eq!(entropy_part, &vec![0u8; 64][..]);
assert_eq!(
std::str::from_utf8(address_part).unwrap(),
"1CdWUVacSQQJ617HuNWByGiisEGXGNx2c"
"1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c"
);
assert_eq!(
std::str::from_utf8(uuid_part).unwrap(),
Expand Down
14 changes: 6 additions & 8 deletions packages/wasm-utxo/src/paygo/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ mod tests {
#[test]
fn test_extract_paygo_attestation_success() {
let output = create_test_output_with_attestation();
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c";
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c";

let result = extract_paygo_attestation(&output, address);
assert!(result.is_ok());
Expand All @@ -210,7 +210,7 @@ mod tests {
#[test]
fn test_extract_paygo_attestation_not_found() {
let output = Output::default();
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c";
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c";

let result = extract_paygo_attestation(&output, address);
assert!(result.is_err());
Expand All @@ -236,7 +236,7 @@ mod tests {
output.proprietary.insert(key, signature);
}

let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c";
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c";
let result = extract_paygo_attestation(&output, address);
assert!(result.is_err());
assert!(result
Expand Down Expand Up @@ -320,7 +320,7 @@ mod tests {
b722b6d0d9adbab782d2d0d66402794b6bd6449dc26f634035ee388a2b5e7b53f6",
)
.unwrap();
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c";
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c";
let pubkey_bytes =
hex::decode("02456f4f788b6af55eb9c54d88692cadef4babdbc34cde75218cc1d6b6de3dea2d")
.unwrap();
Expand All @@ -337,10 +337,8 @@ mod tests {
assert_eq!(attestation.address, address);

// Verify with pubkeys
// Note: Signature verification is not fully working yet with bitcoinjs-message format
// For now, we just verify the function runs without panic
let result = has_paygo_attestation_verify(&output, Some(address), &[pubkey]);
// The verification may fail, but should not panic
let _ = result;
assert!(result.is_ok());
assert!(result.unwrap(), "Signature should be valid");
}
}
9 changes: 3 additions & 6 deletions packages/wasm-utxo/src/paygo/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,7 @@ mod tests {
use super::*;
use crate::paygo::PayGoAttestation;

// TODO: Fix signature verification test - the recovery algorithm needs adjustment
// to match bitcoinjs-message format
#[test]
#[ignore]
fn test_verify_valid_signature() {
use secp256k1::PublicKey;

Expand All @@ -120,7 +117,7 @@ mod tests {
b722b6d0d9adbab782d2d0d66402794b6bd6449dc26f634035ee388a2b5e7b53f6",
)
.unwrap();
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c".to_string();
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c".to_string();
let pubkey_bytes =
hex::decode("02456f4f788b6af55eb9c54d88692cadef4babdbc34cde75218cc1d6b6de3dea2d")
.unwrap();
Expand All @@ -144,7 +141,7 @@ mod tests {
b722b6d0d9adbab782d2d0d66402794b6bd6449dc26f634035ee388a2b5e7b53f6",
)
.unwrap();
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c".to_string();
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c".to_string();

// Different public key
let wrong_pubkey_bytes =
Expand All @@ -165,7 +162,7 @@ mod tests {

let entropy = vec![0u8; 64];
let signature = vec![1u8; 32]; // Too short
let address = "1CdWUVacSQQJ617HuNWByGiisEGXGNx2c".to_string();
let address = "1CdWUVacSQQJ617HfuNWByGiisEGXGNx2c".to_string();
let pubkey_bytes =
hex::decode("02456f4f788b6af55eb9c54d88692cadef4babdbc34cde75218cc1d6b6de3dea2d")
.unwrap();
Expand Down
9 changes: 8 additions & 1 deletion packages/wasm-utxo/src/wasm/try_into_js_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ impl<T: TryIntoJsValue> TryIntoJsValue for Option<T> {
}
}

impl TryIntoJsValue for bool {
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
Ok(JsValue::from_bool(*self))
}
}

impl TryIntoJsValue for XOnlyPublicKey {
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
Ok(JsValue::from_str(&self.to_string()))
Expand Down Expand Up @@ -367,7 +373,8 @@ impl TryIntoJsValue for crate::fixed_script_wallet::bitgo_psbt::ParsedOutput {
"address" => self.address.clone(),
"script" => self.script.clone(),
"value" => self.value,
"scriptId" => self.script_id
"scriptId" => self.script_id,
"paygo" => self.paygo
)
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/wasm-utxo/test/acid-test/acidTest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe("AcidTest", function () {
// Verify no signatures present
const rpKey = test.getReplayProtectionKey();
const replayProtection = { publicKeys: [rpKey.publicKey] };
const parsed = psbt.parseTransactionWithWalletKeys(test.rootWalletKeys, replayProtection);
const parsed = psbt.parseTransactionWithWalletKeys(test.rootWalletKeys, { replayProtection });
const user = test.rootWalletKeys.userKey();
const backup = test.rootWalletKeys.backupKey();
const bitgo = test.rootWalletKeys.bitgoKey();
Expand Down Expand Up @@ -128,7 +128,7 @@ describe("AcidTest", function () {
// Verify one signature per input (user only)
const rpKey = test.getReplayProtectionKey();
const replayProtection = { publicKeys: [rpKey.publicKey] };
const parsed = psbt.parseTransactionWithWalletKeys(test.rootWalletKeys, replayProtection);
const parsed = psbt.parseTransactionWithWalletKeys(test.rootWalletKeys, { replayProtection });
const user = test.rootWalletKeys.userKey();
const backup = test.rootWalletKeys.backupKey();
const bitgo = test.rootWalletKeys.bitgoKey();
Expand Down Expand Up @@ -175,7 +175,7 @@ describe("AcidTest", function () {
// Verify two signatures per input (user + bitgo or user + backup)
const rpKey = test.getReplayProtectionKey();
const replayProtection = { publicKeys: [rpKey.publicKey] };
const parsed = psbt.parseTransactionWithWalletKeys(test.rootWalletKeys, replayProtection);
const parsed = psbt.parseTransactionWithWalletKeys(test.rootWalletKeys, { replayProtection });
const user = test.rootWalletKeys.userKey();
const backup = test.rootWalletKeys.backupKey();
const bitgo = test.rootWalletKeys.bitgoKey();
Expand Down
4 changes: 3 additions & 1 deletion packages/wasm-utxo/test/benchmark/signing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ function benchmarkPerInputSign(
// Clone PSBT for this benchmark
const testPsbt = BitGoPsbt.fromBytes(psbt.serialize(), "bitcoin");

const parsed = testPsbt.parseTransactionWithWalletKeys(walletKeys, { publicKeys: [] });
const parsed = testPsbt.parseTransactionWithWalletKeys(walletKeys, {
replayProtection: { publicKeys: [] },
});

// For MuSig2, generate nonces first (not timed)
if (scriptType.isMuSig2KeyPath) {
Expand Down
4 changes: 3 additions & 1 deletion packages/wasm-utxo/test/fixedScript/dogecoinLOLAmount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ describe("Dogecoin large output limit amount (LOL amounts) (1-in/1-out)", functi
psbt.addWalletInput({ txid, vout: 0, value }, walletKeys, { scriptId: { chain: 0, index: 0 } });
psbt.addWalletOutput(walletKeys, { chain: 0, index: 0, value });

const parsed = psbt.parseTransactionWithWalletKeys(walletKeys, { publicKeys: [] });
const parsed = psbt.parseTransactionWithWalletKeys(walletKeys, {
replayProtection: { publicKeys: [] },
});
assert.strictEqual(parsed.inputs.length, 1);
assert.strictEqual(parsed.outputs.length, 1);
assert.strictEqual(parsed.inputs[0].value, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("parseTransactionWithWalletKeys", function () {

it("should parse transaction and identify internal/external outputs", function () {
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
publicKeys: [replayProtectionKey],
replayProtection: { publicKeys: [replayProtectionKey] },
});

// Verify all inputs have addresses and values
Expand Down Expand Up @@ -190,7 +190,7 @@ describe("parseTransactionWithWalletKeys", function () {

it("should parse inputs with correct scriptType", function () {
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
publicKeys: [replayProtectionKey],
replayProtection: { publicKeys: [replayProtectionKey] },
});

// Verify all inputs have scriptType matching fixture
Expand Down Expand Up @@ -245,7 +245,7 @@ describe("parseTransactionWithWalletKeys", function () {
assert.throws(
() => {
bitgoPsbt.parseTransactionWithWalletKeys(getOtherWalletKeys(), {
publicKeys: [replayProtectionKey],
replayProtection: { publicKeys: [replayProtectionKey] },
});
},
(error: Error) => {
Expand Down
Loading