From ea18e8172d0beaa586f38cd07c48a68fe4d8217b Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Sun, 30 Aug 2026 01:24:09 +0800 Subject: [PATCH 1/5] feat(cold-wallet): review the call a multisig execute dispatches `multisig.execute` now carries the proposal's inner call (chain PR #675), and the chain dispatches it only when it re-encodes to the stored bytes. The cold wallet can therefore review the executed call instead of showing a proposal id and a note saying the contents are not part of what is signed. - `CallDecoder` describes execute like approve: a nested "You are executing" call that also supplies the summary, so the headline reads SEND rather than naming the wrapper. `cancel` and `remove_expired` keep the reference note, which is still true for them. - The debug payload menu gains a `Msig execute` entry. - The standalone reference parser catches up on both calls: `approve` gained its bound call bytes on chain some time ago and was still decoding the old two-field shape, so it could not parse a current approval at all. Execute's inline nesting recurses inside the codec before `MAX_CALL_DEPTH` is reached, so decoding is bounded with `decode_with_depth_limit`. --- cold-wallet-app/lib/debug/debug_payloads.dart | 13 ++ cold-wallet-app/test/call_display_test.dart | 21 +++ quantus_sdk/lib/src/chain/call_decoder.dart | 16 +- quantus_sdk/test/chain/call_decoder_test.dart | 25 +++ rust-transaction-parser/src/lib.rs | 150 ++++++++++++++---- 5 files changed, 196 insertions(+), 29 deletions(-) diff --git a/cold-wallet-app/lib/debug/debug_payloads.dart b/cold-wallet-app/lib/debug/debug_payloads.dart index 077dfbedd..2cdaf287c 100644 --- a/cold-wallet-app/lib/debug/debug_payloads.dart +++ b/cold-wallet-app/lib/debug/debug_payloads.dart @@ -29,6 +29,7 @@ class DebugPayloads { 'Reversible 8h': reversibleTransferWithDelay, 'Msig approve': multisigApproveTransfer, 'Msig propose': multisigProposeTransfer, + 'Msig execute': multisigExecuteTransfer, 'Vote aye': governanceVoteAye, }; @@ -82,6 +83,18 @@ class DebugPayloads { ); } + /// A multisig execution carrying the call it dispatches — the same review as + /// the approval, labelled `You are executing`. + static Uint8List multisigExecuteTransfer() { + return withExtensions( + const multisig_pallet.Txs().execute( + multisigAddress: _debugMultisigAccount, + proposalId: 12, + call: _send(BigInt.from(900000000000)), // 0.9 tokens + ), + ); + } + /// An aye vote on a tech-collective referendum — the governance path a core dev /// uses to enact a runtime upgrade. Moves no value, so the review screen must /// name the call instead of showing an amount. diff --git a/cold-wallet-app/test/call_display_test.dart b/cold-wallet-app/test/call_display_test.dart index 661d9b192..e6b746c69 100644 --- a/cold-wallet-app/test/call_display_test.dart +++ b/cold-wallet-app/test/call_display_test.dart @@ -59,6 +59,27 @@ void main() { expect(find.textContaining('Multisig · approve → Balances · transfer_allow_death'), findsOneWidget); }); + testWidgets('a multisig execute reviews the call it dispatches', (tester) async { + final inner = const balances_pallet.Txs().transferAllowDeath(dest: account(bobId), value: oneToken); + await pumpSignScreen( + tester, + DebugPayloads.withExtensions( + const multisig_pallet.Txs().execute(multisigAddress: aliceId, proposalId: 12, call: inner), + ), + ); + + expect(find.text('MULTISIG EXECUTE'), findsOneWidget); + expect(find.text('SEND'), findsOneWidget); + // The proposal contents are in the payload now, so the screen must not + // claim they are held on chain and unsigned. + expect(find.textContaining('not part of what you sign'), findsNothing); + + await tester.ensureVisible(find.text('ADVANCED')); + await tester.tap(find.text('ADVANCED')); + await tester.pumpAndSettle(); + expect(find.textContaining('Multisig · execute → Balances · transfer_allow_death'), findsOneWidget); + }); + testWidgets('a reversible send names itself and shows its window', (tester) async { await pumpSignScreen( tester, diff --git a/quantus_sdk/lib/src/chain/call_decoder.dart b/quantus_sdk/lib/src/chain/call_decoder.dart index 59946e7a8..e48ca92fd 100644 --- a/quantus_sdk/lib/src/chain/call_decoder.dart +++ b/quantus_sdk/lib/src/chain/call_decoder.dart @@ -211,8 +211,20 @@ class CallDecoder { ); case multisig.Cancel(:final multisigAddress, :final proposalId): return _multisigProposalRef('cancel', multisigAddress, proposalId); - case multisig.Execute(:final multisigAddress, :final proposalId): - return _multisigProposalRef('execute', multisigAddress, proposalId); + case multisig.Execute(:final multisigAddress, :final proposalId, :final call): + // The chain dispatches this call only if it re-encodes to the stored + // proposal, so what is shown here is what executes. + final inner = describe(call); + return DecodedCall( + pallet: 'Multisig', + call: 'execute', + fields: [ + _accountField('Multisig account', multisigAddress), + ValueField('Proposal id', '$proposalId', kind: ValueKind.number), + NestedCallField('You are executing', inner), + ], + summary: inner.summary, + ); case multisig.RemoveExpired(:final multisigAddress, :final proposalId): return _multisigProposalRef('remove_expired', multisigAddress, proposalId); case multisig.ClaimDeposits(:final multisigAddress): diff --git a/quantus_sdk/test/chain/call_decoder_test.dart b/quantus_sdk/test/chain/call_decoder_test.dart index 33fbf5791..d5962a402 100644 --- a/quantus_sdk/test/chain/call_decoder_test.dart +++ b/quantus_sdk/test/chain/call_decoder_test.dart @@ -150,6 +150,31 @@ void main() { expect(valueField(decoded, 'Threshold').value, '2 of 2'); }); + test('execute shows the call it dispatches, not just the proposal id', () { + final inner = const balances_pallet.Txs().transferAllowDeath(dest: dest(bobId), value: oneToken); + final decoded = roundTrip( + const multisig_pallet.Txs().execute(multisigAddress: aliceId, proposalId: 4, call: inner), + ); + + expect(decoded.call, 'execute'); + expect(valueField(decoded, 'Proposal id').value, '4'); + expect(nestedField(decoded, 'You are executing').call.call, 'transfer_allow_death'); + expect(decoded.summary?.amount, oneToken); + }); + + test('execute carries the inner call inline, unlike approve', () { + final inner = const balances_pallet.Txs().transferAllowDeath(dest: dest(bobId), value: oneToken); + final execute = const multisig_pallet.Txs().execute(multisigAddress: aliceId, proposalId: 4, call: inner); + final approve = const multisig_pallet.Txs().approve( + multisigAddress: aliceId, + proposalId: 4, + call: inner.encode(), + ); + + // approve's BoundedVec adds a compact length prefix; execute's Box does not. + expect(execute.encode().length, approve.encode().length - 1); + }); + test('cancel flags that the proposal contents are not in the payload', () { final decoded = roundTrip(const multisig_pallet.Txs().cancel(multisigAddress: aliceId, proposalId: 4)); final field = valueField(decoded, 'Proposal id'); diff --git a/rust-transaction-parser/src/lib.rs b/rust-transaction-parser/src/lib.rs index dab14aaf9..f1d793f5c 100644 --- a/rust-transaction-parser/src/lib.rs +++ b/rust-transaction-parser/src/lib.rs @@ -1,4 +1,4 @@ -use parity_scale_codec::{Decode, Error as CodecError, Input}; +use parity_scale_codec::{Decode, DecodeLimit, Error as CodecError, Input}; use std::fmt; /// Hard cap on the raw signing payload; every supported call is far below this. @@ -31,7 +31,7 @@ const KNOWN_NETWORKS: &[([u8; 32], &str)] = &[ // Mirrors of the on-chain call types, decoded with the same SCALE derive the runtime uses. // `#[codec(index)]` must match the runtime pallet/call indices and `#[codec(compact)]` must -// match `#[pallet::compact]` in the pallet declarations (chain `main`, spec >= 133). +// match `#[pallet::compact]` in the pallet declarations (chain `main`, spec >= 147). // Any pallet, call, or variant not declared here hard-fails decoding. #[derive(Decode)] @@ -95,11 +95,14 @@ enum MultisigCall { Approve { multisig_address: [u8; 32], proposal_id: u32, + call: Vec, }, #[codec(index = 6)] Execute { multisig_address: [u8; 32], proposal_id: u32, + // Inline `Box`, not the length-prefixed bytes `approve` carries. + call: Box, }, } @@ -197,10 +200,12 @@ pub enum QuantusTx { MultisigApprove { multisig: String, proposal_id: u32, + inner: Box, }, MultisigExecute { multisig: String, proposal_id: u32, + inner: Box, }, } @@ -262,13 +267,7 @@ impl QuantusTx { call, expiry, }) => { - if depth >= MAX_CALL_DEPTH { - return Err(format!( - "Multisig call nesting exceeds depth limit {}", - MAX_CALL_DEPTH - )); - } - let inner = decode_call(&call, depth + 1)?; + let inner = decode_call(&call, nested_depth(depth)?)?; Ok(QuantusTx::MultisigPropose { multisig: bytes_to_ss58(&multisig_address), expiry, @@ -278,17 +277,27 @@ impl QuantusTx { RuntimeCall::Multisig(MultisigCall::Approve { multisig_address, proposal_id, - }) => Ok(QuantusTx::MultisigApprove { - multisig: bytes_to_ss58(&multisig_address), - proposal_id, - }), + call, + }) => { + let inner = decode_call(&call, nested_depth(depth)?)?; + Ok(QuantusTx::MultisigApprove { + multisig: bytes_to_ss58(&multisig_address), + proposal_id, + inner: Box::new(inner), + }) + } RuntimeCall::Multisig(MultisigCall::Execute { multisig_address, proposal_id, - }) => Ok(QuantusTx::MultisigExecute { - multisig: bytes_to_ss58(&multisig_address), - proposal_id, - }), + call, + }) => { + let inner = QuantusTx::from_call(*call, nested_depth(depth)?)?; + Ok(QuantusTx::MultisigExecute { + multisig: bytes_to_ss58(&multisig_address), + proposal_id, + inner: Box::new(inner), + }) + } } } } @@ -310,11 +319,11 @@ impl fmt::Display for QuantusTx { QuantusTx::MultisigPropose { multisig, expiry, inner } => { write!(f, "Multisig propose on {} expiry {} call [{}]", multisig, expiry, inner) } - QuantusTx::MultisigApprove { multisig, proposal_id } => { - write!(f, "Multisig approve on {} proposal {}", multisig, proposal_id) + QuantusTx::MultisigApprove { multisig, proposal_id, inner } => { + write!(f, "Multisig approve on {} proposal {} call [{}]", multisig, proposal_id, inner) } - QuantusTx::MultisigExecute { multisig, proposal_id } => { - write!(f, "Multisig execute on {} proposal {}", multisig, proposal_id) + QuantusTx::MultisigExecute { multisig, proposal_id, inner } => { + write!(f, "Multisig execute on {} proposal {} call [{}]", multisig, proposal_id, inner) } } } @@ -353,9 +362,28 @@ fn multi_address_to_ss58(address: MultiAddress) -> String { bytes_to_ss58(&account_id) } +/// Depth of the next nested call, or an error once `MAX_CALL_DEPTH` is reached. +fn nested_depth(depth: u32) -> Result { + if depth >= MAX_CALL_DEPTH { + return Err(format!( + "Multisig call nesting exceeds depth limit {}", + MAX_CALL_DEPTH + )); + } + Ok(depth + 1) +} + +/// `execute` nests inline, so the codec recurses before `MAX_CALL_DEPTH` can be checked. +/// The deepest legitimate payload descends 3 (`execute` -> `execute` -> a `Vec` field). +const MAX_DECODE_DEPTH: u32 = 4; + +fn decode_runtime_call(input: &mut I) -> Result { + RuntimeCall::decode_with_depth_limit(MAX_DECODE_DEPTH, input).map_err(|e| format!("call: {}", e)) +} + fn decode_call(bytes: &[u8], depth: u32) -> Result { let mut input = bytes; - let call = RuntimeCall::decode(&mut input).map_err(|e| format!("call: {}", e))?; + let call = decode_runtime_call(&mut input)?; if !input.is_empty() { return Err(format!("{} trailing bytes after call", input.len())); } @@ -368,7 +396,7 @@ pub fn parse_payload(payload: &[u8]) -> Result { } let mut input = payload; - let call = RuntimeCall::decode(&mut input).map_err(|e| format!("call: {}", e))?; + let call = decode_runtime_call(&mut input)?; let extensions = SignedExtensions::decode(&mut input).map_err(|e| format!("extensions: {}", e))?; if !input.is_empty() { @@ -595,12 +623,42 @@ mod tests { } } + const INNER_TRANSFER: &str = + "020000777777777777777777777777777777777777777777777777777777777777777707002465c709"; + + /// `approve` binds to the proposal's call bytes: length-prefixed `BoundedVec`. + fn approve_wrapping(inner: &[u8]) -> Vec { + let mut call = vec![0x13, 0x02]; + call.extend_from_slice(&[0x99; 32]); + call.extend_from_slice(&7u32.to_le_bytes()); + call.extend(Compact(inner.len() as u32).encode()); + call.extend_from_slice(inner); + call + } + + /// `execute` carries the call it dispatches inline, with no length prefix. + fn execute_wrapping(inner: &[u8]) -> Vec { + let mut call = vec![0x13, 0x06]; + call.extend_from_slice(&[0x99; 32]); + call.extend_from_slice(&7u32.to_le_bytes()); + call.extend_from_slice(inner); + call + } + + fn parse_wrapped(wrapped: Vec) -> Result { + let mut payload = wrapped; + payload.extend(ext_suffix(&[0x00], 0, 0, &PLANCK_GENESIS)); + parse_payload(&payload) + } + #[test] fn test_parse_real_multisig_approve() { - match parse_call_hex("1302999999999999999999999999999999999999999999999999999999999999999907000000") { - QuantusTx::MultisigApprove { multisig, proposal_id } => { + let inner = hex::decode(INNER_TRANSFER).unwrap(); + match parse_wrapped(approve_wrapping(&inner)).unwrap().call { + QuantusTx::MultisigApprove { multisig, proposal_id, inner } => { assert_eq!(multisig, SS58_MULTISIG); assert_eq!(proposal_id, 7); + assert_transfer(&inner, SS58_DEST, 42_000_000_000u128, false, None); } other => panic!("expected MultisigApprove, got {:?}", other), } @@ -608,15 +666,53 @@ mod tests { #[test] fn test_parse_real_multisig_execute() { - match parse_call_hex("1306999999999999999999999999999999999999999999999999999999999999999907000000") { - QuantusTx::MultisigExecute { multisig, proposal_id } => { + let inner = hex::decode(INNER_TRANSFER).unwrap(); + match parse_wrapped(execute_wrapping(&inner)).unwrap().call { + QuantusTx::MultisigExecute { multisig, proposal_id, inner } => { assert_eq!(multisig, SS58_MULTISIG); assert_eq!(proposal_id, 7); + assert_transfer(&inner, SS58_DEST, 42_000_000_000u128, false, None); } other => panic!("expected MultisigExecute, got {:?}", other), } } + #[test] + fn test_reject_multisig_execute_without_inner_call() { + // The pre-PR-675 encoding carried no call; it must not parse. + let mut call = vec![0x13, 0x06]; + call.extend_from_slice(&[0x99; 32]); + call.extend_from_slice(&7u32.to_le_bytes()); + assert!(parse_wrapped(call).is_err()); + } + + #[test] + fn test_reject_undecodable_inner_calls() { + let bad = hex::decode("0500").unwrap(); + assert!(parse_wrapped(approve_wrapping(&bad)).is_err()); + assert!(parse_wrapped(execute_wrapping(&bad)).is_err()); + } + + #[test] + fn test_multisig_execute_nesting_depth_limit() { + let transfer = hex::decode(TRANSFER_CALL_1).unwrap(); + assert!(parse_wrapped(execute_wrapping(&execute_wrapping(&transfer))).is_ok()); + + let err = + parse_wrapped(execute_wrapping(&execute_wrapping(&execute_wrapping(&transfer)))).unwrap_err(); + assert!(err.contains("depth limit"), "{}", err); + } + + #[test] + fn test_execute_nesting_bomb_rejected_by_codec_depth_limit() { + // Inline nesting recurses inside the codec, before any parser-level depth check. + let mut call = hex::decode(TRANSFER_CALL_1).unwrap(); + for _ in 0..150 { + call = execute_wrapping(&call); + } + assert!(parse_wrapped(call).is_err()); + } + #[test] fn test_parse_real_multisig_propose_transfer() { // propose wrapping Balances::transfer_allow_death(dest, 42_000_000_000), expiry 5000 From 666b8369dee2ec32f7293b4fd2ae695bbc89990c Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Sun, 30 Aug 2026 08:59:29 +0800 Subject: [PATCH 2/5] feat(cold-wallet): cap the reviewable call size at 2 KiB Mirrors the firmware limit: a batch_all of 32 transfers is 1667 bytes at the worst-case encoding, 1707 inside a multisig wrapper, so 2 KiB leaves headroom. Checked on the top-level call and on each nested call's bytes; execute's inline inner call is bounded by the top-level check that contains it. --- rust-transaction-parser/src/lib.rs | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/rust-transaction-parser/src/lib.rs b/rust-transaction-parser/src/lib.rs index f1d793f5c..8a1dd17ee 100644 --- a/rust-transaction-parser/src/lib.rs +++ b/rust-transaction-parser/src/lib.rs @@ -5,6 +5,14 @@ use std::fmt; const MAX_PAYLOAD_BYTES: usize = 8 * 1024; /// Maximum nesting of multisig `propose` inner calls (top-level call is depth 0). const MAX_CALL_DEPTH: u32 = 2; +/// Largest encoded call a signer will review. +/// +/// Sized by the biggest call we ever expect to sign: a `batch_all` of 32 transfers — double +/// the 16 a batch is expected to carry — which is 1667 bytes at the worst-case encoding of +/// every field, or 1707 inside a multisig wrapper. The chain's own `MaxCallSize` is 10 KiB; +/// this is deliberately tighter, because a call a signer cannot review is one they cannot +/// meaningfully approve. +const MAX_CALL_BYTES: usize = 2 * 1024; /// Networks this parser will accept: (genesis hash, display name). /// A payload whose `CheckGenesis` hash is not listed here is rejected. @@ -362,6 +370,16 @@ fn multi_address_to_ss58(address: MultiAddress) -> String { bytes_to_ss58(&account_id) } +fn check_call_size(len: usize) -> Result<(), String> { + if len > MAX_CALL_BYTES { + return Err(format!( + "Call is {} bytes, over the {} byte review limit", + len, MAX_CALL_BYTES + )); + } + Ok(()) +} + /// Depth of the next nested call, or an error once `MAX_CALL_DEPTH` is reached. fn nested_depth(depth: u32) -> Result { if depth >= MAX_CALL_DEPTH { @@ -382,6 +400,7 @@ fn decode_runtime_call(input: &mut I) -> Result { } fn decode_call(bytes: &[u8], depth: u32) -> Result { + check_call_size(bytes.len())?; let mut input = bytes; let call = decode_runtime_call(&mut input)?; if !input.is_empty() { @@ -397,6 +416,8 @@ pub fn parse_payload(payload: &[u8]) -> Result { let mut input = payload; let call = decode_runtime_call(&mut input)?; + // Bounds the inline `execute` inner call too, since it is contained in this one. + check_call_size(payload.len() - input.len())?; let extensions = SignedExtensions::decode(&mut input).map_err(|e| format!("extensions: {}", e))?; if !input.is_empty() { @@ -610,6 +631,50 @@ mod tests { parse(&payload_with_suffix(call_hex, &[0x00], 0, 0)).call } + /// `create_multisig` with `signers` accounts: the cheapest way to build a call of a + /// chosen size out of calls the parser actually supports. + fn create_multisig_call(signers: usize) -> Vec { + let mut call = vec![0x13, 0x00]; + call.extend(Compact(signers as u32).encode()); + for _ in 0..signers { + call.extend_from_slice(&[0xaa; 32]); + } + call.extend_from_slice(&2u32.to_le_bytes()); + call.extend_from_slice(&0u64.to_le_bytes()); + call + } + + #[test] + fn test_call_size_limit_covers_the_largest_expected_call() { + // A batch_all of 32 transfers is 1667 bytes at the worst-case encoding, 1707 inside + // a multisig wrapper; the limit must sit above that. + assert!(MAX_CALL_BYTES > 1707); + } + + #[test] + fn test_accepts_a_call_just_under_the_size_limit() { + let call = create_multisig_call(40); + assert!(call.len() < MAX_CALL_BYTES); + assert!(parse_wrapped(call).is_ok()); + } + + #[test] + fn test_reject_oversized_calls_at_every_position() { + // 64 signers encodes to 2064 bytes. + let oversized = create_multisig_call(64); + assert!(oversized.len() > MAX_CALL_BYTES); + + for wrapped in [ + oversized.clone(), + propose_wrapping(&oversized), + approve_wrapping(&oversized), + execute_wrapping(&oversized), + ] { + let err = parse_wrapped(wrapped).unwrap_err(); + assert!(err.contains("review limit"), "{}", err); + } + } + #[test] fn test_parse_real_multisig_create() { let tx = parse_call_hex("13000caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc020000000000000000000000"); From 30ee00ce461a3f4f3c89df2a8a85378f5e1c2161 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Sun, 30 Aug 2026 09:02:32 +0800 Subject: [PATCH 3/5] feat(cold-wallet): refuse a multisig inner call over the review limit Parity with the firmware: propose, approve and execute all refuse to render an inner call larger than 2 KiB, so an oversized proposal fails the same way on every cold signer instead of producing a screen nobody can review. --- quantus_sdk/lib/src/chain/call_decoder.dart | 12 ++++++++++-- quantus_sdk/test/chain/call_decoder_test.dart | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/quantus_sdk/lib/src/chain/call_decoder.dart b/quantus_sdk/lib/src/chain/call_decoder.dart index e662066e7..ed5837fc3 100644 --- a/quantus_sdk/lib/src/chain/call_decoder.dart +++ b/quantus_sdk/lib/src/chain/call_decoder.dart @@ -204,7 +204,7 @@ class CallDecoder { ], ); case multisig.Propose(:final multisigAddress, :final call, :final expiry): - final inner = decodeBytes(call); + final inner = _multisigInner(call); return DecodedCall( pallet: 'Multisig', call: 'propose', @@ -219,7 +219,7 @@ class CallDecoder { // The chain only counts this approval if these bytes are byte-equal to // the stored proposal, so the inner call shown here is the call being // approved — not unverifiable context. - final inner = decodeBytes(call); + final inner = _multisigInner(call); return DecodedCall( pallet: 'Multisig', call: 'approve', @@ -235,6 +235,7 @@ class CallDecoder { case multisig.Execute(:final multisigAddress, :final proposalId, :final call): // The chain dispatches this call only if it re-encodes to the stored // proposal, so what is shown here is what executes. + checkCallSize(call.encode().length); final inner = describe(call); return DecodedCall( pallet: 'Multisig', @@ -259,6 +260,13 @@ class CallDecoder { } } + /// The inner call of a multisig action, refused when larger than a signer will + /// review — the same bound the firmware enforces. + static DecodedCall _multisigInner(List bytes) { + checkCallSize(bytes.length); + return decodeBytes(bytes); + } + static DecodedCall _multisigProposalRef(String name, List multisigAddress, int proposalId) { return DecodedCall( pallet: 'Multisig', diff --git a/quantus_sdk/test/chain/call_decoder_test.dart b/quantus_sdk/test/chain/call_decoder_test.dart index d5962a402..2754162c5 100644 --- a/quantus_sdk/test/chain/call_decoder_test.dart +++ b/quantus_sdk/test/chain/call_decoder_test.dart @@ -175,6 +175,25 @@ void main() { expect(execute.encode().length, approve.encode().length - 1); }); + test('refuses a multisig inner call larger than a signer will review', () { + // 64 signers encodes to 2064 bytes, just over the limit. + final oversized = const multisig_pallet.Txs().createMultisig( + signers: List.generate(64, (i) => Uint8List.fromList(List.filled(32, i))), + threshold: 2, + nonce: BigInt.zero, + ); + final oversizedBytes = oversized.encode(); + expect(oversizedBytes.length, greaterThan(CallDecoder.maxCallBytes)); + + for (final call in [ + const multisig_pallet.Txs().propose(multisigAddress: aliceId, call: oversizedBytes, expiry: 5000), + const multisig_pallet.Txs().approve(multisigAddress: aliceId, proposalId: 4, call: oversizedBytes), + const multisig_pallet.Txs().execute(multisigAddress: aliceId, proposalId: 4, call: oversized), + ]) { + expect(() => roundTrip(call), throwsA(isA())); + } + }); + test('cancel flags that the proposal contents are not in the payload', () { final decoded = roundTrip(const multisig_pallet.Txs().cancel(multisigAddress: aliceId, proposalId: 4)); final field = valueField(decoded, 'Proposal id'); From 1bd02d85245dc06d89a0d74ee0cf28c721be9b81 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Sun, 30 Aug 2026 18:59:02 +0800 Subject: [PATCH 4/5] refactor(cold-wallet): mirror the chain's MaxCallSize instead of a tighter limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A client limit below the chain's refuses proposals the chain accepts. Use the chain's number (10 KiB), applied to nested call bytes only — the outer extrinsic call carries a wrapper that puts a chain-valid propose past it, and stays bounded by MAX_PAYLOAD_BYTES, which rises to 12 KiB so such a proposal can arrive. --- quantus_sdk/lib/src/chain/call_decoder.dart | 18 ++--- quantus_sdk/test/chain/call_decoder_test.dart | 4 +- rust-transaction-parser/src/lib.rs | 65 +++++++++---------- 3 files changed, 38 insertions(+), 49 deletions(-) diff --git a/quantus_sdk/lib/src/chain/call_decoder.dart b/quantus_sdk/lib/src/chain/call_decoder.dart index ed5837fc3..25c3dab03 100644 --- a/quantus_sdk/lib/src/chain/call_decoder.dart +++ b/quantus_sdk/lib/src/chain/call_decoder.dart @@ -43,19 +43,13 @@ import 'package:quantus_sdk/src/services/datetime_formatting_service.dart'; class CallDecoder { const CallDecoder._(); - /// Largest encoded call a multisig proposal may carry. + /// Largest inner call a multisig proposal may carry, mirroring the runtime's + /// `pallet_multisig::Config::MaxCallSize` (`BoundedVec`, 10 KiB). /// - /// Sized by the biggest call we ever expect to put through a multisig: a - /// `batch_all` of 32 transfers — double the 16 a batch is expected to carry — - /// which is 1667 bytes at the worst-case encoding of every field, or 1707 - /// inside a multisig wrapper. The chain's own `MaxCallSize` is 10 KiB; this is - /// deliberately tighter, because a call a signer cannot review is one they - /// cannot meaningfully approve. - /// - /// This bounds the multisig paths only. `system.set_code` and - /// `preimage.note_preimage` legitimately carry a runtime blob and are governed - /// by their own limits. - static const int maxCallBytes = 2 * 1024; + /// Deliberately the chain's number rather than a tighter one of our own: a limit + /// below it would refuse proposals the chain accepts, leaving a multisig no cold + /// signer could act on. + static const int maxCallBytes = 10 * 1024; /// Throws when a call of [length] bytes is larger than a signer will review. static void checkCallSize(int length) { diff --git a/quantus_sdk/test/chain/call_decoder_test.dart b/quantus_sdk/test/chain/call_decoder_test.dart index 2754162c5..678a660df 100644 --- a/quantus_sdk/test/chain/call_decoder_test.dart +++ b/quantus_sdk/test/chain/call_decoder_test.dart @@ -176,9 +176,9 @@ void main() { }); test('refuses a multisig inner call larger than a signer will review', () { - // 64 signers encodes to 2064 bytes, just over the limit. + // 320 signers encodes to 10_256 bytes, just over the chain's MaxCallSize. final oversized = const multisig_pallet.Txs().createMultisig( - signers: List.generate(64, (i) => Uint8List.fromList(List.filled(32, i))), + signers: List.generate(320, (i) => Uint8List.fromList(List.filled(32, i % 256))), threshold: 2, nonce: BigInt.zero, ); diff --git a/rust-transaction-parser/src/lib.rs b/rust-transaction-parser/src/lib.rs index 8a1dd17ee..29ae20b8a 100644 --- a/rust-transaction-parser/src/lib.rs +++ b/rust-transaction-parser/src/lib.rs @@ -1,18 +1,19 @@ use parity_scale_codec::{Decode, DecodeLimit, Error as CodecError, Input}; use std::fmt; -/// Hard cap on the raw signing payload; every supported call is far below this. -const MAX_PAYLOAD_BYTES: usize = 8 * 1024; +/// Hard cap on the raw signing payload. Sized so a chain-maximum proposal still fits: +/// `MAX_CALL_BYTES` of inner call, plus the multisig wrapper and the signed extensions. +const MAX_PAYLOAD_BYTES: usize = 12 * 1024; /// Maximum nesting of multisig `propose` inner calls (top-level call is depth 0). const MAX_CALL_DEPTH: u32 = 2; -/// Largest encoded call a signer will review. +/// Largest inner call a multisig proposal may carry, mirroring the runtime's +/// `pallet_multisig::Config::MaxCallSize` (`BoundedVec`, 10 KiB). /// -/// Sized by the biggest call we ever expect to sign: a `batch_all` of 32 transfers — double -/// the 16 a batch is expected to carry — which is 1667 bytes at the worst-case encoding of -/// every field, or 1707 inside a multisig wrapper. The chain's own `MaxCallSize` is 10 KiB; -/// this is deliberately tighter, because a call a signer cannot review is one they cannot -/// meaningfully approve. -const MAX_CALL_BYTES: usize = 2 * 1024; +/// Deliberately the chain's number rather than a tighter one of our own: a limit below it +/// would refuse proposals the chain accepts, leaving a multisig no cold signer could act on. +const MAX_CALL_BYTES: usize = 10 * 1024; +/// A chain-maximum proposal has to fit in a payload, or the cap above is unreachable. +const _: () = assert!(MAX_PAYLOAD_BYTES > MAX_CALL_BYTES + 256); /// Networks this parser will accept: (genesis hash, display name). /// A payload whose `CheckGenesis` hash is not listed here is rejected. @@ -416,8 +417,6 @@ pub fn parse_payload(payload: &[u8]) -> Result { let mut input = payload; let call = decode_runtime_call(&mut input)?; - // Bounds the inline `execute` inner call too, since it is contained in this one. - check_call_size(payload.len() - input.len())?; let extensions = SignedExtensions::decode(&mut input).map_err(|e| format!("extensions: {}", e))?; if !input.is_empty() { @@ -645,31 +644,23 @@ mod tests { } #[test] - fn test_call_size_limit_covers_the_largest_expected_call() { - // A batch_all of 32 transfers is 1667 bytes at the worst-case encoding, 1707 inside - // a multisig wrapper; the limit must sit above that. - assert!(MAX_CALL_BYTES > 1707); + fn test_accepts_an_inner_call_at_the_chain_limit() { + // A proposal the chain would accept must not be refused here: 319 signers encodes to + // 10_224 bytes, just inside `MaxCallSize`, and the propose wrapper pushes the outer + // call past it — which is why the limit is not applied to the outer call. + let inner = create_multisig_call(319); + assert!(inner.len() <= MAX_CALL_BYTES); + let wrapped = propose_wrapping(&inner); + assert!(wrapped.len() > MAX_CALL_BYTES); + assert!(parse_wrapped(wrapped).is_ok()); } #[test] - fn test_accepts_a_call_just_under_the_size_limit() { - let call = create_multisig_call(40); - assert!(call.len() < MAX_CALL_BYTES); - assert!(parse_wrapped(call).is_ok()); - } - - #[test] - fn test_reject_oversized_calls_at_every_position() { - // 64 signers encodes to 2064 bytes. - let oversized = create_multisig_call(64); + fn test_reject_inner_call_over_the_chain_limit() { + // 320 signers encodes to 10_256 bytes, just over `MaxCallSize`. + let oversized = create_multisig_call(320); assert!(oversized.len() > MAX_CALL_BYTES); - - for wrapped in [ - oversized.clone(), - propose_wrapping(&oversized), - approve_wrapping(&oversized), - execute_wrapping(&oversized), - ] { + for wrapped in [propose_wrapping(&oversized), approve_wrapping(&oversized)] { let err = parse_wrapped(wrapped).unwrap_err(); assert!(err.contains("review limit"), "{}", err); } @@ -818,15 +809,19 @@ mod tests { #[test] fn test_deep_nesting_bomb_rejected_quickly() { - // The audit's C-1 payload shape: hundreds of nested propose levels. Must fail via the - // depth limit, not by exhausting the stack. + // The audit's C-1 payload shape: hundreds of nested propose levels. Must fail on one + // of the counters — depth, payload size, or call size — not by exhausting the stack. let mut call = hex::decode(TRANSFER_CALL_1).unwrap(); for _ in 0..300 { call = propose_wrapping(&call); } call.extend(ext_suffix(&[0x00], 0, 0, &PLANCK_GENESIS)); let err = parse_payload(&call).unwrap_err(); - assert!(err.contains("depth limit") || err.contains("too large"), "{}", err); + assert!( + err.contains("depth limit") || err.contains("too large") || err.contains("review limit"), + "{}", + err + ); } #[test] From edc4e842624cff24fee2837729b49b185e020e41 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Sun, 30 Aug 2026 20:00:06 +0800 Subject: [PATCH 5/5] test(cold-wallet): add a QR fixture corpus and parse every case Nine signing requests, one per call the Keystone firmware parses today. Each case carries the payload, the envelope, the UR parts, one SVG per QR frame and a viewer page that animates them, so the same bytes can be unit tested and shown to a real camera. The test walks the manifest and checks every case decodes to the call it claims, including the nested call for propose, approve and execute. It also checks the manifest and the folders agree, and that the corpus targets the runtime the app bundles, so a metadata regeneration cannot leave the corpus stale unnoticed. Frame counts are the reason this is the reduced set: the payload is hex inside JSON, so QR data is roughly twice the payload, and UR splits it at 200 bytes. Even a 118 byte transfer needs 2 frames. Generated by the quantus-cli `generate_qr_fixtures` example. --- cold-wallet-app/test/fixtures/qr/README.md | 43 ++++++++ .../frames/frame-000.svg | 1 + .../frames/frame-001.svg | 1 + .../01-transfer-allow-death/index.html | 33 +++++++ .../01-transfer-allow-death/payload.hex | 1 + .../01-transfer-allow-death/request.json | 1 + .../qr/reduced/01-transfer-allow-death/ur.txt | 2 + .../frames/frame-000.svg | 1 + .../frames/frame-001.svg | 1 + .../reduced/02-transfer-keep-alive/index.html | 33 +++++++ .../02-transfer-keep-alive/payload.hex | 1 + .../02-transfer-keep-alive/request.json | 1 + .../qr/reduced/02-transfer-keep-alive/ur.txt | 2 + .../03-schedule-transfer/frames/frame-000.svg | 1 + .../03-schedule-transfer/frames/frame-001.svg | 1 + .../reduced/03-schedule-transfer/index.html | 33 +++++++ .../reduced/03-schedule-transfer/payload.hex | 1 + .../reduced/03-schedule-transfer/request.json | 1 + .../qr/reduced/03-schedule-transfer/ur.txt | 2 + .../frames/frame-000.svg | 1 + .../frames/frame-001.svg | 1 + .../index.html | 33 +++++++ .../payload.hex | 1 + .../request.json | 1 + .../04-schedule-transfer-with-delay/ur.txt | 2 + .../05-multisig-create/frames/frame-000.svg | 1 + .../05-multisig-create/frames/frame-001.svg | 1 + .../05-multisig-create/frames/frame-002.svg | 1 + .../qr/reduced/05-multisig-create/index.html | 33 +++++++ .../qr/reduced/05-multisig-create/payload.hex | 1 + .../reduced/05-multisig-create/request.json | 1 + .../qr/reduced/05-multisig-create/ur.txt | 3 + .../frames/frame-000.svg | 1 + .../frames/frame-001.svg | 1 + .../frames/frame-002.svg | 1 + .../06-multisig-propose-transfer/index.html | 33 +++++++ .../06-multisig-propose-transfer/payload.hex | 1 + .../06-multisig-propose-transfer/request.json | 1 + .../06-multisig-propose-transfer/ur.txt | 3 + .../frames/frame-000.svg | 1 + .../frames/frame-001.svg | 1 + .../frames/frame-002.svg | 1 + .../07-multisig-approve-transfer/index.html | 33 +++++++ .../07-multisig-approve-transfer/payload.hex | 1 + .../07-multisig-approve-transfer/request.json | 1 + .../07-multisig-approve-transfer/ur.txt | 3 + .../frames/frame-000.svg | 1 + .../frames/frame-001.svg | 1 + .../08-multisig-execute-transfer/index.html | 33 +++++++ .../08-multisig-execute-transfer/payload.hex | 1 + .../08-multisig-execute-transfer/request.json | 1 + .../08-multisig-execute-transfer/ur.txt | 2 + .../frames/frame-000.svg | 1 + .../frames/frame-001.svg | 1 + .../frames/frame-002.svg | 1 + .../09-multisig-execute-reversible/index.html | 33 +++++++ .../payload.hex | 1 + .../request.json | 1 + .../09-multisig-execute-reversible/ur.txt | 3 + .../test/fixtures/qr/reduced/index.html | 22 +++++ .../test/fixtures/qr/reduced/manifest.json | 97 +++++++++++++++++++ .../test/qr_fixture_corpus_test.dart | 80 +++++++++++++++ 62 files changed, 601 insertions(+) create mode 100644 cold-wallet-app/test/fixtures/qr/README.md create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/frames/frame-000.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/frames/frame-001.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/payload.hex create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/request.json create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/ur.txt create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/frames/frame-000.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/frames/frame-001.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/payload.hex create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/request.json create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/ur.txt create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/frames/frame-000.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/frames/frame-001.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/payload.hex create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/request.json create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/ur.txt create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/frames/frame-000.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/frames/frame-001.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/payload.hex create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/request.json create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/ur.txt create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-000.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-001.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-002.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/payload.hex create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/request.json create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/ur.txt create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-000.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-001.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-002.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/payload.hex create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/request.json create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/ur.txt create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-000.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-001.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-002.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/payload.hex create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/request.json create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/ur.txt create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/frames/frame-000.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/frames/frame-001.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/payload.hex create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/request.json create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/ur.txt create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-000.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-001.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-002.svg create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/payload.hex create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/request.json create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/ur.txt create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/index.html create mode 100644 cold-wallet-app/test/fixtures/qr/reduced/manifest.json create mode 100644 cold-wallet-app/test/qr_fixture_corpus_test.dart diff --git a/cold-wallet-app/test/fixtures/qr/README.md b/cold-wallet-app/test/fixtures/qr/README.md new file mode 100644 index 000000000..03e352748 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/README.md @@ -0,0 +1,43 @@ +# Cold signing QR fixtures + +Test payloads for the cold wallet and the Keystone firmware. Each case is a real signing +request: a call plus its signed extensions, wrapped in the `{"v":1,"signer":..,"payload":..}` +envelope and UR-encoded into QR frames. That is exactly what a cold wallet scans. + +## Layout + +``` +reduced/ + index.html pick a case, then show it to a camera or the simulator + manifest.json every case, what it is, and what a wallet must display + / + index.html animated QR for this case (arrow keys change speed) + frames/frame-NNN.svg one SVG per UR frame + ur.txt the UR parts as text + payload.hex the signing payload + request.json the envelope before UR encoding +``` + +## Sets + +- `reduced/` — the calls the Keystone firmware parses today. Small enough to scan on real + hardware. This is the set to run on the device. +- A full set covering every cold wallet call can be added later. It is not here yet because + large calls take a lot of QR frames (see below). + +## Frame counts + +The payload is hex encoded inside JSON, so the QR data is roughly twice the payload size. +UR splits it into 200 byte fragments. A simple transfer is 118 bytes and still needs 2 +frames. A chain maximum 10 KiB call would need about 100 frames, which is not practical to +scan. Keep test calls small. + +## Regenerating + +``` +cd ../quantus-cli +cargo run --example generate_qr_fixtures -- ../quantus-apps/cold-wallet-app/test/fixtures/qr +``` + +The generator encodes calls through the CLI's bundled chain metadata, so the bytes match +what the chain accepts. Regenerate after a runtime upgrade. diff --git a/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/frames/frame-000.svg b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/frames/frame-000.svg new file mode 100644 index 000000000..17597769d --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/frames/frame-000.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/frames/frame-001.svg b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/frames/frame-001.svg new file mode 100644 index 000000000..f358be990 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/frames/frame-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/index.html b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/index.html new file mode 100644 index 000000000..a0ef631c3 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/index.html @@ -0,0 +1,33 @@ + + +01-transfer-allow-death + + +

Plain transfer of 1 QUAN.

+

01-transfer-allow-death — frame 1/2 — 200ms + (←/→ to change speed)

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/payload.hex b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/payload.hex new file mode 100644 index 000000000..d86bb8822 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/payload.hex @@ -0,0 +1 @@ +0x0200007777777777777777777777777777777777777777777777777777777777777777070010a5d4e80000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100 diff --git a/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/request.json b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/request.json new file mode 100644 index 000000000..97d974544 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/request.json @@ -0,0 +1 @@ +{"v":1,"signer":"qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei","payload":"0x0200007777777777777777777777777777777777777777777777777777777777777777070010a5d4e80000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100"} diff --git a/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/ur.txt b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/ur.txt new file mode 100644 index 000000000..7ee5bf84f --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/01-transfer-allow-death/ur.txt @@ -0,0 +1,2 @@ +UR:QUANTUS-SIGN-REQUEST/1-2/LPADAOCFADFWCYDKTEAXBBHDOYHKADFHKGCPKOCPFTEHDWCPJKINIOJTIHJPCPFTCPJSKNJTESJKJOISENHTJLGYKSKTJKIHGUFGKKJPIEIYGHGOFEHGJNJLKNJKIHKSEMISISFXGEGYGDFLEYESJTIDIOIHJKFLIHINCPDWCPJOHSKKJZJLHSIECPFTCPDYKSDYEYDYDYDYDYEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMDYEMDYDYEHDYHSEOHLIYQD +UR:QUANTUS-SIGN-REQUEST/2-2/LPAOAOCFADFWCYDKTEAXBBHDOYECIEEEIHETDYDYDYDYDYDYDYDYESEODYDYDYDYDYDYDYENDYDYDYDYDYDYEEESDYEHIDIYECIAECEMIYIEEOIYESIHEMEYENHSIYEOESESIAEMENEOIEIHENENEMDYIEIDIEIDEHEHECHSESEHIADYEYEOEMIHEHEMEOIYEHENIHIHIYENECIHEMEYEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHDYDYCPKISKVOUYDA diff --git a/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/frames/frame-000.svg b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/frames/frame-000.svg new file mode 100644 index 000000000..9164fdb9b --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/frames/frame-000.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/frames/frame-001.svg b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/frames/frame-001.svg new file mode 100644 index 000000000..42c2d3b41 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/frames/frame-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/index.html b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/index.html new file mode 100644 index 000000000..7d49097a8 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/index.html @@ -0,0 +1,33 @@ + + +02-transfer-keep-alive + + +

Transfer of 2.5 QUAN that leaves the account above existential deposit.

+

02-transfer-keep-alive — frame 1/2 — 200ms + (←/→ to change speed)

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/payload.hex b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/payload.hex new file mode 100644 index 000000000..990e8a083 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/payload.hex @@ -0,0 +1 @@ +0x02030077777777777777777777777777777777777777777777777777777777777777770b00a89c1346020000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100 diff --git a/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/request.json b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/request.json new file mode 100644 index 000000000..09c73b231 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/request.json @@ -0,0 +1 @@ +{"v":1,"signer":"qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei","payload":"0x02030077777777777777777777777777777777777777777777777777777777777777770b00a89c1346020000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100"} diff --git a/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/ur.txt b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/ur.txt new file mode 100644 index 000000000..0b4bb90a6 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/02-transfer-keep-alive/ur.txt @@ -0,0 +1,2 @@ +UR:QUANTUS-SIGN-REQUEST/1-2/LPADAOCFADFYCYVTAYPTCKHDOEHKADFPKGCPKOCPFTEHDWCPJKINIOJTIHJPCPFTCPJSKNJTESJKJOISENHTJLGYKSKTJKIHGUFGKKJPIEIYGHGOFEHGJNJLKNJKIHKSEMISISFXGEGYGDFLEYESJTIDIOIHJKFLIHINCPDWCPJOHSKKJZJLHSIECPFTCPDYKSDYEYDYEODYDYEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMDYIDDYDYHSETESIASKZCYNEM +UR:QUANTUS-SIGN-REQUEST/2-2/LPAOAOCFADFYCYVTAYPTCKHDOEEHEOEEENDYEYDYDYDYDYDYDYDYDYESEODYDYDYDYDYDYDYENDYDYDYDYDYDYEEESDYEHIDIYECIAECEMIYIEEOIYESIHEMEYENHSIYEOESESIAEMENEOIEIHENENEMDYIEIDIEIDEHEHECHSESEHIADYEYEOEMIHEHEMEOIYEHENIHIHIYENECIHEMEYEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHDYDYCPKISTPESSBA diff --git a/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/frames/frame-000.svg b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/frames/frame-000.svg new file mode 100644 index 000000000..bb74819ed --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/frames/frame-000.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/frames/frame-001.svg b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/frames/frame-001.svg new file mode 100644 index 000000000..ef04a1144 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/frames/frame-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/index.html b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/index.html new file mode 100644 index 000000000..b71260355 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/index.html @@ -0,0 +1,33 @@ + + +03-schedule-transfer + + +

Reversible transfer of 3 QUAN using the account's configured delay.

+

03-schedule-transfer — frame 1/2 — 200ms + (←/→ to change speed)

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/payload.hex b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/payload.hex new file mode 100644 index 000000000..377a9d289 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/payload.hex @@ -0,0 +1 @@ +0x0b030077777777777777777777777777777777777777777777777777777777777777770030ef7dba02000000000000000000000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100 diff --git a/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/request.json b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/request.json new file mode 100644 index 000000000..7f4c9b8e4 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/request.json @@ -0,0 +1 @@ +{"v":1,"signer":"qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei","payload":"0x0b030077777777777777777777777777777777777777777777777777777777777777770030ef7dba02000000000000000000000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100"} diff --git a/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/ur.txt b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/ur.txt new file mode 100644 index 000000000..fc5c4492f --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/03-schedule-transfer/ur.txt @@ -0,0 +1,2 @@ +UR:QUANTUS-SIGN-REQUEST/1-2/LPADAOCFADHFCYUEEMOXRYHDPYHKADGUKGCPKOCPFTEHDWCPJKINIOJTIHJPCPFTCPJSKNJTESJKJOISENHTJLGYKSKTJKIHGUFGKKJPIEIYGHGOFEHGJNJLKNJKIHKSEMISISFXGEGYGDFLEYESJTIDIOIHJKFLIHINCPDWCPJOHSKKJZJLHSIECPFTCPDYKSDYIDDYEODYDYEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMDYDYEODYIHIYEMIEIDHSDYEYDYDYDYDYDYIHJOMNYK +UR:QUANTUS-SIGN-REQUEST/2-2/LPAOAOCFADHFCYUEEMOXRYHDPYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYESEODYDYDYDYDYDYDYENDYDYDYDYDYDYEEESDYEHIDIYECIAECEMIYIEEOIYESIHEMEYENHSIYEOESESIAEMENEOIEIHENENEMDYIEIDIEIDEHEHECHSESEHIADYEYEOEMIHEHEMEOIYEHENIHIHIYENECIHEMEYEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHDYDYCPKIPTFMDANL diff --git a/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/frames/frame-000.svg b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/frames/frame-000.svg new file mode 100644 index 000000000..b766141e0 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/frames/frame-000.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/frames/frame-001.svg b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/frames/frame-001.svg new file mode 100644 index 000000000..e9582f053 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/frames/frame-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/index.html b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/index.html new file mode 100644 index 000000000..0a43eaa87 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/index.html @@ -0,0 +1,33 @@ + + +04-schedule-transfer-with-delay + + +

Reversible transfer of 5 QUAN with an explicit one-hour reversal window.

+

04-schedule-transfer-with-delay — frame 1/2 — 200ms + (←/→ to change speed)

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/payload.hex b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/payload.hex new file mode 100644 index 000000000..52e98e5c2 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/payload.hex @@ -0,0 +1 @@ +0x0b04007777777777777777777777777777777777777777777777777777777777777777005039278c04000000000000000000000180ee3600000000000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100 diff --git a/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/request.json b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/request.json new file mode 100644 index 000000000..f2120e69b --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/request.json @@ -0,0 +1 @@ +{"v":1,"signer":"qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei","payload":"0x0b04007777777777777777777777777777777777777777777777777777777777777777005039278c04000000000000000000000180ee3600000000000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100"} diff --git a/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/ur.txt b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/ur.txt new file mode 100644 index 000000000..89aebdbb1 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/04-schedule-transfer-with-delay/ur.txt @@ -0,0 +1,2 @@ +UR:QUANTUS-SIGN-REQUEST/1-2/LPADAOCFADISCYSTWFIDASHDQZHKADIHKGCPKOCPFTEHDWCPJKINIOJTIHJPCPFTCPJSKNJTESJKJOISENHTJLGYKSKTJKIHGUFGKKJPIEIYGHGOFEHGJNJLKNJKIHKSEMISISFXGEGYGDFLEYESJTIDIOIHJKFLIHINCPDWCPJOHSKKJZJLHSIECPFTCPDYKSDYIDDYEEDYDYEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMDYDYECDYEOESEYEMETIADYEEDYDYDYDYDYDYDYDYDYDYDYDYDYDYEHTITARP +UR:QUANTUS-SIGN-REQUEST/2-2/LPAOAOCFADISCYSTWFIDASHDQZDYDYDYDYDYDYDYEHETDYIHIHEOENDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYESEODYDYDYDYDYDYDYENDYDYDYDYDYDYEEESDYEHIDIYECIAECEMIYIEEOIYESIHEMEYENHSIYEOESESIAEMENEOIEIHENENEMDYIEIDIEIDEHEHECHSESEHIADYEYEOEMIHEHEMEOIYEHENIHIHIYENECIHEMEYEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHDYDYCPKIADMDGHJN diff --git a/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-000.svg b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-000.svg new file mode 100644 index 000000000..f531dbd1f --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-000.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-001.svg b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-001.svg new file mode 100644 index 000000000..550ab1761 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-002.svg b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-002.svg new file mode 100644 index 000000000..774255c37 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/frames/frame-002.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/index.html b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/index.html new file mode 100644 index 000000000..341c9f029 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/index.html @@ -0,0 +1,33 @@ + + +05-multisig-create + + +

Create a 2-of-3 multisig.

+

05-multisig-create — frame 1/3 — 200ms + (←/→ to change speed)

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/payload.hex b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/payload.hex new file mode 100644 index 000000000..a43e926d8 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/payload.hex @@ -0,0 +1 @@ +0x13000caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc0200000000000000000000000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100 diff --git a/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/request.json b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/request.json new file mode 100644 index 000000000..89a1a1cbd --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/request.json @@ -0,0 +1 @@ +{"v":1,"signer":"qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei","payload":"0x13000caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc0200000000000000000000000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100"} diff --git a/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/ur.txt b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/ur.txt new file mode 100644 index 000000000..df10d97ff --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/05-multisig-create/ur.txt @@ -0,0 +1,3 @@ +UR:QUANTUS-SIGN-REQUEST/1-3/LPADAXCFADTOCYCPBWGWWFHDNYHKADSBKGCPKOCPFTEHDWCPJKINIOJTIHJPCPFTCPJSKNJTESJKJOISENHTJLGYKSKTJKIHGUFGKKJPIEIYGHGOFEHGJNJLKNJKIHKSEMISISFXGEGYGDFLEYESJTIDIOIHJKFLIHINCPDWCPJOHSKKJZJLHSIECPFTCPDYKSEHEODYDYDYIAHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSHSKOCNWZLF +UR:QUANTUS-SIGN-REQUEST/2-3/LPAOAXCFADTOCYCPBWGWWFHDNYIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIDIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIAIADYEYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYNNRDPYGL +UR:QUANTUS-SIGN-REQUEST/3-3/LPAXAXCFADTOCYCPBWGWWFHDNYDYDYDYDYDYDYESEODYDYDYDYDYDYDYENDYDYDYDYDYDYEEESDYEHIDIYECIAECEMIYIEEOIYESIHEMEYENHSIYEOESESIAEMENEOIEIHENENEMDYIEIDIEIDEHEHECHSESEHIADYEYEOEMIHEHEMEOIYEHENIHIHIYENECIHEMEYEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHDYDYCPKISRDRFXPT diff --git a/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-000.svg b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-000.svg new file mode 100644 index 000000000..852fce54c --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-000.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-001.svg b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-001.svg new file mode 100644 index 000000000..a3370a4f4 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-002.svg b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-002.svg new file mode 100644 index 000000000..7ee399885 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/frames/frame-002.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/index.html b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/index.html new file mode 100644 index 000000000..a073ad562 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/index.html @@ -0,0 +1,33 @@ + + +06-multisig-propose-transfer + + +

Propose a 42 QUAN transfer from the multisig.

+

06-multisig-propose-transfer — frame 1/3 — 200ms + (←/→ to change speed)

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/payload.hex b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/payload.hex new file mode 100644 index 000000000..257d5ce4b --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/payload.hex @@ -0,0 +1 @@ +0x13019999999999999999999999999999999999999999999999999999999999999999a802000077777777777777777777777777777777777777777777777777777777777777770b00a014e33226404b4c000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100 diff --git a/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/request.json b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/request.json new file mode 100644 index 000000000..636395779 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/request.json @@ -0,0 +1 @@ +{"v":1,"signer":"qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei","payload":"0x13019999999999999999999999999999999999999999999999999999999999999999a802000077777777777777777777777777777777777777777777777777777777777777770b00a014e33226404b4c000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100"} diff --git a/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/ur.txt b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/ur.txt new file mode 100644 index 000000000..b06b1795e --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/06-multisig-propose-transfer/ur.txt @@ -0,0 +1,3 @@ +UR:QUANTUS-SIGN-REQUEST/1-3/LPADAXCFADMOCYDPRDFGTOHDLNHKADMYKGCPKOCPFTEHDWCPJKINIOJTIHJPCPFTCPJSKNJTESJKJOISENHTJLGYKSKTJKIHGUFGKKJPIEIYGHGOFEHGJNJLKNJKIHKSEMISISFXGEGYGDFLEYESJTIDIOIHJKFLIHINCPDWCPJOHSKKJZJLHSIECPFTCPDYKSEHEODYEHESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESRETDEEHP +UR:QUANTUS-SIGN-REQUEST/2-3/LPAOAXCFADMOCYDPRDFGTOHDLNESESESESESESESESESESESESESESESESESESHSETDYEYDYDYDYDYEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMDYIDDYDYHSDYEHEEIHEOEOEYEYENEEDYEEIDEEIADYDYDYDYDYDYDYDYDYDYESEODYDYDYDYDYDYDYENDYDYDYDYRPPTMEAO +UR:QUANTUS-SIGN-REQUEST/3-3/LPAXAXCFADMOCYDPRDFGTOHDLNDYDYEEESDYEHIDIYECIAECEMIYIEEOIYESIHEMEYENHSIYEOESESIAEMENEOIEIHENENEMDYIEIDIEIDEHEHECHSESEHIADYEYEOEMIHEHEMEOIYEHENIHIHIYENECIHEMEYEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHDYDYCPKIGYVSSFEO diff --git a/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-000.svg b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-000.svg new file mode 100644 index 000000000..0ab62012c --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-000.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-001.svg b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-001.svg new file mode 100644 index 000000000..273200b4c --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-002.svg b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-002.svg new file mode 100644 index 000000000..5c3ddb647 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/frames/frame-002.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/index.html b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/index.html new file mode 100644 index 000000000..9c924c16a --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/index.html @@ -0,0 +1,33 @@ + + +07-multisig-approve-transfer + + +

Approve proposal 7, which carries the 42 QUAN transfer being approved.

+

07-multisig-approve-transfer — frame 1/3 — 200ms + (←/→ to change speed)

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/payload.hex b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/payload.hex new file mode 100644 index 000000000..fffb8c8e1 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/payload.hex @@ -0,0 +1 @@ +0x1302999999999999999999999999999999999999999999999999999999999999999907000000a802000077777777777777777777777777777777777777777777777777777777777777770b00a014e332260000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100 diff --git a/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/request.json b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/request.json new file mode 100644 index 000000000..2bdf0cca0 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/request.json @@ -0,0 +1 @@ +{"v":1,"signer":"qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei","payload":"0x1302999999999999999999999999999999999999999999999999999999999999999907000000a802000077777777777777777777777777777777777777777777777777777777777777770b00a014e332260000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100"} diff --git a/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/ur.txt b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/ur.txt new file mode 100644 index 000000000..45c371a81 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/07-multisig-approve-transfer/ur.txt @@ -0,0 +1,3 @@ +UR:QUANTUS-SIGN-REQUEST/1-3/LPADAXCFADMOCYHDEHTBADHDLNHKADMYKGCPKOCPFTEHDWCPJKINIOJTIHJPCPFTCPJSKNJTESJKJOISENHTJLGYKSKTJKIHGUFGKKJPIEIYGHGOFEHGJNJLKNJKIHKSEMISISFXGEGYGDFLEYESJTIDIOIHJKFLIHINCPDWCPJOHSKKJZJLHSIECPFTCPDYKSEHEODYEYESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESCPVLJKJE +UR:QUANTUS-SIGN-REQUEST/2-3/LPAOAXCFADMOCYHDEHTBADHDLNESESESESESESESESESESESESESESESESESESDYEMDYDYDYDYDYDYHSETDYEYDYDYDYDYEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMDYIDDYDYHSDYEHEEIHEOEOEYEYENDYDYDYDYDYDYDYDYESEODYDYDYDYDYDYDYENDYDYDYDYZSLYAHCY +UR:QUANTUS-SIGN-REQUEST/3-3/LPAXAXCFADMOCYHDEHTBADHDLNDYDYEEESDYEHIDIYECIAECEMIYIEEOIYESIHEMEYENHSIYEOESESIAEMENEOIEIHENENEMDYIEIDIEIDEHEHECHSESEHIADYEYEOEMIHEHEMEOIYEHENIHIHIYENECIHEMEYEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHDYDYCPKIKIGMMTPS diff --git a/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/frames/frame-000.svg b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/frames/frame-000.svg new file mode 100644 index 000000000..e624daec5 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/frames/frame-000.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/frames/frame-001.svg b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/frames/frame-001.svg new file mode 100644 index 000000000..801b0a7c4 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/frames/frame-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/index.html b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/index.html new file mode 100644 index 000000000..7c13859c6 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/index.html @@ -0,0 +1,33 @@ + + +08-multisig-execute-transfer + + +

Execute proposal 7, which carries the 42 QUAN transfer being dispatched.

+

08-multisig-execute-transfer — frame 1/2 — 200ms + (←/→ to change speed)

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/payload.hex b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/payload.hex new file mode 100644 index 000000000..c15bbee07 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/payload.hex @@ -0,0 +1 @@ +0x130699999999999999999999999999999999999999999999999999999999999999990700000002000077777777777777777777777777777777777777777777777777777777777777770b00a014e332260000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100 diff --git a/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/request.json b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/request.json new file mode 100644 index 000000000..e6d900e51 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/request.json @@ -0,0 +1 @@ +{"v":1,"signer":"qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei","payload":"0x130699999999999999999999999999999999999999999999999999999999999999990700000002000077777777777777777777777777777777777777777777777777777777777777770b00a014e332260000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100"} diff --git a/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/ur.txt b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/ur.txt new file mode 100644 index 000000000..c6545ead1 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/08-multisig-execute-transfer/ur.txt @@ -0,0 +1,2 @@ +UR:QUANTUS-SIGN-REQUEST/1-2/LPADAOCFADMHCYLBHLRENLHDSPHKADLGKGCPKOCPFTEHDWCPJKINIOJTIHJPCPFTCPJSKNJTESJKJOISENHTJLGYKSKTJKIHGUFGKKJPIEIYGHGOFEHGJNJLKNJKIHKSEMISISFXGEGYGDFLEYESJTIDIOIHJKFLIHINCPDWCPJOHSKKJZJLHSIECPFTCPDYKSEHEODYENESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESDYEMDYDYDYDYDYDYDYEYDYDYDYDYEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMRPVDHLRN +UR:QUANTUS-SIGN-REQUEST/2-2/LPAOAOCFADMHCYLBHLRENLHDSPEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMDYIDDYDYHSDYEHEEIHEOEOEYEYENDYDYDYDYDYDYDYDYESEODYDYDYDYDYDYDYENDYDYDYDYDYDYEEESDYEHIDIYECIAECEMIYIEEOIYESIHEMEYENHSIYEOESESIAEMENEOIEIHENENEMDYIEIDIEIDEHEHECHSESEHIADYEYEOEMIHEHEMEOIYEHENIHIHIYENECIHEMEYEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHDYDYCPKIOTLFHGSN diff --git a/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-000.svg b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-000.svg new file mode 100644 index 000000000..1f4200d5a --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-000.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-001.svg b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-001.svg new file mode 100644 index 000000000..bcce3b925 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-001.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-002.svg b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-002.svg new file mode 100644 index 000000000..e839d6eb9 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/frames/frame-002.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/index.html b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/index.html new file mode 100644 index 000000000..c8db9e7bd --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/index.html @@ -0,0 +1,33 @@ + + +09-multisig-execute-reversible + + +

Execute a proposal whose inner call is a reversible transfer, not a plain one.

+

09-multisig-execute-reversible — frame 1/3 — 200ms + (←/→ to change speed)

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/payload.hex b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/payload.hex new file mode 100644 index 000000000..d5b9ce1b2 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/payload.hex @@ -0,0 +1 @@ +0x13069999999999999999999999999999999999999999999999999999999999999999080000000b03007777777777777777777777777777777777777777777777777777777777777777005039278c04000000000000000000000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100 diff --git a/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/request.json b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/request.json new file mode 100644 index 000000000..205ef23aa --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/request.json @@ -0,0 +1 @@ +{"v":1,"signer":"qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei","payload":"0x13069999999999999999999999999999999999999999999999999999999999999999080000000b03007777777777777777777777777777777777777777777777777777777777777777005039278c04000000000000000000000000000093000000060000004901bf5c57fd3f9e726af399c763de6670dbdb115a91c0237e173f16eef65e72111111111111111111111111111111111111111111111111111111111111111100"} diff --git a/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/ur.txt b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/ur.txt new file mode 100644 index 000000000..f149ec0d6 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/09-multisig-execute-reversible/ur.txt @@ -0,0 +1,3 @@ +UR:QUANTUS-SIGN-REQUEST/1-3/LPADAXCFADOECYGHETKEFZHDLKHKADNEKGCPKOCPFTEHDWCPJKINIOJTIHJPCPFTCPJSKNJTESJKJOISENHTJLGYKSKTJKIHGUFGKKJPIEIYGHGOFEHGJNJLKNJKIHKSEMISISFXGEGYGDFLEYESJTIDIOIHJKFLIHINCPDWCPJOHSKKJZJLHSIECPFTCPDYKSEHEODYENESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESESBYDIHSTI +UR:QUANTUS-SIGN-REQUEST/2-3/LPAOAXCFADOECYGHETKEFZHDLKESESESESESESESESESESESESDYETDYDYDYDYDYDYDYIDDYEODYDYEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMEMDYDYECDYEOESEYEMETIADYEEDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYDYESEODYDYDYDYDYDYDYENHSLFTBBE +UR:QUANTUS-SIGN-REQUEST/3-3/LPAXAXCFADOECYGHETKEFZHDLKDYDYDYDYDYDYEEESDYEHIDIYECIAECEMIYIEEOIYESIHEMEYENHSIYEOESESIAEMENEOIEIHENENEMDYIEIDIEIDEHEHECHSESEHIADYEYEOEMIHEHEMEOIYEHENIHIHIYENECIHEMEYEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHEHDYDYCPKIAEAECNNNLFBG diff --git a/cold-wallet-app/test/fixtures/qr/reduced/index.html b/cold-wallet-app/test/fixtures/qr/reduced/index.html new file mode 100644 index 000000000..f8f574ca9 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/index.html @@ -0,0 +1,22 @@ + + +Cold signing QR fixtures + +

Cold signing QR fixtures

+

Reduced set: the calls the Keystone firmware parses today. Open one, then point the + device or simulator at the animated QR.

+ diff --git a/cold-wallet-app/test/fixtures/qr/reduced/manifest.json b/cold-wallet-app/test/fixtures/qr/reduced/manifest.json new file mode 100644 index 000000000..ac763eed6 --- /dev/null +++ b/cold-wallet-app/test/fixtures/qr/reduced/manifest.json @@ -0,0 +1,97 @@ +{ + "specVersion": 147, + "transactionVersion": 6, + "network": "Planck", + "cases": [ + { + "slug": "01-transfer-allow-death", + "call": "balances.transfer_allow_death", + "description": "Plain transfer of 1 QUAN.", + "innerCall": null, + "signer": "qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei", + "callBytes": 41, + "payloadBytes": 118, + "frames": 2 + }, + { + "slug": "02-transfer-keep-alive", + "call": "balances.transfer_keep_alive", + "description": "Transfer of 2.5 QUAN that leaves the account above existential deposit.", + "innerCall": null, + "signer": "qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei", + "callBytes": 42, + "payloadBytes": 119, + "frames": 2 + }, + { + "slug": "03-schedule-transfer", + "call": "reversible_transfers.schedule_transfer", + "description": "Reversible transfer of 3 QUAN using the account's configured delay.", + "innerCall": null, + "signer": "qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei", + "callBytes": 51, + "payloadBytes": 128, + "frames": 2 + }, + { + "slug": "04-schedule-transfer-with-delay", + "call": "reversible_transfers.schedule_transfer_with_delay", + "description": "Reversible transfer of 5 QUAN with an explicit one-hour reversal window.", + "innerCall": null, + "signer": "qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei", + "callBytes": 60, + "payloadBytes": 137, + "frames": 2 + }, + { + "slug": "05-multisig-create", + "call": "multisig.create_multisig", + "description": "Create a 2-of-3 multisig.", + "innerCall": null, + "signer": "qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei", + "callBytes": 111, + "payloadBytes": 188, + "frames": 3 + }, + { + "slug": "06-multisig-propose-transfer", + "call": "multisig.propose", + "description": "Propose a 42 QUAN transfer from the multisig.", + "innerCall": "balances.transfer_allow_death", + "signer": "qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei", + "callBytes": 81, + "payloadBytes": 158, + "frames": 3 + }, + { + "slug": "07-multisig-approve-transfer", + "call": "multisig.approve", + "description": "Approve proposal 7, which carries the 42 QUAN transfer being approved.", + "innerCall": "balances.transfer_allow_death", + "signer": "qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei", + "callBytes": 81, + "payloadBytes": 158, + "frames": 3 + }, + { + "slug": "08-multisig-execute-transfer", + "call": "multisig.execute", + "description": "Execute proposal 7, which carries the 42 QUAN transfer being dispatched.", + "innerCall": "balances.transfer_allow_death", + "signer": "qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei", + "callBytes": 80, + "payloadBytes": 157, + "frames": 2 + }, + { + "slug": "09-multisig-execute-reversible", + "call": "multisig.execute", + "description": "Execute a proposal whose inner call is a reversible transfer, not a plain one.", + "innerCall": "reversible_transfers.schedule_transfer", + "signer": "qzn9sph6ZoQxwseSFyrdfTUEWmozsex7hhCJQPG29nbgesGei", + "callBytes": 89, + "payloadBytes": 166, + "frames": 3 + } + ] +} diff --git a/cold-wallet-app/test/qr_fixture_corpus_test.dart b/cold-wallet-app/test/qr_fixture_corpus_test.dart new file mode 100644 index 000000000..0a463fbcd --- /dev/null +++ b/cold-wallet-app/test/qr_fixture_corpus_test.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; + +/// `CallDecoder` names pallets as the metadata does (`ReversibleTransfers`); the manifest +/// uses the chain's own call path (`reversible_transfers.schedule_transfer`). +String callPath(DecodedCall call) { + final pallet = call.pallet.replaceAllMapped(RegExp(r'(?<=[a-z0-9])[A-Z]'), (m) => '_${m[0]}').toLowerCase(); + return '$pallet.${call.call}'; +} + +/// Every fixture in `test/fixtures/qr/reduced` is a real signing request. The cold wallet +/// must decode all of them, and show the call the manifest says it should. +/// +/// Regenerate the corpus with the quantus-cli `generate_qr_fixtures` example. See the +/// README next to the fixtures. +void main() { + final root = Directory('test/fixtures/qr/reduced'); + final manifest = jsonDecode(File('${root.path}/manifest.json').readAsStringSync()) as Map; + final cases = (manifest['cases'] as List).cast>(); + + group('QR fixture corpus', () { + test('manifest and fixture directories agree', () { + final onDisk = + root.listSync().whereType().map((d) => d.path.split(Platform.pathSeparator).last).toList()..sort(); + final listed = cases.map((c) => c['slug'] as String).toList()..sort(); + expect(onDisk, listed, reason: 'a fixture folder is missing from manifest.json, or vice versa'); + }); + + test('every case targets the runtime the app bundles', () { + expect(manifest['specVersion'], AppConstants.bundledSpecVersion); + expect(manifest['transactionVersion'], AppConstants.bundledTransactionVersion); + }); + + for (final testCase in cases) { + final slug = testCase['slug'] as String; + final dir = '${root.path}/$slug'; + + test('$slug decodes to ${testCase['call']}', () { + final hexText = File('$dir/payload.hex').readAsStringSync().trim(); + final payload = Uint8List.fromList(hex.decode(hexText.substring(2))); + expect(payload, hasLength(testCase['payloadBytes'])); + + final parsed = QuantusPayloadParser.parsePayload(payload); + + expect(parsed.network, manifest['network']); + // A stale bundled runtime would make the wallet warn on a valid payload. + expect(parsed.specMatchesBundled, isTrue); + + final call = parsed.call; + expect(callPath(call), testCase['call']); + + final innerCall = testCase['innerCall'] as String?; + if (innerCall == null) { + expect(call.fields.whereType(), isEmpty); + } else { + final nested = call.fields.whereType().single; + expect(callPath(nested.call), innerCall); + } + }); + + test('$slug carries the signing request the device scans', () { + final request = jsonDecode(File('$dir/request.json').readAsStringSync()) as Map; + expect(request['v'], 1); + expect(request['signer'], testCase['signer']); + expect(request['payload'], File('$dir/payload.hex').readAsStringSync().trim()); + + // One QR frame per UR part, so a viewer never runs past the end of the animation. + final urParts = File('$dir/ur.txt').readAsLinesSync().where((l) => l.isNotEmpty); + expect(urParts, hasLength(testCase['frames'])); + final frames = Directory('$dir/frames').listSync().where((f) => f.path.endsWith('.svg')); + expect(frames, hasLength(testCase['frames'])); + }); + } + }); +}