def deserialize(raw_transaction: bytes) -> Transaction: """Parse a wire transaction into a Transaction object. >>> raw_transaction = bytes.fromhex( ... '019d53be8af3a7c30f86c1092d2c3ea61d270c0cfa2' ... '75a23ba504674c8fbbb724827b23b42dc8e08019e23' ... '120f1b6f40f9799355ce54185b4415be37ca2cee6e0' ... 'e010001034cb5abf6ad79fbf5abbccafcc269d85cd2' ... '651ed4b885b5869f241aedf0a5ba290000000000000' ... '0000000000000000000000000000000000000000000' ... '0000000200000000000000000000000000000000000' ... '0000000000000000000000000000000000000000000' ... '0000000000000000000000000000000000000000000' ... '000000301020200010c02000000e803000000000000' ... ) >>> type(Transaction.deserialize(raw_transaction)) <class 'solana.transaction.Transaction'> """ signatures = [] signature_count, offset = shortvec.decode_length(raw_transaction) for _ in range(signature_count): signatures.append( b58encode(raw_transaction[offset:offset + SIG_LENGTH])) # noqa: E203 offset += SIG_LENGTH return Transaction.populate( Message.deserialize(raw_transaction[offset:]), signatures)
def populate(message: Message, signatures: List[bytes]) -> Transaction: """Populate Transaction object from message and signatures. Example: >>> raw_message = bytes.fromhex( ... '0200030500000000000000000000000000000000000000000000' ... '0000000000000000000100000000000000000000000000000000' ... '0000000000000000000000000000000200000000000000000000' ... '0000000000000000000000000000000000000000000300000000' ... '0000000000000000000000000000000000000000000000000000' ... '0004000000000000000000000000000000000000000000000000' ... '0000000000000005c49ae77603782054f17a9decea43b444eba0' ... 'edb12c6f1d31c6e0e4a84bf052eb010403010203050909090909' ... ) >>> from based58 import b58encode >>> from solana.message import Message >>> msg = Message.deserialize(raw_message) >>> signatures = [b58encode(bytes([1] * SIG_LENGTH)), b58encode(bytes([2] * SIG_LENGTH))] >>> type(Transaction.populate(msg, signatures)) <class 'solana.transaction.Transaction'> Returns: The populated transaction. """ transaction = Transaction(recent_blockhash=message.recent_blockhash) for idx, sig in enumerate(signatures): signature = None if sig == b58encode( Transaction.__DEFAULT_SIG) else b58decode(sig) transaction.signatures.append( SigPubkeyPair(pubkey=message.account_keys[idx], signature=signature)) for instr in message.instructions: account_metas: List[AccountMeta] = [] for acc_idx in instr.accounts: pubkey = message.account_keys[acc_idx] is_signer = any((pubkey == sigkeypair.pubkey for sigkeypair in transaction.signatures)) account_metas.append( AccountMeta( pubkey=pubkey, is_signer=is_signer, is_writable=message.is_account_writable(acc_idx))) program_id = message.account_keys[instr.program_id_index] transaction.instructions.append( TransactionInstruction(keys=account_metas, program_id=program_id, data=b58decode(instr.data))) return transaction
def test_populate(stubbed_blockhash): """Test populating transaction with a message and two signatures.""" account_keys = [str(PublicKey(i + 1)) for i in range(5)] msg = Message( MessageArgs( account_keys=account_keys, header=MessageHeader( num_readonly_signed_accounts=0, num_readonly_unsigned_accounts=3, num_required_signatures=2 ), instructions=[CompiledInstruction(accounts=[1, 2, 3], data=b58encode(bytes([9] * 5)), program_id_index=4)], recent_blockhash=stubbed_blockhash, ) ) signatures = [b58encode(bytes([1] * txlib.SIG_LENGTH)), b58encode(bytes([2] * txlib.SIG_LENGTH))] transaction = txlib.Transaction.populate(msg, signatures) assert len(transaction.instructions) == len(msg.instructions) assert len(transaction.signatures) == len(signatures) assert transaction.recent_blockhash == msg.recent_blockhash
def compile_message(self) -> Message: """Compile transaction data.""" if self.nonce_info and self.instructions[ 0] != self.nonce_info.nonce_instruction: self.recent_blockhash = self.nonce_info.nonce self.instructions = [self.nonce_info.nonce_instruction ] + self.instructions if not self.recent_blockhash: raise AttributeError("transaction recentBlockhash required") if len(self.instructions) < 1: raise AttributeError("no instructions provided") account_metas, program_ids = [], set() for instr in self.instructions: if not instr.program_id or not instr.keys: raise AttributeError("invalid instruction:", instr) account_metas.extend(instr.keys) program_ids.add(str(instr.program_id)) # Append programID account metas. for pg_id in program_ids: account_metas.append(AccountMeta(PublicKey(pg_id), False, False)) # Prefix accountMetas with feePayer here whenever that gets implemented. # 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 seen: Dict[str, int] = dict() 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 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 # Split out signing from nonsigning keys and count readonlys signed_keys: List[str] = [] unsigned_keys: List[str] = [] num_readonly_signed_accounts = num_readonly_unsigned_accounts = 0 for a_m in uniq_metas: if a_m.is_signer: # Promote the first signer to writable as it is the fee payer if len(signed_keys) != 0 and not a_m.is_writable: num_readonly_signed_accounts += 1 signed_keys.append(str(a_m.pubkey)) 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: 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), ) for instr in self.instructions ] return Message( MessageArgs( header=MessageHeader( num_required_signatures=len(self.signatures), num_readonly_signed_accounts=num_readonly_signed_accounts, num_readonly_unsigned_accounts= num_readonly_unsigned_accounts, ), account_keys=account_keys, instructions=compiled_instructions, recent_blockhash=self.recent_blockhash, ))
def compile_message(self) -> Message: # pylint: disable=too-many-locals """Compile transaction data. Returns: The compiled message. """ if self.nonce_info and self.instructions[ 0] != self.nonce_info.nonce_instruction: self.recent_blockhash = self.nonce_info.nonce self.instructions = [self.nonce_info.nonce_instruction ] + self.instructions if not self.recent_blockhash: raise AttributeError("transaction recentBlockhash required") if len(self.instructions) < 1: raise AttributeError("no instructions provided") fee_payer = self.fee_payer if not fee_payer and len( self.signatures) > 0 and self.signatures[0].pubkey: # Use implicit fee payer fee_payer = self.signatures[0].pubkey 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 else: uniq_metas = ([uniq_metas[fee_payer_idx]] + uniq_metas[:fee_payer_idx] + uniq_metas[fee_payer_idx + 1:] # noqa: E203 ) # 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: 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), ) for instr in self.instructions ] return Message( MessageArgs( header=MessageHeader( num_required_signatures=num_required_signatures, num_readonly_signed_accounts=num_readonly_signed_accounts, num_readonly_unsigned_accounts= num_readonly_unsigned_accounts, ), account_keys=account_keys, instructions=compiled_instructions, recent_blockhash=self.recent_blockhash, ))