Example #1
0
    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)
Example #2
0
    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
Example #3
0
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
Example #4
0
    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,
            ))
Example #5
0
    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,
            ))