diff --git a/index.ts b/index.ts index 42f9f76..699b61f 100644 --- a/index.ts +++ b/index.ts @@ -280,8 +280,12 @@ export enum TxSpendingConditionSingleSigHashMode { export enum TxSpendingConditionMultiSigHashMode { /** hash160(multisig-redeem-script), same as bitcoin's multisig p2sh */ P2SH = 0x01, + /** hash160(multisig-redeem-script), same as bitcoin's multisig p2sh (non-sequential signing) */ + P2SHNonSequential = 0x05, /** hash160(segwit-program-00(public-keys)), same as bitcoin's p2sh-p2wsh */ P2WSH = 0x03, + /** hash160(segwit-program-00(public-keys)), same as bitcoin's p2sh-p2wsh (non-sequential signing) */ + P2WSHNonSequential = 0x07, } export enum ClarityVersion { diff --git a/src/address/stacks_address.rs b/src/address/stacks_address.rs index 4bf817e..ffeb2e7 100644 --- a/src/address/stacks_address.rs +++ b/src/address/stacks_address.rs @@ -40,10 +40,12 @@ impl StacksAddress { pub enum AddressHashMode { // serialization modes for public keys to addresses. // We support four different modes due to legacy compatibility with Stacks v1 addresses: - SerializeP2PKH = 0x00, // hash160(public-key), same as bitcoin's p2pkh - SerializeP2SH = 0x01, // hash160(multisig-redeem-script), same as bitcoin's multisig p2sh + SerializeP2PKH = 0x00, // hash160(public-key), same as bitcoin's p2pkh + SerializeP2SH = 0x01, // hash160(multisig-redeem-script), same as bitcoin's multisig p2sh + SerializeP2SHNonSequential = 0x05, // hash160(multisig-redeem-script), same as bitcoin's multisig p2sh (non-sequential signing) SerializeP2WPKH = 0x02, // hash160(segwit-program-00(p2pkh)), same as bitcoin's p2sh-p2wpkh SerializeP2WSH = 0x03, // hash160(segwit-program-00(public-keys)), same as bitcoin's p2sh-p2wsh + SerializeP2WSHNonSequential = 0x07, // hash160(segwit-program-00(public-keys)), same as bitcoin's p2sh-p2wsh (non-sequential signing) } impl AddressHashMode { @@ -70,10 +72,16 @@ impl TryFrom for AddressHashMode { match value { x if x == AddressHashMode::SerializeP2PKH as u8 => Ok(AddressHashMode::SerializeP2PKH), x if x == AddressHashMode::SerializeP2SH as u8 => Ok(AddressHashMode::SerializeP2SH), + x if x == AddressHashMode::SerializeP2SHNonSequential as u8 => { + Ok(AddressHashMode::SerializeP2SHNonSequential) + } x if x == AddressHashMode::SerializeP2WPKH as u8 => { Ok(AddressHashMode::SerializeP2WPKH) } x if x == AddressHashMode::SerializeP2WSH as u8 => Ok(AddressHashMode::SerializeP2WSH), + x if x == AddressHashMode::SerializeP2WSHNonSequential as u8 => { + Ok(AddressHashMode::SerializeP2WSHNonSequential) + } _ => Err(format!("Invalid version {}", value)), } } diff --git a/src/stacks_tx/deserialize.rs b/src/stacks_tx/deserialize.rs index 0f302d6..acfdaf9 100644 --- a/src/stacks_tx/deserialize.rs +++ b/src/stacks_tx/deserialize.rs @@ -308,7 +308,13 @@ impl MultisigHashMode { pub fn from_u8(n: u8) -> Option { match n { x if x == MultisigHashMode::P2SH as u8 => Some(MultisigHashMode::P2SH), + x if x == MultisigHashMode::P2SHNonSequential as u8 => { + Some(MultisigHashMode::P2SHNonSequential) + } x if x == MultisigHashMode::P2WSH as u8 => Some(MultisigHashMode::P2WSH), + x if x == MultisigHashMode::P2WSHNonSequential as u8 => { + Some(MultisigHashMode::P2WSHNonSequential) + } _ => None, } } @@ -632,7 +638,9 @@ pub struct SinglesigSpendingCondition { #[derive(PartialEq, Copy, Clone)] pub enum MultisigHashMode { P2SH = 0x01, + P2SHNonSequential = 0x05, P2WSH = 0x03, + P2WSHNonSequential = 0x07, } #[repr(u8)] diff --git a/tests/tx-decode-3.0.test.ts b/tests/tx-decode-3.0.test.ts index 7cc10f2..0103ff6 100644 --- a/tests/tx-decode-3.0.test.ts +++ b/tests/tx-decode-3.0.test.ts @@ -6,9 +6,11 @@ import { PrincipalTypeID, TenureChangeCause, TransactionVersion, + TxAuthFieldTypeID, TxPayloadNakamotoCoinbase, TxPayloadTypeID, - TxPublicKeyEncoding + TxPublicKeyEncoding, + TxSpendingConditionMultiSigHashMode } from '../index.js'; test('stacks3.0 - decode tx - tenure change', () => { @@ -175,3 +177,61 @@ test('stacks3.0 - decode tx - nakamoto coinbase - no alt recipient (stacks-core expect(txType).toEqual(TxPayloadTypeID.NakamotoCoinbase); expect(payload.recipient).not.toBeNull(); }); + +test("stacks 3.0 - decode tx - non-sequential multi-sig", () => { + const tx = + "8080000000040535e2fdeee173024af6848ca6e335691b55498fc4000000000000000000000000000000640000000300028bd9dd96b66534e23cbcce4e69447b92bf1d738edb83182005cfb3b402666e42020158146dc95e76926e3add7289821e983e0dd2f2b0bf464c8e94bb082a213a91067ced1381a64bd03afa662992099b04d4c3f538cc6afa3d043ae081e25ebbde6f0300e30e7e744c6eef7c0a4d1a2dad6f0daa3c7655eb6e9fd6c34d1efa87b648d3e55cdd004ca4e8637cddad3316f3fbd6146665fad2e7ca26725ad09f58c4e43aa0000203020000000000051a70f696e2bda63701e044609eb7a7ce5876571905000000000000271000000000000000000000000000000000000000000000000000000000000000000000"; + const decoded = decodeTransaction(tx); + expect(decoded).toEqual({ + "tx_id": "0xf7f30ad912e9433743fb614b17842e8a366a04cc882e7fd94ff59fa9c2638674", + "version": TransactionVersion.Testnet, + "chain_id": 0x80000000, + "auth": { + "type_id": PostConditionAuthFlag.Standard, + "origin_condition": { + "tx_fee": "100", + "nonce": "0", + "fields": [ + { + "type_id": TxAuthFieldTypeID.PublicKeyCompressed, + "public_key": + "0x028bd9dd96b66534e23cbcce4e69447b92bf1d738edb83182005cfb3b402666e42", + }, + { + "type_id": TxAuthFieldTypeID.SignatureCompressed, + "signature": + "0x0158146dc95e76926e3add7289821e983e0dd2f2b0bf464c8e94bb082a213a91067ced1381a64bd03afa662992099b04d4c3f538cc6afa3d043ae081e25ebbde6f", + }, + { + "type_id": TxAuthFieldTypeID.SignatureUncompressed, + "signature": + "0x00e30e7e744c6eef7c0a4d1a2dad6f0daa3c7655eb6e9fd6c34d1efa87b648d3e55cdd004ca4e8637cddad3316f3fbd6146665fad2e7ca26725ad09f58c4e43aa0", + }, + ], + "hash_mode": TxSpendingConditionMultiSigHashMode.P2SHNonSequential, + "signatures_required": 2, + "signer": { + "address": "SNTY5ZFEW5SG4JQPGJ6ADRSND4DNAJCFRHVZBYR8", + "address_hash_bytes": "0x35e2fdeee173024af6848ca6e335691b55498fc4", + "address_version": 21, + }, + }, + }, + "anchor_mode": AnchorModeID.Any, + "post_condition_mode": PostConditionModeID.Deny, + "post_conditions": [], + "post_conditions_buffer": "0x0200000000", + "payload": { + "type_id": TxPayloadTypeID.TokenTransfer, + "amount": "10000", + "recipient": { + "type_id": PrincipalTypeID.Standard, + "address": "ST1RFD5Q2QPK3E0F08HG9XDX7SSC7CNRS0QR0SGEV", + "address_hash_bytes": "0x70f696e2bda63701e044609eb7a7ce5876571905", + "address_version": 26, + }, + "memo_hex": + "0x00000000000000000000000000000000000000000000000000000000000000000000", + }, + }); +}); \ No newline at end of file