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/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'])); + }); + } + }); +} diff --git a/quantus_sdk/lib/src/chain/call_decoder.dart b/quantus_sdk/lib/src/chain/call_decoder.dart index c45f3a3c5..25c3dab03 100644 --- a/quantus_sdk/lib/src/chain/call_decoder.dart +++ b/quantus_sdk/lib/src/chain/call_decoder.dart @@ -198,7 +198,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', @@ -213,7 +213,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', @@ -226,8 +226,21 @@ 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. + checkCallSize(call.encode().length); + 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): @@ -241,6 +254,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 33fbf5791..678a660df 100644 --- a/quantus_sdk/test/chain/call_decoder_test.dart +++ b/quantus_sdk/test/chain/call_decoder_test.dart @@ -150,6 +150,50 @@ 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('refuses a multisig inner call larger than a signer will review', () { + // 320 signers encodes to 10_256 bytes, just over the chain's MaxCallSize. + final oversized = const multisig_pallet.Txs().createMultisig( + signers: List.generate(320, (i) => Uint8List.fromList(List.filled(32, i % 256))), + 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'); diff --git a/rust-transaction-parser/src/lib.rs b/rust-transaction-parser/src/lib.rs index dab14aaf9..29ae20b8a 100644 --- a/rust-transaction-parser/src/lib.rs +++ b/rust-transaction-parser/src/lib.rs @@ -1,10 +1,19 @@ -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. -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 inner call a multisig proposal may carry, mirroring the runtime's +/// `pallet_multisig::Config::MaxCallSize` (`BoundedVec`, 10 KiB). +/// +/// 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. @@ -31,7 +40,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 +104,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 +209,12 @@ pub enum QuantusTx { MultisigApprove { multisig: String, proposal_id: u32, + inner: Box, }, MultisigExecute { multisig: String, proposal_id: u32, + inner: Box, }, } @@ -262,13 +276,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 +286,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 +328,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 +371,39 @@ 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 { + 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 { + check_call_size(bytes.len())?; 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 +416,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() { @@ -582,6 +630,42 @@ 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_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_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 [propose_wrapping(&oversized), approve_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"); @@ -595,12 +679,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 +722,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 @@ -657,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]