From ad40ca555ae4b80d4d1568940ab74002a32766ea Mon Sep 17 00:00:00 2001 From: abacus-x <79712764+abacus-x@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:20:53 -0400 Subject: [PATCH] Make transaction message compilation consistent with @solana/web3.js (#228) * Updated AccountMeta sorting and adding signatures Three primary changes: 1. Updated AccountMeta sorting to match solana/web3.js package. The sorting was a bit different, which resulted in different account key indexes and serialized messages (identical transaction had different serialized output vs js package). The js package has a third sort key that references the AccountMeta PublicKey. 2. Updated the way signatures are added. Previously it added signer signatures if none were present - now it iterates through signers to see if any are missing. 3. Simplified the way AccountMetas are culled * Update transaction.py * Update transaction.py * Update transaction.py * Add unit test for AccountMeta sorting * Linting * Update test_transaction.py * Update test_vote_program.py * Update test_system_program.py * Update test_transaction.py * Update transaction.py * Update src/solana/transaction.py * Update tests/unit/test_system_program.py * Update tests/unit/test_vote_program.py * Update tests/unit/test_system_program.py * Update src/solana/transaction.py * type: exiting to existing * Update transaction.py * Update transaction.py * Update test_vote_program.py * Update test_system_program.py * Update transaction.py * Update test_vote_program.py * Update test_system_program.py * Update transaction.py * Update tests/unit/test_transaction.py * Update src/solana/transaction.py * Update tests/unit/test_vote_program.py --- src/solana/transaction.py | 147 ++++++++------- tests/unit/test_system_program.py | 24 ++- tests/unit/test_transaction.py | 299 ++++++++++++++++++++++++++++++ tests/unit/test_vote_program.py | 30 +-- 4 files changed, 414 insertions(+), 86 deletions(-) diff --git a/src/solana/transaction.py b/src/solana/transaction.py index 6abeded7..ccbe994a 100644 --- a/src/solana/transaction.py +++ b/src/solana/transaction.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from sys import maxsize from typing import Any, Dict, List, NamedTuple, NewType, Optional, Union from based58 import b58decode, b58encode @@ -155,79 +154,91 @@ def compile_message(self) -> Message: # pylint: disable=too-many-locals if not fee_payer: raise AttributeError("transaction feePayer required") - account_metas, program_ids = [], [] - for instr in self.instructions: - if not instr.program_id: - raise AttributeError("invalid instruction:", instr) - account_metas.extend(instr.keys) - if str(instr.program_id) not in program_ids: - program_ids.append(str(instr.program_id)) - - # Append programID account metas. - for pg_id in program_ids: - account_metas.append(AccountMeta(PublicKey(pg_id), False, False)) - - # Sort. Prioritizing first by signer, then by writable and converting from set to list. - account_metas.sort(key=lambda account: (not account.is_signer, not account.is_writable)) - - # Cull duplicate accounts - fee_payer_idx = maxsize - seen: Dict[str, int] = {} - uniq_metas: List[AccountMeta] = [] - for sig in self.signatures: - pubkey = str(sig.pubkey) - if pubkey in seen: - uniq_metas[seen[pubkey]].is_signer = True - else: - uniq_metas.append(AccountMeta(sig.pubkey, True, True)) - seen[pubkey] = len(uniq_metas) - 1 - if sig.pubkey == fee_payer: - fee_payer_idx = min(fee_payer_idx, seen[pubkey]) - - for a_m in account_metas: - pubkey = str(a_m.pubkey) - if pubkey in seen: - idx = seen[pubkey] - uniq_metas[idx].is_writable = uniq_metas[idx].is_writable or a_m.is_writable - else: - uniq_metas.append(a_m) - seen[pubkey] = len(uniq_metas) - 1 - if a_m.pubkey == fee_payer: - fee_payer_idx = min(fee_payer_idx, seen[pubkey]) - - # Move fee payer to the front - if fee_payer_idx == maxsize: - uniq_metas = [AccountMeta(fee_payer, True, True)] + uniq_metas + # Organize account_metas + account_metas: Dict[str, AccountMeta] = {} + + for instruction in self.instructions: + + if not instruction.program_id: + raise AttributeError("invalid instruction:", instruction) + + # Update `is_signer` and `is_writable` as iterate through instructions + for key in instruction.keys: + pubkey = str(key.pubkey) + if pubkey not in account_metas: + account_metas[pubkey] = key + else: + account_metas[pubkey].is_signer = True if key.is_signer else account_metas[pubkey].is_signer + account_metas[pubkey].is_writable = True if key.is_writable else account_metas[pubkey].is_writable + + # Add program_id to account_metas + instruction_program_id = str(instruction.program_id) + if instruction_program_id not in account_metas: + account_metas[instruction_program_id] = AccountMeta( + pubkey=instruction.program_id, + is_signer=False, + is_writable=False, + ) + + # Separate `fee_payer_am` and sort the remaining account_metas + # Sort keys are: + # 1. is_signer, with `is_writable`=False ordered last + # 2. is_writable + # 3. PublicKey + fee_payer_am = account_metas.pop(str(fee_payer), None) + if fee_payer_am: + fee_payer_am.is_signer = True + fee_payer_am.is_writable = True else: - uniq_metas = ( - [uniq_metas[fee_payer_idx]] + uniq_metas[:fee_payer_idx] + uniq_metas[fee_payer_idx + 1 :] # noqa: E203 - ) + fee_payer_am = AccountMeta(fee_payer, True, True) + + sorted_account_metas = sorted(account_metas.values(), key=lambda am: (str(am.pubkey).lower())) + signer_am = sorted([x for x in sorted_account_metas if x.is_signer], key=lambda am: not am.is_writable) + writable_am = [x for x in sorted_account_metas if (not x.is_signer and x.is_writable)] + rest_am = [x for x in sorted_account_metas if (not x.is_signer and not x.is_writable)] + + joined_am = [fee_payer_am] + signer_am + writable_am + rest_am + + # Get signature counts for header + + # The number of signatures required for this message to be considered valid. The + # signatures must match the first `num_required_signatures` of `account_keys`. + # NOTE: Serialization-related changes must be paired with the direct read at sigverify. + num_required_signatures: int = len([x for x in joined_am if x.is_signer]) + # The last num_readonly_signed_accounts of the signed keys are read-only accounts. Programs + # may process multiple transactions that load read-only accounts within a single PoH entry, + # but are not permitted to credit or debit lamports or modify account data. Transactions + # targeting the same read-write account are evaluated sequentially. + num_readonly_signed_accounts: int = len([x for x in joined_am if (not x.is_writable and x.is_signer)]) + # The last num_readonly_unsigned_accounts of the unsigned keys are read-only accounts. + num_readonly_unsigned_accounts: int = len([x for x in joined_am if (not x.is_writable and not x.is_signer)]) - # Split out signing from nonsigning keys and count readonlys - signed_keys: List[str] = [] - unsigned_keys: List[str] = [] - num_required_signatures = num_readonly_signed_accounts = num_readonly_unsigned_accounts = 0 - for a_m in uniq_metas: - if a_m.is_signer: - signed_keys.append(str(a_m.pubkey)) - num_required_signatures += 1 - num_readonly_signed_accounts += int(not a_m.is_writable) - else: - num_readonly_unsigned_accounts += int(not a_m.is_writable) - unsigned_keys.append(str(a_m.pubkey)) # Initialize signature array, if needed - if not self.signatures: - self.signatures = [SigPubkeyPair(pubkey=PublicKey(key), signature=None) for key in signed_keys] + account_keys = [(str(x.pubkey), x.is_signer) for x in joined_am] + + self.signatures = [] if not self.signatures else self.signatures + existing_signature_pubkeys: List[str] = [str(x.pubkey) for x in self.signatures] + + # Append missing signatures + signer_pubkeys = [k for (k, is_signer) in account_keys if is_signer] + for signer_pubkey in signer_pubkeys: + if signer_pubkey not in existing_signature_pubkeys: + self.signatures.append(SigPubkeyPair(pubkey=PublicKey(signer_pubkey), signature=None)) + + # Ensure fee_payer signature is first + fee_payer_signature = [x for x in self.signatures if x.pubkey == fee_payer] + other_signatures = [x for x in self.signatures if x.pubkey != fee_payer] + self.signatures = fee_payer_signature + other_signatures + + account_indices: Dict[str, int] = {k: idx for idx, (k, _) in enumerate(account_keys)} - account_keys: List[str] = signed_keys + unsigned_keys - account_indices: Dict[str, int] = {str(key): i for i, key in enumerate(account_keys)} compiled_instructions: List[CompiledInstruction] = [ CompiledInstruction( - accounts=[account_indices[str(a_m.pubkey)] for a_m in instr.keys], - program_id_index=account_indices[str(instr.program_id)], - data=b58encode(instr.data), + accounts=[account_indices[str(am.pubkey)] for am in instruction.keys], + program_id_index=account_indices[str(instruction.program_id)], + data=b58encode(instruction.data), ) - for instr in self.instructions + for instruction in self.instructions ] return Message( @@ -237,7 +248,7 @@ def compile_message(self) -> Message: # pylint: disable=too-many-locals num_readonly_signed_accounts=num_readonly_signed_accounts, num_readonly_unsigned_accounts=num_readonly_unsigned_accounts, ), - account_keys=account_keys, + account_keys=[k for (k, _) in account_keys], instructions=compiled_instructions, recent_blockhash=self.recent_blockhash, ) diff --git a/tests/unit/test_system_program.py b/tests/unit/test_system_program.py index 8582fe31..b7313112 100644 --- a/tests/unit/test_system_program.py +++ b/tests/unit/test_system_program.py @@ -211,7 +211,7 @@ def test_create_nonce_account(): ) ) - wire_txn = base64.b64decode( + cli_wire_txn = base64.b64decode( b"AtZYPHSaLIQsFnHm4O7Lk0YdQRzovtsp0eKbKRPknDvZINd62tZaLPRzhm6N1LeINLzy31iHY6QE0bGW5c9aegu9g9SQqwsj" b"dKfNTYI0JLmzQd98HCUczjMM5H/gvGx+4k+sM/SreWkC3y1X+I1yh4rXehtVW5Sqo5nyyl7z88wOAgADBTqF5SfUR/5I9i2g" b"nIHHEr01j2JItmpFHSaRd74NaZ1wvICzr4gFWblct6+DODXkCxQiipQzG81MS5S4IkqB7uEGp9UXGSxWjuCKhF9z0peIzwNc" @@ -219,7 +219,11 @@ def test_create_nonce_account(): b"AAAAAABXbYHxIfw3Z5Qq1LH8aj6Sj6LuqbCuwFhAmo21XevlfwIEAgABNAAAAACAhB4AAAAAAFAAAAAAAAAAAAAAAAAAAAAA" b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAwECAyQGAAAAOoXlJ9RH/kj2LaCcgccSvTWPYki2akUdJpF3vg1pnXA=" ) - expected_txn = txlib.Transaction.deserialize(wire_txn) + js_wire_txn = base64.b64decode( + b"AkBAiPTJfOYZRLOZUpH7vIxyJQovMxO7X8FxXyRzae8CECBZ9LS5G8hxZVMdVL6uSIzLHb/0aLYhO5FEVmfhwguY5ZtOCOGqjwyAOVr8L2eBXgX482L/rcmF6ELORIcD1GdAFBQ/1Hc/LByer9TbJfNqzjesdzTJEHohnStduU4OAgADBTqF5SfUR/5I9i2gnIHHEr01j2JItmpFHSaRd74NaZ1wvICzr4gFWblct6+DODXkCxQiipQzG81MS5S4IkqB7uEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAan1RcZLFaO4IqEX3PSl4jPA1wxRbIas0TYBi6pQAAABqfVFxksXFEhjMlMPUrxf1ja7gibof1E49vZigAAAABXbYHxIfw3Z5Qq1LH8aj6Sj6LuqbCuwFhAmo21XevlfwICAgABNAAAAACAhB4AAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAwEDBCQGAAAAOoXlJ9RH/kj2LaCcgccSvTWPYki2akUdJpF3vg1pnXA=" # noqa: E501 pylint: disable=line-too-long + ) + cli_expected_txn = txlib.Transaction.deserialize(cli_wire_txn) # noqa: F841 + js_expected_txn = txlib.Transaction.deserialize(js_wire_txn) create_account_txn = sp.create_nonce_account( sp.CreateNonceAccountParams( @@ -236,7 +240,9 @@ def test_create_nonce_account(): create_account_txn.add_signature(from_keypair.public_key, from_keypair.sign(create_account_hash).signature) create_account_txn.add_signature(nonce_keypair.public_key, nonce_keypair.sign(create_account_hash).signature) - assert create_account_txn == expected_txn + assert create_account_txn == js_expected_txn + # XXX: Cli message serialization do not sort on account metas producing discrepency + # assert create_account_txn == cli_expected_txn def test_advance_nonce_and_transfer(): @@ -451,15 +457,19 @@ def test_advance_nonce_and_transfer(): ) ) - wire_txn = base64.b64decode( + cli_wire_txn = base64.b64decode( b"Abh4hJNaP/IUJlHGpQttaGNWkjOZx71uLEnVpT0SBaedmThsTogjsh87FW+EHeuJrsZii+tJbrq3oJ5UYXPzXwwBAAIFOoXl" b"J9RH/kj2LaCcgccSvTWPYki2akUdJpF3vg1pnXC8gLOviAVZuVy3r4M4NeQLFCKKlDMbzUxLlLgiSoHu4fx8NgMSbd8b4Rw7" b"yjH49BGlIWU72U/q2ftVCQYoAN0KBqfVFxksVo7gioRfc9KXiM8DXDFFshqzRNgGLqlAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" b"AAAAAAAAAAAAAAAAAE13Mu8zaQSpG0zzGHpG62nK56DbGhuS4kXMF/ChHY1jAgQDAQMABAQAAAAEAgACDAIAAACAhB4AAAAA" b"AA==" ) + js_wire_txn = base64.b64decode( + b"Af67rLfP5WxsOgvZWndq34S2KbQq++x03eZkZagzbVQ2tRyfFyn6OWByp8q3P2a03HDeVtpUWhq1y1a6R0DcPAIBAAIFOoXlJ9RH/kj2LaCcgccSvTWPYki2akUdJpF3vg1pnXC8gLOviAVZuVy3r4M4NeQLFCKKlDMbzUxLlLgiSoHu4fx8NgMSbd8b4Rw7yjH49BGlIWU72U/q2ftVCQYoAN0KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAE13Mu8zaQSpG0zzGHpG62nK56DbGhuS4kXMF/ChHY1jAgMDAQQABAQAAAADAgACDAIAAACAhB4AAAAAAA==" # noqa: E501 pylint: disable=line-too-long + ) - expected_txn = txlib.Transaction.deserialize(wire_txn) + cli_expected_txn = txlib.Transaction.deserialize(cli_wire_txn) # noqa: F841 + js_expected_txn = txlib.Transaction.deserialize(js_wire_txn) txn = txlib.Transaction(fee_payer=from_keypair.public_key) txn.recent_blockhash = "6DPp9aRRX6cLBqj5FepEvoccHFs3s8gUhd9t9ftTwAta" @@ -483,4 +493,6 @@ def test_advance_nonce_and_transfer(): txn.add_signature(from_keypair.public_key, from_keypair.sign(txn_hash).signature) - assert txn == expected_txn + assert txn == js_expected_txn + # XXX: Cli message serialization do not sort on account metas producing discrepency + # assert txn == cli_expected_txn diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py index 68039044..e49c318e 100644 --- a/tests/unit/test_transaction.py +++ b/tests/unit/test_transaction.py @@ -121,3 +121,302 @@ def test_serialize_unsigned_transaction(stubbed_blockhash, stubbed_receiver, stu ) assert txn.serialize() == expected_serialization assert len(txn.signatures) == 1 + + +def test_sort_account_metas(stubbed_blockhash): + """ + Test AccountMeta sorting after calling Transaction.compile_message() + """ + + # S6EA7XsNyxg4yx4DJRMm7fP21jgZb1fuzBAUGhgVtkP + signer_one = Keypair.from_seed( + bytes( + [ + 216, + 214, + 184, + 213, + 199, + 75, + 129, + 160, + 237, + 96, + 96, + 228, + 46, + 251, + 146, + 3, + 71, + 162, + 37, + 117, + 121, + 70, + 143, + 16, + 128, + 78, + 53, + 189, + 222, + 230, + 165, + 249, + ] + ) + ) + + # BKdt9U6V922P17ui81dzLoqgSY2B5ds1UD13rpwFB2zi + receiver_one = Keypair.from_seed( + bytes( + [ + 3, + 140, + 94, + 243, + 0, + 38, + 92, + 138, + 52, + 79, + 153, + 83, + 42, + 236, + 220, + 82, + 227, + 187, + 101, + 104, + 126, + 159, + 103, + 100, + 29, + 183, + 242, + 68, + 144, + 184, + 114, + 211, + ] + ) + ) + + # DtDZCnXEN69n5W6rN5SdJFgedrWdK8NV9bsMiJekNRyu + signer_two = Keypair.from_seed( + bytes( + [ + 177, + 182, + 154, + 154, + 5, + 145, + 253, + 138, + 211, + 126, + 222, + 195, + 21, + 64, + 117, + 211, + 225, + 47, + 115, + 31, + 247, + 242, + 80, + 195, + 38, + 8, + 236, + 155, + 255, + 27, + 20, + 142, + ] + ) + ) + + # FXgds3n6SNCoVVV4oELSumv8nKzAfqSgmeu7cNPikKFT + receiver_two = Keypair.from_seed( + bytes( + [ + 180, + 204, + 139, + 131, + 244, + 6, + 180, + 121, + 191, + 193, + 45, + 109, + 198, + 50, + 163, + 140, + 34, + 4, + 172, + 76, + 129, + 45, + 194, + 83, + 192, + 112, + 76, + 58, + 32, + 174, + 49, + 248, + ] + ) + ) + + # C2UwQHqJ3BmEJHSMVmrtZDQGS2fGv8fZrWYGi18nHF5k + signer_three = Keypair.from_seed( + bytes( + [ + 29, + 79, + 73, + 16, + 137, + 117, + 183, + 2, + 131, + 0, + 209, + 142, + 134, + 100, + 190, + 35, + 95, + 220, + 200, + 163, + 247, + 237, + 161, + 70, + 226, + 223, + 100, + 148, + 49, + 202, + 154, + 180, + ] + ) + ) + + # 8YPqwYXZtWPd31puVLEUPamS4wTv6F89n8nXDA5Ce2Bg + receiver_three = Keypair.from_seed( + bytes( + [ + 167, + 102, + 49, + 166, + 202, + 0, + 132, + 182, + 239, + 182, + 252, + 59, + 25, + 103, + 76, + 217, + 65, + 215, + 210, + 159, + 168, + 50, + 10, + 229, + 144, + 231, + 221, + 74, + 182, + 161, + 52, + 193, + ] + ) + ) + + fee_payer = signer_one + sorted_signers = sorted([x.public_key for x in [signer_one, signer_two, signer_three]], key=lambda x: str(x)) + sorted_signers_excluding_fee_payer = [x for x in sorted_signers if str(x) != str(fee_payer.public_key)] + sorted_receivers = sorted( + [x.public_key for x in [receiver_one, receiver_two, receiver_three]], key=lambda x: str(x) + ) + + txn = txlib.Transaction(recent_blockhash=stubbed_blockhash) + txn.fee_payer = fee_payer.public_key + + # Add three transfer transactions + txn.add( + sp.transfer( + sp.TransferParams( + from_pubkey=signer_one.public_key, + to_pubkey=receiver_one.public_key, + lamports=2_000_000, + ) + ) + ) + txn.add( + sp.transfer( + sp.TransferParams( + from_pubkey=signer_two.public_key, + to_pubkey=receiver_two.public_key, + lamports=2_000_000, + ) + ) + ) + txn.add( + sp.transfer( + sp.TransferParams( + from_pubkey=signer_three.public_key, + to_pubkey=receiver_three.public_key, + lamports=2_000_000, + ) + ) + ) + + tx_msg = txn.compile_message() + + js_msg_b64_check = b"AwABBwZtbiRMvgQjcE2kVx9yon8XqPSO5hwc2ApflnOZMu0Qo9G5/xbhB0sp8/03Rv9x4MKSkQ+k4LB6lNLvCgKZ/ju/aw+EyQpTObVa3Xm+NA1gSTzutgFCTfkDto/0KtuIHHAMpKRb92NImxKeWQJ2/291j6nTzFj1D6nW25p7TofHmVsGt8uFnTv7+8vsWZ0uN7azdxa+jCIIm4WzKK+4uKfX39t5UA7S1soBQaJkTGOQkSbBo39gIjDkbW0TrevslgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusDBgIABAwCAAAAgIQeAAAAAAAGAgIFDAIAAACAhB4AAAAAAAYCAQMMAgAAAICEHgAAAAAA" # noqa: E501 pylint: disable=line-too-long + + assert b64encode(tx_msg.serialize()) == js_msg_b64_check + + # Transaction should organize AccountMetas by PublicKey + assert tx_msg.account_keys[0] == fee_payer.public_key + assert tx_msg.account_keys[1] == sorted_signers_excluding_fee_payer[0] + assert tx_msg.account_keys[2] == sorted_signers_excluding_fee_payer[1] + assert tx_msg.account_keys[3] == sorted_receivers[0] + assert tx_msg.account_keys[4] == sorted_receivers[1] + assert tx_msg.account_keys[5] == sorted_receivers[2] diff --git a/tests/unit/test_vote_program.py b/tests/unit/test_vote_program.py index 605701da..ce3ebff0 100644 --- a/tests/unit/test_vote_program.py +++ b/tests/unit/test_vote_program.py @@ -1,8 +1,8 @@ """Unit tests for solana.vote_program.""" import base64 -import solana.vote_program as vp import solana.transaction as txlib +import solana.vote_program as vp from solana.keypair import Keypair from solana.publickey import PublicKey @@ -81,16 +81,6 @@ def test_withdraw_from_vote_account(): vote_account_pubkey = PublicKey("CWqJy1JpmBcx7awpeANfrPk6AsQKkmego8ujjaYPGFEk") receiver_account_pubkey = PublicKey("A1V5gsis39WY42djdTKUFsgE5oamk4nrtg16WnKTuzZK") - # solana withdraw-from-vote-account --dump-transaction-message \ - # CWqJy1JpmBcx7awpeANfrPk6AsQKkmego8ujjaYPGFEk A1V5gsis39WY42djdTKUFsgE5oamk4nrtg16WnKTuzZK \ - # --authorized-withdrawer withdrawer.json \ - # 2 \ - # --blockhash Add1tV7kJgNHhTtx3Dgs6dhC7kyXrGJQZ2tJGW15tLDH \ - # --sign-only -k withdrawer.json - wire_msg = base64.b64decode( - b"AQABBDqF5SfUR/5I9i2gnIHHEr01j2JItmpFHSaRd74NaZ1wqxUGDtH5ah3TqEKWjcTmfHkpZC1h57NJL8Sx7Q6Olm2F2O70oOvzt1HgIVu+nySaSrWtJiK1eDacPPDWRxCwFgdhSB01dHS7fE12JOvTvbPYNV5z0RBD/A2jU4AAAAAAjxrQaMS7FjmaR++mvFr3XE6XbzMUTMJUIpITrUWBzGwBAwMBAgAMAwAAAACUNXcAAAAA" # noqa: E501 pylint: disable=line-too-long - ) - txn = txlib.Transaction(fee_payer=withdrawer_keypair.public_key) txn.recent_blockhash = "Add1tV7kJgNHhTtx3Dgs6dhC7kyXrGJQZ2tJGW15tLDH" @@ -105,5 +95,21 @@ def test_withdraw_from_vote_account(): ) ) + # solana withdraw-from-vote-account --dump-transaction-message \ + # CWqJy1JpmBcx7awpeANfrPk6AsQKkmego8ujjaYPGFEk A1V5gsis39WY42djdTKUFsgE5oamk4nrtg16WnKTuzZK \ + # --authorized-withdrawer withdrawer.json \ + # 2 \ + # --blockhash Add1tV7kJgNHhTtx3Dgs6dhC7kyXrGJQZ2tJGW15tLDH \ + # --sign-only -k withdrawer.json + cli_wire_msg = base64.b64decode( # noqa: F841 + b"AQABBDqF5SfUR/5I9i2gnIHHEr01j2JItmpFHSaRd74NaZ1wqxUGDtH5ah3TqEKWjcTmfHkpZC1h57NJL8Sx7Q6Olm2F2O70oOvzt1HgIVu+nySaSrWtJiK1eDacPPDWRxCwFgdhSB01dHS7fE12JOvTvbPYNV5z0RBD/A2jU4AAAAAAjxrQaMS7FjmaR++mvFr3XE6XbzMUTMJUIpITrUWBzGwBAwMBAgAMAwAAAACUNXcAAAAA" # noqa: E501 pylint: disable=line-too-long + ) + js_wire_msg = base64.b64decode( + b"AQABBDqF5SfUR/5I9i2gnIHHEr01j2JItmpFHSaRd74NaZ1whdju9KDr87dR4CFbvp8kmkq1rSYitXg2nDzw1kcQsBarFQYO0flqHdOoQpaNxOZ8eSlkLWHns0kvxLHtDo6WbQdhSB01dHS7fE12JOvTvbPYNV5z0RBD/A2jU4AAAAAAjxrQaMS7FjmaR++mvFr3XE6XbzMUTMJUIpITrUWBzGwBAwMCAQAMAwAAAACUNXcAAAAA" # noqa: E501 pylint: disable=line-too-long + ) + serialized_message = txn.serialize_message() - assert serialized_message == wire_msg + + assert serialized_message == js_wire_msg + # XXX: Cli message serialization do not sort on account metas producing discrepency + # serialized_message txn == cli_wire_msg